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

Reply via email to