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

Change subject: Find and replace
......................................................................


Find and replace

Bug: T50368
Change-Id: I8dc406ffe8455dedf35eef02a1909de2d79adeb0
---
M .docs/eg-iframe.html
M build/modules.json
M demos/ve/desktop.html
M demos/ve/mobile.html
M i18n/en.json
M i18n/qqq.json
M src/ce/ve.ce.Surface.js
M src/dm/ve.dm.Document.js
M src/init/ve.init.Target.js
M src/ui/actions/ve.ui.WindowAction.js
M src/ui/dialogs/ve.ui.CommandHelpDialog.js
A src/ui/dialogs/ve.ui.FindAndReplaceDialog.js
A src/ui/dialogs/ve.ui.ToolbarDialog.js
M src/ui/styles/dialogs/ve.ui.CommandHelpDialog.css
A src/ui/styles/dialogs/ve.ui.FindAndReplaceDialog.css
A src/ui/styles/dialogs/ve.ui.ToolbarDialog.css
M src/ui/ve.ui.CommandRegistry.js
M src/ui/ve.ui.Surface.js
M src/ui/ve.ui.Toolbar.js
M src/ui/ve.ui.TriggerRegistry.js
A src/ui/windowmanagers/ve.ui.ToolbarDialogWindowManager.js
M tests/index.html
22 files changed, 648 insertions(+), 8 deletions(-)

Approvals:
  Jforrester: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/.docs/eg-iframe.html b/.docs/eg-iframe.html
index c6170a1..44a82ef 100644
--- a/.docs/eg-iframe.html
+++ b/.docs/eg-iframe.html
@@ -24,7 +24,9 @@
                <link rel=stylesheet 
href="../src/ce/styles/nodes/ve.ce.TableNode.css">
                <link rel=stylesheet href="../src/ce/styles/ve.ce.css">
                <link rel=stylesheet href="../src/ce/styles/ve.ce.Surface.css">
+               <link rel=stylesheet 
href="../src/ui/styles/dialogs/ve.ui.ToolbarDialog.css">
                <link rel=stylesheet 
href="../src/ui/styles/dialogs/ve.ui.CommandHelpDialog.css">
+               <link rel=stylesheet 
href="../src/ui/styles/dialogs/ve.ui.FindAndReplaceDialog.css">
                <link rel=stylesheet 
href="../src/ui/styles/dialogs/ve.ui.ProgressDialog.css">
                <link rel=stylesheet 
href="../src/ui/styles/tools/ve.ui.FormatTool.css">
                <link rel=stylesheet 
href="../src/ui/styles/widgets/ve.ui.LanguageInputWidget.css">
@@ -309,13 +311,16 @@
                <script 
src="../src/ui/commands/ve.ui.IndentationCommand.js"></script>
                <script 
src="../src/ui/commands/ve.ui.MergeCellsCommand.js"></script>
                <script 
src="../src/ui/commands/ve.ui.TableCaptionCommand.js"></script>
-               <script 
src="../src/ui/dialogs/ve.ui.CommandHelpDialog.js"></script>
                <script 
src="../src/ui/dialogs/ve.ui.FragmentDialog.js"></script>
                <script src="../src/ui/dialogs/ve.ui.NodeDialog.js"></script>
+               <script src="../src/ui/dialogs/ve.ui.ToolbarDialog.js"></script>
+               <script 
src="../src/ui/dialogs/ve.ui.CommandHelpDialog.js"></script>
+               <script 
src="../src/ui/dialogs/ve.ui.FindAndReplaceDialog.js"></script>
                <script 
src="../src/ui/dialogs/ve.ui.ProgressDialog.js"></script>
                <script 
src="../src/ui/filedrophandlers/ve.ui.DSVFileDropHandler.js"></script>
                <script 
src="../src/ui/filedrophandlers/ve.ui.PlainTextFileDropHandler.js"></script>
                <script 
src="../src/ui/filedrophandlers/ve.ui.HTMLFileDropHandler.js"></script>
+               <script 
src="../src/ui/windowmanagers/ve.ui.ToolbarDialogWindowManager.js"></script>
                <script 
src="../src/ui/widgets/ve.ui.LanguageSearchWidget.js"></script>
                <script 
src="../src/ui/widgets/ve.ui.LanguageResultWidget.js"></script>
                <script 
src="../src/ui/dialogs/ve.ui.LanguageSearchDialog.js"></script>
diff --git a/build/modules.json b/build/modules.json
index 2edf6ca..feb75bb 100644
--- a/build/modules.json
+++ b/build/modules.json
@@ -341,13 +341,16 @@
                        "src/ui/commands/ve.ui.IndentationCommand.js",
                        "src/ui/commands/ve.ui.MergeCellsCommand.js",
                        "src/ui/commands/ve.ui.TableCaptionCommand.js",
-                       "src/ui/dialogs/ve.ui.CommandHelpDialog.js",
                        "src/ui/dialogs/ve.ui.FragmentDialog.js",
                        "src/ui/dialogs/ve.ui.NodeDialog.js",
+                       "src/ui/dialogs/ve.ui.ToolbarDialog.js",
+                       "src/ui/dialogs/ve.ui.CommandHelpDialog.js",
+                       "src/ui/dialogs/ve.ui.FindAndReplaceDialog.js",
                        "src/ui/dialogs/ve.ui.ProgressDialog.js",
                        "src/ui/filedrophandlers/ve.ui.DSVFileDropHandler.js",
                        
