Bug 1282752 - use the awesome new Range.getRectsAndTexts() API to fetch the text content for each rect of a range. r?jaws draft
authorMike de Boer <mdeboer@mozilla.com>
Fri, 11 Nov 2016 12:41:00 +0100
changeset 437691 0543e3a079db9376ca95692d84785c855f521dfe
parent 437690 c7344732fadf77fe7cc8ff6c82d556cd369fa287
child 536703 6777d1bf1829e438cf6a2f719f4861966f4033ac
push id35486
push usermdeboer@mozilla.com
push dateFri, 11 Nov 2016 11:41:39 +0000
reviewersjaws
bugs1282752
milestone52.0a1
Bug 1282752 - use the awesome new Range.getRectsAndTexts() API to fetch the text content for each rect of a range. r?jaws MozReview-Commit-ID: BKAx4TdBms0
toolkit/modules/FinderHighlighter.jsm
toolkit/modules/tests/browser/browser_FinderHighlighter.js
--- a/toolkit/modules/FinderHighlighter.jsm
+++ b/toolkit/modules/FinderHighlighter.jsm
@@ -155,17 +155,17 @@ FinderHighlighter.prototype = {
    * For each window we track a number of properties which _at least_ consist of
    *  - {Boolean} detectedGeometryChange Whether the geometry of the found ranges'
    *                                     rectangles has changed substantially
    *  - {Set}     dynamicRangesSet       Set of ranges that may move around, depending
    *                                     on page layout changes and user input
    *  - {Map}     frames                 Collection of frames that were encountered
    *                                     when inspecting the found ranges
    *  - {Map}     modalHighlightRectsMap Collection of ranges and their corresponding
-   *                                     Rects
+   *                                     Rects and texts
    *
    * @param  {nsIDOMWindow} window
    * @return {Object}
    */
   getForWindow(window, propName = null) {
     if (!gWindows.has(window)) {
       gWindows.set(window, {
         detectedGeometryChange: false,
@@ -822,17 +822,17 @@ FinderHighlighter.prototype = {
    * Read and store the rectangles that encompass the entire region of a range
    * for use by the drawing function of the highlighter.
    *
    * @param  {nsIDOMRange} range  Range to fetch the rectangles from
    * @param  {Object}      [dict] Dictionary of properties belonging to
    *                              the currently active window
    * @return {Set}         Set of rects that were found for the range
    */
-  _getRangeRects(range, dict = null) {
+  _getRangeRectsAndTexts(range, dict = null) {
     let window = range.startContainer.ownerDocument.defaultView;
     let bounds;
     // If the window is part of a frameset, try to cache the bounds query.
     if (dict && dict.frames.has(window)) {
       bounds = dict.frames.get(window);
       if (!bounds) {
         bounds = this._getRootBounds(window);
         dict.frames.set(window, bounds);
@@ -840,25 +840,26 @@ FinderHighlighter.prototype = {
     } else
       bounds = this._getRootBounds(window);
 
     let topBounds = this._getRootBounds(window.top, false);
     let rects = [];
     // A range may consist of multiple rectangles, we can also do these kind of
     // precise cut-outs. range.getBoundingClientRect() returns the fully
     // encompassing rectangle, which is too much for our purpose here.
-    for (let rect of range.getClientRects()) {
+    let {rectList, textList} = range.getClientRectsAndTexts();
+    for (let rect of rectList) {
       rect = Rect.fromRect(rect);
       rect.x += bounds.x;
       rect.y += bounds.y;
       // If the rect is not even visible from the top document, we can ignore it.
       if (rect.intersects(topBounds))
         rects.push(rect);
     }
-    return rects;
+    return {rectList: rects, textList};
   },
 
   /**
    * Read and store the rectangles that encompass the entire region of a range
    * for use by the drawing function of the highlighter and store them in the
    * cache.
    *
    * @param  {nsIDOMRange} range            Range to fetch the rectangles from
@@ -867,29 +868,29 @@ FinderHighlighter.prototype = {
    *                                        `_isInDynamicContainer()`. Optional,
    *                                        defaults to `true`
    * @param  {Object}      [dict]           Dictionary of properties belonging to
    *                                        the currently active window
    * @return {Set}         Set of rects that were found for the range
    */
   _updateRangeRects(range, checkIfDynamic = true, dict = null) {
     let window = range.startContainer.ownerDocument.defaultView;
-    let rects = this._getRangeRects(range, dict);
+    let rectsAndTexts = this._getRangeRectsAndTexts(range, dict);
 
     // Only fetch the rect at this point, if not passed in as argument.
     dict = dict || this.getForWindow(window.top);
-    let oldRects = dict.modalHighlightRectsMap.get(range);
-    dict.modalHighlightRectsMap.set(range, rects);
+    let oldRectsAndTexts = dict.modalHighlightRectsMap.get(range);
+    dict.modalHighlightRectsMap.set(range, rectsAndTexts);
     // Check here if we suddenly went down to zero rects from more than zero before,
     // which indicates that we should re-iterate the document.
-    if (oldRects && oldRects.length && !rects.length)
+    if (oldRectsAndTexts && oldRectsAndTexts.rectList.length && !rectsAndTexts.rectList.length)
       dict.detectedGeometryChange = true;
     if (checkIfDynamic && this._isInDynamicContainer(range))
       dict.dynamicRangesSet.add(range);
-    return rects;
+    return rectsAndTexts;
   },
 
   /**
    * Re-read the rectangles of the ranges that we keep track of separately,
    * because they're enclosed by a position: fixed container DOM node or (i)frame.
    *
    * @param {Object} dict Dictionary of properties belonging to the currently
    *                      active window
@@ -919,21 +920,21 @@ FinderHighlighter.prototype = {
     let range = dict.currentFoundRange;
     if (!range)
       return;
 
     fontStyle = fontStyle || this._getRangeFontStyle(range);
     // Text color in the outline is determined by kModalStyles.
     delete fontStyle.color;
 
-    let rects = this._getRangeRects(range);
+    let rectsAndTexts = this._getRangeRectsAndTexts(range);
     textContent = textContent || this._getRangeContentArray(range);
 
     let outlineAnonNode = dict.modalHighlightOutline;
-    let rectCount = rects.length;
+    let rectCount = rectsAndTexts.rectList.length;
     // (re-)Building the outline is conditional and happens when one of the
     // following conditions is met:
     // 1. No outline nodes were built before, or
     // 2. When the amount of rectangles to draw is different from before, or
     // 3. When there's more than one rectangle to draw, because it's impossible
     //    to animate that consistently with AnonymousContent nodes.
     let rebuildOutline = (!outlineAnonNode || rectCount !== dict.previousRangeRectsCount ||
       rectCount != 1);
@@ -949,48 +950,45 @@ FinderHighlighter.prototype = {
         try {
           document.removeAnonymousContent(outlineAnonNode);
         } catch (ex) {}
       }
       dict.modalHighlightOutline = null;
     }
 
     // Abort when there's no text to highlight.
-    if (!textContent.length)
+    if (!rectsAndTexts.textList.length)
       return;
 
     let outlineBox;
     if (rebuildOutline) {
       // Create the main (yellow) highlight outline box.
       outlineBox = document.createElementNS(kNSHTML, "div");
       outlineBox.setAttribute("id", kModalOutlineId);
     }
 
     const kModalOutlineTextId = kModalOutlineId + "-text";
     let i = 0;
-    for (let rect of rects) {
-      // if the current rect is the last rect, then text is set to the rest of
-      // the textContent with single spaces injected between the text. Otherwise
-      // text is set to the current textContent for the matching rect.
-      let text = (i == rectCount - 1) ? textContent.slice(i).join(" ") : textContent[i];
+    for (let rect of rectsAndTexts.rectList) {
+      let text = rectsAndTexts.textList[i];
 
       // Next up is to check of the outline box' borders will not overlap with
       // rects that we drew before or will draw after this one.
       // We're taking the width of the border into account, which is
       // `kOutlineBoxBorderSize` pixels.
       // When left and/ or right sides will overlap with the current, previous
       // or next rect, make sure to make the necessary adjustments to the style.
       // These adjustments will override the styles as defined in `kModalStyles.outlineNode`.
       let intersectingSides = new Set();
-      let previous = rects[i - 1];
+      let previous = rectsAndTexts.rectList[i - 1];
       if (previous &&
           rect.left - previous.right <= 2 * kOutlineBoxBorderSize) {
         intersectingSides.add("left");
       }
-      let next = rects[i + 1];
+      let next = rectsAndTexts.rectList[i + 1];
       if (next &&
           next.left - rect.right <= 2 * kOutlineBoxBorderSize) {
         intersectingSides.add("right");
       }
       let borderStyles = [...intersectingSides].map(side => [ "border-" + side, 0 ]);
       if (intersectingSides.size) {
         borderStyles.push([ "margin",  `-${kOutlineBoxBorderSize}px 0 0 ${
           intersectingSides.has("left") ? 0 : -kOutlineBoxBorderSize}px !important`]);
@@ -1129,26 +1127,26 @@ FinderHighlighter.prototype = {
 
     this._updateRangeOutline(dict);
 
     let allRects = [];
     if (paintContent || dict.modalHighlightAllMask) {
       this._updateDynamicRangesRects(dict);
 
       let DOMRect = window.DOMRect;
-      for (let [range, rects] of dict.modalHighlightRectsMap) {
+      for (let [range, rectsAndTexts] of dict.modalHighlightRectsMap) {
         if (dict.updateAllRanges)
-          rects = this._updateRangeRects(range);
+          rectsAndTexts = this._updateRangeRects(range);
 
         // If a geometry change was detected, we bail out right away here, because
         // the current set of ranges has been invalidated.
         if (dict.detectedGeometryChange)
           return;
 
-        for (let rect of rects)
+        for (let rect of rectsAndTexts.rectList)
           allRects.push(new DOMRect(rect.x, rect.y, rect.width, rect.height));
       }
       dict.updateAllRanges = false;
     }
 
     dict.modalHighlightAllMask.setCutoutRectsForElement(kMaskId, allRects);
   },
 
--- a/toolkit/modules/tests/browser/browser_FinderHighlighter.js
+++ b/toolkit/modules/tests/browser/browser_FinderHighlighter.js
@@ -453,8 +453,34 @@ add_task(function* testHideOnClear() {
       removeCalls: [1, 2]
     });
     findbar.clear();
     yield promise;
 
     findbar.close(true);
   });
 });
+
+add_task(function* testRectsAndTexts() {
+  let url = "data:text/html;charset=utf-8," +
+    encodeURIComponent("<div style=\"width: 150px; border: 1px solid black\">" +
+    "Here are a lot of words Please use find to highlight some words that wrap" +
+    " across a line boundary and see what happens.</div>");
+  yield BrowserTestUtils.withNewTab(url, function* (browser) {
+    let findbar = gBrowser.getFindBar();
+    yield promiseOpenFindbar(findbar);
+
+    let word = "words please use find to";
+    let expectedResult = {
+      rectCount: 2,
+      insertCalls: [2, 4],
+      removeCalls: [0, 2]
+    };
+    let promise = promiseTestHighlighterOutput(browser, word, expectedResult, (maskNode, outlineNode) => {
+      let boxes = outlineNode.getElementsByTagName("span");
+      Assert.equal(boxes.length, 2, "There should be two outline boxes containing text");
+      Assert.equal(boxes[0].textContent.trim(), "words", "First text should match");
+      Assert.equal(boxes[1].textContent.trim(), "Please use find to", "Second word should match");
+    });
+    yield promiseEnterStringIntoFindField(findbar, word);
+    yield promise;
+  });
+});