jenkins-bot has submitted this change and it was merged.

Change subject: Use viewport clipping when lots of search results found
......................................................................


Use viewport clipping when lots of search results found

When find returns more than a hundred fragments, calculate the viewport's
approximate DM range and use that to only render results which are visible.

Introduces getViewportRange to ce.Surface which uses a binary search to find
the viewport range.

Keep track of which subset of the results have been rendered in a ve.Range
so we know how to focus results properly.

Bug: T78234
Change-Id: Id3c2da6f341d6f1f252064a01c1e58ea2d6681a3
---
M src/ce/ve.ce.Surface.js
M src/ui/dialogs/ve.ui.FindAndReplaceDialog.js
2 files changed, 108 insertions(+), 24 deletions(-)

Approvals:
  Catrope: Looks good to me, but someone else must approve
  Jforrester: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/src/ce/ve.ce.Surface.js b/src/ce/ve.ce.Surface.js
index 0046dac..1f9d236 100644
--- a/src/ce/ve.ce.Surface.js
+++ b/src/ce/ve.ce.Surface.js
@@ -3071,6 +3071,50 @@
 };
 
 /**
+ * Get an approximate range covering data visible in the viewport
+ *
+ * It is assumed that vertical offset increases as you progress through the DM.
+ * Items with custom positioning may throw off results given by this method, so
+ * it should only be treated as an approximation.
+ *
+ * @return {ve.Range} Range covering data visible in the viewport
+ */
+ve.ce.Surface.prototype.getViewportRange = function () {
+       var surface = this,
+               documentModel = this.getModel().getDocument(),
+               data = documentModel.data,
+               surfaceRect = this.getSurface().getBoundingClientRect(),
+               padding = 50,
+               top = Math.max( this.surface.toolbarHeight - surfaceRect.top - 
padding, 0 ),
+               bottom = top + this.$window.height() - 
this.surface.toolbarHeight + ( padding * 2 ),
+               documentRange = new ve.Range( 0, 
this.getModel().getDocument().getInternalList().getListNode().getOuterRange().start
 );
+
+       function binarySearch( offset, range, side ) {
+               var mid, rect, start = range.start, end = range.end, lastLength 
= Infinity;
+               while ( range.getLength() < lastLength ) {
+                       lastLength = range.getLength();
+                       mid = data.getNearestContentOffset(
+                               Math.round( ( range.start + range.end ) / 2 )
+                       );
+                       rect = surface.getSelectionBoundingRect( new 
ve.dm.LinearSelection( documentModel, new ve.Range( mid ) ) );
+                       if ( rect[side] > offset ) {
+                               end = mid;
+                               range = new ve.Range( range.start, end );
+                       } else {
+                               start = mid;
+                               range = new ve.Range( start, range.end );
+                       }
+               }
+               return side === 'bottom' ? start : end;
+       }
+
+       return new ve.Range(
+               binarySearch( top, documentRange, 'bottom' ),
+               binarySearch( bottom, documentRange, 'top' )
+       );
+};
+
+/**
  * Show selection
  *
  * @method
diff --git a/src/ui/dialogs/ve.ui.FindAndReplaceDialog.js 
b/src/ui/dialogs/ve.ui.FindAndReplaceDialog.js
index 285f651..110cf3e 100644
--- a/src/ui/dialogs/ve.ui.FindAndReplaceDialog.js
+++ b/src/ui/dialogs/ve.ui.FindAndReplaceDialog.js
@@ -32,13 +32,6 @@
 
 ve.ui.FindAndReplaceDialog.static.title = OO.ui.deferMsg( 
'visualeditor-find-and-replace-title' );
 
-/**
- * Maximum number of results to render
- *
- * @property {number}
- */
-ve.ui.FindAndReplaceDialog.static.maxRenderedResults = 200;
-
 /* Methods */
 
 /**
@@ -51,7 +44,9 @@
        this.$findResults = this.$( '<div>' ).addClass( 
've-ui-findAndReplaceDialog-findResults' );
        this.fragments = [];
        this.results = 0;
-       this.renderedResults = 0;
+       // Range over the list of fragments indicating which ones where 
rendered,
+       // e.g. [1,3] means fragments 1 & 2 were rendered
+       this.renderedFragments = null;
        this.replacing = false;
        this.focusedIndex = 0;
        this.query = null;
@@ -129,6 +124,7 @@
                $replaceRow = this.$( '<div>' ).addClass( 
've-ui-findAndReplaceDialog-row' );
 
        // Events
+       this.onWindowScrollDebounced = ve.debounce( this.onWindowScroll.bind( 
this ), 250 );
        this.updateFragmentsDebounced = ve.debounce( this.updateFragments.bind( 
this ) );
        this.positionResultsDebounced = ve.debounce( this.positionResults.bind( 
this ) );
        this.findText.connect( this, {
@@ -173,8 +169,11 @@
                .first( function () {
                        this.surface = data.surface;
                        this.surface.$selections.append( this.$findResults );
+
+                       // Events
                        this.surface.getModel().connect( this, { 
documentUpdate: this.updateFragmentsDebounced } );
                        this.surface.getView().connect( this, { position: 
this.positionResultsDebounced } );
+                       this.surface.getView().$window.on( 'scroll', 
this.onWindowScrollDebounced );
 
                        var text = data.fragment.getText();
                        if ( text ) {
@@ -202,13 +201,27 @@
        return 
ve.ui.FindAndReplaceDialog.super.prototype.getTeardownProcess.call( this, data )
                .next( function () {
                        var surfaceView = this.surface.getView();
+
+                       // Events
                        this.surface.getModel().disconnect( this );
                        surfaceView.disconnect( this );
+                       this.surface.getView().$window.off( 'scroll', 
this.onWindowScrollDebounced );
+
                        surfaceView.focus();
                        this.$findResults.empty().detach();
                        this.fragment = [];
                        this.surface = null;
                }, this );
+};
+
+/**
+ * Handle window scroll events
+ */
+ve.ui.FindAndReplaceDialog.prototype.onWindowScroll = function () {
+       if ( this.renderedFragments.getLength() < this.results ) {
+               // If viewport clipping is being used, reposition results based 
on the current viewport
+               this.positionResults();
+       }
 };
 
 /**
@@ -268,7 +281,6 @@
                }
        }
        this.results = this.fragments.length;
-       this.renderedResults = Math.min( this.results, 
this.constructor.static.maxRenderedResults );
        this.focusedIndex = Math.min( this.focusedIndex, this.results ? 
this.results - 1 : 0 );
        this.nextButton.setDisabled( !this.results );
        this.previousButton.setDisabled( !this.results );
@@ -284,11 +296,25 @@
                return;
        }
 
-       var i, j, jlen, rects, $result, top;
+       var i, j, jlen, rects, $result, top, selection, viewportRange,
+               start = 0, end = this.results;
+
+       // When there are a large number of results, calculate the viewport 
range for clipping
+       if ( this.results > 100 ) {
+               viewportRange = this.surface.getView().getViewportRange();
+       }
 
        this.$findResults.empty();
-       // Limit number of results rendered to stop the browser exploding
-       for ( i = 0; i < this.renderedResults; i++ ) {
+       for ( i = 0; i < this.results; i++ ) {
+               selection = this.fragments[i].getSelection();
+               if ( viewportRange && selection.getRange().start < 
viewportRange.start ) {
+                       start = i + 1;
+                       continue;
+               }
+               if ( viewportRange && selection.getRange().end > 
viewportRange.end ) {
+                       end = i;
+                       break;
+               }
                rects = this.surface.getView().getSelectionRects( 
this.fragments[i].getSelection() );
                $result = this.$( '<div>' ).addClass( 
've-ui-findAndReplaceDialog-findResult' );
                top = Infinity;
@@ -304,6 +330,7 @@
                $result.data( 'top', top );
                this.$findResults.append( $result );
        }
+       this.renderedFragments = new ve.Range( start, end );
        this.highlightFocused();
 };
 
@@ -313,14 +340,9 @@
  * @param {boolean} scrollIntoView Scroll the marker into view
  */
 ve.ui.FindAndReplaceDialog.prototype.highlightFocused = function ( 
scrollIntoView ) {
-       var windowScrollTop, windowScrollHeight, offset,
-               surfaceView = this.surface.getView(),
-               $result = this.$findResults.children().eq( this.focusedIndex );
-
-       this.$findResults
-               .find( '.ve-ui-findAndReplaceDialog-findResult-focused' )
-               .removeClass( 've-ui-findAndReplaceDialog-findResult-focused' );
-       $result.addClass( 've-ui-findAndReplaceDialog-findResult-focused' );
+       var $result, rect, top,
+               offset, windowScrollTop, windowScrollHeight,
+               surfaceView = this.surface.getView();
 
        if ( this.results ) {
                this.focusedIndexLabel.setLabel(
@@ -328,12 +350,30 @@
                );
        } else {
                this.focusedIndexLabel.setLabel( '' );
+               return;
+       }
+
+       this.$findResults
+               .find( '.ve-ui-findAndReplaceDialog-findResult-focused' )
+               .removeClass( 've-ui-findAndReplaceDialog-findResult-focused' );
+
+       if ( this.renderedFragments.containsOffset( this.focusedIndex ) ) {
+               $result = this.$findResults.children().eq( this.focusedIndex - 
this.renderedFragments.start )
+                       .addClass( 
've-ui-findAndReplaceDialog-findResult-focused' );
+
+               top = $result.data( 'top' );
+       } else {
+               // Focused result hasn't been rendered yet so find its offset 
manually
+               rect = surfaceView.getSelectionBoundingRect( 
this.fragments[this.focusedIndex].getSelection() );
+               top = rect.top;
        }
 
        if ( scrollIntoView ) {
-               offset = $result.data( 'top' ) + 
surfaceView.$element.offset().top;
+               surfaceView = this.surface.getView();
+               offset = top + surfaceView.$element.offset().top;
                windowScrollTop = surfaceView.$window.scrollTop() + 
this.surface.toolbarHeight;
                windowScrollHeight = surfaceView.$window.height() - 
this.surface.toolbarHeight;
+
                if ( offset < windowScrollTop || offset > windowScrollTop + 
windowScrollHeight ) {
                        surfaceView.$( 'body, html' ).animate( { scrollTop: 
offset - ( windowScrollHeight / 2  ) }, 'fast' );
                }
@@ -344,7 +384,7 @@
  * Handle click events on the next button
  */
 ve.ui.FindAndReplaceDialog.prototype.onNextButtonClick = function () {
-       this.focusedIndex = ( this.focusedIndex + 1 ) % this.renderedResults;
+       this.focusedIndex = ( this.focusedIndex + 1 ) % this.results;
        this.highlightFocused( true );
 };
 
@@ -352,7 +392,7 @@
  * Handle click events on the previous button
  */
 ve.ui.FindAndReplaceDialog.prototype.onPreviousButtonClick = function () {
-       this.focusedIndex = ( this.focusedIndex + this.renderedResults - 1 ) % 
this.renderedResults;
+       this.focusedIndex = ( this.focusedIndex + this.results - 1 ) % 
this.results;
        this.highlightFocused( true );
 };
 
@@ -382,7 +422,7 @@
                this.focusedIndex++;
        }
        // We may have iterated off the end
-       this.focusedIndex = this.focusedIndex % this.renderedResults;
+       this.focusedIndex = this.focusedIndex % this.results;
 };
 
 /**

-- 
To view, visit https://gerrit.wikimedia.org/r/180635
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Id3c2da6f341d6f1f252064a01c1e58ea2d6681a3
Gerrit-PatchSet: 4
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Esanders <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: Esanders <[email protected]>
Gerrit-Reviewer: Jforrester <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to