"src/ui/filedrophandlers/ve.ui.PlainTextFileDropHandler.js",
                        "src/ui/filedrophandlers/ve.ui.HTMLFileDropHandler.js",
+                       
"src/ui/windowmanagers/ve.ui.ToolbarDialogWindowManager.js",
                        "src/ui/widgets/ve.ui.LanguageSearchWidget.js",
                        "src/ui/widgets/ve.ui.LanguageResultWidget.js",
                        "src/ui/dialogs/ve.ui.LanguageSearchDialog.js",
@@ -390,7 +393,9 @@
                        "src/ce/styles/nodes/ve.ce.TableNode.css",
                        "src/ce/styles/ve.ce.css",
                        "src/ce/styles/ve.ce.Surface.css",
+                       "src/ui/styles/dialogs/ve.ui.ToolbarDialog.css",
                        "src/ui/styles/dialogs/ve.ui.CommandHelpDialog.css",
+                       "src/ui/styles/dialogs/ve.ui.FindAndReplaceDialog.css",
                        "src/ui/styles/dialogs/ve.ui.ProgressDialog.css",
                        "src/ui/styles/tools/ve.ui.FormatTool.css",
                        "src/ui/styles/widgets/ve.ui.LanguageInputWidget.css",
diff --git a/demos/ve/desktop.html b/demos/ve/desktop.html
index 31e8a72..774ea58 100644
--- a/demos/ve/desktop.html
+++ b/demos/ve/desktop.html
@@ -33,7 +33,9 @@
                <link rel=stylesheet 
href="../../src/ce/styles/nodes/ve.ce.TableNode.css">
                <link rel=stylesheet href="../../src/ce/styles/ve.ce.css">
                <link rel=stylesheet 
href="../../src/ce/styles/ve.ce.Surface.css">
+               <link rel=stylesheet 
href="../../src/ui/styles/dialogs/ve.ui.ToolbarDialog.css">
                <link rel=stylesheet 
href="../../src/ui/styles/dialogs/ve.ui.CommandHelpDialog.css">
+               <link rel=stylesheet 
href="../../src/ui/styles/dialogs/ve.ui.FindAndReplaceDialog.css">
                <link rel=stylesheet 
href="../../src/ui/styles/dialogs/ve.ui.ProgressDialog.css">
                <link rel=stylesheet 
href="../../src/ui/styles/tools/ve.ui.FormatTool.css">
                <link rel=stylesheet 
href="../../src/ui/styles/widgets/ve.ui.LanguageInputWidget.css">
@@ -322,13 +324,16 @@
                <script 
src="../../src/ui/commands/ve.ui.IndentationCommand.js"></script>
                <script 
src="../../src/ui/commands/ve.ui.MergeCellsCommand.js"></script>
                <script 
src="../../src/ui/commands/ve.ui.TableCaptionCommand.js"></script>
-               <script 
src="../../src/ui/dialogs/ve.ui.CommandHelpDialog.js"></script>
                <script 
src="../../src/ui/dialogs/ve.ui.FragmentDialog.js"></script>
                <script src="../../src/ui/dialogs/ve.ui.NodeDialog.js"></script>
+               <script 
src="../../src/ui/dialogs/ve.ui.ToolbarDialog.js"></script>
+               <script 
src="../../src/ui/dialogs/ve.ui.CommandHelpDialog.js"></script>
+               <script 
src="../../src/ui/dialogs/ve.ui.FindAndReplaceDialog.js"></script>
                <script 
src="../../src/ui/dialogs/ve.ui.ProgressDialog.js"></script>
                <script 
src="../../src/ui/filedrophandlers/ve.ui.DSVFileDropHandler.js"></script>
                <script 
src="../../src/ui/filedrophandlers/ve.ui.PlainTextFileDropHandler.js"></script>
                <script 
src="../../src/ui/filedrophandlers/ve.ui.HTMLFileDropHandler.js"></script>
+               <script 
src="../../src/ui/windowmanagers/ve.ui.ToolbarDialogWindowManager.js"></script>
                <script 
src="../../src/ui/widgets/ve.ui.LanguageSearchWidget.js"></script>
                <script 
src="../../src/ui/widgets/ve.ui.LanguageResultWidget.js"></script>
                <script 
src="../../src/ui/dialogs/ve.ui.LanguageSearchDialog.js"></script>
diff --git a/demos/ve/mobile.html b/demos/ve/mobile.html
index 501b96b..d5bd99d 100644
--- a/demos/ve/mobile.html
+++ b/demos/ve/mobile.html
@@ -33,7 +33,9 @@
                <link rel=stylesheet 
href="../../src/ce/styles/nodes/ve.ce.TableNode.css">
                <link rel=stylesheet href="../../src/ce/styles/ve.ce.css">
                <link rel=stylesheet 
href="../../src/ce/styles/ve.ce.Surface.css">
+               <link rel=stylesheet 
href="../../src/ui/styles/dialogs/ve.ui.ToolbarDialog.css">
                <link rel=stylesheet 
href="../../src/ui/styles/dialogs/ve.ui.CommandHelpDialog.css">
+               <link rel=stylesheet 
href="../../src/ui/styles/dialogs/ve.ui.FindAndReplaceDialog.css">
                <link rel=stylesheet 
href="../../src/ui/styles/dialogs/ve.ui.ProgressDialog.css">
                <link rel=stylesheet 
href="../../src/ui/styles/tools/ve.ui.FormatTool.css">
                <link rel=stylesheet 
