browser/components/tabview/search.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:62bad7b5a79b
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/. */
4
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 * ***************************** */
17
18 // **********
19 // Title: search.js
20 // Implementation for the search functionality of Firefox Panorama.
21
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 },
34
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 },
46
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 },
56
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 },
63
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 };
75
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 }
84
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 },
92
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 });
105
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 });
111
112 return tabs;
113 },
114
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 },
127
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 = [];
138
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 },
147
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 [];
159
160 let tabs = this._getTabsForOtherWindows();
161 return this._filterAndSortForMatches(tabs);
162 },
163
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 [];
171
172 let tabs = TabItems.getItems();
173 return this._filterAndSortForMatches(tabs);
174 },
175
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;
183
184 return this._filterForUnmatches(tabs);
185 },
186
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();
201
202 matches.forEach(function(tab, i) {
203 matchFunc(tab, i);
204 });
205
206 otherMatches.forEach(function(tab,i) {
207 otherFunc(tab, i+matches.length);
208 });
209
210 unmatched.forEach(function(tab, i) {
211 unmatchFunc(tab, i);
212 });
213 },
214
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();
224
225 if (pattern.length == 0)
226 return 0.9;
227 if (pattern.length > matched.length)
228 return 0.0;
229
230 for (let i = pattern.length; i > 0; i--) {
231 let sub_pattern = pattern.substring(0,i);
232 let index = matched.indexOf(sub_pattern);
233
234 if (index < 0)
235 continue;
236 if (index + pattern.length > matched.length + offset)
237 continue;
238
239 let next_string = matched.substring(index+sub_pattern.length);
240 let next_pattern = null;
241
242 if (i >= pattern.length)
243 next_pattern = '';
244 else
245 next_pattern = pattern.substring(i);
246
247 let remaining_score = this._scorePatternMatch(next_pattern, next_string, offset + index);
248
249 if (remaining_score > 0) {
250 let score = matched.length-next_string.length;
251
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 }
263
264 score += remaining_score * next_string.length;
265 score /= matched.length;
266 return score;
267 }
268 }
269 return 0.0;
270 }
271 };
272
273 // ##########
274 // Class: TabHandlers
275 //
276 // A object that handles all of the event handlers.
277 let TabHandlers = {
278 _mouseDownLocation: null,
279
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");
286
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);
293
294 tab.$canvas
295 .mousedown(TabHandlers._hideHandler)
296 .mouseup(TabHandlers._showHandler);
297 },
298
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");
305
306 tab.$canvas
307 .unbind("mousedown", TabHandlers._hideHandler)
308 .unbind("mouseup", TabHandlers._showHandler);
309 },
310
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 });
325
326 iQ("<img/>")
327 .attr("src", TabUtils.faviconURLOf(tab))
328 .appendTo(item);
329
330 iQ("<span/>")
331 .text(TabUtils.nameOf(tab))
332 .appendTo(item);
333
334 index != 0 ? item.addClass("notMainMatch") : item.removeClass("notMainMatch");
335 item.appendTo("#results");
336 iQ("#otherresults").show();
337 },
338
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 },
347
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 }
360
361 iQ("#searchshade").show();
362 iQ("#search").show();
363 iQ("#searchbox")[0].focus();
364 // Marshal the search.
365 setTimeout(Search.perform, 0);
366 }
367 };
368
369 // ##########
370 // Class: Search
371 //
372 // A object that handles the search feature.
373 let Search = {
374 _initiatedBy: "",
375 _blockClick: false,
376 _currentHandler: null,
377
378 // ----------
379 // Function: toString
380 // Prints [Search] for debug use.
381 toString: function Search_toString() {
382 return "[Search]";
383 },
384
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;
391
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 });
397
398 iQ("#searchbox").keyup(function Search_init_box_keyup() {
399 self.perform();
400 })
401 .attr("title", tabviewString("button.searchTabs"));
402
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"));
409
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);
418
419 this.switchToBeforeMode();
420 },
421
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;
429
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 }
441
442 // If we are already in an input field, allow typing as normal.
443 if (event.target.nodeName == "INPUT")
444 return;
445
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 }
452
453 this.switchToInMode();
454 this._initiatedBy = "keydown";
455 this.ensureShown(true);
456 },
457
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 }
469
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 },
482
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 },
495
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 },
508
509 createSearchTabMatcher: function Search_createSearchTabMatcher() {
510 return new TabMatcher(iQ("#searchbox").val());
511 },
512
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 },
519
520 // ----------
521 // Function: hide
522 // Hides search mode.
523 hide: function Search_hide(event) {
524 if (!this.isEnabled())
525 return;
526
527 iQ("#searchbox").val("");
528 iQ("#searchshade").hide();
529 iQ("#search").hide();
530
531 iQ("#searchbutton").css({ opacity:.8 });
532
533 #ifdef XP_MACOSX
534 UI.setTitlebarColors(true);
535 #endif
536
537 this.perform();
538 this.switchToBeforeMode();
539
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 }
549
550 // Return focus to the tab window
551 UI.blurAll();
552 gTabViewFrame.contentWindow.focus();
553
554 let newEvent = document.createEvent("Events");
555 newEvent.initEvent("tabviewsearchdisabled", false, false);
556 dispatchEvent(newEvent);
557 },
558
559 // ----------
560 // Function: perform
561 // Performs a search.
562 perform: function Search_perform() {
563 let matcher = this.createSearchTabMatcher();
564
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"));
570
571 matcher.doSearch(TabHandlers.onMatch, TabHandlers.onUnmatch, TabHandlers.onOther);
572 },
573
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 });
584
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 };
592
593 if (!this.isEnabled()) {
594 $searchShade.show();
595 $search.show();
596
597 #ifdef XP_MACOSX
598 UI.setTitlebarColors({active: "#717171", inactive: "#EDEDED"});
599 #endif
600
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 };
616

mercurial