browser/components/tabview/search.js

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 /* ******************************
     6  *
     7  * This file incorporates work from:
     8  * Quicksilver Score (qs_score):
     9  * http://rails-oceania.googlecode.com/svn/lachiecox/qs_score/trunk/qs_score.js
    10  * This incorporated work is covered by the following copyright and
    11  * permission notice:
    12  * Copyright 2008 Lachie Cox
    13  * Licensed under the MIT license.
    14  * http://jquery.org/license
    15  *
    16  *  ***************************** */
    18 // **********
    19 // Title: search.js
    20 // Implementation for the search functionality of Firefox Panorama.
    22 // ##########
    23 // Class: TabUtils
    24 //
    25 // A collection of helper functions for dealing with both <TabItem>s and
    26 // <xul:tab>s without having to worry which one is which.
    27 let TabUtils = {
    28   // ----------
    29   // Function: toString
    30   // Prints [TabUtils] for debug use.
    31   toString: function TabUtils_toString() {
    32     return "[TabUtils]";
    33   },
    35   // ---------
    36   // Function: nameOfTab
    37   // Given a <TabItem> or a <xul:tab> returns the tab's name.
    38   nameOf: function TabUtils_nameOf(tab) {
    39     // We can have two types of tabs: A <TabItem> or a <xul:tab>
    40     // because we have to deal with both tabs represented inside
    41     // of active Panoramas as well as for windows in which
    42     // Panorama has yet to be activated. We uses object sniffing to
    43     // determine the type of tab and then returns its name.     
    44     return tab.label != undefined ? tab.label : tab.$tabTitle[0].textContent;
    45   },
    47   // ---------
    48   // Function: URLOf
    49   // Given a <TabItem> or a <xul:tab> returns the URL of tab.
    50   URLOf: function TabUtils_URLOf(tab) {
    51     // Convert a <TabItem> to <xul:tab>
    52     if ("tab" in tab)
    53       tab = tab.tab;
    54     return tab.linkedBrowser.currentURI.spec;
    55   },
    57   // ---------
    58   // Function: faviconURLOf
    59   // Given a <TabItem> or a <xul:tab> returns the URL of tab's favicon.
    60   faviconURLOf: function TabUtils_faviconURLOf(tab) {
    61     return tab.image != undefined ? tab.image : tab.$favImage[0].src;
    62   },
    64   // ---------
    65   // Function: focus
    66   // Given a <TabItem> or a <xul:tab>, focuses it and it's window.
    67   focus: function TabUtils_focus(tab) {
    68     // Convert a <TabItem> to a <xul:tab>
    69     if ("tab" in tab)
    70       tab = tab.tab;
    71     tab.ownerDocument.defaultView.gBrowser.selectedTab = tab;
    72     tab.ownerDocument.defaultView.focus();
    73   }
    74 };
    76 // ##########
    77 // Class: TabMatcher
    78 //
    79 // A class that allows you to iterate over matching and not-matching tabs, 
    80 // given a case-insensitive search term.
    81 function TabMatcher(term) {
    82   this.term = term;
    83 }
    85 TabMatcher.prototype = {
    86   // ----------
    87   // Function: toString
    88   // Prints [TabMatcher (term)] for debug use.
    89   toString: function TabMatcher_toString() {
    90     return "[TabMatcher (" + this.term + ")]";
    91   },
    93   // ---------
    94   // Function: _filterAndSortForMatches
    95   // Given an array of <TabItem>s and <xul:tab>s returns a new array
    96   // of tabs whose name matched the search term, sorted by lexical
    97   // closeness.
    98   _filterAndSortForMatches: function TabMatcher__filterAndSortForMatches(tabs) {
    99     let self = this;
   100     tabs = tabs.filter(function TabMatcher__filterAndSortForMatches_filter(tab) {
   101       let name = TabUtils.nameOf(tab);
   102       let url = TabUtils.URLOf(tab);
   103       return name.match(self.term, "i") || url.match(self.term, "i");
   104     });
   106     tabs.sort(function TabMatcher__filterAndSortForMatches_sort(x, y) {
   107       let yScore = self._scorePatternMatch(self.term, TabUtils.nameOf(y));
   108       let xScore = self._scorePatternMatch(self.term, TabUtils.nameOf(x));
   109       return yScore - xScore;
   110     });
   112     return tabs;
   113   },
   115   // ---------
   116   // Function: _filterForUnmatches
   117   // Given an array of <TabItem>s returns an unsorted array of tabs whose name
   118   // does not match the the search term.
   119   _filterForUnmatches: function TabMatcher__filterForUnmatches(tabs) {
   120     let self = this;
   121     return tabs.filter(function TabMatcher__filterForUnmatches_filter(tab) {
   122       let name = tab.$tabTitle[0].textContent;
   123       let url = TabUtils.URLOf(tab);
   124       return !name.match(self.term, "i") && !url.match(self.term, "i");
   125     });
   126   },
   128   // ---------
   129   // Function: _getTabsForOtherWindows
   130   // Returns an array of <TabItem>s and <xul:tabs>s representing tabs
   131   // from all windows but the current window. <TabItem>s will be returned
   132   // for windows in which Panorama has been activated at least once, while
   133   // <xul:tab>s will be returned for windows in which Panorama has never
   134   // been activated.
   135   _getTabsForOtherWindows: function TabMatcher__getTabsForOtherWindows() {
   136     let enumerator = Services.wm.getEnumerator("navigator:browser");
   137     let allTabs = [];
   139     while (enumerator.hasMoreElements()) {
   140       let win = enumerator.getNext();
   141       // This function gets tabs from other windows, not from the current window
   142       if (win != gWindow)
   143         allTabs.push.apply(allTabs, win.gBrowser.tabs);
   144     }
   145     return allTabs;
   146   },
   148   // ----------
   149   // Function: matchedTabsFromOtherWindows
   150   // Returns an array of <TabItem>s and <xul:tab>s that match the search term
   151   // from all windows but the current window. <TabItem>s will be returned for
   152   // windows in which Panorama has been activated at least once, while
   153   // <xul:tab>s will be returned for windows in which Panorama has never
   154   // been activated.
   155   // (new TabMatcher("app")).matchedTabsFromOtherWindows();
   156   matchedTabsFromOtherWindows: function TabMatcher_matchedTabsFromOtherWindows() {
   157     if (this.term.length < 2)
   158       return [];
   160     let tabs = this._getTabsForOtherWindows();
   161     return this._filterAndSortForMatches(tabs);
   162   },
   164   // ----------
   165   // Function: matched
   166   // Returns an array of <TabItem>s which match the current search term.
   167   // If the term is less than 2 characters in length, it returns nothing.
   168   matched: function TabMatcher_matched() {
   169     if (this.term.length < 2)
   170       return [];
   172     let tabs = TabItems.getItems();
   173     return this._filterAndSortForMatches(tabs);
   174   },
   176   // ----------
   177   // Function: unmatched
   178   // Returns all of <TabItem>s that .matched() doesn't return.
   179   unmatched: function TabMatcher_unmatched() {
   180     let tabs = TabItems.getItems();
   181     if (this.term.length < 2)
   182       return tabs;
   184     return this._filterForUnmatches(tabs);
   185   },
   187   // ----------
   188   // Function: doSearch
   189   // Performs the search. Lets you provide three functions.
   190   // The first is on all matched tabs in the window, the second on all unmatched
   191   // tabs in the window, and the third on all matched tabs in other windows.
   192   // The first two functions take two parameters: A <TabItem> and its integer index
   193   // indicating the absolute rank of the <TabItem> in terms of match to
   194   // the search term. The last function also takes two paramaters, but can be
   195   // passed both <TabItem>s and <xul:tab>s and the index is offset by the
   196   // number of matched tabs inside the window.
   197   doSearch: function TabMatcher_doSearch(matchFunc, unmatchFunc, otherFunc) {
   198     let matches = this.matched();
   199     let unmatched = this.unmatched();
   200     let otherMatches = this.matchedTabsFromOtherWindows();
   202     matches.forEach(function(tab, i) {
   203       matchFunc(tab, i);
   204     });
   206     otherMatches.forEach(function(tab,i) {
   207       otherFunc(tab, i+matches.length);
   208     });
   210     unmatched.forEach(function(tab, i) {
   211       unmatchFunc(tab, i);
   212     });
   213   },
   215   // ----------
   216   // Function: _scorePatternMatch
   217   // Given a pattern string, returns a score between 0 and 1 of how well
   218   // that pattern matches the original string. It mimics the heuristics
   219   // of the Mac application launcher Quicksilver.
   220   _scorePatternMatch: function TabMatcher__scorePatternMatch(pattern, matched, offset) {
   221     offset = offset || 0;
   222     pattern = pattern.toLowerCase();
   223     matched = matched.toLowerCase();
   225     if (pattern.length == 0)
   226       return 0.9;
   227     if (pattern.length > matched.length)
   228       return 0.0;
   230     for (let i = pattern.length; i > 0; i--) {
   231       let sub_pattern = pattern.substring(0,i);
   232       let index = matched.indexOf(sub_pattern);
   234       if (index < 0)
   235         continue;
   236       if (index + pattern.length > matched.length + offset)
   237         continue;
   239       let next_string = matched.substring(index+sub_pattern.length);
   240       let next_pattern = null;
   242       if (i >= pattern.length)
   243         next_pattern = '';
   244       else
   245         next_pattern = pattern.substring(i);
   247       let remaining_score = this._scorePatternMatch(next_pattern, next_string, offset + index);
   249       if (remaining_score > 0) {
   250         let score = matched.length-next_string.length;
   252         if (index != 0) {
   253           let c = matched.charCodeAt(index-1);
   254           if (c == 32 || c == 9) {
   255             for (let j = (index - 2); j >= 0; j--) {
   256               c = matched.charCodeAt(j);
   257               score -= ((c == 32 || c == 9) ? 1 : 0.15);
   258             }
   259           } else {
   260             score -= index;
   261           }
   262         }
   264         score += remaining_score * next_string.length;
   265         score /= matched.length;
   266         return score;
   267       }
   268     }
   269     return 0.0;
   270   }
   271 };
   273 // ##########
   274 // Class: TabHandlers
   275 // 
   276 // A object that handles all of the event handlers.
   277 let TabHandlers = {
   278   _mouseDownLocation: null,
   280   // ---------
   281   // Function: onMatch
   282   // Adds styles and event listeners to the matched tab items.
   283   onMatch: function TabHandlers_onMatch(tab, index) {
   284     tab.addClass("onTop");
   285     index != 0 ? tab.addClass("notMainMatch") : tab.removeClass("notMainMatch");
   287     // Remove any existing handlers before adding the new ones.
   288     // If we don't do this, then we may add more handlers than
   289     // we remove.
   290     tab.$canvas
   291       .unbind("mousedown", TabHandlers._hideHandler)
   292       .unbind("mouseup", TabHandlers._showHandler);
   294     tab.$canvas
   295       .mousedown(TabHandlers._hideHandler)
   296       .mouseup(TabHandlers._showHandler);
   297   },
   299   // ---------
   300   // Function: onUnmatch
   301   // Removes styles and event listeners from the unmatched tab items.
   302   onUnmatch: function TabHandlers_onUnmatch(tab, index) {
   303     tab.$container.removeClass("onTop");
   304     tab.removeClass("notMainMatch");
   306     tab.$canvas
   307       .unbind("mousedown", TabHandlers._hideHandler)
   308       .unbind("mouseup", TabHandlers._showHandler);
   309   },
   311   // ---------
   312   // Function: onOther
   313   // Removes styles and event listeners from the unmatched tabs.
   314   onOther: function TabHandlers_onOther(tab, index) {
   315     // Unlike the other on* functions, in this function tab can
   316     // either be a <TabItem> or a <xul:tab>. In other functions
   317     // it is always a <TabItem>. Also note that index is offset
   318     // by the number of matches within the window.
   319     let item = iQ("<div/>")
   320       .addClass("inlineMatch")
   321       .click(function TabHandlers_onOther_click(event) {
   322         Search.hide(event);
   323         TabUtils.focus(tab);
   324       });
   326     iQ("<img/>")
   327       .attr("src", TabUtils.faviconURLOf(tab))
   328       .appendTo(item);
   330     iQ("<span/>")
   331       .text(TabUtils.nameOf(tab))
   332       .appendTo(item);
   334     index != 0 ? item.addClass("notMainMatch") : item.removeClass("notMainMatch");
   335     item.appendTo("#results");
   336     iQ("#otherresults").show();
   337   },
   339   // ---------
   340   // Function: _hideHandler
   341   // Performs when mouse down on a canvas of tab item.
   342   _hideHandler: function TabHandlers_hideHandler(event) {
   343     iQ("#search").fadeOut();
   344     iQ("#searchshade").fadeOut();
   345     TabHandlers._mouseDownLocation = {x:event.clientX, y:event.clientY};
   346   },
   348   // ---------
   349   // Function: _showHandler
   350   // Performs when mouse up on a canvas of tab item.
   351   _showHandler: function TabHandlers_showHandler(event) {
   352     // If the user clicks on a tab without moving the mouse then
   353     // they are zooming into the tab and we need to exit search
   354     // mode.
   355     if (TabHandlers._mouseDownLocation.x == event.clientX &&
   356         TabHandlers._mouseDownLocation.y == event.clientY) {
   357       Search.hide();
   358       return;
   359     }
   361     iQ("#searchshade").show();
   362     iQ("#search").show();
   363     iQ("#searchbox")[0].focus();
   364     // Marshal the search.
   365     setTimeout(Search.perform, 0);
   366   }
   367 };
   369 // ##########
   370 // Class: Search
   371 // 
   372 // A object that handles the search feature.
   373 let Search = {
   374   _initiatedBy: "",
   375   _blockClick: false,
   376   _currentHandler: null,
   378   // ----------
   379   // Function: toString
   380   // Prints [Search] for debug use.
   381   toString: function Search_toString() {
   382     return "[Search]";
   383   },
   385   // ----------
   386   // Function: init
   387   // Initializes the searchbox to be focused, and everything else to be hidden,
   388   // and to have everything have the appropriate event handlers.
   389   init: function Search_init() {
   390     let self = this;
   392     iQ("#search").hide();
   393     iQ("#searchshade").hide().mousedown(function Search_init_shade_mousedown(event) {
   394       if (event.target.id != "searchbox" && !self._blockClick)
   395         self.hide();
   396     });
   398     iQ("#searchbox").keyup(function Search_init_box_keyup() {
   399       self.perform();
   400     })
   401     .attr("title", tabviewString("button.searchTabs"));
   403     iQ("#searchbutton").mousedown(function Search_init_button_mousedown() {
   404       self._initiatedBy = "buttonclick";
   405       self.ensureShown();
   406       self.switchToInMode();
   407     })
   408     .attr("title", tabviewString("button.searchTabs"));
   410     window.addEventListener("focus", function Search_init_window_focus() {
   411       if (self.isEnabled()) {
   412         self._blockClick = true;
   413         setTimeout(function() {
   414           self._blockClick = false;
   415         }, 0);
   416       }
   417     }, false);
   419     this.switchToBeforeMode();
   420   },
   422   // ----------
   423   // Function: _beforeSearchKeyHandler
   424   // Handles all keydown before the search interface is brought up.
   425   _beforeSearchKeyHandler: function Search__beforeSearchKeyHandler(event) {
   426     // Only match reasonable text-like characters for quick search.
   427     if (event.altKey || event.ctrlKey || event.metaKey)
   428       return;
   430     if ((event.keyCode > 0 && event.keyCode <= event.DOM_VK_DELETE) ||
   431         event.keyCode == event.DOM_VK_CONTEXT_MENU ||
   432         event.keyCode == event.DOM_VK_SLEEP ||
   433         (event.keyCode >= event.DOM_VK_F1 &&
   434          event.keyCode <= event.DOM_VK_SCROLL_LOCK) ||
   435         event.keyCode == event.DOM_VK_META ||
   436         event.keyCode == 91 || // 91 = left windows key
   437         event.keyCode == 92 || // 92 = right windows key
   438         (!event.keyCode && !event.charCode)) {
   439       return;
   440     }
   442     // If we are already in an input field, allow typing as normal.
   443     if (event.target.nodeName == "INPUT")
   444       return;
   446     // / is used to activate the search feature so the key shouldn't be entered 
   447     // into the search box.
   448     if (event.keyCode == KeyEvent.DOM_VK_SLASH) {
   449       event.stopPropagation();
   450       event.preventDefault();
   451     }
   453     this.switchToInMode();
   454     this._initiatedBy = "keydown";
   455     this.ensureShown(true);
   456   },
   458   // ----------
   459   // Function: _inSearchKeyHandler
   460   // Handles all keydown while search mode.
   461   _inSearchKeyHandler: function Search__inSearchKeyHandler(event) {
   462     let term = iQ("#searchbox").val();
   463     if ((event.keyCode == event.DOM_VK_ESCAPE) ||
   464         (event.keyCode == event.DOM_VK_BACK_SPACE && term.length <= 1 &&
   465          this._initiatedBy == "keydown")) {
   466       this.hide(event);
   467       return;
   468     }
   470     let matcher = this.createSearchTabMatcher();
   471     let matches = matcher.matched();
   472     let others =  matcher.matchedTabsFromOtherWindows();
   473     if (event.keyCode == event.DOM_VK_RETURN &&
   474         (matches.length > 0 || others.length > 0)) {
   475       this.hide(event);
   476       if (matches.length > 0) 
   477         matches[0].zoomIn();
   478       else
   479         TabUtils.focus(others[0]);
   480     }
   481   },
   483   // ----------
   484   // Function: switchToBeforeMode
   485   // Make sure the event handlers are appropriate for the before-search mode.
   486   switchToBeforeMode: function Search_switchToBeforeMode() {
   487     let self = this;
   488     if (this._currentHandler)
   489       iQ(window).unbind("keydown", this._currentHandler);
   490     this._currentHandler = function Search_switchToBeforeMode_handler(event) {
   491       self._beforeSearchKeyHandler(event);
   492     }
   493     iQ(window).keydown(this._currentHandler);
   494   },
   496   // ----------
   497   // Function: switchToInMode
   498   // Make sure the event handlers are appropriate for the in-search mode.
   499   switchToInMode: function Search_switchToInMode() {
   500     let self = this;
   501     if (this._currentHandler)
   502       iQ(window).unbind("keydown", this._currentHandler);
   503     this._currentHandler = function Search_switchToInMode_handler(event) {
   504       self._inSearchKeyHandler(event);
   505     }
   506     iQ(window).keydown(this._currentHandler);
   507   },
   509   createSearchTabMatcher: function Search_createSearchTabMatcher() {
   510     return new TabMatcher(iQ("#searchbox").val());
   511   },
   513   // ----------
   514   // Function: isEnabled
   515   // Checks whether search mode is enabled or not.
   516   isEnabled: function Search_isEnabled() {
   517     return iQ("#search").css("display") != "none";
   518   },
   520   // ----------
   521   // Function: hide
   522   // Hides search mode.
   523   hide: function Search_hide(event) {
   524     if (!this.isEnabled())
   525       return;
   527     iQ("#searchbox").val("");
   528     iQ("#searchshade").hide();
   529     iQ("#search").hide();
   531     iQ("#searchbutton").css({ opacity:.8 });
   533 #ifdef XP_MACOSX
   534     UI.setTitlebarColors(true);
   535 #endif
   537     this.perform();
   538     this.switchToBeforeMode();
   540     if (event) {
   541       // when hiding the search mode, we need to prevent the keypress handler
   542       // in UI__setTabViewFrameKeyHandlers to handle the key press again. e.g. Esc
   543       // which is already handled by the key down in this class.
   544       if (event.type == "keydown")
   545         UI.ignoreKeypressForSearch = true;
   546       event.preventDefault();
   547       event.stopPropagation();
   548     }
   550     // Return focus to the tab window
   551     UI.blurAll();
   552     gTabViewFrame.contentWindow.focus();
   554     let newEvent = document.createEvent("Events");
   555     newEvent.initEvent("tabviewsearchdisabled", false, false);
   556     dispatchEvent(newEvent);
   557   },
   559   // ----------
   560   // Function: perform
   561   // Performs a search.
   562   perform: function Search_perform() {
   563     let matcher =  this.createSearchTabMatcher();
   565     // Remove any previous other-window search results and
   566     // hide the display area.
   567     iQ("#results").empty();
   568     iQ("#otherresults").hide();
   569     iQ("#otherresults>.label").text(tabviewString("search.otherWindowTabs"));
   571     matcher.doSearch(TabHandlers.onMatch, TabHandlers.onUnmatch, TabHandlers.onOther);
   572   },
   574   // ----------
   575   // Function: ensureShown
   576   // Ensures the search feature is displayed.  If not, display it.
   577   // Parameters:
   578   //  - a boolean indicates whether this is triggered by a keypress or not
   579   ensureShown: function Search_ensureShown(activatedByKeypress) {
   580     let $search = iQ("#search");
   581     let $searchShade = iQ("#searchshade");
   582     let $searchbox = iQ("#searchbox");
   583     iQ("#searchbutton").css({ opacity: 1 });
   585     // NOTE: when this function is called by keydown handler, next keypress
   586     // event or composition events of IME will be fired on the focused editor.
   587     function dispatchTabViewSearchEnabledEvent() {
   588       let newEvent = document.createEvent("Events");
   589       newEvent.initEvent("tabviewsearchenabled", false, false);
   590       dispatchEvent(newEvent);
   591     };
   593     if (!this.isEnabled()) {
   594       $searchShade.show();
   595       $search.show();
   597 #ifdef XP_MACOSX
   598       UI.setTitlebarColors({active: "#717171", inactive: "#EDEDED"});
   599 #endif
   601       if (activatedByKeypress) {
   602         // set the focus so key strokes are entered into the textbox.
   603         $searchbox[0].focus();
   604         dispatchTabViewSearchEnabledEvent();
   605       } else {
   606         // marshal the focusing, otherwise it ends up with searchbox[0].focus gets
   607         // called before the search button gets the focus after being pressed.
   608         setTimeout(function setFocusAndDispatchSearchEnabledEvent() {
   609           $searchbox[0].focus();
   610           dispatchTabViewSearchEnabledEvent();
   611         }, 0);
   612       }
   613     }
   614   }
   615 };

mercurial