href="../../src/ui/styles/widgets/ve.ui.LanguageInputWidget.css">
@@ -323,13 +325,16 @@
                <script 
src="../../src/ui/commands/ve.ui.IndentationCommand.js"></script>
                <script 
src="../../src/ui/commands/ve.ui.MergeCellsCommand.js"></script>
                <script 
src="../../src/ui/commands/ve.ui.TableCaptionCommand.js"></script>
-               <script 
src="../../src/ui/dialogs/ve.ui.CommandHelpDialog.js"></script>
                <script 
src="../../src/ui/dialogs/ve.ui.FragmentDialog.js"></script>
                <script src="../../src/ui/dialogs/ve.ui.NodeDialog.js"></script>
+               <script 
src="../../src/ui/dialogs/ve.ui.ToolbarDialog.js"></script>
+               <script 
src="../../src/ui/dialogs/ve.ui.CommandHelpDialog.js"></script>
+               <script 
src="../../src/ui/dialogs/ve.ui.FindAndReplaceDialog.js"></script>
                <script 
src="../../src/ui/dialogs/ve.ui.ProgressDialog.js"></script>
                <script 
src="../../src/ui/filedrophandlers/ve.ui.DSVFileDropHandler.js"></script>
                <script 
src="../../src/ui/filedrophandlers/ve.ui.PlainTextFileDropHandler.js"></script>
                <script 
src="../../src/ui/filedrophandlers/ve.ui.HTMLFileDropHandler.js"></script>
+               <script 
src="../../src/ui/windowmanagers/ve.ui.ToolbarDialogWindowManager.js"></script>
                <script 
src="../../src/ui/widgets/ve.ui.LanguageSearchWidget.js"></script>
                <script 
src="../../src/ui/widgets/ve.ui.LanguageResultWidget.js"></script>
                <script 
src="../../src/ui/dialogs/ve.ui.LanguageSearchDialog.js"></script>
diff --git a/i18n/en.json b/i18n/en.json
index 2a94ca0..5329f16 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -49,6 +49,13 @@
     "visualeditor-dialog-language-search-title": "Select language",
     "visualeditor-dimensionswidget-px": "px",
     "visualeditor-dimensionswidget-times": "×",
+    "visualeditor-find-and-replace-find-text": "Find",
+    "visualeditor-find-and-replace-match-case": "Match case",
+    "visualeditor-find-and-replace-replace-all-button": "Replace all",
+    "visualeditor-find-and-replace-replace-button": "Replace",
+    "visualeditor-find-and-replace-replace-text": "Replace",
+    "visualeditor-find-and-replace-results": "$1 of $2",
+    "visualeditor-find-and-replace-title": "Find and replace",
     "visualeditor-formatdropdown-format-blockquote": "Block quote",
     "visualeditor-formatdropdown-format-heading-label": "Heading (1-6)",
     "visualeditor-formatdropdown-format-heading1": "Heading 1",
diff --git a/i18n/qqq.json b/i18n/qqq.json
index b574fb6..8a4a712 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -55,6 +55,13 @@
        "visualeditor-dialog-language-search-title": "Title for language search 
dialog\n{{Identical|Select language}}",
        "visualeditor-dimensionswidget-px": "{{optional}}\nLabel for the 
dimensions properties denoting pixel units.",
        "visualeditor-dimensionswidget-times": "{{optional}}\nLabel for the 
dimensions properties denoting 'by', as in width x height.",
+       "visualeditor-find-and-replace-find-text": "Label for find text in find 
and replace",
+       "visualeditor-find-and-replace-match-case": "Label for match case 
toggle in find and replace",
+       "visualeditor-find-and-replace-replace-all-button": "Label for replace 
all button in find and replace",
+       "visualeditor-find-and-replace-replace-button": "Label for replace 
button in find and replace",
+       "visualeditor-find-and-replace-replace-text": "Label for replace text 
in find and replace",
+       "visualeditor-find-and-replace-results": "Label for find results 
showing how many results were found ($2), and which one is currently 
highlighted ($1)",
+       "visualeditor-find-and-replace-title": "Title for find and replace",
        "visualeditor-formatdropdown-format-blockquote": "Item in the 
formatting dropdown for block quote text. A block quote is a quotation which is 
a whole paragraph, or several paragraphs.",
        "visualeditor-formatdropdown-format-heading-label": "Label for heading 
level commands.\n{{Identical|Heading}}",
        "visualeditor-formatdropdown-format-heading1": "Item in the generic 
