Modules (181)

FindBar

Description

UI for the Find/Replace and Find in Files modal bar.

Dependencies

Variables

Private

_searchBarTemplate

Type
string
Private
    var _searchBarTemplate = require("text!htmlContent/findreplace-bar.html");

    var lastTypedTime = 0,
        currentTime = 0,
        intervalId = 0,
        lastQueriedText = "",
        lastTypedText = "",
        lastKeyCode;

Functions

getInitialQueryFromSelection

Returns the string used to prepopulate the find bar

editor
non-nullable
Editor
Returns:
string
first line of primary selection to populate the find bar
        function getInitialQueryFromSelection(editor) {
            var selectionText = editor.getSelectedText();
            if (selectionText) {
                return selectionText
                    .replace(/^\n*/, "") // Trim possible newlines at the very beginning of the selection
                    .split("\n")[0];
            }
            return "";
        }

        if (currentFindBar && !currentFindBar.isClosed()) {
            // The modalBar was already up. When creating the new modalBar, copy the
            // current query instead of using the passed-in selected text.
            query = currentFindBar.getQueryInfo().query;
            replaceText = currentFindBar.getReplaceText();
        } else {
            var openedFindBar = FindBar._bars && _.find(FindBar._bars, function (bar) {
                    return !bar.isClosed();
                });

            if (openedFindBar) {
                query = openedFindBar.getQueryInfo().query;
                replaceText = openedFindBar.getReplaceText();
            } else if (editor) {
                query = getInitialQueryFromSelection(editor) || lastTypedText;
            }
        }

        return {query: query, replaceText: replaceText};
    };

    PreferencesManager.stateManager.definePreference("caseSensitive", "boolean", false);
    PreferencesManager.stateManager.definePreference("regexp", "boolean", false);
    PreferencesManager.stateManager.definePreference("searchHistory", "array", []);
    PreferencesManager.definePreference("maxSearchHistory", "number", 10, {
        description: Strings.FIND_HISTORY_MAX_COUNT
    });

    exports.FindBar = FindBar;
});

Classes

Constructor

FindBar

