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