formatting dropdown for a level 1 heading.\n{{Identical|Heading}}",
diff --git a/src/ce/ve.ce.Surface.js b/src/ce/ve.ce.Surface.js
index 110fe4c..e9d3efa 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;
diff --git a/src/dm/ve.dm.Document.js b/src/dm/ve.dm.Document.js
index e8dae5e..b9cd0d2 100644
--- a/src/dm/ve.dm.Document.js
+++ b/src/dm/ve.dm.Document.js
@@ -1242,6 +1242,35 @@
 };
 
 /**
+ * Find a text string within the document
+ *
+ * @param {string} query Text to find
+ * @param {boolean} [caseSensitive] Case sensitive search
+ * @param {boolean} [noOverlaps] Avoid overlapping matches
+ * @return {number[]} List of offsets where the string was found
+ */
+ve.dm.Document.prototype.findText = function ( query, caseSensitive, 
noOverlaps ) {
+       var offset = -1,
+               offsets = [],
+               len = query.length,
+               text = this.data.getText(
+                       true,
+                       new ve.Range( 0, 
this.getInternalList().getListNode().getOuterRange().start )
+               );
+
+       if ( !caseSensitive ) {
+               text = text.toLowerCase();
+               query = query.toLowerCase();
+       }
+
+       while ( ( offset = text.indexOf( query, offset ) ) !== -1 ) {
+               offsets.push( offset );
+               offset += noOverlaps ? len : 1;
+       }
+       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/init/ve.init.Target.js b/src/init/ve.init.Target.js
index 009ab4e..feb58a3 100644
--- a/src/init/ve.init.Target.js
+++ b/src/init/ve.init.Target.js
@@ -223,6 +223,7 @@
        this.toolbar = new ve.ui.TargetToolbar( this, this.surface, config );
        this.toolbar.setup( this.constructor.static.toolbarGroups );
        this.toolbar.$element.insertBefore( this.surface.$element );
+       this.toolbar.$bar.append( this.surface.toolbarDialogs.$element );
 };
 
 /**
diff --git a/src/ui/actions/ve.ui.WindowAction.js 
b/src/ui/actions/ve.ui.WindowAction.js
index c23dc31..17402d0 100644
--- a/src/ui/actions/ve.ui.WindowAction.js
+++ b/src/ui/actions/ve.ui.WindowAction.js
@@ -58,7 +58,9 @@
 
        data = ve.extendObject( { dir: dir }, data, { fragment: fragment } );
 
-       if ( windowType === 'dialog' ) {
+       if ( windowType === 'toolbar' ) {
+               data = ve.extendObject( data, { surface: surface } );
+       } else if ( windowType === 'dialog' ) {
                // For non-isolated dialogs, remove the selection and re-apply 
on close
                surface.getView().nativeSelection.removeAllRanges();
                onOpen = function ( opened ) {
@@ -131,12 +133,14 @@
  * Get the type of a window class
  *
  * @param {string} name Window name
- * @return {string|null} Window type: 'inspector' or 'dialog'
+ * @return {string|null} Window type: 'inspector', 'toolbar' or 'dialog'
  */
 ve.ui.WindowAction.prototype.getWindowType = function ( name ) {
        var windowClass = ve.ui.windowFactory.lookup( name );
        if ( windowClass.prototype instanceof ve.ui.FragmentInspector ) {
                return 'inspector';
+       } else if ( windowClass.prototype instanceof ve.ui.ToolbarDialog ) {
+               return 'toolbar';
        } else if ( windowClass.prototype instanceof OO.ui.Dialog ) {
                return 'dialog';
        }
@@ -153,6 +157,8 @@
        switch ( windowType ) {
                case 'inspector':
                        return this.surface.getContext().getInspectors();
+               case 'toolbar':
+                       return this.surface.toolbarDialogs;
                case 'dialog':
                        return this.surface.getDialogs();
        }
diff --git a/src/ui/dialogs/ve.ui.CommandHelpDialog.js 
b/src/ui/dialogs/ve.ui.CommandHelpDialog.js
index c4a3446..7580b60 100644
--- a/src/ui/dialogs/ve.ui.CommandHelpDialog.js
+++ b/src/ui/dialogs/ve.ui.CommandHelpDialog.js
@@ -183,6 +183,7 @@
                other: {
                        title: 'visualeditor-shortcuts-other',
                        commands: [
+                               { trigger: 'findAndReplace', msg: 
'visualeditor-find-and-replace-title' },
                                { trigger: 'selectAll', msg: 
'visualeditor-content-select-all' },
                                { trigger: 'commandHelp', msg: 
'visualeditor-dialog-command-help-title' }
                        ]
diff --git a/src/ui/dialogs/ve.ui.FindAndReplaceDialog.js 
b/src/ui/dialogs/ve.ui.FindAndReplaceDialog.js
new file mode 100644
index 0000000..c55c74f
--- /dev/null
+++ b/src/ui/dialogs/ve.ui.FindAndReplaceDialog.js
@@ -0,0 +1,364 @@
+/*!
+ * VisualEditor UserInterface FindAndReplaceDialog class.
+ *
+ * @copyright 2011-2014 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+/**
+ * Find and replace dialog.
+ *
+ * @class
+ * @extends ve.ui.ToolbarDialog
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+ve.ui.FindAndReplaceDialog = function VeUiFindAndReplaceDialog( config ) {
+       // Parent constructor
+       ve.ui.FindAndReplaceDialog.super.call( this, config );
+
+       // Properties
+       this.surface = null;
+
+       // Pre-initialization
+       this.$element.addClass( 've-ui-findAndReplaceDialog' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ui.FindAndReplaceDialog, ve.ui.ToolbarDialog );
+
+ve.ui.FindAndReplaceDialog.static.name = 'findAndReplace';
+
+ve.ui.FindAndReplaceDialog.static.title = OO.ui.deferMsg( 
'visualeditor-find-and-replace-title' );
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+ve.ui.FindAndReplaceDialog.prototype.initialize = function () {
+       // Parent method
+       ve.ui.FindAndReplaceDialog.super.prototype.initialize.call( this );
+
+       this.$findResults = this.$( '<div>' ).addClass( 
've-ui-findAndReplaceDialog-findResults' );
+       this.fragments = [];
+       this.replacing = false;
+       this.focusedIndex = 0;
+       this.findText = new OO.ui.TextInputWidget( {
+               $: this.$,
+               classes: ['ve-ui-findAndReplaceDialog-cell', 
've-ui-findAndReplaceDialog-findText'],
+               placeholder: ve.msg( 'visualeditor-find-and-replace-find-text' )
+       } );
+       this.matchCaseToggle = new OO.ui.ToggleSwitchWidget( { $: this.$ } );
+       this.focusedIndexLabel = new OO.ui.LabelWidget( {
+               $: this.$,
+               classes: ['ve-ui-findAndReplaceDialog-focusedIndexLabel']
+       } );
+       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.$,
+               classes: ['ve-ui-findAndReplaceDialog-cell'],
+               placeholder: ve.msg( 
'visualeditor-find-and-replace-replace-text' )
+       } );
+       this.replaceButton = new OO.ui.ButtonWidget( {
+               $: this.$,
+               label: ve.msg( 'visualeditor-find-and-replace-replace-button' )
+       } );
+       this.replaceAllButton = new OO.ui.ButtonWidget( {
+               $: this.$,
+               label: ve.msg( 
'visualeditor-find-and-replace-replace-all-button' )
+       } );
+
+       var checkboxField = new OO.ui.FieldLayout(
+                       this.matchCaseToggle,
+                       {
+                               $: this.$,
+                               classes: ['ve-ui-findAndReplaceDialog-cell'],
+                               align: 'inline',
+                               label: ve.msg( 
'visualeditor-find-and-replace-match-case' )
+                       }
+               ),
+               navigateGroup = new OO.ui.ButtonGroupWidget( {
+                       $: this.$,
+                       classes: ['ve-ui-findAndReplaceDialog-cell'],
+                       items: [
+                               this.previousButton,
+                               this.nextButton
+                       ]
+               } ),
+               replaceGroup = new OO.ui.ButtonGroupWidget( {
+                       $: this.$,
+                       classes: ['ve-ui-findAndReplaceDialog-cell'],
+                       items: [
+                               this.replaceButton,
+                               this.replaceAllButton
+                       ]
+               } ),
+               $findRow = this.$( '<div>' ).addClass( 
've-ui-findAndReplaceDialog-row' ),
+               $replaceRow = this.$( '<div>' ).addClass( 
've-ui-findAndReplaceDialog-row' );
+
+       // Events
+       this.updateFragmentsDebounced = ve.debounce( this.updateFragments.bind( 
this ) );
+       this.positionResultsDebounced = ve.debounce( this.positionResults.bind( 
this ) );
+       this.findText.connect( this, {
+               change: 'onFindChange',
+               enter: 'onFindTextEnter'
+       } );
+       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.$content.on( 'keydown', this.onContentKeyDown.bind( this ) );
+
+       // Initialization
+       this.findText.$input.attr( 'tabIndex', 1 );
+       this.replaceText.$input.attr( 'tabIndex', 2 );
+       this.$content.addClass( 've-ui-findAndReplaceDialog-content' );
+       this.$body
+               .append(
+                       $findRow.append(
+                               this.findText.$element.append(
+                                       this.focusedIndexLabel.$element
+                               ),
+                               navigateGroup.$element,
+                               checkboxField.$element
+                       ),
+                       $replaceRow.append(
+                               this.replaceText.$element,
+                               replaceGroup.$element
+                       )
+               );
+};
+
+/**
+ * @inheritdoc
+ */
+ve.ui.FindAndReplaceDialog.prototype.getSetupProcess = function ( data ) {
+       data = data || {};
+       return ve.ui.FindAndReplaceDialog.super.prototype.getSetupProcess.call( 
this, data )
+               .first( function () {
+                       this.surface = data.surface;
+                       this.surface.$controls.append( this.$findResults );
+                       this.surface.getModel().connect( this, { 
documentUpdate: this.updateFragmentsDebounced } );
+                       this.surface.getView().connect( this, { position: 
this.positionResultsDebounced } );
+
+                       var text = data.fragment.getText();
+                       if ( text ) {
+                               this.findText.setValue( text );
+                       } else {
+                               this.updateFragments();
+                       }
+               }, this );
+};
+
+/**
+ * @inheritdoc
+ */
+ve.ui.FindAndReplaceDialog.prototype.getReadyProcess = function ( data ) {
+       return ve.ui.FindAndReplaceDialog.super.prototype.getReadyProcess.call( 
this, data )
+               .next( function () {
+                       this.findText.focus().select();
+               }, this );
+};
+
+/**
+ * @inheritdoc
+ */
+ve.ui.FindAndReplaceDialog.prototype.getTeardownProcess = function ( data ) {
+       return 
ve.ui.FindAndReplaceDialog.super.prototype.getTeardownProcess.call( this, data )
+               .next( function () {
+                       var surfaceView = this.surface.getView();
+                       this.surface.getModel().disconnect( this );
+                       surfaceView.disconnect( this );
+                       surfaceView.focus();
+                       this.$findResults.empty().detach();
+                       this.fragment = [];
+                       this.surface = null;
+               }, this );
+};
+
+/**
+ * Handle keydown events inside the dialog
+ */
+ve.ui.FindAndReplaceDialog.prototype.onContentKeyDown = function ( e ) {
+       var command, trigger = new ve.ui.Trigger( e );
+       if ( trigger.isComplete() ) {
+               command = this.surface.getCommandByTrigger( trigger.toString() 
);
+               if ( command && command.getName() === 'findAndReplace' ) {
+                       e.preventDefault();
+                       this.close();
+               }
+       }
+};
+
+/**
+ * Handle change events to the find inputs (text or match case)
+ */
+ve.ui.FindAndReplaceDialog.prototype.onFindChange = function () {
+       this.updateFragments();
+       this.positionResults();
+       this.highlightFocused( true );
+};
+
+/**
+ * Handle enter events on the find text input
+ */
+ve.ui.FindAndReplaceDialog.prototype.onFindTextEnter = function () {
+       this.onNextButtonClick();
+};
+
+/**
+ * Update search result fragments
+ */
+ve.ui.FindAndReplaceDialog.prototype.updateFragments = function () {
+       var i, l, findLen,
+               endOffset = 0,
+               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, true );
+               findLen = find.length;
+               for ( i = 0, l = offsets.length; i < l; i++ ) {
+                       endOffset = offsets[i] + findLen;
+                       this.fragments.push( surfaceModel.getLinearFragment( 
new ve.Range( offsets[i], endOffset ), true, true ) );
+               }
+       }
+       this.focusedIndex = Math.min( this.focusedIndex, this.fragments.length 
);
+       this.nextButton.setDisabled( !this.fragments.length );
+       this.previousButton.setDisabled( !this.fragments.length );
+       this.replaceButton.setDisabled( !this.fragments.length );
+       this.replaceAllButton.setDisabled( !this.fragments.length );
+};
+
+/**
+ * Position results markers
+ */
+ve.ui.FindAndReplaceDialog.prototype.positionResults = function () {
+       if ( this.replacing ) {
+               return;
+       }
+
+       var i, l, j, rects, $result, top;
+
+       this.$findResults.empty();
+       for ( i = 0, l = this.fragments.length; i < l; i++ ) {
+               rects = this.surface.getView().getSelectionRects( 
this.fragments[i].getSelection() );
+               $result = this.$( '<div>' ).addClass( 
've-ui-findAndReplaceDialog-findResult' );
+               top = Infinity;
+               for ( j in rects ) {
+                       top = Math.min( top, rects[j].top );
+                       $result.append( this.$( '<div>' ).css( rects[j] ) );
+               }
+               $result.data( 'top', top );
+               this.$findResults.append( $result );
+       }
+       this.highlightFocused();
+};
+
+/**
+ * Highlight the focused result marker
+ *
+ * @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' );
+
+       if ( this.fragments.length ) {
+               this.focusedIndexLabel.setLabel(
+                       ve.msg( 'visualeditor-find-and-replace-results', 
this.focusedIndex + 1, this.fragments.length )
+               );
+       } else {
+               this.focusedIndexLabel.setLabel( '' );
+       }
+
+       if ( scrollIntoView ) {
+               offset = $result.data( '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' );
+               }
+       }
+};
+
+/**
+ * Handle click events on the next button
+ */
+ve.ui.FindAndReplaceDialog.prototype.onNextButtonClick = function () {
+       this.focusedIndex = ( this.focusedIndex + 1 ) % this.fragments.length;
+       this.highlightFocused( true );
+};
+
+/**
+ * Handle click events on the previous button
+ */
+ve.ui.FindAndReplaceDialog.prototype.onPreviousButtonClick = function () {
+       this.focusedIndex = ( this.focusedIndex + this.fragments.length - 1 ) % 
this.fragments.length;
+       this.highlightFocused( true );
+};
+
+/**
+ * Handle click events on the replace button
+ */
+ve.ui.FindAndReplaceDialog.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;
+       // updateFragmentsDebounced is triggered by insertContent, but call it 
immediately
+       // so we can find the next fragment to select.
+       this.updateFragments();
+       if ( !this.fragments.length ) {
+               this.focusedIndex = 0;
+               return;
+       }
+       while ( this.fragments[this.focusedIndex] && 
this.fragments[this.focusedIndex].getSelection().getRange().end <= end ) {
+               this.focusedIndex++;
+       }
+       // We may have iterated off the end
+       this.focusedIndex = this.focusedIndex % this.fragments.length;
+};
+
+/**
+ * Handle click events on the previous all button
+ */
+ve.ui.FindAndReplaceDialog.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 );
+       }
+};
+
+/* Registration */
+
+ve.ui.windowFactory.register( ve.ui.FindAndReplaceDialog );
diff --git a/src/ui/dialogs/ve.ui.ToolbarDialog.js 
b/src/ui/dialogs/ve.ui.ToolbarDialog.js
new file mode 100644
index 0000000..8a221e6
--- /dev/null
+++ b/src/ui/dialogs/ve.ui.ToolbarDialog.js
@@ -0,0 +1,32 @@
+/*!
+ * VisualEditor UserInterface ToolbarDialog class.
+ *
+ * @copyright 2011-2014 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+/**
+ * Toolbar dialog.
+ *
+ * @class
+ * @abstract
+ * @extends OO.ui.Dialog
+ *
+ * @constructor
+ * @param {ve.ui.Surface} surface
+ * @param {Object} [config] Configuration options
+ */
+ve.ui.ToolbarDialog = function VeUiToolbarDialog( config ) {
+       // Parent constructor
+       ve.ui.ToolbarDialog.super.call( this, config );
+
+       // Pre-initialization
+       // This class needs to exist before setup to constrain the height
+       // of the dialog when it first loads.
+       this.$element.addClass( 've-ui-toolbarDialog' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ui.ToolbarDialog, OO.ui.Dialog );
+
+ve.ui.ToolbarDialog.static.size = 'full';
diff --git a/src/ui/styles/dialogs/ve.ui.CommandHelpDialog.css 
b/src/ui/styles/dialogs/ve.ui.CommandHelpDialog.css
index 9447ca7..9dc22b4 100644
--- a/src/ui/styles/dialogs/ve.ui.CommandHelpDialog.css
+++ b/src/ui/styles/dialogs/ve.ui.CommandHelpDialog.css
@@ -14,7 +14,7 @@
         * Hack 1.1: The height isn't calculated correctly with the inline-block
         * hack so fix it to prevent unnecessary scroll bars.
         */
-       height: 37em;
+       height: 39em;
 }
 
 .ve-ui-commandHelpDialog-section {
diff --git a/src/ui/styles/dialogs/ve.ui.FindAndReplaceDialog.css 
b/src/ui/styles/dialogs/ve.ui.FindAndReplaceDialog.css
new file mode 100644
index 0000000..e254ede
--- /dev/null
+++ b/src/ui/styles/dialogs/ve.ui.FindAndReplaceDialog.css
@@ -0,0 +1,64 @@
+/*!
+ * VisualEditor UserInterface FindAndReplaceDialog styles.
+ *
+ * @copyright 2011-2014 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+.ve-ui-findAndReplaceDialog-row {
+       display: table;
+       min-width: 30em;
+       padding-bottom: 0.3em;
+}
+
+.ve-ui-findAndReplaceDialog-row:last-child {
+       /* Extra pixel for button shadows */
+       padding-bottom: 1px;
+}
+
+.ve-ui-findAndReplaceDialog-row .ve-ui-findAndReplaceDialog-cell {
+       display: table-cell;
+       vertical-align: middle;
+       white-space: nowrap;
+       padding-right: 1em;
+}
+
+.ve-ui-findAndReplaceDialog-row .ve-ui-findAndReplaceDialog-cell:last-child {
+       padding-right: 0;
+}
+
+.ve-ui-findAndReplaceDialog-findText input {
+       padding-right: 6em;
+}
+
+.ve-ui-findAndReplaceDialog-focusedIndexLabel {
+       position: absolute;
+       right: 1.4em;
+       color: #888;
+}
+
+.ve-ui-findAndReplaceDialog-findResults {
+       position: absolute;
+       top: 0;
+       left: 0;
+       pointer-events: none;
+}
+
+.ve-ui-findAndReplaceDialog-findResult {
+       opacity: 0.2;
+}
+
+.ve-ui-findAndReplaceDialog-findResult div {
+       background: #28bb0b;
+       position: absolute;
+       margin-top: -0.15em;
+       padding: 0.15em 0;
+       border-radius: 0.15em;
+}
+
+.ve-ui-findAndReplaceDialog-findResult-focused {
+       opacity: 0.4;
+}
+
+.ve-ui-findAndReplaceDialog-findResult-focused div {
+       background: #1f850b;
+}
diff --git a/src/ui/styles/dialogs/ve.ui.ToolbarDialog.css 
b/src/ui/styles/dialogs/ve.ui.ToolbarDialog.css
new file mode 100644
index 0000000..1e10dbe
--- /dev/null
+++ b/src/ui/styles/dialogs/ve.ui.ToolbarDialog.css
@@ -0,0 +1,26 @@
+/*!
+ * VisualEditor UserInterface ToolbarDialog styles.
+ *
+ * @copyright 2011-2014 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+.ve-ui-toolbarDialog {
+       padding: 0.375em; /* 0.3em / 0.8 */
+       padding-top: 0;
+       overflow-y: hidden;
+       max-height: 0;
+       -webkit-transition: max-height 250ms;
+       -moz-transition: max-height 250ms;
+       -o-transition: max-height 250ms;
+       transition: max-height 250ms;
+}
+
+.ve-ui-toolbarDialog.oo-ui-window-ready {
+       /* approximate max height for transition */
+       max-height: 150px;
+}
+
+.ve-ui-toolbarDialog-content > .oo-ui-window-body {
+       bottom: auto;
+       box-shadow: none;
+}
diff --git a/src/ui/ve.ui.CommandRegistry.js b/src/ui/ve.ui.CommandRegistry.js
index 085c767..1161ac3 100644
--- a/src/ui/ve.ui.CommandRegistry.js
+++ b/src/ui/ve.ui.CommandRegistry.js
@@ -141,6 +141,11 @@
 );
 ve.ui.commandRegistry.register(
        new ve.ui.Command(
+               'findAndReplace', 'window', 'toggle', { args: 
['findAndReplace'] }
+       )
+);
+ve.ui.commandRegistry.register(
+       new ve.ui.Command(
                'code', 'annotation', 'toggle',
                { args: ['textStyle/code'], supportedSelections: ['linear', 
'table'] }
        )
diff --git a/src/ui/ve.ui.Surface.js b/src/ui/ve.ui.Surface.js
index d648ffd..15d850a 100644
--- a/src/ui/ve.ui.Surface.js
+++ b/src/ui/ve.ui.Surface.js
@@ -57,6 +57,12 @@
        this.filibuster = null;
 
        this.toolbarHeight = 0;
+       this.toolbarDialogs = new ve.ui.ToolbarDialogWindowManager( {
+               $: this.$,
+               factory: ve.ui.windowFactory,
+               modal: false,
+               isolate: true
+       } );
 
        // Initialization
        this.setupCommands( config.excludeCommands );
diff --git a/src/ui/ve.ui.Toolbar.js b/src/ui/ve.ui.Toolbar.js
index b24dd13..9fd5f7a 100644
--- a/src/ui/ve.ui.Toolbar.js
+++ b/src/ui/ve.ui.Toolbar.js
@@ -49,6 +49,10 @@
                .addClass( 've-ui-dir-block-' + this.contextDirection.block );
        // Events
        this.surface.getModel().connect( this, { contextChange: 
'onContextChange' } );
+       this.surface.toolbarDialogs.connect( this, {
+               opening: 'onToolbarWindowOpeningOrClosing',
+               closing: 'onToolbarWindowOpeningOrClosing'
+       } );
 };
 
 /* Inheritance */