options.multifile
boolean
- true if this is a Find/Replace in Files (changes the behavior of Enter in the fields, hides the navigator controls, shows the scope/filter controls, and if in replace mode, hides the Replace button (so there's only Replace All)
options.replace
boolean
- true to show the Replace controls - default false
options.queryPlaceholder
string
- label to show in the Find field - default empty string
options.initialQuery
string
- query to populate in the Find field on open - default empty string
scopeLabel
string
- HTML label to show for the scope of the search, expected to be already escaped - default empty string
    function FindBar(options) {
        var defaults = {
            multifile: false,
            replace: false,
            queryPlaceholder: "",
            initialQuery: "",
            initialReplaceText: "",
            scopeLabel: ""
        };
        this._options = _.extend(defaults, options);
        this._closed = false;
        this._enabled = true;
        this.lastQueriedText = "";
        this.lastTypedText = "";
    }
    EventDispatcher.makeEventDispatcher(FindBar.prototype);

Properties

Private

_closed

Type
boolean
Private
    FindBar.prototype._closed = false;
Private

_enabled

Type
boolean
Private
    FindBar.prototype._enabled = true;
Private

_modalBar

Type
?ModalBar
Private
    FindBar.prototype._modalBar = null;
Private

_options

Type
!{multifile: boolean, replace: boolean, queryPlaceholder: string, initialQuery: string, scopeLabel: string}
Private
    FindBar.prototype._options = null;

Methods

Private

$

selector
string
The selector for the element.
Returns:
jQueryObject
The jQuery object for the element, or an empty object if the Find bar isn't yet in the DOM or the element doesn't exist.
    FindBar.prototype.$ = function (selector) {
        if (this._modalBar) {
            return $(selector, this._modalBar.getRoot());
        } else {
            return $();
        }
    };

    // TODO: change IDs to classes
Private

_addElementToSearchHistory

search
string
string that needs to be added to history.
    FindBar.prototype._addElementToSearchHistory = function (searchVal) {
        if (searchVal) {
            var searchHistory = PreferencesManager.getViewState("searchHistory");
            var maxCount = PreferencesManager.get("maxSearchHistory");
            var searchQueryIndex = searchHistory.indexOf(searchVal);
            if (searchQueryIndex !== -1) {
                searchHistory.splice(searchQueryIndex, 1);
            } else {
                if (searchHistory.length === maxCount) {
                    searchHistory.pop();
                }
            }
            searchHistory.unshift(searchVal);
            PreferencesManager.setViewState("searchHistory", searchHistory);
        }
    };
Private

_addShortcutToTooltip

$elem
jQueryObject
The element to add the shortcut to.
commandId
string
The ID for the command whose keyboard shortcut to show.
    FindBar.prototype._addShortcutToTooltip = function ($elem, commandId) {
        var replaceShortcut = KeyBindingManager.getKeyBindings(commandId)[0];
        if (replaceShortcut) {
            var oldTitle = $elem.attr("title");
            oldTitle = (oldTitle ? oldTitle + " " : "");
            $elem.attr("title", oldTitle + "(" + KeyBindingManager.formatKeyDescriptor(replaceShortcut.displayKey) + ")");
        }
    };
Private

_focus

selector
string
The selector for the field.
    FindBar.prototype._focus = function (selector) {
        this.$(selector)
            .focus()
            .get(0).select();
    };
Private

_updatePrefsFromSearchBar

    FindBar.prototype._updatePrefsFromSearchBar = function () {
        PreferencesManager.setViewState("caseSensitive", this.$("#find-case-sensitive").is(".active"));
        PreferencesManager.setViewState("regexp",        this.$("#find-regexp").is(".active"));
    };
Private

_updateSearchBarFromPrefs

    FindBar.prototype._updateSearchBarFromPrefs = function () {
        // Have to make sure we explicitly cast the second parameter to a boolean, because
        // toggleClass expects literal true/false.
        this.$("#find-case-sensitive").toggleClass("active", !!PreferencesManager.getViewState("caseSensitive"));
        this.$("#find-regexp").toggleClass("active", !!PreferencesManager.getViewState("regexp"));
    };

close

Closes this Find bar. If already closed, does nothing.

suppressAnimation
boolean
If true, don't do the standard closing animation. Default false.
    FindBar.prototype.close = function (suppressAnimation) {
        if (this._modalBar) {
            // 1st arg = restore scroll pos; 2nd arg = no animation, since getting replaced immediately
            this._modalBar.close(true, !suppressAnimation);
        }
    };

enable

Enables or disables the controls in the Find bar. Note that if enable is true, all controls will be re-enabled, even if some were previously disabled using enableNavigation() or enableReplace(), so you will need to refresh their enable state after calling this.

enable
boolean
Whether to enable or disable the controls.
    FindBar.prototype.enable = function (enable) {
        this.$("#find-what, #replace-with, #find-prev, #find-next, #find-case-sensitive, #find-regexp").prop("disabled", !enable);
        this._enabled = enable;
    };

    FindBar.prototype.focus = function (enable) {
        this.$("#find-what").focus();
    };

enableNavigation

Enable or disable the navigation controls if present. Note that if the Find bar is currently disabled (i.e. isEnabled() returns false), this will have no effect.

enable
boolean
Whether to enable the controls.
    FindBar.prototype.enableNavigation = function (enable) {
        if (this.isEnabled()) {
            this.$("#find-prev, #find-next").prop("disabled", !enable);
        }
    };

enableReplace

Enable or disable the replace controls if present. Note that if the Find bar is currently disabled (i.e. isEnabled() returns false), this will have no effect.

enable
boolean
Whether to enable the controls.
    FindBar.prototype.enableReplace = function (enable) {
        if (this.isEnabled) {
            this.$("#replace-yes, #replace-batch, #replace-all").prop("disabled", !enable);
        }
    };

focusQuery

Sets focus to the query field and selects its text.

    FindBar.prototype.focusQuery = function () {
        this._focus("#find-what");
    };

focusReplace

Sets focus to the replace field and selects its text.

    FindBar.prototype.focusReplace = function () {
        this._focus("#replace-with");
    };

getOptions

Returns:
Object
The options passed into the FindBar.
    FindBar.prototype.getOptions = function () {
        return this._options;
    };

getQueryInfo

Returns the current query and parameters.

Returns:
{query: string,caseSensitive: boolean,isRegexp: boolean}
    FindBar.prototype.getQueryInfo = function () {
        return {
            query:           this.$("#find-what").val() || "",
            isCaseSensitive: this.$("#find-case-sensitive").is(".active"),
            isRegexp:        this.$("#find-regexp").is(".active")
        };
    };

getReplaceText

Returns the current replace text.

Returns:
string
    FindBar.prototype.getReplaceText = function () {
        return this.$("#replace-with").val() || "";
    };

isClosed

Returns:
boolean
true if this FindBar has been closed.
    FindBar.prototype.isClosed = function () {
        return this._closed;
    };

isEnabled

Returns:
boolean
true if the FindBar is enabled.
    FindBar.prototype.isEnabled = function () {
        return this._enabled;
    };

isReplaceEnabled

Returns:
boolean
true if the Replace button is enabled.
    FindBar.prototype.isReplaceEnabled = function () {
        return this.$("#replace-yes").is(":enabled");
    };

open

Opens the Find bar, closing any other existing Find bars.

    FindBar.prototype.open = function () {
        var self = this;

        // Normally, creating a new Find bar will simply cause the old one to close
        // automatically. This can cause timing issues because the focus change might
        // cause the new one to think it should close, too. So we simply explicitly
        // close the old Find bar (with no animation) before creating a new one.
        // TODO: see note above - this will move to ModalBar eventually.
        FindBar._closeFindBars();
        if (this._options.multifile) {
            HealthLogger.searchDone(HealthLogger.SEARCH_NEW);
        }

        var templateVars = _.clone(this._options);
        templateVars.Strings = Strings;
        templateVars.replaceBatchLabel = (templateVars.multifile ? Strings.BUTTON_REPLACE_ALL_IN_FILES : Strings.BUTTON_REPLACE_BATCH);
        templateVars.replaceAllLabel = Strings.BUTTON_REPLACE_ALL;
        
        self._addElementToSearchHistory(this._options.initialQuery);

        this._modalBar = new ModalBar(Mustache.render(_searchBarTemplate, templateVars), true);  // 2nd arg = auto-close on Esc/blur

        // When the ModalBar closes, clean ourselves up.
        this._modalBar.on("close", function (event) {
            // Hide error popup, since it hangs down low enough to make the slide-out look awkward
            self.showError(null);
            self._modalBar = null;
            self._closed = true;
            window.clearInterval(intervalId);
            intervalId = 0;
            lastTypedTime = 0;
            FindBar._removeFindBar(self);
            MainViewManager.focusActivePane();
            self.trigger("close");
            if (self.searchField) {
                self.searchField.destroy();
            }
        });

        FindBar._addFindBar(this);

        var $root = this._modalBar.getRoot();
        var historyIndex = 0;
        $root
            .on("input", "#find-what", function () {
                self.trigger("queryChange");
                lastTypedText = self.getQueryInfo().query;
            })
            .on("click", "#find-case-sensitive, #find-regexp", function (e) {
                $(e.currentTarget).toggleClass("active");
                self._updatePrefsFromSearchBar();
                self.trigger("queryChange");
                if (self._options.multifile) {  //instant search
                    self.trigger("doFind");
                }
            })
            .on("click", ".dropdown-icon", function (e) {
                var quickSearchContainer = $(".quick-search-container");
                if (!self.searchField) {
                    self.showSearchHints();
                } else if (quickSearchContainer.is(':visible')) {
                    quickSearchContainer.hide();
                } else {
                    self.searchField.setText(self.$("#find-what").val());
                    quickSearchContainer.show();
                }
                self.$("#find-what").focus();
            })
            .on("keydown", "#find-what, #replace-with", function (e) {
                lastTypedTime = new Date().getTime();
                lastKeyCode = e.keyCode;
                var executeSearchIfNeeded = function () {
                    // We only do instant search via node.
                    if (FindUtils.isNodeSearchDisabled() || FindUtils.isInstantSearchDisabled()) {
                        // we still keep the intrval timer up as instant search could get enabled/disabled based on node busy state
                        return;
                    }
                    if (self._closed) {
                        return;
                    }
                    currentTime = new Date().getTime();
                    if (lastTypedTime && (currentTime - lastTypedTime >= 100) && self.getQueryInfo().query !==  lastQueriedText &&
                            !FindUtils.isNodeSearchInProgress()) {
                        // init Search
                        if (self._options.multifile) {
                            if ($(e.target).is("#find-what")) {
                                if (!self._options.replace) {
                                    HealthLogger.searchDone(HealthLogger.SEARCH_INSTANT);
                                    self.trigger("doFind");
                                    lastQueriedText = self.getQueryInfo().query;
                                }
                            }
                        }
                    }
                };
                if (intervalId === 0) {
                    intervalId = window.setInterval(executeSearchIfNeeded, 50);
                }
                if (e.keyCode === KeyEvent.DOM_VK_RETURN) {
                    e.preventDefault();
                    e.stopPropagation();
                    self._addElementToSearchHistory(self.$("#find-what").val());
                    lastQueriedText = self.getQueryInfo().query;
                    if (self._options.multifile) {
                        if ($(e.target).is("#find-what")) {
                            if (self._options.replace) {
                                // Just set focus to the Replace field.
                                self.focusReplace();
                            } else {
                                HealthLogger.searchDone(HealthLogger.SEARCH_ON_RETURN_KEY);
                                // Trigger a Find (which really means "Find All" in this context).
                                self.trigger("doFind");
                            }
                        } else {
                            HealthLogger.searchDone(HealthLogger.SEARCH_REPLACE_ALL);
                            self.trigger("doReplaceBatch");
                        }
                    } else {
                        // In the single file case, we just want to trigger a Find Next (or Find Previous
                        // if Shift is held down).
                        self.trigger("doFind", e.shiftKey);
                    }
                    historyIndex = 0;
                } else if (e.keyCode === KeyEvent.DOM_VK_DOWN || e.keyCode === KeyEvent.DOM_VK_UP) {
                    var quickSearchContainer = $(".quick-search-container");
                    if (!self.searchField) {
                        self.showSearchHints();
                    } else if (!quickSearchContainer.is(':visible')) {
                        quickSearchContainer.show();
                    }
                }
            });

        if (!this._options.multifile) {
            this._addShortcutToTooltip($("#find-next"), Commands.CMD_FIND_NEXT);
            this._addShortcutToTooltip($("#find-prev"), Commands.CMD_FIND_PREVIOUS);
            $root
                .on("click", "#find-next", function (e) {
                    self.trigger("doFind", false);
                })
                .on("click", "#find-prev", function (e) {
                    self.trigger("doFind", true);
                });
        }

        if (this._options.replace) {
            this._addShortcutToTooltip($("#replace-yes"), Commands.CMD_REPLACE);
            $root
                .on("click", "#replace-yes", function (e) {
                    self.trigger("doReplace");
                })
                .on("click", "#replace-batch", function (e) {
                    self.trigger("doReplaceBatch");
                })
                .on("click", "#replace-all", function (e) {
                    self.trigger("doReplaceAll");
                })
                // One-off hack to make Find/Replace fields a self-contained tab cycle
                // TODO: remove once https://trello.com/c/lTSJgOS2 implemented
                .on("keydown", function (e) {
                    if (e.keyCode === KeyEvent.DOM_VK_TAB && !e.ctrlKey && !e.metaKey && !e.altKey) {
                        if (e.target.id === "replace-with" && !e.shiftKey) {
                            self.$("#find-what").focus();
                            e.preventDefault();
                        } else if (e.target.id === "find-what" && e.shiftKey) {
                            self.$("#replace-with").focus();
                            e.preventDefault();
                        }
                    }
                });
        }

        if (this._options.multifile && FindUtils.isIndexingInProgress()) {
            this.showIndexingSpinner();
        }

        // Set up the initial UI state.
        this._updateSearchBarFromPrefs();
        this.focusQuery();
    };

redoInstantSearch

Force a search again

    FindBar.prototype.redoInstantSearch = function () {
        this.trigger("doFind");
    };

showError

Show or clear an error message related to the query.

error
nullable
string
The error message to show, or null to hide the error display.
isHTML
optional
boolean
Whether the error message is HTML that should remain unescaped.
    FindBar.prototype.showError = function (error, isHTML) {
        var $error = this.$(".error");
        if (error) {
            if (isHTML) {
                $error.html(error);
            } else {
                $error.text(error);
            }
            $error.show();
        } else {
            $error.hide();
        }
    };

showFindCount

Set the find count.

count
string
The find count message to show. Can be the empty string to hide it.
    FindBar.prototype.showFindCount = function (count) {
        this.$("#find-counter").text(count);
    };

showIndexingSpinner

The indexing spinner is usually shown when node is indexing files

    FindBar.prototype.showIndexingSpinner = function () {
        this.$("#indexing-spinner").removeClass("forced-hidden");
    };

    FindBar.prototype.hideIndexingSpinner = function () {
        this.$("#indexing-spinner").addClass("forced-hidden");
    };

showNoResults

Show or hide the no-results indicator and optional message. This is also used to indicate regular expression errors.

showIndicator
boolean
showMessage
boolean
    FindBar.prototype.showNoResults = function (showIndicator, showMessage) {
        ViewUtils.toggleClass(this.$("#find-what"), "no-results", showIndicator);

        var $msg = this.$(".no-results-message");
        if (showMessage) {
            $msg.show();
        } else {
            $msg.hide();
        }
    };
Private

showSearchHints

    FindBar.prototype.showSearchHints = function () {
        var self = this;
        var searchFieldInput = self.$("#find-what");
        this.searchField = new QuickSearchField(searchFieldInput, {
            verticalAdjust: searchFieldInput.offset().top > 0 ? 0 : this._modalBar.getRoot().outerHeight(),
            maxResults: 20,
            firstHighlightIndex: null,
            resultProvider: function (query) {
                var asyncResult = new $.Deferred();
                asyncResult.resolve(PreferencesManager.getViewState("searchHistory"));
                return asyncResult.promise();
            },
            formatter: function (item, query) {
                return "<li>" + item + "</li>";
            },
            onCommit: function (selectedItem, query) {
                if (selectedItem) {
                    self.$("#find-what").val(selectedItem);
                    self.trigger("queryChange");
                } else if (query.length) {
                    self.searchField.setText(query);
                }
                self.$("#find-what").focus();
                $(".quick-search-container").hide();
            },
            onHighlight: function (selectedItem, query, explicit) {},
            highlightZeroResults: false
        });
        this.searchField.setText(searchFieldInput.val());
    };