Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Move Find dialog out of editor #2314

Merged
merged 11 commits into from

2 participants

@njx
Owner
njx commented

For #2095, this moves the Find bar out of the editor and into the toolbar. The visual appearance is similar to what we have today, and the functional behavior should be the same. The main difference is that the Find bar no longer overlaps the editor content, so if a found item scrolls to the top of the viewport, it's no longer hidden by the Find bar. Find in Files and Quick Open were changed to use the same implementation.

To do this, I replaced the dialog.js implementation we were using from CodeMirror with a new class, ModalBar, that mimics the same functionality but does not create the bar inside the CodeMirror wrapper element. If ModalBar is constructed with autoClose set to true, it mimics the CodeMirror dialog.js functionality of closing the dialog on Return and Esc keys and blur events, but dispatches its own events for those cases instead of calling a callback. Find/Replace uses this functionality, whereas Find in Files and Quick Open don't.

Additionally, whenever the ModalBar appears or disappears, it attempts to preserve the apparent scroll position in the editor, so the code doesn't shift around. The only case where it can't do this is if the editor is scrolled all the way to the top while the ModalBar is open--in that case, of course, it's not possible to keep the code in the same place; it has to shift back up.

Note that there are a couple of bugs I know about in this implementation (which is why it's just up for initial review):

  • When the Quick Open bar closes, the code shifts upward. I'm pretty sure this is because of the hack we're doing when closing the bar (see the comment online #380); removing the raw DOM node makes it so ModalBar.close() can't figure out how much to scroll back up (since it needs to measure its current height, and that height will be 0 if the DOM node has already been removed). I can think of some workarounds, but want to consult with @peterflynn about the hack first.
  • When an invalid regexp is entered, the parse error message shows up as before, pushing the editor downward and causing the status bar to disappear. I could add code to fix this, but want to think about alternatives for showing the error state.

In a separate branch, I actually have a much more radical replacement for Find/Replace that unifies them into a single modal bar and eliminates a good deal of the messy code (and simplifies ModalBar as well), getting closer to the Find/Replace design that's in the original Northstar. It has one bug I'm having trouble figuring out, but if I can get that figured out, we might want to go ahead and just go straight to that version. I'll consult with some folks before putting up that pull request though.

njx added some commits
@njx njx For #2095, move the Find bar out of the editor and into the toolbar s…
…o it doesn't overlap,

and manage the scroll position when the Find bar appears/disappears so the code doesn't
appear to shift if possible.
As part of this, created new ModalBar class that replaces the old CodeMirror dialog.js.
af89077
@njx njx Change FindInFiles and QuickOpen to use ModalBar bafbefa
@njx njx Remove references to dialog.js, dialog.css and search.js, which are n…
…o longer used anywhere in Brackets
2080bb7
@njx
Owner
njx commented

Probably either @peterflynn or @gruehle should take a look.

@njx njx Make ModalBar return the value of the input field (if any) when closi…
…ng so that callers can get it without having to access the removed text field. Rename to _.
595cf74
@njx
Owner
njx commented

Also, note that I didn't change any of the weird workflows with the way the dialog bar works--it still gets destroyed and recreated every time you do a Find Next, for example. (I did improve that in my other unified-dialog branch.)

@gruehle gruehle was assigned
@njx
Owner

To @gruehle -- I still need to address the two issues above, but feel free to take a look.

@gruehle
Owner

I ran into a couple of bugs when testing with inline editors:

  1. With the focus in an inline editor, Cmd-F moves the outer document down. I think the auto-scroll code needs to always operate on the current main editor instead of the active editor.
  2. Autoscroll can interfere with the positioning of the rule list. Open an inline editor, put the cursor at the top of the main document, do a search that has a hit in the first line of text (you'll see the document scroll down to show the top line), close the search bar. The contents of the inline editor rule list are redrawn incorrectly.
src/widgets/ModalBar.js
((114 lines not shown))
+ scrollPos;
+ if (activeEditor) {
+ scrollPos = activeEditor.getScrollPos();
+ }
+ EditorManager.resizeEditor();
+ if (activeEditor) {
+ activeEditor._codeMirror.scrollTo(scrollPos.x, scrollPos.y - barHeight);
+ }
+ EditorManager.focusEditor();
+ };
+
+ /**
+ * If autoClose is set, handles the RETURN/ESC keys in the input field.
+ */
+ ModalBar.prototype._handleInputKeydown = function (e) {
+ if (e.keyCode === 13 || e.keyCode === 27) {
@gruehle Owner
gruehle added a note

Should use the constants from KeyEvent here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
njx added some commits
@njx njx Main editor properly resizes/scrolls when find is opened from inline …
…editor
9adb2e3
@njx njx Change regexp error to show up to right of field instead of increasin…
…g height of modal bar
83bc3c7
@njx njx Remove hack to remove raw DOM nodes when closing Quick Open dialog--d…
…oesn't seem necessary and interferes with modal bar close code
428e9c0
@njx njx Merge from master c079def
@njx njx Updated FindReplace unit tests to work with ModalBar. Split a unit te…
…st into two pieces because one edge case behavior (hitting Find Next with an empty query) changed to dismiss the modal bar instead of keeping it open, and it didn't seem worth trying to get back the old behavior right now.
4e1c8ff
@njx njx Align error message with input field 523e35f
@njx
Owner

Fixed all known issues, merged with master, and updated FindReplace unit tests to work with the new modal bar. Note that one edge case behavior has changed slightly: if you do Find with an empty string, and then immediately hit Find Next, the modal bar dismisses instead of staying open, due to a race condition if you create a new ModalBar without closing the previous one. This issue will go away in my unified find/replace version, but for now it didn't seem worth trying to fix. Instead, I split the existing unit test that hit this edge case into two separate tests to avoid this specific interaction.

Ready for re-review.

@gruehle
Owner

Looks good. Merging.

@gruehle gruehle merged commit 99c7605 into master
@gruehle gruehle referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 8, 2012
  1. @njx

    For #2095, move the Find bar out of the editor and into the toolbar s…

    njx authored
    …o it doesn't overlap,
    
    and manage the scroll position when the Find bar appears/disappears so the code doesn't
    appear to shift if possible.
    As part of this, created new ModalBar class that replaces the old CodeMirror dialog.js.
  2. @njx
  3. @njx

    Remove references to dialog.js, dialog.css and search.js, which are n…

    njx authored
    …o longer used anywhere in Brackets
  4. @njx

    Make ModalBar return the value of the input field (if any) when closi…

    njx authored
    …ng so that callers can get it without having to access the removed text field. Rename to _.
Commits on Dec 10, 2012
  1. @njx
  2. @njx
  3. @njx

    Remove hack to remove raw DOM nodes when closing Quick Open dialog--d…

    njx authored
    …oesn't seem necessary and interferes with modal bar close code
Commits on Dec 11, 2012
  1. @njx

    Merge from master

    njx authored
Commits on Dec 12, 2012
  1. @njx

    Updated FindReplace unit tests to work with ModalBar. Split a unit te…

    njx authored
    …st into two pieces because one edge case behavior (hitting Find Next with an empty query) changed to dismiss the modal bar instead of keeping it open, and it didn't seem worth trying to get back the old behavior right now.
  2. @njx
  3. @njx
This page is out of date. Refresh to see the latest.
View
7 src/editor/Editor.js
@@ -992,8 +992,13 @@ define(function (require, exports, module) {
/**
* Re-renders the editor UI
*/
- Editor.prototype.refresh = function () {
+ Editor.prototype.refresh = function (handleResize) {
this._codeMirror.refresh();
+ if (handleResize) {
+ // If the editor has been resized, the position of inline widgets relative to the
+ // browser window might have changed.
+ this._fireWidgetOffsetTopChanged(0);
+ }
};
/**
View
2  src/editor/EditorManager.js
@@ -370,7 +370,7 @@ define(function (require, exports, module) {
if (_currentEditor) {
$(_currentEditor.getScrollerElement()).height(editorAreaHt);
if (!skipRefresh) {
- _currentEditor.refresh();
+ _currentEditor.refresh(true);
}
}
}
View
5 src/index.html
@@ -29,9 +29,6 @@
<!-- CSS/LESS -->
- <!-- CSS for CodeMirror search support, currently for debugging only -->
- <link rel="stylesheet" href="thirdparty/CodeMirror2/lib/util/dialog.css">
-
<!-- Temporary CSS for unobtrusive scrollbars. This can't live in LESS because it uses
nonstandard WebKit-specific syntax. -->
<link rel="stylesheet" href="styles/quiet-scrollbars.css">
@@ -78,9 +75,7 @@
<script src="thirdparty/CodeMirror2/lib/codemirror.js"></script>
<!-- JS for CodeMirror search support -->
- <script src="thirdparty/CodeMirror2/lib/util/dialog.js"></script>
<script src="thirdparty/CodeMirror2/lib/util/searchcursor.js"></script>
- <script src="thirdparty/CodeMirror2/lib/util/search.js"></script>
<script src="thirdparty/CodeMirror2/lib/util/closetag.js"></script>
</head>
View
29 src/search/FindInFiles.js
@@ -53,7 +53,8 @@ define(function (require, exports, module) {
FileIndexManager = require("project/FileIndexManager"),
KeyEvent = require("utils/KeyEvent"),
AppInit = require("utils/AppInit"),
- StatusBar = require("widgets/StatusBar");
+ StatusBar = require("widgets/StatusBar"),
+ ModalBar = require("widgets/ModalBar").ModalBar;
var searchResults = [];
@@ -62,8 +63,9 @@ define(function (require, exports, module) {
function _getQueryRegExp(query) {
// Clear any pending RegEx error message
- $(".CodeMirror-dialog .alert-message").remove();
-
+ $(".modal-bar .message").css("display", "inline-block");
+ $(".modal-bar .error").css("display", "none");
+
// If query is a regular expression, use it directly
var isRE = query.match(/^\/(.*)\/(g|i)*$/);
if (isRE) {
@@ -75,7 +77,10 @@ define(function (require, exports, module) {
try {
return new RegExp(isRE[1], flags);
} catch (e) {
- $(".CodeMirror-dialog div").append("<div class='alert-message' style='margin-bottom: 0'>" + e.message + "</div>");
+ $(".modal-bar .message").css("display", "none");
+ $(".modal-bar .error")
+ .css("display", "inline-block")
+ .html("<div class='alert-message' style='margin-bottom: 0'>" + e.message + "</div>");
return null;
}
}
@@ -116,16 +121,6 @@ define(function (require, exports, module) {
}
/**
- * Creates a dialog div floating on top of the current code mirror editor
- */
- FindInFilesDialog.prototype._createDialogDiv = function (template) {
- this.dialog = $("<div />")
- .attr("class", "CodeMirror-dialog")
- .html("<div>" + template + "</div>")
- .prependTo($("#editor-holder"));
- };
-
- /**
* Closes the search dialog and resolves the promise that showDialog returned
*/
FindInFilesDialog.prototype._close = function (value) {
@@ -134,7 +129,7 @@ define(function (require, exports, module) {
}
this.closed = true;
- this.dialog.remove();
+ this.modalBar.close();
EditorManager.focusEditor();
this.result.resolve(value);
};
@@ -149,9 +144,9 @@ define(function (require, exports, module) {
// Note the prefix label is a simple "Find:" - the "in ..." part comes after the text field
var dialogHTML = Strings.CMD_FIND +
": <input type='text' id='findInFilesInput' style='width: 10em'> <span id='findInFilesScope'></span> &nbsp;" +
- "<span style='color: #888'>(" + Strings.SEARCH_REGEXP_INFO + ")</span>";
+ "<div class='message'><span style='color: #888'>(" + Strings.SEARCH_REGEXP_INFO + ")</span></div><div class='error'></div>";
this.result = new $.Deferred();
- this._createDialogDiv(dialogHTML);
+ this.modalBar = new ModalBar(dialogHTML, false);
var $searchField = $("input#findInFilesInput");
var that = this;
View
98 src/search/FindReplace.js
@@ -29,14 +29,6 @@
* Adds Find and Replace commands
*
* Originally based on the code in CodeMirror2/lib/util/search.js.
- *
- * Define search commands. Depends on dialog.js or another
- * implementation of the openDialog method.
- *
- * Replace works a little oddly -- it will do the replace on the next findNext press.
- * You prevent a replace by making sure the match is no longer selected when hitting
- * findNext.
- *
*/
define(function (require, exports, module) {
"use strict";
@@ -44,8 +36,9 @@ define(function (require, exports, module) {
var CommandManager = require("command/CommandManager"),
Commands = require("command/Commands"),
Strings = require("strings"),
- EditorManager = require("editor/EditorManager");
-
+ EditorManager = require("editor/EditorManager"),
+ ModalBar = require("widgets/ModalBar").ModalBar;
+
function SearchState() {
this.posFrom = this.posTo = this.query = null;
this.marked = [];
@@ -62,34 +55,22 @@ define(function (require, exports, module) {
// Heuristic: if the query string is all lowercase, do a case insensitive search.
return cm.getSearchCursor(query, pos, typeof query === "string" && query === query.toLowerCase());
}
-
- function dialog(cm, text, shortText, f) {
- if (cm.openDialog) {
- cm.openDialog(text, f);
- } else {
- f(prompt(shortText, ""));
- }
- }
-
- function confirmDialog(cm, text, shortText, fs) {
- if (cm.openConfirm) {
- cm.openConfirm(text, fs);
- } else if (confirm(shortText)) {
- fs[0]();
- }
- }
- function getDialogTextField() {
- return $(".CodeMirror-dialog input[type='text']");
+ function getDialogTextField(modalBar) {
+ return $("input[type='text']", modalBar.getRoot());
}
function parseQuery(query) {
var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
- $(".CodeMirror-dialog .alert-message").remove();
+ $(".modal-bar .message").css("display", "inline-block");
+ $(".modal-bar .error").css("display", "none");
try {
return isRE ? new RegExp(isRE[1], isRE[2].indexOf("i") === -1 ? "" : "i") : query;
} catch (e) {
- $(".CodeMirror-dialog div").append("<div class='alert-message' style='margin-bottom: 0'>" + e.message + "</div>");
+ $(".modal-bar .message").css("display", "none");
+ $(".modal-bar .error")
+ .css("display", "inline-block")
+ .html("<div class='alert-message' style='margin-bottom: 0'>" + e.message + "</div>");
return "";
}
}
@@ -136,8 +117,8 @@ define(function (require, exports, module) {
}
var queryDialog = Strings.CMD_FIND +
- ': <input type="text" style="width: 10em"/> <span style="color: #888">(' +
- Strings.SEARCH_REGEXP_INFO + ')</span>';
+ ': <input type="text" style="width: 10em"/> <div class="message"><span style="color: #888">(' +
+ Strings.SEARCH_REGEXP_INFO + ')</span></div><div class="error"></div>';
/**
* If no search pending, opens the search dialog. If search is already open, moves to
@@ -157,7 +138,7 @@ define(function (require, exports, module) {
// Called each time the search query changes while being typed. Jumps to the first matching
// result, starting from the original cursor position
- function findFirst(query) {
+ function findFirst(query, modalBar) {
cm.operation(function () {
if (!query) {
return;
@@ -180,55 +161,61 @@ define(function (require, exports, module) {
state.posFrom = state.posTo = searchStartPos;
var foundAny = findNext(cm, rev);
- getDialogTextField().toggleClass("no-results", !foundAny);
+ getDialogTextField(modalBar).toggleClass("no-results", !foundAny);
});
}
- dialog(cm, queryDialog, Strings.CMD_FIND, function (query) {
+ var modalBar = new ModalBar(queryDialog, true);
+ $(modalBar).on("closeOk", function (e, query) {
if (!state.findNextCalled) {
// If findNextCalled is false, this means the user has *not*
// entered any search text *or* pressed Cmd-G/F3 to find the
// next occurrence. In this case we want to start searching
// *after* the current selection so we find the next occurrence.
searchStartPos = cm.getCursor(false);
- findFirst(query);
+ findFirst(query, modalBar);
}
});
+ var $input = getDialogTextField(modalBar);
+ $input.on("input", function () {
+ findFirst($input.attr("value"), modalBar);
+ });
+
// Prepopulate the search field with the current selection, if any.
if (initialQuery !== undefined) {
- getDialogTextField()
+ $input
.attr("value", initialQuery)
.get(0).select();
- findFirst(initialQuery);
+ findFirst(initialQuery, modalBar);
// Clear the "findNextCalled" flag here so we have a clean start
state.findNextCalled = false;
}
-
- getDialogTextField().on("input", function () {
- findFirst(getDialogTextField().attr("value"));
- });
}
var replaceQueryDialog = Strings.CMD_REPLACE +
- ': <input type="text" style="width: 10em"/> <span style="color: #888">(' +
- Strings.SEARCH_REGEXP_INFO + ')</span>';
+ ': <input type="text" style="width: 10em"/> <div class="message"><span style="color: #888">(' +
+ Strings.SEARCH_REGEXP_INFO + ')</span></div><div class="error"></div>';
var replacementQueryDialog = Strings.WITH +
': <input type="text" style="width: 10em"/>';
// style buttons to match height/margins/border-radius of text input boxes
var style = ' style="padding:5px 15px;border:1px #999 solid;border-radius:3px;margin:2px 2px 5px;"';
var doReplaceConfirm = Strings.CMD_REPLACE +
- '? <button' + style + '>' + Strings.BUTTON_YES +
- '</button> <button' + style + '>' + Strings.BUTTON_NO +
+ '? <button id="replace-yes"' + style + '>' + Strings.BUTTON_YES +
+ '</button> <button id="replace-no"' + style + '>' + Strings.BUTTON_NO +
'</button> <button' + style + '>' + Strings.BUTTON_STOP + '</button>';
function replace(cm, all) {
- dialog(cm, replaceQueryDialog, Strings.CMD_REPLACE, function (query) {
+ var modalBar = new ModalBar(replaceQueryDialog, true);
+ $(modalBar).on("closeOk", function (e, query) {
if (!query) {
return;
}
+
query = parseQuery(query);
- dialog(cm, replacementQueryDialog, Strings.WITH, function (text) {
+ modalBar = new ModalBar(replacementQueryDialog, true);
+ $(modalBar).on("closeOk", function (e, text) {
+ text = text || "";
var match,
fnMatch = function (w, i) { return match[i]; };
if (all) {
@@ -260,8 +247,15 @@ define(function (require, exports, module) {
}
}
cm.setSelection(cursor.from(), cursor.to());
- confirmDialog(cm, doReplaceConfirm, Strings.CMD_REPLACE + "?",
- [function () { doReplace(match); }, advance]);
+ modalBar = new ModalBar(doReplaceConfirm, true);
+ modalBar.getRoot().on("click", function (e) {
+ modalBar.close();
+ if (e.target.id === "replace-yes") {
+ doReplace(match);
+ } else if (e.target.id === "replace-no") {
+ advance();
+ }
+ });
};
var doReplace = function (match) {
cursor.replace(typeof query === "string" ? text :
@@ -273,8 +267,8 @@ define(function (require, exports, module) {
});
});
- // Prepopulate the replace field with the current selection, if any.
- getDialogTextField()
+ // Prepopulate the replace field with the current selection, if any
+ getDialogTextField(modalBar)
.attr("value", cm.getSelection())
.get(0).select();
}
View
25 src/search/QuickOpen.js
@@ -47,7 +47,8 @@ define(function (require, exports, module) {
StringUtils = require("utils/StringUtils"),
Commands = require("command/Commands"),
ProjectManager = require("project/ProjectManager"),
- KeyEvent = require("utils/KeyEvent");
+ KeyEvent = require("utils/KeyEvent"),
+ ModalBar = require("widgets/ModalBar").ModalBar;
/** @type Array.<QuickOpenPlugin> */
@@ -170,16 +171,6 @@ define(function (require, exports, module) {
this._resultsFormatterCallback = this._resultsFormatterCallback.bind(this);
}
- /**
- * Creates a dialog div floating on top of the current code mirror editor
- */
- QuickNavigateDialog.prototype._createDialogDiv = function (template) {
- this.dialog = $("<div />")
- .attr("class", "CodeMirror-dialog")
- .html("<div align='right'>" + template + "</div>")
- .prependTo($("#editor-holder"));
- };
-
function _filenameFromPath(path, includeExtension) {
var end;
if (includeExtension) {
@@ -423,12 +414,10 @@ define(function (require, exports, module) {
// Closing the dialog is a little tricky (see #1384): some Smart Autocomplete code may run later (e.g.
// (because it's a later handler of the event that just triggered _close()), and that code expects to
// find metadata that it stuffed onto the DOM node earlier. But $.remove() strips that metadata.
- // So, to hide the dialog immediately it's only safe to remove using raw DOM APIs:
- this.dialog[0].parentNode.removeChild(this.dialog[0]);
+ // So we wait until after this call chain is complete before actually closing the dialog.
var self = this;
setTimeout(function () {
- // Now that it's safe, call the real jQuery API to clear the metadata & prevent a memory leak
- self.dialog.remove();
+ self.modalBar.close();
}, 0);
$(".smart_autocomplete_container").remove();
@@ -898,7 +887,7 @@ define(function (require, exports, module) {
* where the popup closes that we want the dialog to remain open (e.g. deleting search term via backspace).
*/
QuickNavigateDialog.prototype._handleDocumentMouseDown = function (e) {
- if ($(this.dialog).find(e.target).length === 0 && $(".smart_autocomplete_container").find(e.target).length === 0) {
+ if (this.modalBar.getRoot().find(e.target).length === 0 && $(".smart_autocomplete_container").find(e.target).length === 0) {
this._close();
} else {
// Allow clicks in the search field to propagate. Clicks in the menu should be
@@ -944,8 +933,8 @@ define(function (require, exports, module) {
}
// Show the search bar ("dialog")
- var dialogHTML = "<span class='find-dialog-label'></span>: <input type='text' autocomplete='off' id='quickOpenSearch' style='width: 30em'>";
- this._createDialogDiv(dialogHTML);
+ var dialogHTML = "<div align='right'><span class='find-dialog-label'></span>: <input type='text' autocomplete='off' id='quickOpenSearch' style='width: 30em'></div>";
+ this.modalBar = new ModalBar(dialogHTML, false);
this.$searchField = $("input#quickOpenSearch");
this.$searchField.smartAutoComplete({
View
39 src/styles/brackets.less
@@ -719,27 +719,24 @@ a, img {
}
}
-/* Find & Replace search bars - temporary UI, to be replaced with a richer search feature later */
+/* Modal bar for Find/Quick Open */
-.CodeMirror-dialog {
- position: relative;
- z-index: @z-index-cm-dialog-override;
-}
-
-.CodeMirror-dialog > div {
+.modal-bar {
+ text-align: left;
+ width: 100%;
font-family: @fontstack-sans-serif;
- position: absolute;
- top: 0; left: 0; right: 0;
- background: @background-color-2;
+ font-size: 13px;
color: @content-color;
- border-bottom: 1px solid @bc-gray;
- .box-shadow(0 1px 3px 0 fadeout(@bc-black, 70%));
- z-index: 1;
- padding: .5em .8em;
+ border-top: 1px solid darken(@background-color-3, @bc-color-step-size);
+ background: #eee;
+ margin-left: -8px;
+ margin-top: 4px;
+ margin-bottom: -4px; // bleed into toolbar padding
+ padding: 6px 14px 4px;
overflow: hidden;
}
-.CodeMirror-dialog input {
+.modal-bar input {
font-family: @fontstack-sans-serif;
border: 1px solid @content-color-weaker;
outline: none;
@@ -752,6 +749,18 @@ a, img {
}
}
+.modal-bar .message {
+ display: inline-block;
+}
+.modal-bar .error {
+ display: none;
+
+ .alert-message {
+ padding-top: 4px;
+ padding-bottom: 4px;
+ }
+}
+
.CodeMirror-searching {
background-color: inherit;
}
View
160 src/widgets/ModalBar.js
@@ -0,0 +1,160 @@
+/*
+ * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ */
+
+
+/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
+/*global define, $, brackets, window */
+
+/**
+ * A "modal bar" component. This is a lightweight replacement for modal dialogs that
+ * appears at the top of the editor area for operations like Find and Quick Open.
+ */
+define(function (require, exports, module) {
+ "use strict";
+
+ var EditorManager = require("editor/EditorManager"),
+ KeyEvent = require("utils/KeyEvent");
+
+ /**
+ * @constructor
+ *
+ * Creates a modal bar whose contents are the given template.
+ * @param {string} template The HTML contents of the modal bar.
+ * @param {boolean} autoClose If true, then close the dialog if the user hits RETURN or ESC
+ * in the first input field, or if the modal bar loses focus to an outside item. Dispatches
+ * jQuery events for these cases: "closeOk" on RETURN, "closeCancel" on ESC, and "closeBlur"
+ * on focus loss.
+ */
+ function ModalBar(template, autoClose) {
+ this._handleInputKeydown = this._handleInputKeydown.bind(this);
+ this._handleFocusChange = this._handleFocusChange.bind(this);
+
+ this._$root = $("<div class='modal-bar'/>")
+ .html(template)
+ .appendTo("#main-toolbar");
+
+ if (autoClose) {
+ this._autoClose = true;
+ var $firstInput = this._getFirstInput()
+ .on("keydown", this._handleInputKeydown);
+ window.document.body.addEventListener("focusin", this._handleFocusChange, true);
+
+ // Set focus to the first input field, or the first button if there is no input field.
+ if ($firstInput.length > 0) {
+ $firstInput.focus();
+ } else {
+ $("button", this._$root).first().focus();
+ }
+ }
+
+ // Preserve scroll position of the current full editor across the editor refresh, adjusting for the
+ // height of the modal bar so the code doesn't appear to shift if possible.
+ var fullEditor = EditorManager.getCurrentFullEditor(),
+ scrollPos;
+ if (fullEditor) {
+ scrollPos = fullEditor.getScrollPos();
+ }
+ EditorManager.resizeEditor();
+ if (fullEditor) {
+ fullEditor._codeMirror.scrollTo(scrollPos.x, scrollPos.y + this._$root.outerHeight());
+ }
+ }
+
+ /**
+ * A jQuery object containing the root node of the ModalBar.
+ */
+ ModalBar.prototype._$root = null;
+
+ /**
+ * True if this ModalBar is set to autoclose.
+ */
+ ModalBar.prototype._autoClose = false;
+
+ /**
+ * Returns a jQuery object for the first input field in the dialog. Will be 0-length if there is none.
+ */
+ ModalBar.prototype._getFirstInput = function () {
+ return $("input[type='text']", this._$root).first();
+ };
+
+ /**
+ * Closes the modal bar and returns focus to the active editor.
+ */
+ ModalBar.prototype.close = function () {
+ var barHeight = this._$root.outerHeight();
+
+ if (this._autoClose) {
+ window.document.body.removeEventListener("focusin", this._handleFocusChange, true);
+ }
+
+ this._$root.remove();
+
+ // Preserve scroll position of the current full editor across the editor refresh, adjusting for the
+ // height of the modal bar so the code doesn't appear to shift if possible.
+ var fullEditor = EditorManager.getCurrentFullEditor(),
+ scrollPos;
+ if (fullEditor) {
+ scrollPos = fullEditor.getScrollPos();
+ }
+ EditorManager.resizeEditor();
+ if (fullEditor) {
+ fullEditor._codeMirror.scrollTo(scrollPos.x, scrollPos.y - barHeight);
+ }
+ EditorManager.focusEditor();
+ };
+
+ /**
+ * If autoClose is set, handles the RETURN/ESC keys in the input field.
+ */
+ ModalBar.prototype._handleInputKeydown = function (e) {
+ if (e.keyCode === KeyEvent.DOM_VK_RETURN || e.keyCode === KeyEvent.DOM_VK_ESCAPE) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ var value = this._getFirstInput().val();
+ this.close();
+ $(this).triggerHandler(e.keyCode === KeyEvent.DOM_VK_RETURN ? "closeOk" : "closeCancel", [value]);
+ }
+ };
+
+ /**
+ * If autoClose is set, detects when something other than the modal bar is getting focus and
+ * dismisses the modal bar.
+ */
+ ModalBar.prototype._handleFocusChange = function (e) {
+ if (!$.contains(this._$root.get(0), e.target)) {
+ var value = this._getFirstInput().val();
+ this.close();
+ $(this).triggerHandler("closeBlur", [value]);
+ }
+ };
+
+ /**
+ * @return {jQueryObject} A jQuery object representing the root of the ModalBar.
+ */
+ ModalBar.prototype.getRoot = function () {
+ return this._$root;
+ };
+
+ exports.ModalBar = ModalBar;
+});
View
26 test/spec/FindReplace-test.js
@@ -22,7 +22,7 @@
*/
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
-/*global define, describe, it, expect, beforeEach, afterEach, waitsFor, runs, $ */
+/*global define, describe, it, expect, beforeEach, afterEach, waitsFor, runs, window, $ */
define(function (require, exports, module) {
'use strict';
@@ -56,7 +56,7 @@ define(function (require, exports, module) {
var CH_REQUIRE_PAREN = CH_REQUIRE_START + "require".length;
- var myDocument, myEditor;
+ var myDocument, myEditor, $myToolbar;
function setupFullEditor() {
// create dummy Document and Editor
@@ -67,8 +67,14 @@ define(function (require, exports, module) {
myEditor.focus();
}
+ beforeEach(function () {
+ $myToolbar = $("<div id='main-toolbar'/>").appendTo(window.document.body);
+ });
+
afterEach(function () {
SpecRunnerUtils.destroyMockEditor(myDocument);
+ $myToolbar.remove();
+ $myToolbar = null;
myEditor = null;
myDocument = null;
});
@@ -87,10 +93,10 @@ define(function (require, exports, module) {
function getSearchBar() {
- return $(".CodeMirror-dialog");
+ return $(".modal-bar");
}
function getSearchField() {
- return $(".CodeMirror-dialog input[type='text']");
+ return $(".modal-bar input[type='text']");
}
function expectSearchBarOpen() {
@@ -370,7 +376,7 @@ define(function (require, exports, module) {
expectSelection({start: {line: LINE_FIRST_REQUIRE + 1, ch: CH_REQUIRE_START}, end: {line: LINE_FIRST_REQUIRE + 1, ch: CH_REQUIRE_PAREN}});
});
- it("should no-op on Enter with blank search", function () {
+ it("should no-op on Find Next with blank search", function () {
myEditor.setCursorPos(LINE_FIRST_REQUIRE, 0);
CommandManager.execute(Commands.EDIT_FIND);
@@ -379,6 +385,14 @@ define(function (require, exports, module) {
CommandManager.execute(Commands.EDIT_FIND_NEXT);
expectCursorAt({line: LINE_FIRST_REQUIRE, ch: 0}); // no change
+ });
+
+ it("should no-op on Enter with blank search", function () {
+ myEditor.setCursorPos(LINE_FIRST_REQUIRE, 0);
+
+ CommandManager.execute(Commands.EDIT_FIND);
+ expectCursorAt({line: LINE_FIRST_REQUIRE, ch: 0});
+
pressEnter();
expectSearchBarClosed();
@@ -445,7 +459,7 @@ define(function (require, exports, module) {
// This is interpreted as a regexp (has both "/"es) but is invalid; should show error message
enterSearchText("/+/");
- expect($(".CodeMirror-dialog .alert-message").length).toBe(1);
+ expect($(".modal-bar .error").length).toBe(1);
expectCursorAt({line: 0, ch: 0}); // no change
});
Something went wrong with that request. Please try again.