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