@@ -119,6 +123,27 @@
 };
 
 /**
+ * Handle windows opening or closing in the toolbar window manager.
+ *
+ * @param {OO.ui.Window} win
+ * @param {jQuery.Promise} openingOrClosing
+ * @param {Object} data
+ */
+ve.ui.Toolbar.prototype.onToolbarWindowOpeningOrClosing = function ( win, 
openingOrClosing ) {
+       var toolbar = this;
+       openingOrClosing.then( function () {
+               // Wait for window transition
+               setTimeout( function () {
+                       if ( toolbar.floating ) {
+                               // Re-calculate height
+                               toolbar.unfloat();
+                               toolbar.float();
+                       }
+               }, 250 );
+       } );
+};
+
+/**
  * Handle context changes on the surface.
  *
  * @fires updateState
diff --git a/src/ui/ve.ui.TriggerRegistry.js b/src/ui/ve.ui.TriggerRegistry.js
index 865f0a3..bc620b0 100644
--- a/src/ui/ve.ui.TriggerRegistry.js
+++ b/src/ui/ve.ui.TriggerRegistry.js
@@ -168,3 +168,6 @@
 ve.ui.triggerRegistry.register(
        'pasteSpecial', { mac: new ve.ui.Trigger( 'cmd+shift+v' ), pc: new 
ve.ui.Trigger( 'ctrl+shift+v' ) }
 );
+ve.ui.triggerRegistry.register(
+       'findAndReplace', { mac: new ve.ui.Trigger( 'cmd+f' ), pc: new 
ve.ui.Trigger( 'ctrl+f' ) }
+);
diff --git a/src/ui/windowmanagers/ve.ui.ToolbarDialogWindowManager.js 
b/src/ui/windowmanagers/ve.ui.ToolbarDialogWindowManager.js
new file mode 100644
index 0000000..b4aea54
--- /dev/null
+++ b/src/ui/windowmanagers/ve.ui.ToolbarDialogWindowManager.js
@@ -0,0 +1,40 @@
+/*!
+ * VisualEditor UserInterface ToolbarDialogWindowManager class.
+ *
+ * @copyright 2011-2014 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+/**
+ * Window manager for toolbar dialogs.
+ *
+ * @class
+ * @extends ve.ui.WindowManager
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {ve.ui.Overlay} [overlay] Overlay to use for menus
+ */
+ve.ui.ToolbarDialogWindowManager = function VeUiToolbarDialogWindowManager( 
config ) {
+       // Parent constructor
+       ve.ui.ToolbarDialogWindowManager.super.call( this, config );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ui.ToolbarDialogWindowManager, ve.ui.WindowManager );
+
+/* Static Properties */
+
+ve.ui.ToolbarDialogWindowManager.static.sizes.full = {
+       width: '100%',
+       maxHeight: '100%'
+};
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+ve.ui.ToolbarDialogWindowManager.prototype.getTeardownDelay = function () {
+       return 250;
+};
diff --git a/tests/index.html b/tests/index.html
index a7c2daa..b6da759 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -272,13 +272,16 @@
                <script 
