Esanders has uploaded a new change for review.
https://gerrit.wikimedia.org/r/174491
Change subject: WIP Find and replace
......................................................................
WIP Find and replace
To-do:
* UI
* Show/hide trigger
Change-Id: I8dc406ffe8455dedf35eef02a1909de2d79adeb0
---
M .docs/eg-iframe.html
M build/modules.json
M demos/ve/desktop.html
M demos/ve/mobile.html
M src/ce/styles/nodes/ve.ce.FocusableNode.css
M src/ce/ve.ce.FocusableNode.js
M src/ce/ve.ce.Surface.js
M src/dm/lineardata/ve.dm.ElementLinearData.js
M src/dm/ve.dm.Document.js
M src/dm/ve.dm.SurfaceFragment.js
M src/init/ve.init.Target.js
A src/ui/styles/ve.ui.Find.css
A src/ui/ve.ui.Find.js
M src/ui/ve.ui.Toolbar.js
M tests/index.html
15 files changed, 349 insertions(+), 11 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/VisualEditor/VisualEditor
refs/changes/91/174491/1
diff --git a/.docs/eg-iframe.html b/.docs/eg-iframe.html
index 819e94f..5d21f72 100644
--- a/.docs/eg-iframe.html
+++ b/.docs/eg-iframe.html
@@ -41,6 +41,7 @@
<link rel=stylesheet href="../src/ui/styles/ve.ui.Surface.css">
<link rel=stylesheet
href="../src/ui/styles/widgets/ve.ui.SurfaceWidget.css">
<link rel=stylesheet
href="../src/ui/styles/ve.ui.TableContext.css">
+ <link rel=stylesheet href="../src/ui/styles/ve.ui.Find.css">
<link rel=stylesheet href="../src/ui/styles/ve.ui.Toolbar.css">
<link rel=stylesheet href="../src/ui/styles/ve.ui.Icons.css">
@@ -276,6 +277,7 @@
<script src="../src/ui/ve.ui.Surface.js"></script>
<script src="../src/ui/ve.ui.Context.js"></script>
<script src="../src/ui/ve.ui.TableContext.js"></script>
+ <script src="../src/ui/ve.ui.Find.js"></script>
<script src="../src/ui/ve.ui.Tool.js"></script>
<script src="../src/ui/ve.ui.Toolbar.js"></script>
<script src="../src/ui/ve.ui.TargetToolbar.js"></script>
diff --git a/build/modules.json b/build/modules.json
index 8e77260..4fbec32 100644
--- a/build/modules.json
+++ b/build/modules.json
@@ -301,6 +301,7 @@
"src/ui/ve.ui.Surface.js",
"src/ui/ve.ui.Context.js",
"src/ui/ve.ui.TableContext.js",
+ "src/ui/ve.ui.Find.js",
"src/ui/ve.ui.Tool.js",
"src/ui/ve.ui.Toolbar.js",
"src/ui/ve.ui.TargetToolbar.js",
@@ -392,6 +393,7 @@
"src/ui/styles/ve.ui.Surface.css",
"src/ui/styles/widgets/ve.ui.SurfaceWidget.css",
"src/ui/styles/ve.ui.TableContext.css",
+ "src/ui/styles/ve.ui.Find.css",
"src/ui/styles/ve.ui.Toolbar.css",
"src/ui/styles/ve.ui.Icons.css"
],
diff --git a/demos/ve/desktop.html b/demos/ve/desktop.html
index 8641a09..14eb08c 100644
--- a/demos/ve/desktop.html
+++ b/demos/ve/desktop.html
@@ -50,6 +50,7 @@
<link rel=stylesheet
href="../../src/ui/styles/ve.ui.Surface.css">
<link rel=stylesheet
href="../../src/ui/styles/widgets/ve.ui.SurfaceWidget.css">
<link rel=stylesheet
href="../../src/ui/styles/ve.ui.TableContext.css">
+ <link rel=stylesheet href="../../src/ui/styles/ve.ui.Find.css">
<link rel=stylesheet
href="../../src/ui/styles/ve.ui.Toolbar.css">
<link rel=stylesheet href="../../src/ui/styles/ve.ui.Icons.css">
@@ -288,6 +289,7 @@
<script src="../../src/ui/ve.ui.Surface.js"></script>
<script src="../../src/ui/ve.ui.Context.js"></script>
<script src="../../src/ui/ve.ui.TableContext.js"></script>
+ <script src="../../src/ui/ve.ui.Find.js"></script>
<script src="../../src/ui/ve.ui.Tool.js"></script>
<script src="../../src/ui/ve.ui.Toolbar.js"></script>
<script src="../../src/ui/ve.ui.TargetToolbar.js"></script>
diff --git a/demos/ve/mobile.html b/demos/ve/mobile.html
index 3fc0487..5f2a546 100644
--- a/demos/ve/mobile.html
+++ b/demos/ve/mobile.html
@@ -50,6 +50,7 @@
<link rel=stylesheet
href="../../src/ui/styles/ve.ui.Surface.css">
<link rel=stylesheet
href="../../src/ui/styles/widgets/ve.ui.SurfaceWidget.css">
<link rel=stylesheet
href="../../src/ui/styles/ve.ui.TableContext.css">
+ <link rel=stylesheet href="../../src/ui/styles/ve.ui.Find.css">
<link rel=stylesheet
href="../../src/ui/styles/ve.ui.Toolbar.css">
<link rel=stylesheet href="../../src/ui/styles/ve.ui.Icons.css">
@@ -289,6 +290,7 @@
<script src="../../src/ui/ve.ui.Surface.js"></script>
<script src="../../src/ui/ve.ui.Context.js"></script>
<script src="../../src/ui/ve.ui.TableContext.js"></script>
+ <script src="../../src/ui/ve.ui.Find.js"></script>
<script src="../../src/ui/ve.ui.Tool.js"></script>
<script src="../../src/ui/ve.ui.Toolbar.js"></script>
<script src="../../src/ui/ve.ui.TargetToolbar.js"></script>
diff --git a/src/ce/styles/nodes/ve.ce.FocusableNode.css
b/src/ce/styles/nodes/ve.ce.FocusableNode.css
index 946cb9b..f3492d5 100644
--- a/src/ce/styles/nodes/ve.ce.FocusableNode.css
+++ b/src/ce/styles/nodes/ve.ce.FocusableNode.css
@@ -4,6 +4,11 @@
* @copyright 2011-2014 VisualEditor Team and others; see
http://ve.mit-license.org
*/
+.ve-ce-focusableNode-highlights {
+ position: absolute;
+ pointer-events: none;
+}
+
.ve-ce-surface-highlights-focused .ve-ce-focusableNode-highlights {
opacity: 0.5;
}
@@ -27,6 +32,7 @@
background: #6da9f7;
box-shadow: inset 0 0 0 1px #4C76ac;
position: absolute;
+ pointer-events: auto;
}
.ve-ce-focusableNode-highlight-relocatable-marker {
diff --git a/src/ce/ve.ce.FocusableNode.js b/src/ce/ve.ce.FocusableNode.js
index 59b7f0e..94007e1 100644
--- a/src/ce/ve.ce.FocusableNode.js
+++ b/src/ce/ve.ce.FocusableNode.js
@@ -508,6 +508,11 @@
} )
);
}
+
+ this.$highlights.css( {
+ width: this.boundingRect.left + this.boundingRect.width,
+ height: this.boundingRect.top + this.boundingRect.height
+ } );
};
/**
diff --git a/src/ce/ve.ce.Surface.js b/src/ce/ve.ce.Surface.js
index a6d5490..884f36f 100644
--- a/src/ce/ve.ce.Surface.js
+++ b/src/ce/ve.ce.Surface.js
@@ -57,6 +57,7 @@
this.$highlights = this.$( '<div>' ).append(
this.$highlightsFocused, this.$highlightsBlurred
);
+ this.$findResults = this.$( '<div>' );
this.$dropMarker = this.$( '<div>' ).addClass(
've-ce-focusableNode-dropMarker' );
this.$lastDropTarget = null;
this.lastDropPosition = null;
@@ -156,6 +157,7 @@
this.$highlights.addClass( 've-ce-surface-highlights' );
this.$highlightsFocused.addClass( 've-ce-surface-highlights-focused' );
this.$highlightsBlurred.addClass( 've-ce-surface-highlights-blurred' );
+ this.$findResults.addClass( 've-ce-surface-findResults' );
this.$pasteTarget.addClass( 've-ce-surface-paste' )
.attr( 'tabIndex', -1 )
.prop( 'contentEditable', 'true' );
@@ -163,6 +165,7 @@
// Add elements to the DOM
this.$element.append( this.$documentNode, this.$pasteTarget );
this.surface.$blockers.append( this.$highlights );
+ this.surface.$controls.append( this.$findResults );
};
/* Inheritance */
@@ -1942,13 +1945,17 @@
* Handle documentUpdate events on the surface model.
*/
ve.ce.Surface.prototype.onModelDocumentUpdate = function () {
+ var surface = this;
if ( this.contentBranchNodeChanged ) {
// Update the selection state from model
this.onModelSelect( this.surface.getModel().selection );
}
// Update the state of the SurfaceObserver
this.surfaceObserver.pollOnceNoEmit();
- this.emit( 'position' );
+ // Wait for other documentUpdate listeners to run before emitting
+ setTimeout( function () {
+ surface.emit( 'position' );
+ } );
};
/**
diff --git a/src/dm/lineardata/ve.dm.ElementLinearData.js
b/src/dm/lineardata/ve.dm.ElementLinearData.js
index 1f71b5e..16db9a1 100644
--- a/src/dm/lineardata/ve.dm.ElementLinearData.js
+++ b/src/dm/lineardata/ve.dm.ElementLinearData.js
@@ -503,6 +503,27 @@
};
/**
+ * Get the data as plain text
+ *
+ * @param {boolean} maintainIndices Maintain data offset to string index
alignment by replacing elements with line breaks
+ * @param {ve.Range} range Range to get the data for. The whole data set if
not specified.
+ * @return {string} Data as plain text
+ */
+ve.dm.ElementLinearData.prototype.getText = function ( maintainIndices, range
) {
+ var i, text = '';
+ range = range || new ve.Range( 0, this.getLength() );
+
+ for ( i = range.start; i < range.end; i++ ) {
+ if ( !this.isElementData( i ) ) {
+ text += this.getCharacterData( i );
+ } else if ( maintainIndices ) {
+ text += '\n';
+ }
+ }
+ return text;
+};
+
+/**
* Get an offset at a distance to an offset that passes a validity test.
*
* - If {offset} is not already valid, one step will be used to move it to a
valid one.
diff --git a/src/dm/ve.dm.Document.js b/src/dm/ve.dm.Document.js
index 7a643c3..49408d7 100644
--- a/src/dm/ve.dm.Document.js
+++ b/src/dm/ve.dm.Document.js
@@ -1229,6 +1229,29 @@
};
/**
+ * Find a text string within the document
+ *
+ * @param {string} query Text to find
+ * @param {boolean} caseSensitive Case sensitive search
+ * @return {number[]} List of offsets where the string was found
+ */
+ve.dm.Document.prototype.findText = function ( query, caseSensitive ) {
+ var offset = -1,
+ offsets = [],
+ text = this.data.getText( true );
+
+ if ( !caseSensitive ) {
+ text = text.toLowerCase();
+ query = query.toLowerCase();
+ }
+
+ while ( ( offset = text.indexOf( query, offset + 1 ) ) !== -1 ) {
+ offsets.push( offset ) ;
+ }
+ return offsets;
+};
+
+/**
* Get the length of the complete history stack. This is also the current
pointer.
* @returns {number} Length of the complete history stack
*/
diff --git a/src/dm/ve.dm.SurfaceFragment.js b/src/dm/ve.dm.SurfaceFragment.js
index 673d70b..f06cb16 100644
--- a/src/dm/ve.dm.SurfaceFragment.js
+++ b/src/dm/ve.dm.SurfaceFragment.js
@@ -429,16 +429,7 @@
if ( !( this.selection instanceof ve.dm.LinearSelection ) ) {
return '';
}
- var i, length,
- text = '',
- data = this.document.getData( this.getSelection( true
).getRange() );
- for ( i = 0, length = data.length; i < length; i++ ) {
- if ( data[i].type === undefined ) {
- // Annotated characters have a string at index 0, plain
characters are 1-char strings
- text += typeof data[i] === 'string' ? data[i] :
data[i][0];
- }
- }
- return text;
+ return this.document.data.getText( false, this.getSelection( true
).getRange() );
};
/**
diff --git a/src/init/ve.init.Target.js b/src/init/ve.init.Target.js
index ba5550c..dd4168a 100644
--- a/src/init/ve.init.Target.js
+++ b/src/init/ve.init.Target.js
@@ -245,6 +245,8 @@
this.toolbar.setup( this.constructor.static.toolbarGroups );
this.surface.addCommands( this.constructor.static.surfaceCommands );
this.toolbar.$element.insertBefore( this.surface.$element );
+ this.find = new ve.ui.Find( this.surface, { $: this.surface.$ } );
+ this.toolbar.$bar.append( this.find.$element );
};
/**
diff --git a/src/ui/styles/ve.ui.Find.css b/src/ui/styles/ve.ui.Find.css
new file mode 100644
index 0000000..d469868
--- /dev/null
+++ b/src/ui/styles/ve.ui.Find.css
@@ -0,0 +1,49 @@
+/*!
+ * VisualEditor UserInterface Fin dstyles.
+ *
+ * @copyright 2011-2014 VisualEditor Team and others; see
http://ve.mit-license.org
+ */
+
+.ve-ui-find {
+ position: absolute;
+ right: 0;
+ padding: 1em;
+ font-size: 0.8em;
+ border: 1px solid #ddd;
+ border-width: 0 0 1px 1px;
+ margin-top: 1px;
+ background: #fff;
+ border-bottom-left-radius: 0.25em;
+ z-index: 2;
+}
+
+.ve-ui-find .oo-ui-textInputWidget {
+ width: 10em;
+}
+
+.ve-ce-surface-findResults {
+ position: absolute;
+ top: 0;
+ left: 0;
+ pointer-events: none;
+}
+
+.ve-ce-surface-findResult {
+ opacity: 0.2;
+}
+
+.ve-ce-surface-findResult div {
+ background: #28bb0b;
+ position: absolute;
+ margin-top: -0.15em;
+ padding: 0.15em 0;
+ border-radius: 0.15em;
+}
+
+.ve-ce-surface-findResult-focused {
+ opacity: 0.4;
+}
+
+.ve-ce-surface-findResult-focused div {
+ background: #1f850b;
+}
diff --git a/src/ui/ve.ui.Find.js b/src/ui/ve.ui.Find.js
new file mode 100644
index 0000000..5d385b9
--- /dev/null
+++ b/src/ui/ve.ui.Find.js
@@ -0,0 +1,224 @@
+/*!
+ * VisualEditor UserInterface Find class.
+ *
+ * @copyright 2011-2014 VisualEditor Team and others; see
http://ve.mit-license.org
+ */
+
+/**
+ * UserInterface context.
+ *
+ * @class
+ * @abstract
+ * @extends OO.ui.Element
+ *
+ * @constructor
+ * @param {ve.ui.Surface} surface
+ * @param {Object} [config] Configuration options
+ */
+ve.ui.Find = function VeUiFind( surface, config ) {
+ // Parent constructor
+ OO.ui.Element.call( this, config );
+
+ // Properties
+ this.surface = surface;
+ this.fragments = [];
+ this.replacing = false;
+ this.focusedIndex = 0;
+// this.visible = false;
+ this.findText = new OO.ui.TextInputWidget( {
+ $: this.$,
+ placeholder: 'Find'
+ } );
+ this.matchCaseToggle = new OO.ui.ToggleSwitchWidget( { $: this.$ } );
+ this.previousButton = new OO.ui.ButtonWidget( {
+ $: this.$,
+ icon: 'previous'
+ } );
+ this.nextButton = new OO.ui.ButtonWidget( {
+ $: this.$,
+ icon: 'next'
+ } );
+ this.replaceText = new OO.ui.TextInputWidget( {
+ $: this.$,
+ placeholder: 'Replace'
+ } );
+ this.replaceButton = new OO.ui.ButtonWidget( {
+ $: this.$,
+ label: 'Replace'
+ } );
+ this.replaceAllButton = new OO.ui.ButtonWidget( {
+ $: this.$,
+ label: 'Replace all'
+ } );
+
+ var checkboxField = new OO.ui.FieldLayout(
+ this.matchCaseToggle,
+ {
+ align: 'inline',
+ label: 'Match case'
+ }
+ ),
+ navigateGroup = new OO.ui.ButtonGroupWidget( {
+ $: this.$,
+ items: [
+ this.previousButton,
+ this.nextButton
+ ]
+ } );
+
+ // Events
+ this.updateFragmentsDebounced = ve.debounce( this.updateFragments.bind(
this ) );
+ this.positionResultsDebounced = ve.debounce( this.positionResults.bind(
this ) );
+ this.findText.connect( this, { change: 'onFindChange' } );
+ this.matchCaseToggle.connect( this, { change: 'onFindChange' } );
+ this.nextButton.connect( this, { click: 'onNextButtonClick' } );
+ this.previousButton.connect( this, { click: 'onPreviousButtonClick' } );
+ this.replaceButton.connect( this, { click: 'onReplaceButtonClick' } );
+ this.replaceAllButton.connect( this, { click: 'onReplaceAllButtonClick'
} );
+ this.surface.getModel().connect( this, { documentUpdate:
this.updateFragmentsDebounced } );
+ this.surface.getView().connect( this, { position:
this.positionResultsDebounced } );
+
+ // Initialization
+ this.$element
+ .addClass( 've-ui-find' )
+ .append(
+ this.findText.$element,
+ checkboxField.$element,
+ navigateGroup.$element,
+ this.replaceText.$element,
+ this.replaceButton.$element,
+ this.replaceAllButton.$element
+ );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ui.Find, OO.ui.Element );
+
+/* Methods */
+
+/**
+ * Handle change events to the find inputs (text or match case)
+ */
+ve.ui.Find.prototype.onFindChange = function () {
+ this.updateFragments();
+ this.positionResults();
+};
+
+/**
+ * Update search result fragments
+ */
+ve.ui.Find.prototype.updateFragments = function () {
+ var i, l, findLen,
+ surfaceModel = this.surface.getModel(),
+ documentModel = surfaceModel.getDocument(),
+ offsets = [],
+ matchCase = this.matchCaseToggle.getValue(),
+ find = this.findText.getValue();
+
+ this.fragments = [];
+ if ( find ) {
+ offsets = documentModel.findText( find, matchCase );
+ findLen = find.length;
+ for ( i = 0, l = offsets.length; i < l; i++ ) {
+ this.fragments.push( surfaceModel.getLinearFragment(
new ve.Range( offsets[i], offsets[i] + findLen ), true ) );
+ }
+ }
+ this.focusedIndex = Math.min( this.focusedIndex, this.fragments.length
);
+};
+
+/**
+ * Position results markers
+ */
+ve.ui.Find.prototype.positionResults = function () {
+ if ( this.replacing ) {
+ return;
+ }
+
+ var i, l, j, rects, $result, top,
+ surfaceView = this.surface.getView();
+
+ surfaceView.$findResults.empty();
+ for ( i = 0, l = this.fragments.length; i < l; i++ ) {
+ rects = this.surface.getView().getSelectionRects(
this.fragments[i].getSelection() );
+ $result = $( '<div>' ).addClass( 've-ce-surface-findResult' );
+ top = Infinity;
+ for ( j in rects ) {
+ top = Math.min( top, rects[j].top );
+ $result.append( $( '<div>' ).css( rects[j] ) );
+ }
+ $result.data( 'top', top );
+ surfaceView.$findResults.append( $result );
+ }
+ this.highlightFocused();
+};
+
+/**
+ * Highlight the focused result marker
+ */
+ve.ui.Find.prototype.highlightFocused = function () {
+ var windowScrollTop, windowScrollHeight, offset,
+ surfaceView = this.surface.getView(),
+ $result = surfaceView.$findResults.children().eq(
this.focusedIndex );
+
+ surfaceView.$findResults.find( '.ve-ce-surface-findResult-focused'
).removeClass( 've-ce-surface-findResult-focused' );
+ $result.addClass( 've-ce-surface-findResult-focused' );
+
+ offset = $result.data( 'top' ) + surfaceView.$element.offset().top;
+ windowScrollTop = surfaceView.$window.scrollTop();
+ windowScrollHeight = surfaceView.$window.height();
+ if ( offset < windowScrollTop || offset > windowScrollTop +
windowScrollHeight ) {
+ this.$( 'body, html' ).animate( { scrollTop: offset - (
windowScrollHeight / 2 ) }, 'fast' );
+ }
+};
+
+/**
+ * Handle click events on the next button
+ */
+ve.ui.Find.prototype.onNextButtonClick = function () {
+ this.focusedIndex = ( this.focusedIndex + 1 ) % this.fragments.length;
+ this.highlightFocused();
+};
+
+/**
+ * Handle click events on the previous button
+ */
+ve.ui.Find.prototype.onPreviousButtonClick = function () {
+ this.focusedIndex = ( this.focusedIndex + this.fragments.length - 1 ) %
this.fragments.length;
+ this.highlightFocused();
+};
+
+/**
+ * Handle click events on the replace button
+ */
+ve.ui.Find.prototype.onReplaceButtonClick = function () {
+ var end, replace = this.replaceText.getValue();
+
+ if ( !this.fragments.length ) {
+ return;
+ }
+
+ this.fragments[this.focusedIndex].insertContent( replace, true );
+
+ // Find the next fragment after this one ends. Ensures that if we
replace
+ // 'foo' with 'foofoo' we don't select the just-inserted text.
+ end = this.fragments[this.focusedIndex].getSelection().getRange().end;
+ this.updateFragments();
+ this.focusedIndex = 0;
+ while ( this.fragments[this.focusedIndex] &&
this.fragments[this.focusedIndex].getSelection().getRange().end <= end ) {
+ this.focusedIndex++;
+ }
+ this.focusedIndex = this.focusedIndex % this.fragments.length;
+};
+
+/**
+ * Handle click events on the previous all button
+ */
+ve.ui.Find.prototype.onReplaceAllButtonClick = function () {
+ var i, l,
+ replace = this.replaceText.getValue();
+
+ for ( i = 0, l = this.fragments.length; i < l; i++ ) {
+ this.fragments[i].insertContent( replace, true );
+ }
+};
diff --git a/src/ui/ve.ui.Toolbar.js b/src/ui/ve.ui.Toolbar.js
index fb7df94..020d639 100644
--- a/src/ui/ve.ui.Toolbar.js
+++ b/src/ui/ve.ui.Toolbar.js
@@ -42,6 +42,7 @@
};
// Default directions
this.contextDirection = { inline: 'ltr', block: 'ltr' };
+
// The following classes can be used here:
// ve-ui-dir-inline-ltr
// ve-ui-dir-inline-rtl
diff --git a/tests/index.html b/tests/index.html
index f35fb53..293ae0b 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -239,6 +239,7 @@
<script src="../src/ui/ve.ui.Surface.js"></script>
<script src="../src/ui/ve.ui.Context.js"></script>
<script src="../src/ui/ve.ui.TableContext.js"></script>
+ <script src="../src/ui/ve.ui.Find.js"></script>
<script src="../src/ui/ve.ui.Tool.js"></script>
<script src="../src/ui/ve.ui.Toolbar.js"></script>
<script src="../src/ui/ve.ui.TargetToolbar.js"></script>
--
To view, visit https://gerrit.wikimedia.org/r/174491
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I8dc406ffe8455dedf35eef02a1909de2d79adeb0
Gerrit-PatchSet: 1
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Esanders <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits