Modules (188)

FindBar

Description

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

Dependencies

Variables

Private

_searchBarTemplate

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

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

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 = "";
        this.lastTypedTextWasRegexp = false;
    }
    EventDispatcher.makeEventDispatcher(FindBar.prototype);

Properties

Private

_closed

Type
boolean
    FindBar.prototype._closed = false;
Private

_enabled

Type
boolean
    FindBar.prototype._enabled = true;
Private

_modalBar

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

_options

Type
!{multifile: boolean, replace: boolean, queryPlaceholder: string, initialQuery: string, scopeLabel: string}
    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

_addFindBar STATIC

findBar non-nullable FindBar
The find bar to register.
    FindBar._addFindBar = function (findBar) {
        FindBar._bars = FindBar._bars || [];
        FindBar._bars.push(findBar);
    };
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

_closeFindBars STATIC

    FindBar._closeFindBars = function () {
        var bars = FindBar._bars;
        if (bars) {
            bars.forEach(function (bar) {
                bar.close(true, false);
            });
            bars = [];
        }
    };
Private

_focus

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

_getInitialQueryFromSelection STATIC

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
    FindBar._getInitialQueryFromSelection = function(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 "";
    };
Private

_removeFindBar STATIC

findBar FindBar
The bar to remove.
    FindBar._removeFindBar = function (findBar) {
        if (FindBar._bars) {
            _.pull(FindBar._bars, findBar);
        }
    };
Private

_updatePrefsFromSearchBar

    FindBar.prototype._updatePrefsFromSearchBar = function () {
        var isRegexp = this.$("#find-regexp").is(".active");
        PreferencesManager.setViewState("caseSensitive", this.$("#find-case-sensitive").is(".active"));
        PreferencesManager.setViewState("regexp", isRegexp);
        lastTypedTextWasRegexp = isRegexp;
    };
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");
    };

getInitialQuery STATIC

Gets you the right query and replace text to prepopulate the Find Bar.

currentFindBar nullable FindBar
The currently open Find Bar, if any
The nullable Editor
active editor, if any
Returns: query: string,replaceText: string
Query and Replace text to prepopulate the Find Bar with
static
    FindBar.getInitialQuery = function (currentFindBar, editor) {
        var query,
            selection = editor ? FindBar._getInitialQueryFromSelection(editor) : "",
            replaceText = "";

        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.
            var queryInfo = currentFindBar.getQueryInfo();
            query = (!queryInfo.isRegexp && selection) || queryInfo.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 = (!lastTypedTextWasRegexp && selection) || lastQueriedText || 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;
});

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),
            !!PreferencesManager.get('autoHideSearch')		// 2nd arg = auto-close on Esc/blur
        );
        
        // Done this way because ModalBar.js seems to react unreliably when
        // modifying it to handle the escape key - the findbar wasn't getting
        // closed as it should, instead persisting in the background
        function _handleKeydown(e) {
            if (e.keyCode === KeyEvent.DOM_VK_ESCAPE) {
                e.stopPropagation();
                e.preventDefault();
                self.close();
            }
        }
        window.document.body.addEventListener("keydown", _handleKeydown, true);
        
        // When the ModalBar closes, clean ourselves up.
        this._modalBar.on("close", function (event) {
            window.document.body.removeEventListener("keydown", _handleKeydown, true);

            // 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");
                var queryInfo = self.getQueryInfo();
                lastTypedText = queryInfo.query;
                lastTypedTextWasRegexp = queryInfo.isRegexp;
            })
            .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 interval 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();
                    }
                }
            })
            .on("click", ".close", function () {
                self.close();
            });

        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());
    };