src="../src/ui/commands/ve.ui.IndentationCommand.js"></script>
                <script 
src="../src/ui/commands/ve.ui.MergeCellsCommand.js"></script>
                <script 
src="../src/ui/commands/ve.ui.TableCaptionCommand.js"></script>
-               <script 
src="../src/ui/dialogs/ve.ui.CommandHelpDialog.js"></script>
                <script 
src="../src/ui/dialogs/ve.ui.FragmentDialog.js"></script>
                <script src="../src/ui/dialogs/ve.ui.NodeDialog.js"></script>
+               <script src="../src/ui/dialogs/ve.ui.ToolbarDialog.js"></script>
+               <script 
src="../src/ui/dialogs/ve.ui.CommandHelpDialog.js"></script>
+               <script 
src="../src/ui/dialogs/ve.ui.FindAndReplaceDialog.js"></script>
                <script 
src="../src/ui/dialogs/ve.ui.ProgressDialog.js"></script>
                <script 
src="../src/ui/filedrophandlers/ve.ui.DSVFileDropHandler.js"></script>
                <script 
src="../src/ui/filedrophandlers/ve.ui.PlainTextFileDropHandler.js"></script>
                <script 
src="../src/ui/filedrophandlers/ve.ui.HTMLFileDropHandler.js"></script>
+               <script 
src="../src/ui/windowmanagers/ve.ui.ToolbarDialogWindowManager.js"></script>
                <script 
src="../src/ui/widgets/ve.ui.LanguageSearchWidget.js"></script>
                <script 
src="../src/ui/widgets/ve.ui.LanguageResultWidget.js"></script>
                <script 
src="../src/ui/dialogs/ve.ui.LanguageSearchDialog.js"></script>

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

Gerrit-MessageType: merged
Gerrit-Change-Id: I8dc406ffe8455dedf35eef02a1909de2d79adeb0
Gerrit-PatchSet: 18
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: Trevor Parscal <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

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

Reply via email to