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