diff -r 000000000000 -r 6474c204b198 browser/components/tabview/search.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/components/tabview/search.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,616 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* ****************************** + * + * This file incorporates work from: + * Quicksilver Score (qs_score): + * http://rails-oceania.googlecode.com/svn/lachiecox/qs_score/trunk/qs_score.js + * This incorporated work is covered by the following copyright and + * permission notice: + * Copyright 2008 Lachie Cox + * Licensed under the MIT license. + * http://jquery.org/license + * + * ***************************** */ + +// ********** +// Title: search.js +// Implementation for the search functionality of Firefox Panorama. + +// ########## +// Class: TabUtils +// +// A collection of helper functions for dealing with both s and +// s without having to worry which one is which. +let TabUtils = { + // ---------- + // Function: toString + // Prints [TabUtils] for debug use. + toString: function TabUtils_toString() { + return "[TabUtils]"; + }, + + // --------- + // Function: nameOfTab + // Given a or a returns the tab's name. + nameOf: function TabUtils_nameOf(tab) { + // We can have two types of tabs: A or a + // because we have to deal with both tabs represented inside + // of active Panoramas as well as for windows in which + // Panorama has yet to be activated. We uses object sniffing to + // determine the type of tab and then returns its name. + return tab.label != undefined ? tab.label : tab.$tabTitle[0].textContent; + }, + + // --------- + // Function: URLOf + // Given a or a returns the URL of tab. + URLOf: function TabUtils_URLOf(tab) { + // Convert a to + if ("tab" in tab) + tab = tab.tab; + return tab.linkedBrowser.currentURI.spec; + }, + + // --------- + // Function: faviconURLOf + // Given a or a returns the URL of tab's favicon. + faviconURLOf: function TabUtils_faviconURLOf(tab) { + return tab.image != undefined ? tab.image : tab.$favImage[0].src; + }, + + // --------- + // Function: focus + // Given a or a , focuses it and it's window. + focus: function TabUtils_focus(tab) { + // Convert a to a + if ("tab" in tab) + tab = tab.tab; + tab.ownerDocument.defaultView.gBrowser.selectedTab = tab; + tab.ownerDocument.defaultView.focus(); + } +}; + +// ########## +// Class: TabMatcher +// +// A class that allows you to iterate over matching and not-matching tabs, +// given a case-insensitive search term. +function TabMatcher(term) { + this.term = term; +} + +TabMatcher.prototype = { + // ---------- + // Function: toString + // Prints [TabMatcher (term)] for debug use. + toString: function TabMatcher_toString() { + return "[TabMatcher (" + this.term + ")]"; + }, + + // --------- + // Function: _filterAndSortForMatches + // Given an array of s and s returns a new array + // of tabs whose name matched the search term, sorted by lexical + // closeness. + _filterAndSortForMatches: function TabMatcher__filterAndSortForMatches(tabs) { + let self = this; + tabs = tabs.filter(function TabMatcher__filterAndSortForMatches_filter(tab) { + let name = TabUtils.nameOf(tab); + let url = TabUtils.URLOf(tab); + return name.match(self.term, "i") || url.match(self.term, "i"); + }); + + tabs.sort(function TabMatcher__filterAndSortForMatches_sort(x, y) { + let yScore = self._scorePatternMatch(self.term, TabUtils.nameOf(y)); + let xScore = self._scorePatternMatch(self.term, TabUtils.nameOf(x)); + return yScore - xScore; + }); + + return tabs; + }, + + // --------- + // Function: _filterForUnmatches + // Given an array of s returns an unsorted array of tabs whose name + // does not match the the search term. + _filterForUnmatches: function TabMatcher__filterForUnmatches(tabs) { + let self = this; + return tabs.filter(function TabMatcher__filterForUnmatches_filter(tab) { + let name = tab.$tabTitle[0].textContent; + let url = TabUtils.URLOf(tab); + return !name.match(self.term, "i") && !url.match(self.term, "i"); + }); + }, + + // --------- + // Function: _getTabsForOtherWindows + // Returns an array of s and s representing tabs + // from all windows but the current window. s will be returned + // for windows in which Panorama has been activated at least once, while + // s will be returned for windows in which Panorama has never + // been activated. + _getTabsForOtherWindows: function TabMatcher__getTabsForOtherWindows() { + let enumerator = Services.wm.getEnumerator("navigator:browser"); + let allTabs = []; + + while (enumerator.hasMoreElements()) { + let win = enumerator.getNext(); + // This function gets tabs from other windows, not from the current window + if (win != gWindow) + allTabs.push.apply(allTabs, win.gBrowser.tabs); + } + return allTabs; + }, + + // ---------- + // Function: matchedTabsFromOtherWindows + // Returns an array of s and s that match the search term + // from all windows but the current window. s will be returned for + // windows in which Panorama has been activated at least once, while + // s will be returned for windows in which Panorama has never + // been activated. + // (new TabMatcher("app")).matchedTabsFromOtherWindows(); + matchedTabsFromOtherWindows: function TabMatcher_matchedTabsFromOtherWindows() { + if (this.term.length < 2) + return []; + + let tabs = this._getTabsForOtherWindows(); + return this._filterAndSortForMatches(tabs); + }, + + // ---------- + // Function: matched + // Returns an array of s which match the current search term. + // If the term is less than 2 characters in length, it returns nothing. + matched: function TabMatcher_matched() { + if (this.term.length < 2) + return []; + + let tabs = TabItems.getItems(); + return this._filterAndSortForMatches(tabs); + }, + + // ---------- + // Function: unmatched + // Returns all of s that .matched() doesn't return. + unmatched: function TabMatcher_unmatched() { + let tabs = TabItems.getItems(); + if (this.term.length < 2) + return tabs; + + return this._filterForUnmatches(tabs); + }, + + // ---------- + // Function: doSearch + // Performs the search. Lets you provide three functions. + // The first is on all matched tabs in the window, the second on all unmatched + // tabs in the window, and the third on all matched tabs in other windows. + // The first two functions take two parameters: A and its integer index + // indicating the absolute rank of the in terms of match to + // the search term. The last function also takes two paramaters, but can be + // passed both s and s and the index is offset by the + // number of matched tabs inside the window. + doSearch: function TabMatcher_doSearch(matchFunc, unmatchFunc, otherFunc) { + let matches = this.matched(); + let unmatched = this.unmatched(); + let otherMatches = this.matchedTabsFromOtherWindows(); + + matches.forEach(function(tab, i) { + matchFunc(tab, i); + }); + + otherMatches.forEach(function(tab,i) { + otherFunc(tab, i+matches.length); + }); + + unmatched.forEach(function(tab, i) { + unmatchFunc(tab, i); + }); + }, + + // ---------- + // Function: _scorePatternMatch + // Given a pattern string, returns a score between 0 and 1 of how well + // that pattern matches the original string. It mimics the heuristics + // of the Mac application launcher Quicksilver. + _scorePatternMatch: function TabMatcher__scorePatternMatch(pattern, matched, offset) { + offset = offset || 0; + pattern = pattern.toLowerCase(); + matched = matched.toLowerCase(); + + if (pattern.length == 0) + return 0.9; + if (pattern.length > matched.length) + return 0.0; + + for (let i = pattern.length; i > 0; i--) { + let sub_pattern = pattern.substring(0,i); + let index = matched.indexOf(sub_pattern); + + if (index < 0) + continue; + if (index + pattern.length > matched.length + offset) + continue; + + let next_string = matched.substring(index+sub_pattern.length); + let next_pattern = null; + + if (i >= pattern.length) + next_pattern = ''; + else + next_pattern = pattern.substring(i); + + let remaining_score = this._scorePatternMatch(next_pattern, next_string, offset + index); + + if (remaining_score > 0) { + let score = matched.length-next_string.length; + + if (index != 0) { + let c = matched.charCodeAt(index-1); + if (c == 32 || c == 9) { + for (let j = (index - 2); j >= 0; j--) { + c = matched.charCodeAt(j); + score -= ((c == 32 || c == 9) ? 1 : 0.15); + } + } else { + score -= index; + } + } + + score += remaining_score * next_string.length; + score /= matched.length; + return score; + } + } + return 0.0; + } +}; + +// ########## +// Class: TabHandlers +// +// A object that handles all of the event handlers. +let TabHandlers = { + _mouseDownLocation: null, + + // --------- + // Function: onMatch + // Adds styles and event listeners to the matched tab items. + onMatch: function TabHandlers_onMatch(tab, index) { + tab.addClass("onTop"); + index != 0 ? tab.addClass("notMainMatch") : tab.removeClass("notMainMatch"); + + // Remove any existing handlers before adding the new ones. + // If we don't do this, then we may add more handlers than + // we remove. + tab.$canvas + .unbind("mousedown", TabHandlers._hideHandler) + .unbind("mouseup", TabHandlers._showHandler); + + tab.$canvas + .mousedown(TabHandlers._hideHandler) + .mouseup(TabHandlers._showHandler); + }, + + // --------- + // Function: onUnmatch + // Removes styles and event listeners from the unmatched tab items. + onUnmatch: function TabHandlers_onUnmatch(tab, index) { + tab.$container.removeClass("onTop"); + tab.removeClass("notMainMatch"); + + tab.$canvas + .unbind("mousedown", TabHandlers._hideHandler) + .unbind("mouseup", TabHandlers._showHandler); + }, + + // --------- + // Function: onOther + // Removes styles and event listeners from the unmatched tabs. + onOther: function TabHandlers_onOther(tab, index) { + // Unlike the other on* functions, in this function tab can + // either be a or a . In other functions + // it is always a . Also note that index is offset + // by the number of matches within the window. + let item = iQ("
") + .addClass("inlineMatch") + .click(function TabHandlers_onOther_click(event) { + Search.hide(event); + TabUtils.focus(tab); + }); + + iQ("") + .attr("src", TabUtils.faviconURLOf(tab)) + .appendTo(item); + + iQ("") + .text(TabUtils.nameOf(tab)) + .appendTo(item); + + index != 0 ? item.addClass("notMainMatch") : item.removeClass("notMainMatch"); + item.appendTo("#results"); + iQ("#otherresults").show(); + }, + + // --------- + // Function: _hideHandler + // Performs when mouse down on a canvas of tab item. + _hideHandler: function TabHandlers_hideHandler(event) { + iQ("#search").fadeOut(); + iQ("#searchshade").fadeOut(); + TabHandlers._mouseDownLocation = {x:event.clientX, y:event.clientY}; + }, + + // --------- + // Function: _showHandler + // Performs when mouse up on a canvas of tab item. + _showHandler: function TabHandlers_showHandler(event) { + // If the user clicks on a tab without moving the mouse then + // they are zooming into the tab and we need to exit search + // mode. + if (TabHandlers._mouseDownLocation.x == event.clientX && + TabHandlers._mouseDownLocation.y == event.clientY) { + Search.hide(); + return; + } + + iQ("#searchshade").show(); + iQ("#search").show(); + iQ("#searchbox")[0].focus(); + // Marshal the search. + setTimeout(Search.perform, 0); + } +}; + +// ########## +// Class: Search +// +// A object that handles the search feature. +let Search = { + _initiatedBy: "", + _blockClick: false, + _currentHandler: null, + + // ---------- + // Function: toString + // Prints [Search] for debug use. + toString: function Search_toString() { + return "[Search]"; + }, + + // ---------- + // Function: init + // Initializes the searchbox to be focused, and everything else to be hidden, + // and to have everything have the appropriate event handlers. + init: function Search_init() { + let self = this; + + iQ("#search").hide(); + iQ("#searchshade").hide().mousedown(function Search_init_shade_mousedown(event) { + if (event.target.id != "searchbox" && !self._blockClick) + self.hide(); + }); + + iQ("#searchbox").keyup(function Search_init_box_keyup() { + self.perform(); + }) + .attr("title", tabviewString("button.searchTabs")); + + iQ("#searchbutton").mousedown(function Search_init_button_mousedown() { + self._initiatedBy = "buttonclick"; + self.ensureShown(); + self.switchToInMode(); + }) + .attr("title", tabviewString("button.searchTabs")); + + window.addEventListener("focus", function Search_init_window_focus() { + if (self.isEnabled()) { + self._blockClick = true; + setTimeout(function() { + self._blockClick = false; + }, 0); + } + }, false); + + this.switchToBeforeMode(); + }, + + // ---------- + // Function: _beforeSearchKeyHandler + // Handles all keydown before the search interface is brought up. + _beforeSearchKeyHandler: function Search__beforeSearchKeyHandler(event) { + // Only match reasonable text-like characters for quick search. + if (event.altKey || event.ctrlKey || event.metaKey) + return; + + if ((event.keyCode > 0 && event.keyCode <= event.DOM_VK_DELETE) || + event.keyCode == event.DOM_VK_CONTEXT_MENU || + event.keyCode == event.DOM_VK_SLEEP || + (event.keyCode >= event.DOM_VK_F1 && + event.keyCode <= event.DOM_VK_SCROLL_LOCK) || + event.keyCode == event.DOM_VK_META || + event.keyCode == 91 || // 91 = left windows key + event.keyCode == 92 || // 92 = right windows key + (!event.keyCode && !event.charCode)) { + return; + } + + // If we are already in an input field, allow typing as normal. + if (event.target.nodeName == "INPUT") + return; + + // / is used to activate the search feature so the key shouldn't be entered + // into the search box. + if (event.keyCode == KeyEvent.DOM_VK_SLASH) { + event.stopPropagation(); + event.preventDefault(); + } + + this.switchToInMode(); + this._initiatedBy = "keydown"; + this.ensureShown(true); + }, + + // ---------- + // Function: _inSearchKeyHandler + // Handles all keydown while search mode. + _inSearchKeyHandler: function Search__inSearchKeyHandler(event) { + let term = iQ("#searchbox").val(); + if ((event.keyCode == event.DOM_VK_ESCAPE) || + (event.keyCode == event.DOM_VK_BACK_SPACE && term.length <= 1 && + this._initiatedBy == "keydown")) { + this.hide(event); + return; + } + + let matcher = this.createSearchTabMatcher(); + let matches = matcher.matched(); + let others = matcher.matchedTabsFromOtherWindows(); + if (event.keyCode == event.DOM_VK_RETURN && + (matches.length > 0 || others.length > 0)) { + this.hide(event); + if (matches.length > 0) + matches[0].zoomIn(); + else + TabUtils.focus(others[0]); + } + }, + + // ---------- + // Function: switchToBeforeMode + // Make sure the event handlers are appropriate for the before-search mode. + switchToBeforeMode: function Search_switchToBeforeMode() { + let self = this; + if (this._currentHandler) + iQ(window).unbind("keydown", this._currentHandler); + this._currentHandler = function Search_switchToBeforeMode_handler(event) { + self._beforeSearchKeyHandler(event); + } + iQ(window).keydown(this._currentHandler); + }, + + // ---------- + // Function: switchToInMode + // Make sure the event handlers are appropriate for the in-search mode. + switchToInMode: function Search_switchToInMode() { + let self = this; + if (this._currentHandler) + iQ(window).unbind("keydown", this._currentHandler); + this._currentHandler = function Search_switchToInMode_handler(event) { + self._inSearchKeyHandler(event); + } + iQ(window).keydown(this._currentHandler); + }, + + createSearchTabMatcher: function Search_createSearchTabMatcher() { + return new TabMatcher(iQ("#searchbox").val()); + }, + + // ---------- + // Function: isEnabled + // Checks whether search mode is enabled or not. + isEnabled: function Search_isEnabled() { + return iQ("#search").css("display") != "none"; + }, + + // ---------- + // Function: hide + // Hides search mode. + hide: function Search_hide(event) { + if (!this.isEnabled()) + return; + + iQ("#searchbox").val(""); + iQ("#searchshade").hide(); + iQ("#search").hide(); + + iQ("#searchbutton").css({ opacity:.8 }); + +#ifdef XP_MACOSX + UI.setTitlebarColors(true); +#endif + + this.perform(); + this.switchToBeforeMode(); + + if (event) { + // when hiding the search mode, we need to prevent the keypress handler + // in UI__setTabViewFrameKeyHandlers to handle the key press again. e.g. Esc + // which is already handled by the key down in this class. + if (event.type == "keydown") + UI.ignoreKeypressForSearch = true; + event.preventDefault(); + event.stopPropagation(); + } + + // Return focus to the tab window + UI.blurAll(); + gTabViewFrame.contentWindow.focus(); + + let newEvent = document.createEvent("Events"); + newEvent.initEvent("tabviewsearchdisabled", false, false); + dispatchEvent(newEvent); + }, + + // ---------- + // Function: perform + // Performs a search. + perform: function Search_perform() { + let matcher = this.createSearchTabMatcher(); + + // Remove any previous other-window search results and + // hide the display area. + iQ("#results").empty(); + iQ("#otherresults").hide(); + iQ("#otherresults>.label").text(tabviewString("search.otherWindowTabs")); + + matcher.doSearch(TabHandlers.onMatch, TabHandlers.onUnmatch, TabHandlers.onOther); + }, + + // ---------- + // Function: ensureShown + // Ensures the search feature is displayed. If not, display it. + // Parameters: + // - a boolean indicates whether this is triggered by a keypress or not + ensureShown: function Search_ensureShown(activatedByKeypress) { + let $search = iQ("#search"); + let $searchShade = iQ("#searchshade"); + let $searchbox = iQ("#searchbox"); + iQ("#searchbutton").css({ opacity: 1 }); + + // NOTE: when this function is called by keydown handler, next keypress + // event or composition events of IME will be fired on the focused editor. + function dispatchTabViewSearchEnabledEvent() { + let newEvent = document.createEvent("Events"); + newEvent.initEvent("tabviewsearchenabled", false, false); + dispatchEvent(newEvent); + }; + + if (!this.isEnabled()) { + $searchShade.show(); + $search.show(); + +#ifdef XP_MACOSX + UI.setTitlebarColors({active: "#717171", inactive: "#EDEDED"}); +#endif + + if (activatedByKeypress) { + // set the focus so key strokes are entered into the textbox. + $searchbox[0].focus(); + dispatchTabViewSearchEnabledEvent(); + } else { + // marshal the focusing, otherwise it ends up with searchbox[0].focus gets + // called before the search button gets the focus after being pressed. + setTimeout(function setFocusAndDispatchSearchEnabledEvent() { + $searchbox[0].focus(); + dispatchTabViewSearchEnabledEvent(); + }, 0); + } + } + } +}; +