├── _locales └── en │ └── messages.json ├── .gitignore ├── icons ├── th-32x32.png ├── th-48x48.png ├── th-96x96.png ├── th-128x128.png ├── martini-16x16.png ├── martini-24x24.png ├── martini-32x32.png └── tabhunter-install.png ├── popup ├── images │ ├── restore12.png │ └── restore24.png ├── prefs.css ├── prefs.html.erb ├── _prefs.html.erb ├── tabhunter.html.erb ├── tabhunter.css ├── browser-polyfill-0.2.1.min.js ├── punycode.js ├── prefs.js.erb └── jquery-3.5.1.slim.min.js ├── screenshots ├── chrome-audio.png ├── firefox-audio.png ├── chrome-standard.png ├── firefox-standard.png └── chrome-standard-1280x800.png ├── defaults └── preferences │ └── prefs.js ├── updates.md ├── PRIVACY.md ├── LICENSE.txt ├── HISTORY.md ├── HISTORY.txt ├── manifest.json.erb ├── README.md ├── Makefile └── content └── matchText.js.erb /_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | #* 3 | build/ 4 | .DS_Store 5 | tabhunter.tgz 6 | -------------------------------------------------------------------------------- /icons/th-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericpromislow/tabhunter/HEAD/icons/th-32x32.png -------------------------------------------------------------------------------- /icons/th-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericpromislow/tabhunter/HEAD/icons/th-48x48.png -------------------------------------------------------------------------------- /icons/th-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericpromislow/tabhunter/HEAD/icons/th-96x96.png -------------------------------------------------------------------------------- /icons/th-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericpromislow/tabhunter/HEAD/icons/th-128x128.png -------------------------------------------------------------------------------- /icons/martini-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericpromislow/tabhunter/HEAD/icons/martini-16x16.png -------------------------------------------------------------------------------- /icons/martini-24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericpromislow/tabhunter/HEAD/icons/martini-24x24.png -------------------------------------------------------------------------------- /icons/martini-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericpromislow/tabhunter/HEAD/icons/martini-32x32.png -------------------------------------------------------------------------------- /icons/tabhunter-install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericpromislow/tabhunter/HEAD/icons/tabhunter-install.png -------------------------------------------------------------------------------- /popup/images/restore12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericpromislow/tabhunter/HEAD/popup/images/restore12.png -------------------------------------------------------------------------------- /popup/images/restore24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericpromislow/tabhunter/HEAD/popup/images/restore24.png -------------------------------------------------------------------------------- /screenshots/chrome-audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericpromislow/tabhunter/HEAD/screenshots/chrome-audio.png -------------------------------------------------------------------------------- /screenshots/firefox-audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericpromislow/tabhunter/HEAD/screenshots/firefox-audio.png -------------------------------------------------------------------------------- /screenshots/chrome-standard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericpromislow/tabhunter/HEAD/screenshots/chrome-standard.png -------------------------------------------------------------------------------- /screenshots/firefox-standard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericpromislow/tabhunter/HEAD/screenshots/firefox-standard.png -------------------------------------------------------------------------------- /screenshots/chrome-standard-1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericpromislow/tabhunter/HEAD/screenshots/chrome-standard-1280x800.png -------------------------------------------------------------------------------- /defaults/preferences/prefs.js: -------------------------------------------------------------------------------- 1 | pref("extensions.tabhunter.closeOnReturn", true); 2 | pref("extensions.tabhunter.showStatusBarIcon", true); 3 | pref("extensions.tabhunter.showMenuItem", true); 4 | -------------------------------------------------------------------------------- /updates.md: -------------------------------------------------------------------------------- 1 | ## 2.1.7 - numeric sub-selection: 2 | 3 | * putting `{w:N}` at the start of a pattern shows only tabs in window N 4 | 5 | * putting `{w:N:T1-T2}` at the start of a pattern shows only tabs T1 through T2 in window N 6 | 7 | * putting `{w:N:T}` shows tabs T through the end in window N 8 | 9 | * putting `{w:N:-T}` shows tabs 1 through T in window N 10 | -------------------------------------------------------------------------------- /popup/prefs.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | height: auto; /* 24em; */ 4 | border: solid 1px; 5 | } 6 | 7 | div#change-shortcut-div.hide { 8 | display: none; 9 | visibility: hidden; 10 | } 11 | 12 | div#change-shortcut-div.show { 13 | display: block; 14 | visibility: visible; 15 | } 16 | 17 | button.restoreButton { 18 | border: none; 19 | } 20 | 21 | button.restoreButton:hover { 22 | border-color: rgb(109, 109, 109); 23 | background-color: #ddd; 24 | } 25 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | Tabhunter saves the following data in local storage: 4 | 5 | * search-pattern 6 | * show-audio checkbox 7 | * whether to hide discarded tabs 8 | * whether to close on the go button 9 | * the preferences 10 | 11 | That's it. Tabhunter doesn't save anything else, and doesn't 12 | communicate any data to other machines. With the only exception that 13 | if you have somehow mapped the volume containing your local-storage 14 | directory to another machine, that data will be transmitted remotely. 15 | 16 | -------------------------------------------------------------------------------- /popup/prefs.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Tabhunter Preferences 9 | 10 | <% if ENV['TARGET'] == 'chrome' %> 11 | 12 | <% end %> 13 | 14 | 15 | 16 |
17 |

Tabhunter Preferences

18 |
19 | <%= ERB.new(File.read(File.expand_path("../_prefs.html.erb", __FILE__)), 20 | nil, nil, eoutvar='_sub01').result(binding) %> 21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | * Version: MPL 1.1/GPL 2.0/LGPL 2.1 2 | * 3 | * The contents of this file are subject to the Mozilla Public License 4 | * Version 1.1 (the "License"); you may not use this file except in 5 | * compliance with the License. You may obtain a copy of the License at 6 | * http://www.mozilla.org/MPL/ 7 | * 8 | * Software distributed under the License is distributed on an "AS IS" 9 | * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the 10 | * License for the specific language governing rights and limitations 11 | * under the License. 12 | * 13 | * Portions created by Eric Promislow are Copyright (C) 2008-2017. 14 | * All Rights Reserved. 15 | * 16 | * Initial Contributor(s): 17 | * Eric Promislow 18 | * 19 | * Alternatively, the contents of this file may be used under the terms of 20 | * either the GNU General Public License Version 2 or later (the "GPL"), or 21 | * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 22 | * in which case the provisions of the GPL or the LGPL are applicable instead 23 | * of those above. If you wish to allow use of your version of this file only 24 | * under the terms of either the GPL or the LGPL, and not to allow others to 25 | * use your version of this file under the terms of the MPL, indicate your 26 | * decision by deleting the provisions above and replace them with the notice 27 | * and other provisions required by the GPL or the LGPL. If you do not delete 28 | * the provisions above, a recipient may use your version of this file under 29 | * the terms of any one of the MPL, the GPL or the LGPL. 30 | * 31 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # Tabhunter Changes 2 | 3 | ## 2.2.2 - Adds drag & drop 4 | 5 | ## 3.0.0 - Support searching patterns in loaded tabs 6 | 7 | Press the "Search text in tabs" button to get to the secondary pattern field. Searching is limited to the current set of matched tabs, and then only in those active tabs that aren't internal `about:X` type tabs. 8 | 9 | The `Pause/Resume` and `Cancel` buttons do what you think. 10 | 11 | You can search by plain text, JavaScript regular expressions, or XPath expressions -- no need to specify which you want. And no worries if you don't understand that sentence -- searching should work just fine for you. XPath expressions don't support arbitrary namespaces (and if you understand that and are miffed by it, you know enough to fork the project and implement namespace support; as usual, pull requests are welcome). 12 | 13 | ## 3.0.1 - Text searching improvements 14 | 15 | * Update the list of matched tabs as we go 16 | * Support a 'regex:' and 'xpath:' prefix on the text patterns because there are too many false positives with regexes for intended xpath expressions. 17 | 18 | ## 3.0.3 - UI changes 19 | 20 | * Explain that the main pattern field is read-only while searching text 21 | * Add a green fade for matched search-text hits 22 | 23 | ## 3.1.0 - Select All 24 | 25 | * Press the "Select All" button or type "Ctrl-A" (Cmd-A on macos) to select all the matched tabs 26 | 27 | ## 3.1.1 - Reload Tabs 28 | 29 | * Added button to reload the selected tabs 30 | 31 | ## 3.1.2 - Select All: Key binding changed 32 | 33 | * On Mac, Shift-Command-A brings up the add-ons page. The keybinding is now Shift-Ctrl-A, and Ctrl-A/Cmd-A goes back to selecting all the text in the pattern field. 34 | -------------------------------------------------------------------------------- /HISTORY.txt: -------------------------------------------------------------------------------- 1 | This extension lets browser power-users easily juggle dozens of tabs at any time. Enter a search string (could be a standard JavaScript regex), highlight the URL you want to bring up, and press return, double-click it, or press the "Go" button. Tabhunter will even find and bring up minimized windows containing the target tab. 2 | 3 | Tabhunter also lets you easily bulk-close large numbers of tabs. Let's say you have a bunch of github pages all related to a project we'll call "inconsolable-toucan". Enter a pattern like 'github.com/example-user/inconsolable-toucan', click on the first hit in the list, shift-click the last hit, and press the "Close Tabs" button. No-longer-needed tabs gone like that. 4 | 5 | Bring up Tabhunter by either pressing the hunter's scope icon, or using the command-key Ctrl-Shift-T on macos, Alt-Shift-T on Linux and Windows. 6 | 7 | Drowning in a sea of tabs? Stay afloat with Tabhunter. Users of that other browser have been happily using it since 2008. Now you Chrome folks can use it too. 8 | 9 | Under no circumstances should this software be confused with Tab Hunter, an actor best known for his work in the mid-20th century. 10 | 11 | Updates: 12 | 2.1.6 - support moving a group of tabs to a selected window 13 | 14 | 2.1.7 - support selecting windows in the pattern field: 15 | * putting `{w:N}` at the start of a pattern shows only tabs in window N 16 | * putting `{w:N:T1-T2}` at the start of a pattern shows only tabs T1 through T2 in window N 17 | * putting `{w:N:T}` shows tabs T through the end in window N 18 | * putting `{w:N:-T}` shows tabs 1 through T in window N 19 | 20 | 2.2.0 - prefs are now available in the main window via the "More..." button and no longer change only when the "Submit" button is pressed. 21 | 22 | 2.2.2 - Support for drag+drop when tabs are sorted by window/tab position. 23 | 24 | 3.0.1 - Reinstate support for searching *inside* the tabs, including by regex or xpath 25 | 26 | 3.1.0 - Support for "Select All" (Ctrl-A , Cmd-A on macos) 27 | 28 | 3.2.4 - Rearchitected to deal with very large numbers of tabs (like 1000 or more) gracefully 29 | 30 | 3.3.0 - Leave maximized and fullscreen windows at that size. 31 | 32 | 3.4.0 - Migrate the add-on to use the Chrome Manifest V3 version 33 | 34 | 3.5.0 - Press Backspace or Delete to close selected tabs 35 | -------------------------------------------------------------------------------- /manifest.json.erb: -------------------------------------------------------------------------------- 1 | <%# -*- Mode: javascript; indent-tab-mode: nil; js-indent-level: 2 -*- -%> 2 | <% 3 | version = ENV['VERSION'] or die("tabhunter/manifest.json.erb: VERSION not set") 4 | target = ENV['TARGET'] or die("build.sh.erb: TARGET not set") %>{ 5 | "author": "Eric Promislow", 6 | <% if target == 'firefox' %> 7 | "browser_action": { 8 | <% else %> 9 | "action": { 10 | <% end %> 11 | <% if target == 'firefox' %> 12 | "browser_style": true, 13 | <% end %> 14 | "default_icon": { 15 | "16": "icons/martini-16x16.png", 16 | "24": "icons/martini-24x24.png", 17 | "32": "icons/martini-32x32.png" 18 | }, 19 | "default_title": "Tabhunter", 20 | "default_popup": "popup/tabhunter.html" 21 | }, 22 | "commands": { 23 | "_execute_browser_action": { 24 | "suggested_key": { 25 | "mac": "MacCtrl+Shift+T", 26 | "windows": "Ctrl+Shift+S", 27 | "linux": "Ctrl+5" 28 | } 29 | } 30 | }, 31 | "default_locale": "en", 32 | "description": "Wade through all your tabs fast", 33 | <% if target == 'firefox' %> 34 | "developer": { 35 | "name": "Eric Promislow", 36 | "url": "https://github.com/ericpromislow/tabhunter" 37 | }, 38 | <% end %> 39 | "homepage_url": "https://github.com/ericpromislow/tabhunter", 40 | "icons": { 41 | "48": "icons/th-48x48.png", 42 | "96": "icons/th-96x96.png", 43 | "128": "icons/th-128x128.png" 44 | }, 45 | <% if target == 'firefox' %> 46 | "manifest_version": 2, 47 | <% else %> 48 | "manifest_version": 3, 49 | <% end %> 50 | "name": "Tabhunter", 51 | "options_ui": { 52 | <% if target == 'firefox' %> 53 | "browser_style": true, 54 | <% end %> 55 | "page": "popup/prefs.html" 56 | }, 57 | "permissions": [ 58 | "activeTab", 59 | "clipboardWrite", 60 | "storage", 61 | <% if target == 'firefox' %> 62 | "bookmarks", 63 | "", 64 | <% else %> 65 | "scripting", 66 | <% end %> 67 | "tabs" 68 | ], 69 | <% if target == 'chrome' %> 70 | "host_permissions": [ 71 | "" 72 | ], 73 | <% end %> 74 | <% if target == 'firefox' %> 75 | "browser_specific_settings": { 76 | "gecko": { 77 | <% if ENV['DEBUG_EXTENSIONS'] %> 78 | "id": "tabhunter@<%= ENV['USER'] %>" 79 | <% else %> 80 | "id": "tabhunter@ericpromislow.com" 81 | <% end %> 82 | } 83 | }, 84 | <% end %> 85 | "short_name": "Tabhunter", 86 | "version": "<%= version %>" 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tabhunter 2 | 3 | ## Help wade through all your tabs 4 | 5 | ### Based on the much-loved, classic Firefox extension 6 | 7 | This extension lets you easily juggle dozens of tabs at any time. Enter a 8 | search string (could be a JavaScript regex for you power-users), highlight 9 | the URL you want to bring up, and press return or double-click it. 10 | Tabhunter will even bring up minimized windows containing the desired tab. 11 | 12 | Inspired by Davide Ficano's tab selector extension for ActiveState's 13 | Komodo IDE, and my own need for some assistance in managing my 14 | web-based workload. Not to be confused by the actor from the 1950s 15 | and early '60s, known by some as 'Tab "Space" Hunter' to avoid any 16 | ambiguity. Note the proper spelling of this add-on is "Tabhunter" 17 | with a capital T, lower-case h. 18 | 19 | ### Download 20 | * https://addons.mozilla.org/addon/tabhunter/ 21 | 22 | ### Usage 23 | 24 | Press `Shift-Ctrl-T` on Macs, `Ctrl-5` on Linux machines, and as of 25 | this update, `Shift-Ctrl-S` on Windows 26 | or click on the icon (the '5' was chosen because it's about the closest 27 | key to the 'T' that wasn't used by standard Firefox and Chrome, but 28 | this doesn't work on Windows). 29 | 30 | You can select more than one entry, and do a bulk-close or 31 | bulk copy-and-paste -- see the other buttons on the dialog. They used 32 | to hide in a context menu, but no more. 33 | 34 | Version 2.0 introduces the "Audio Only" checkbox, just what you were 35 | looking for to find that tab that started playing an ad or a 36 | video. 37 | 38 | Version 3.0 reinstates searching the text in tabs as well, and 39 | supports JS regexes (put `regex:` at the start of your search 40 | pattern to force this (not the url/title matcher)), and XPath expressions 41 | (highly recommended you put `xpath:` at the start of the search 42 | term, because a specific xpath expression with a long condition 43 | part in square brackets will happily work as a regex for many more 44 | documents). 45 | 46 | ### See LICENSE.txt for license details. 47 | 48 | ## Build Instructions 49 | Make sure [Ruby's ERB gem](https://github.com/ruby/erb) is installed. 50 | 51 | ``` 52 | git clone git@github.com:ericpromislow/tabhunter.git 53 | cd tabhunter 54 | make 55 | ``` 56 | 57 | In firefox, load `.../tabhunter/build/firefox/build/tabhunter-firefox-VERSION.zip`. 58 | In chrome, load `.../tabhunter/build/firefox/chrome/tabhunter-firefox-VERSION.zip`. 59 | 60 | See the build output for `VERSION`. The `VERSION` value is set on line 2 of `Makefile`. 61 | -------------------------------------------------------------------------------- /popup/_prefs.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | Sort by... 4 | 6 | 7 | 9 | 10 | 12 | 13 | <% if ENV['TARGET'] == 'firefox' %> 14 | 16 | 17 | <% end %> 18 |
19 | 24 | 27 | 28 |
29 | 34 | 37 | 38 |
39 |
40 |
41 |
42 | Keyboard Shortcut 43 | 48 | 49 | 53 | 60 |
61 |
62 |
63 |
64 | Close When Changing Tabs 65 | 70 | 73 | 74 |
75 |
76 |
77 |
78 | Font Size 79 | 84 | 85 | 88 |
89 |
90 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGET=firefox 2 | VERSION=3.6.9 3 | 4 | TDIR=build/${TARGET} 5 | TBDIR=$(TDIR)/build 6 | ZIPPER=$(TBDIR)/tabhunter-$(TARGET)-$(VERSION).zip 7 | TDIRS=$(TDIR) $(TDIR)/build $(TDIR)/_locales $(TDIR)/_locales/en $(TDIR)/icons \ 8 | $(TDIR)/popup $(TDIR)/popup/images $(TDIR)/content 9 | 10 | SOURCES=$(TDIR) $(TBDIR) $(TDIR)/_locales $(TDIR)/_locales/en $(TDIR)/icons $(TDIR)/popup \ 11 | $(TDIR)/_locales/en/messages.json \ 12 | $(TDIR)/icons/martini-16x16.png \ 13 | $(TDIR)/icons/martini-24x24.png \ 14 | $(TDIR)/icons/martini-32x32.png \ 15 | $(TDIR)/icons/tabhunter-install.png \ 16 | $(TDIR)/icons/th-128x128.png \ 17 | $(TDIR)/icons/th-32x32.png \ 18 | $(TDIR)/icons/th-48x48.png \ 19 | $(TDIR)/icons/th-96x96.png \ 20 | $(TDIR)/LICENSE.txt \ 21 | $(TDIR)/README.md \ 22 | $(TDIR)/build.sh \ 23 | $(TDIR)/manifest.json \ 24 | $(TDIR)/content/matchText.js \ 25 | $(TDIR)/popup/jquery-3.5.1.slim.min.js \ 26 | $(TDIR)/popup/punycode.js \ 27 | $(TDIR)/popup/prefs.css \ 28 | $(TDIR)/popup/prefs.html \ 29 | $(TDIR)/popup/prefs.js \ 30 | $(TDIR)/popup/images/restore12.png \ 31 | $(TDIR)/popup/images/restore24.png \ 32 | $(TDIR)/popup/tabhunter.css \ 33 | $(TDIR)/popup/tabhunter.html \ 34 | $(TDIR)/popup/tabhunter.js 35 | 36 | all: build all-firefox all-chrome 37 | 38 | all-firefox: 39 | $(MAKE) -e TARGET=firefox do-firefox 40 | 41 | all-chrome: 42 | $(MAKE) -e TARGET=chrome do-chrome 43 | 44 | do-firefox: $(TDIRS) $(ZIPPER) 45 | 46 | do-chrome: $(TDIRS) $(TDIR)/popup/browser-polyfill.min.js $(ZIPPER) 47 | 48 | $(ZIPPER): $(SOURCES) 49 | cd $(TDIR) ; ./build.sh 50 | 51 | build: 52 | mkdir -p $@ 53 | 54 | $(TDIR) $(TDIR)/build $(TDIR)/_locales $(TDIR)/_locales/en $(TDIR)/icons $(TDIR)/popup $(TDIR)/popup/images $(TDIR)/content: 55 | mkdir -p $@ 56 | 57 | $(TDIR)/_locales/en/messages.json: _locales/en/messages.json 58 | cp $< $@ 59 | 60 | $(TDIR)/icons/martini-16x16.png: icons/martini-16x16.png 61 | cp $< $@ 62 | 63 | $(TDIR)/icons/martini-24x24.png: icons/martini-24x24.png 64 | cp $< $@ 65 | 66 | $(TDIR)/icons/martini-32x32.png: icons/martini-32x32.png 67 | cp $< $@ 68 | 69 | $(TDIR)/icons/tabhunter-install.png: icons/tabhunter-install.png 70 | cp $< $@ 71 | 72 | $(TDIR)/icons/th-128x128.png: icons/th-128x128.png 73 | cp $< $@ 74 | 75 | $(TDIR)/icons/th-32x32.png: icons/th-32x32.png 76 | cp $< $@ 77 | 78 | $(TDIR)/icons/th-48x48.png: icons/th-48x48.png 79 | cp $< $@ 80 | 81 | $(TDIR)/icons/th-96x96.png: icons/th-96x96.png 82 | cp $< $@ 83 | 84 | $(TDIR)/LICENSE.txt : LICENSE.txt 85 | cp $< $@ 86 | 87 | $(TDIR)/README.md : README.md 88 | cp $< $@ 89 | 90 | $(TDIR)/build.sh : build.sh.erb Makefile 91 | TARGET=${TARGET} VERSION=${VERSION} erb -T 2 $< > $@ 92 | chmod +x $@ 93 | 94 | $(TDIR)/manifest.json : manifest.json.erb Makefile 95 | TARGET=${TARGET} VERSION=${VERSION} ruby -rjson -rerb -e 'File.write("$@", JSON.pretty_generate(JSON.parse(ERB.new(File.read("$<"), trim_mode: 2).result)))' 96 | 97 | $(TDIR)/popup/browser-polyfill.min.js: popup/browser-polyfill-0.2.1.min.js 98 | cp $< $@ 99 | 100 | $(TDIR)/popup/jquery-3.5.1.slim.min.js: popup/jquery-3.5.1.slim.min.js 101 | cp $< $@ 102 | 103 | $(TDIR)/popup/punycode.js: popup/punycode.js 104 | cp $< $@ 105 | 106 | $(TDIR)/content/matchText.js: content/matchText.js.erb 107 | TARGET=${TARGET} VERSION=${VERSION} erb -T 2 $< > $@ 108 | node -c $@ 109 | 110 | $(TDIR)/popup/prefs.css: popup/prefs.css 111 | cp $< $@ 112 | 113 | $(TDIR)/popup/prefs.js: popup/prefs.js.erb Makefile 114 | TARGET=${TARGET} VERSION=${VERSION} erb -T 2 $< > $@ 115 | node -c $@ 116 | 117 | $(TDIR)/popup/tabhunter.css: popup/tabhunter.css 118 | cp $< $@ 119 | 120 | $(TDIR)/popup/tabhunter.html: popup/tabhunter.html.erb popup/_prefs.html.erb Makefile 121 | TARGET=${TARGET} VERSION=${VERSION} erb -T 2 $< > $@ 122 | 123 | $(TDIR)/popup/tabhunter.js: popup/tabhunter.js.erb Makefile 124 | TARGET=${TARGET} VERSION=${VERSION} erb -T 2 $< > $@ 125 | node -c $@ 126 | 127 | $(TDIR)/popup/prefs.html: popup/prefs.html.erb popup/_prefs.html.erb Makefile 128 | TARGET=${TARGET} VERSION=${VERSION} erb -T 2 $< > $@ 129 | 130 | $(TDIR)/popup/images/restore12.png: popup/images/restore12.png 131 | cp $< $@ 132 | 133 | $(TDIR)/popup/images/restore24.png: popup/images/restore24.png 134 | cp $< $@ 135 | 136 | tarSource: tabhunter.tgz 137 | 138 | tabhunter.tgz: HISTORY.md LICENSE.txt Makefile README.md _locales/ build.sh.erb content/ defaults/ icons/ m* popup/ screenshots/ updates.md PRIVACY.md 139 | tar cfz $@ $^ 140 | -------------------------------------------------------------------------------- /popup/tabhunter.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <% if ENV['TARGET'] == 'chrome' %> 10 | 11 | <% end %> 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 |
    28 |
    29 | 30 |
    31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 46 | 47 |
    48 | 49 |
    50 |
    51 | Search text in tabs 52 |
    53 | 54 | 55 | 56 |
    57 | Progress: 58 |
    59 | 60 |
    61 |
    62 |

    63 |
    64 | 65 |
    66 |
    67 | 68 | 69 |
    70 | <% if ENV['TARGET'] == 'firefox' %> 71 |
    72 | 73 | 74 |
    75 | <% end %> 76 |
    77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 |
    85 |
    86 | <% indent = " " %> 87 | <%= ERB.new(File.read(File.expand_path("../_prefs.html.erb", __FILE__)), nil, nil, eoutvar='_sub01').result(binding).gsub(/^/, indent) %> 88 |
    89 |
    90 |

    Tabhunter ...

    91 |
    92 |
    93 |
    94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /content/matchText.js.erb: -------------------------------------------------------------------------------- 1 | // -*- Mode: javascript; indent-tabs-mode: nil; js-indent-level: 4 -*- 2 | 3 | <% if ENV['TARGET'] == 'firefox' %> 4 | browser. 5 | <% else %> 6 | chrome. 7 | <% end %> 8 | runtime.onMessage.addListener(matchText); 9 | 10 | function matchText(message, sender, sendResponse) { 11 | try { 12 | message.result = matchTextAux(message.pattern, document.documentElement.innerHTML); 13 | } catch(ex) { 14 | console.log(`tabhunter: matching text failed: ${ex}`); 15 | message.result = false; 16 | } 17 | sendResponse(message); 18 | } 19 | 20 | function matchTextAux(pattern, textContent) { 21 | // Try plain text before trying anything else 22 | if (matchPlainText(pattern, textContent)) { 23 | return true; 24 | } 25 | /* 26 | * ^//?(?:\*|\w) - xpath only 27 | * .[?*+] or .{\d+ ... } regex only 28 | * /(?:*|\w)\[...\]/ - regex only 29 | * otherwise: regex first, then xpath 30 | */ 31 | var thingsToTry = {regex:false, xpath:false}; 32 | var m = /^xpath:(.*)$/.exec(pattern); 33 | if (m) { 34 | pattern = m[1]; 35 | thingsToTry.xpath = true; 36 | } else if (!!(m = /^regex:(.*)$/.exec(pattern))) { 37 | pattern = m[1]; 38 | thingsToTry.regex = true; 39 | } else if (!/[\.\*\+\?\[\{\(\\\$\^]/.test(pattern)) { 40 | // No regex chars, so don't bother 41 | thingsToTry.xpath = true; 42 | } else { 43 | // try everything else, but some failed xpaths might match regexes 44 | thingsToTry.xpath = true; 45 | thingsToTry.regex = true; 46 | } 47 | if (thingsToTry.xpath && matchXPath(pattern)) { 48 | return true; 49 | } 50 | if (thingsToTry.regex && matchRegex(pattern, textContent)) { 51 | return true; 52 | } 53 | return false; 54 | } 55 | 56 | function matchPlainText(pattern, textContent) { 57 | var res = textContent.indexOf(pattern) >= 0; 58 | if (res) { 59 | return res; 60 | } 61 | return (textContent.toLowerCase()).indexOf(pattern.toLowerCase()) >= 0; 62 | } 63 | 64 | function matchRegex(pattern, textContent) { 65 | if (!/[\*\?\[\(\{\\]/.test(pattern)) { 66 | return false; 67 | } 68 | try { 69 | let p = new RegExp(pattern, 'i'); 70 | let result = p.test(textContent); 71 | if (result) { 72 | return result; 73 | } 74 | let m = /^\/(.*)\/$/.exec(pattern); 75 | if (m) { 76 | p = new RegExp(m[1], 'i'); 77 | return p.test(textContent); 78 | } 79 | } catch(ex) {} 80 | return false; 81 | } 82 | 83 | function matchXPath(pattern) { 84 | if (pattern.indexOf("/") == -1) { 85 | // Not quite right, but don't bother setting up an xpath evaluation if 86 | // there's no path. 87 | return false; 88 | } 89 | try { 90 | return matchXPathAux(pattern); 91 | } catch(ex) { 92 | console.log(`tabhunter: xpath search (#1): ${msg}`); 93 | return false; 94 | } 95 | } 96 | 97 | function matchXPathAux(pattern) { 98 | var contextNode = document.documentElement; 99 | var namespaceResolver = 100 | document.createNSResolver(contextNode.ownerDocument == null 101 | ? contextNode.documentElement 102 | : contextNode.ownerDocument.documentElement); 103 | var resultType = XPathResult.ANY_TYPE; 104 | var nodeSet = null; 105 | try { 106 | nodeSet = document.evaluate(pattern, contextNode, namespaceResolver, 107 | resultType, null); 108 | switch (nodeSet.resultType) { 109 | case XPathResult.NUMBER_TYPE: 110 | return nodeSet.numberValue(); 111 | case XPathResult.STRING_TYPE: 112 | return nodeSet.stringValue(); 113 | case XPathResult.BOOLEAN_TYPE: 114 | return nodeSet.booleanValue(); 115 | case XPathResult.FIRST_ORDERED_NODE_TYPE: 116 | case XPathResult.ORDERED_NODE_ITERATOR_TYPE: 117 | case XPathResult.UNORDERED_NODE_ITERATOR_TYPE: 118 | return !!nodeSet.iterateNext(); 119 | case XPathResult.ORDERED_NODE_SNAPSHOT_TYPE: 120 | case XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE: 121 | return (nodeSet.snapshotLength > 0 && !!nodeSet.snapshotItem(0)); 122 | default: 123 | throw new Error(`Unexpected xcode result type: ${nodeSet.resultType}`); 124 | } 125 | } catch(ex) { 126 | var msg = ex.message; 127 | if (ex.inner) msg += "; " + ex.inner; 128 | if (ex.data) msg += "; " + ex.data; 129 | console.log(`tabhunter: xpath search: ${msg}`); 130 | } 131 | return false; 132 | } 133 | -------------------------------------------------------------------------------- /popup/tabhunter.css: -------------------------------------------------------------------------------- 1 | /* tabhunter.css -- See LICENSE.txt for copyright and license details. */ 2 | 3 | /* Color names and values from https://design.firefox.com/photon/visuals/color.html */ 4 | 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | html { 10 | background: rgb(240,240,240); 11 | /* height: 300px; */ 12 | margin: 0; 13 | } 14 | 15 | body { 16 | font-family: sans-serif; 17 | font-size: 10px; 18 | width: 600px; 19 | background: rgb(240,240,240); 20 | margin: 0 auto; 21 | padding: 2px; 22 | height: inherit; 23 | } 24 | 25 | body.dark { 26 | background: rgb(15, 15, 15); 27 | color: #f9f9fa; 28 | } 29 | 30 | div#form { 31 | width: 98%; 32 | /* To see the outline of the form */ 33 | border: 1px solid #CCC; 34 | border-radius: 0.1em; 35 | } 36 | 37 | div#form div + div { 38 | margin-top: 6px; 39 | } 40 | 41 | label.text { 42 | font-size: 9pt; 43 | display: inline-block; 44 | width: 70px; 45 | text-align: right 46 | } 47 | 48 | .row { 49 | margin-top: 6px; 50 | } 51 | 52 | .row input { 53 | width: 80%; 54 | -moz-box-sizing: border-box; 55 | box-sizing: border-box; 56 | 57 | border: 1px solid #999; 58 | } 59 | 60 | input.input-text { 61 | border: 1px solid rgb(218,218,218); 62 | padding: 4px; 63 | background: white; 64 | border-radius: 3px; 65 | } 66 | 67 | input.input-text.dark { 68 | background: #2a2a2e; 69 | color: #f9f9fa; 70 | border-color: #bbb; 71 | } 72 | 73 | input.input-text:focus { 74 | border-color: #000; 75 | } 76 | 77 | input.input-text.dark:focus { 78 | border-color: #d9d9d9; 79 | } 80 | 81 | input#command_key.dark { 82 | background: #2a2a2e; 83 | color: #f9f9fa; 84 | border-color: #bbb; 85 | } 86 | 87 | input#command_key.dark:focus { 88 | border-color: #d9d9d9; 89 | } 90 | 91 | .buttondiv { 92 | margin-top: 3pt; 93 | font-size: 8pt; 94 | } 95 | 96 | span.hide { 97 | display: none; 98 | visibility: hidden; 99 | } 100 | 101 | span.show { 102 | display: inline; 103 | visibility: visible; 104 | } 105 | 106 | div.subarea { 107 | padding-left: 6pt; 108 | padding-bottom: 2pt; 109 | } 110 | 111 | div.subarea.hide { 112 | display: none; 113 | visibility: hidden; 114 | } 115 | 116 | div.subarea.show { 117 | border-top: 1px solid; 118 | padding-top: 2pt; 119 | display: block; 120 | visibility: visible; 121 | } 122 | 123 | div#statusbar.hide { 124 | display: none; 125 | visibility: hidden; 126 | } 127 | 128 | div#statusbar.show { 129 | display: block; 130 | visibility: visible; 131 | } 132 | 133 | div#moveToBookmarkContainer { 134 | border-top: 1px solid; 135 | padding-top: 2pt; 136 | padding-bottom: 2pt; 137 | } 138 | 139 | div#discardArea { 140 | border-top: 1px solid; 141 | padding-top: 2pt; 142 | } 143 | 144 | input.xbutton { 145 | border: 1px solid; 146 | margin-left: 6px; 147 | margin-right: 0px; 148 | } 149 | 150 | input.xbutton.dark { 151 | background-color: #2a2a2e; 152 | color: #f9f9fa; 153 | } 154 | 155 | input.xbutton.dark.highlighted { 156 | border-color: rgb(146, 146, 146); 157 | } 158 | 159 | input.xbutton.dark.pressed { 160 | border-color: #222222; 161 | } 162 | 163 | label.checkbox { 164 | font-size: 8pt; 165 | } 166 | 167 | .button { 168 | margin-left: .5em; 169 | border: 1px solid rgb(218,218,218); 170 | padding: 2px; 171 | background: white; 172 | border-radius: 3px; 173 | } 174 | 175 | .button.highlighted { 176 | border-color: rgb(109, 109, 109); 177 | } 178 | 179 | .button.pressed { 180 | background-color: #ddd; 181 | } 182 | 183 | .button.dark { 184 | border-color: #d7d7db; /* grey 30 */ 185 | background: #2a2a2e; /* Grey 80 */ 186 | color: #f9f9fa; /* Grey 10 */ 187 | } 188 | 189 | .button.dark:disabled { 190 | border-color: #737373; /* grey 50 */ 191 | background: #4a4a4f; /* Grey 60 */ 192 | color: #b1b1b3; /* Grey 40 */ 193 | } 194 | 195 | .button.dark.highlighted { 196 | border-color: rgb(146, 146, 146); 197 | } 198 | 199 | .button.dark.pressed { 200 | background-color: #222; 201 | } 202 | 203 | div#main { 204 | } 205 | 206 | ul { 207 | height: 200px; 208 | overflow: scroll; 209 | overflow-x: scroll; 210 | width: auto; 211 | white-space: nowrap; 212 | } 213 | 214 | ul#list { 215 | border: 1px solid #999; 216 | list-style-type: none; 217 | list-style-position: outside; 218 | list-style-image: none; 219 | padding-left: 2px; 220 | font-size: 9pt; 221 | background: white; 222 | } 223 | 224 | ul#list.dark { 225 | border-color: rgb(146, 146, 146); 226 | background: #2a2a2a; 227 | color: #f9f9fa; 228 | scrollbar-color: #f9f9fa #2a2a2e; 229 | } 230 | 231 | ul#list.dark li.selected { 232 | background-color: #002275; /* Blue 80 */ 233 | } 234 | 235 | ul#list li { 236 | margin-top: 3px; 237 | padding-left: 20px; 238 | background-position-x: 0; 239 | background-position-y: 0; 240 | background-size: 16px auto; 241 | background-repeat: no-repeat; 242 | } 243 | 244 | ul#list li.tabenter { 245 | border-top: 1px dotted green; 246 | } 247 | 248 | ul#list li.tabenter-after { 249 | border-bottom: 1px dotted green; 250 | } 251 | 252 | ul#list li.duplicate.discarded { 253 | color: #CD853F; /* peru; */ 254 | } 255 | 256 | ul#list li.duplicate { 257 | color: orange; 258 | } 259 | 260 | ul#list li.discarded { 261 | color: #708090; /* slategrey */ 262 | } 263 | 264 | ul#list li.selected { 265 | background-color: #ccc; 266 | } 267 | 268 | ul#list li.selectedFade { 269 | animation: 2s selectedFade; 270 | animation-iteration-count: 1; 271 | } 272 | 273 | @keyframes selectedFade { 274 | from { 275 | background-color: #6E9A6E; 276 | } 277 | to { 278 | background-color: #fff; 279 | } 280 | } 281 | 282 | span#activity { 283 | font-size: 10pt; 284 | margin-left: 0.5em; 285 | } 286 | 287 | 288 | #closeTabs { 289 | margin-left: 1em; 290 | margin-right: 1em; 291 | } 292 | 293 | #textarea { 294 | display: none; 295 | } 296 | 297 | div#statusbar-text.hide { 298 | display: none; 299 | visibility: hidden; 300 | } 301 | 302 | div#statusbar-text.show { 303 | display: block; 304 | visibility: visible; 305 | } 306 | 307 | select#bookmarkList.dark { 308 | background: #2a2a2e; /* Grey 80 */ 309 | color: #f9f9fa; /* Grey 10 */ 310 | } 311 | 312 | select#windowList.dark { 313 | background: #2a2a2e; /* Grey 80 */ 314 | color: #f9f9fa; /* Grey 10 */ 315 | } 316 | -------------------------------------------------------------------------------- /popup/browser-polyfill-0.2.1.min.js: -------------------------------------------------------------------------------- 1 | (function(a,b){if("function"==typeof define&&define.amd)define("webextension-polyfill",["module"],b);else if("undefined"!=typeof exports)b(module);else{var c={exports:{}};b(c),a.browser=c.exports}})(this,function(a){"use strict";if("undefined"==typeof browser){a.exports=(()=>{const c={alarms:{clear:{minArgs:0,maxArgs:1},clearAll:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getAll:{minArgs:0,maxArgs:0}},bookmarks:{create:{minArgs:1,maxArgs:1},export:{minArgs:0,maxArgs:0},get:{minArgs:1,maxArgs:1},getChildren:{minArgs:1,maxArgs:1},getRecent:{minArgs:1,maxArgs:1},getTree:{minArgs:0,maxArgs:0},getSubTree:{minArgs:1,maxArgs:1},import:{minArgs:0,maxArgs:0},move:{minArgs:2,maxArgs:2},remove:{minArgs:1,maxArgs:1},removeTree:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1},update:{minArgs:2,maxArgs:2}},browserAction:{getBadgeBackgroundColor:{minArgs:1,maxArgs:1},getBadgeText:{minArgs:1,maxArgs:1},getPopup:{minArgs:1,maxArgs:1},getTitle:{minArgs:1,maxArgs:1},setIcon:{minArgs:1,maxArgs:1}},commands:{getAll:{minArgs:0,maxArgs:0}},contextMenus:{update:{minArgs:2,maxArgs:2},remove:{minArgs:1,maxArgs:1},removeAll:{minArgs:0,maxArgs:0}},cookies:{get:{minArgs:1,maxArgs:1},getAll:{minArgs:1,maxArgs:1},getAllCookieStores:{minArgs:0,maxArgs:0},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}},devtools:{inspectedWindow:{eval:{minArgs:1,maxArgs:2}},panels:{create:{minArgs:3,maxArgs:3,singleCallbackArg:!0}}},downloads:{download:{minArgs:1,maxArgs:1},cancel:{minArgs:1,maxArgs:1},erase:{minArgs:1,maxArgs:1},getFileIcon:{minArgs:1,maxArgs:2},open:{minArgs:1,maxArgs:1},pause:{minArgs:1,maxArgs:1},removeFile:{minArgs:1,maxArgs:1},resume:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1},show:{minArgs:1,maxArgs:1}},extension:{isAllowedFileSchemeAccess:{minArgs:0,maxArgs:0},isAllowedIncognitoAccess:{minArgs:0,maxArgs:0}},history:{addUrl:{minArgs:1,maxArgs:1},getVisits:{minArgs:1,maxArgs:1},deleteAll:{minArgs:0,maxArgs:0},deleteRange:{minArgs:1,maxArgs:1},deleteUrl:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1}},i18n:{detectLanguage:{minArgs:1,maxArgs:1},getAcceptLanguages:{minArgs:0,maxArgs:0}},identity:{launchWebAuthFlow:{minArgs:1,maxArgs:1}},idle:{queryState:{minArgs:1,maxArgs:1}},management:{get:{minArgs:1,maxArgs:1},getAll:{minArgs:0,maxArgs:0},getSelf:{minArgs:0,maxArgs:0},uninstallSelf:{minArgs:0,maxArgs:1}},notifications:{clear:{minArgs:1,maxArgs:1},create:{minArgs:1,maxArgs:2},getAll:{minArgs:0,maxArgs:0},getPermissionLevel:{minArgs:0,maxArgs:0},update:{minArgs:2,maxArgs:2}},pageAction:{getPopup:{minArgs:1,maxArgs:1},setPopup:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},getTitle:{minArgs:1,maxArgs:1},setTitle:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},hide:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setIcon:{minArgs:1,maxArgs:1},getIcon:{minArgs:1,maxArgs:1},show:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},runtime:{getBackgroundPage:{minArgs:0,maxArgs:0},getBrowserInfo:{minArgs:0,maxArgs:0},getPlatformInfo:{minArgs:0,maxArgs:0},openOptionsPage:{minArgs:0,maxArgs:0},requestUpdateCheck:{minArgs:0,maxArgs:0},sendMessage:{minArgs:1,maxArgs:3},sendNativeMessage:{minArgs:2,maxArgs:2},setUninstallURL:{minArgs:1,maxArgs:1}},storage:{local:{clear:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}},managed:{get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1}},sync:{clear:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}}},tabs:{create:{minArgs:1,maxArgs:1},captureVisibleTab:{minArgs:0,maxArgs:2},detectLanguage:{minArgs:0,maxArgs:1},duplicate:{minArgs:1,maxArgs:1},executeScript:{minArgs:1,maxArgs:2},get:{minArgs:1,maxArgs:1},getCurrent:{minArgs:0,maxArgs:0},getZoom:{minArgs:0,maxArgs:1},getZoomSettings:{minArgs:0,maxArgs:1},highlight:{minArgs:1,maxArgs:1},insertCSS:{minArgs:1,maxArgs:2},move:{minArgs:2,maxArgs:2},reload:{minArgs:0,maxArgs:2},remove:{minArgs:1,maxArgs:1},query:{minArgs:1,maxArgs:1},removeCSS:{minArgs:1,maxArgs:2},sendMessage:{minArgs:2,maxArgs:3},setZoom:{minArgs:1,maxArgs:2},setZoomSettings:{minArgs:1,maxArgs:2},update:{minArgs:1,maxArgs:2}},webNavigation:{getAllFrames:{minArgs:1,maxArgs:1},getFrame:{minArgs:1,maxArgs:1}},webRequest:{handlerBehaviorChanged:{minArgs:0,maxArgs:0}},windows:{create:{minArgs:0,maxArgs:1},get:{minArgs:1,maxArgs:2},getAll:{minArgs:0,maxArgs:1},getCurrent:{minArgs:0,maxArgs:1},getLastFocused:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},update:{minArgs:2,maxArgs:2}}};if(0===Object.keys(c).length)throw new Error("api-metadata.json has not been included in browser-polyfill");class d extends WeakMap{constructor(n,o=void 0){super(o),this.createItem=n}get(n){return this.has(n)||this.set(n,this.createItem(n)),super.get(n)}}const e=n=>{return n&&"object"==typeof n&&"function"==typeof n.then},f=(n,o)=>{return(...p)=>{chrome.runtime.lastError?n.reject(chrome.runtime.lastError):o.singleCallbackArg||1===p.length?n.resolve(p[0]):n.resolve(p)}},g=(n,o)=>{const p=q=>1==q?"argument":"arguments";return function(r,...s){if(s.lengtho.maxArgs)throw new Error(`Expected at most ${o.maxArgs} ${p(o.maxArgs)} for ${n}(), got ${s.length}`);return new Promise((t,u)=>{if(o.fallbackToNoCallback)try{r[n](...s,f({resolve:t,reject:u},o))}catch(v){console.warn(`${n} API method doesn't seem to support the callback parameter, `+"falling back to call it without a callback: ",v),r[n](...s),o.fallbackToNoCallback=!1,o.noCallback=!0,t()}else o.noCallback?(r[n](...s),t()):r[n](...s,f({resolve:t,reject:u},o))})}},h=(n,o,p)=>{return new Proxy(o,{apply(q,r,s){return p.call(r,n,...s)}})};let i=Function.call.bind(Object.prototype.hasOwnProperty);const j=(n,o={},p={})=>{let q=Object.create(null),r={has(t,u){return u in n||u in q},get(t,u){if(u in q)return q[u];if(u in n){let w=n[u];if("function"==typeof w){if("function"==typeof o[u])w=h(n,n[u],o[u]);else if(i(p,u)){let x=g(u,p[u]);w=h(n,n[u],x)}else w=w.bind(n);}else if("object"==typeof w&&null!==w&&(i(o,u)||i(p,u)))w=j(w,o[u],p[u]);else return Object.defineProperty(q,u,{configurable:!0,enumerable:!0,get(){return n[u]},set(x){n[u]=x}}),w;return q[u]=w,w}},set(t,u,v){return u in q?q[u]=v:n[u]=v,!0},defineProperty(t,u,v){return Reflect.defineProperty(q,u,v)},deleteProperty(t,u){return Reflect.deleteProperty(q,u)}},s=Object.create(n);return new Proxy(s,r)},l=new d(n=>{return"function"==typeof n?function(p,q,r){let t,s=!1,u=new Promise(x=>{t=function(y){s=!0,x(y)}}),v=n(p,q,t);const w=!0!==v&&e(v);return(!0===v||w||s)&&(w?v.then(r,x=>{console.error(x),r(void 0)}):u.then(r,x=>{console.error(x),r(void 0)}),!0)}:n}),m={runtime:{onMessage:(n=>({addListener(o,p,...q){o.addListener(n.get(p),...q)},hasListener(o,p){return o.hasListener(n.get(p))},removeListener(o,p){o.removeListener(n.get(p))}}))(l)}};return j(chrome,m,c)})()}else a.exports=browser}); 2 | //# sourceMappingURL=browser-polyfill.min.js.map 3 | 4 | 5 | // webextension-polyfill v.0.2.1 (https://github.com/mozilla/webextension-polyfill) 6 | 7 | /* This Source Code Form is subject to the terms of the Mozilla Public 8 | * License, v. 2.0. If a copy of the MPL was not distributed with this 9 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -------------------------------------------------------------------------------- /popup/punycode.js: -------------------------------------------------------------------------------- 1 | var punycode = (function() { 2 | /** Highest positive signed 32-bit float value */ 3 | var maxInt = 2147483647; // aka. 0x7FFFFFFF or 2^31-1 4 | 5 | /** Bootstring parameters */ 6 | var base = 36; 7 | var tMin = 1; 8 | var tMax = 26; 9 | var skew = 38; 10 | var damp = 700; 11 | var initialBias = 72; 12 | var initialN = 128; // 0x80 13 | var delimiter = '-'; // '\x2D' 14 | 15 | /** Regular expressions */ 16 | var regexPunycode = /^xn--/; 17 | var regexNonASCII = /[^\x20-\x7E]/; // unprintable ASCII chars + non-ASCII chars 18 | var regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g; // RFC 3490 separators 19 | 20 | /** Error messages */ 21 | var errors = { 22 | 'overflow': 'Overflow: input needs wider integers to process', 23 | 'not-basic': 'Illegal input >= 0x80 (not a basic code point)', 24 | 'invalid-input': 'Invalid input' 25 | }; 26 | 27 | /** Convenience shortcuts */ 28 | var baseMinusTMin = base - tMin; 29 | var floor = Math.floor; 30 | var stringFromCharCode = String.fromCharCode; 31 | 32 | /*--------------------------------------------------------------------------*/ 33 | 34 | /** 35 | * A generic error utility function. 36 | * @private 37 | * @param {String} type The error type. 38 | * @returns {Error} Throws a `RangeError` with the applicable error message. 39 | */ 40 | function error(type) { 41 | throw new RangeError(errors[type]); 42 | } 43 | 44 | /** 45 | * A generic `Array#map` utility function. 46 | * @private 47 | * @param {Array} array The array to iterate over. 48 | * @param {Function} callback The function that gets called for every array 49 | * item. 50 | * @returns {Array} A new array of values returned by the callback function. 51 | */ 52 | function map(array, fn) { 53 | var result = []; 54 | var length = array.length; 55 | while (length--) { 56 | result[length] = fn(array[length]); 57 | } 58 | return result; 59 | } 60 | 61 | /** 62 | * A simple `Array#map`-like wrapper to work with domain name strings or email 63 | * addresses. 64 | * @private 65 | * @param {String} domain The domain name or email address. 66 | * @param {Function} callback The function that gets called for every 67 | * character. 68 | * @returns {Array} A new string of characters returned by the callback 69 | * function. 70 | */ 71 | function mapDomain(string, fn) { 72 | var parts = string.split('@'); 73 | var result = ''; 74 | if (parts.length > 1) { 75 | // In email addresses, only the domain name should be punycoded. Leave 76 | // the local part (everything up to `@`) intact. 77 | result = parts[0] + '@'; 78 | string = parts[1]; 79 | } 80 | // Avoid `split(regex)` for IE8 compatibility. See #17. 81 | string = string.replace(regexSeparators, '\x2E'); 82 | var labels = string.split('.'); 83 | var encoded = map(labels, fn).join('.'); 84 | return result + encoded; 85 | } 86 | 87 | /** 88 | * Creates an array containing the numeric code points of each Unicode 89 | * character in the string. While JavaScript uses UCS-2 internally, 90 | * this function will convert a pair of surrogate halves (each of which 91 | * UCS-2 exposes as separate characters) into a single code point, 92 | * matching UTF-16. 93 | * @see `punycode.ucs2.decode` 94 | * @see 95 | * @memberOf punycode.ucs2 96 | * @name decode 97 | * @param {String} string The Unicode input string (e.g. 'Manuel Quiñones'). 98 | * @returns {Array} The new array of code points. 99 | */ 100 | function ucs2decode(string) { 101 | var output = []; 102 | var counter = 0; 103 | var length = string.length; 104 | var value; 105 | var extra; 106 | while (counter < length) { 107 | value = string.charCodeAt(counter++); 108 | if (value >= 0xD800 && value <= 0xDBFF && counter < length) { 109 | // high surrogate, and there is a next character 110 | extra = string.charCodeAt(counter++); 111 | if ((extra & 0xFC00) == 0xDC00) { // low surrogate 112 | output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); 113 | } else { 114 | // unmatched surrogate; only append this code unit, in case the next 115 | // code unit is the high surrogate of a surrogate pair 116 | output.push(value); 117 | counter--; 118 | } 119 | } else { 120 | output.push(value); 121 | } 122 | } 123 | return output; 124 | } 125 | 126 | /** 127 | * Creates a string based on an array of numeric code points. 128 | * @see `punycode.ucs2.encode` 129 | * @memberOf punycode.ucs2 130 | * @name encode 131 | * @param {Array} codePoints The array of numeric code points. 132 | * @returns {String} The new Unicode string. 133 | */ 134 | function ucs2encode(array) { 135 | return map(array, function(value) { 136 | var output = ''; 137 | if (value > 0xFFFF) { 138 | value -= 0x10000; 139 | output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800); 140 | value = 0xDC00 | value & 0x3FF; 141 | } 142 | output += stringFromCharCode(value); 143 | return output; 144 | }).join(''); 145 | } 146 | 147 | /** 148 | * Converts a basic code point into a digit/integer. 149 | * @see `digitToBasic()` 150 | * @private 151 | * @param {Number} codePoint The basic code point. 152 | * @returns {Number} The numeric value of a basic code point (for use in 153 | * representing integers) in the range `0` to `base - 1`, or `base` if 154 | * the code point does not represent a value. 155 | */ 156 | function basicToDigit(codePoint) { 157 | if (codePoint - 48 < 10) { 158 | return codePoint - 22; 159 | } 160 | if (codePoint - 65 < 26) { 161 | return codePoint - 65; 162 | } 163 | if (codePoint - 97 < 26) { 164 | return codePoint - 97; 165 | } 166 | return base; 167 | } 168 | 169 | /** 170 | * Converts a digit/integer into a basic code point. 171 | * @see `basicToDigit()` 172 | * @private 173 | * @param {Number} digit The numeric value of a basic code point. 174 | * @returns {Number} The basic code point whose value (when used for 175 | * representing integers) is `digit`, which needs to be in the range 176 | * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is 177 | * used. 178 | */ 179 | function digitToBasic(digit, flag) { 180 | // 0..25 map to ASCII a..z or A..Z 181 | // 26..35 map to ASCII 0..9 182 | return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5); 183 | } 184 | 185 | /** 186 | * Bias adaptation function as per section 3.4 of RFC 3492. 187 | * http://tools.ietf.org/html/rfc3492#section-3.4 188 | * @private 189 | */ 190 | function adapt(delta, numPoints, firstTime) { 191 | var k = 0; 192 | delta = firstTime ? floor(delta / damp) : delta >> 1; 193 | delta += floor(delta / numPoints); 194 | for (/* no initialization */; delta > baseMinusTMin * tMax >> 1; k += base) { 195 | delta = floor(delta / baseMinusTMin); 196 | } 197 | return floor(k + (baseMinusTMin + 1) * delta / (delta + skew)); 198 | } 199 | 200 | /** 201 | * Converts a Punycode string of ASCII-only symbols to a string of Unicode 202 | * symbols. 203 | * @memberOf punycode 204 | * @param {String} input The Punycode string of ASCII-only symbols. 205 | * @returns {String} The resulting string of Unicode symbols. 206 | */ 207 | function decode(input) { 208 | // Don't use UCS-2 209 | var output = []; 210 | var inputLength = input.length; 211 | var i = 0; 212 | var n = initialN; 213 | var bias = initialBias; 214 | 215 | // Handle the basic code points: let `basic` be the number of input code 216 | // points before the last delimiter, or `0` if there is none, then copy 217 | // the first `basic` code points to the output. 218 | 219 | var basic = input.lastIndexOf(delimiter); 220 | if (basic < 0) { 221 | basic = 0; 222 | } 223 | 224 | for (var j = 0; j < basic; ++j) { 225 | // if it's not a basic code point 226 | if (input.charCodeAt(j) >= 0x80) { 227 | error('not-basic'); 228 | } 229 | output.push(input.charCodeAt(j)); 230 | } 231 | 232 | // Main decoding loop: start just after the last delimiter if any basic code 233 | // points were copied; start at the beginning otherwise. 234 | 235 | for (var index = basic > 0 ? basic + 1 : 0; index < inputLength; /* no final expression */) { 236 | 237 | // `index` is the index of the next character to be consumed. 238 | // Decode a generalized variable-length integer into `delta`, 239 | // which gets added to `i`. The overflow checking is easier 240 | // if we increase `i` as we go, then subtract off its starting 241 | // value at the end to obtain `delta`. 242 | var oldi = i; 243 | for (var w = 1, k = base; /* no condition */; k += base) { 244 | 245 | if (index >= inputLength) { 246 | error('invalid-input'); 247 | } 248 | 249 | var digit = basicToDigit(input.charCodeAt(index++)); 250 | 251 | if (digit >= base || digit > floor((maxInt - i) / w)) { 252 | error('overflow'); 253 | } 254 | 255 | i += digit * w; 256 | var t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias); 257 | 258 | if (digit < t) { 259 | break; 260 | } 261 | 262 | var baseMinusT = base - t; 263 | if (w > floor(maxInt / baseMinusT)) { 264 | error('overflow'); 265 | } 266 | w *= baseMinusT; 267 | 268 | } 269 | 270 | var out = output.length + 1; 271 | bias = adapt(i - oldi, out, oldi == 0); 272 | 273 | // `i` was supposed to wrap around from `out` to `0`, 274 | // incrementing `n` each time, so we'll fix that now: 275 | if (floor(i / out) > maxInt - n) { 276 | error('overflow'); 277 | } 278 | 279 | n += floor(i / out); 280 | i %= out; 281 | 282 | // Insert `n` at position `i` of the output 283 | output.splice(i++, 0, n); 284 | 285 | } 286 | 287 | return ucs2encode(output); 288 | } 289 | 290 | /** 291 | * Converts a string of Unicode symbols (e.g. a domain name label) to a 292 | * Punycode string of ASCII-only symbols. 293 | * @memberOf punycode 294 | * @param {String} input The string of Unicode symbols. 295 | * @returns {String} The resulting Punycode string of ASCII-only symbols. 296 | */ 297 | function encode(input) { 298 | var n; 299 | var delta; 300 | var h; 301 | var b; 302 | var bias; 303 | var j; 304 | var m; 305 | var q; 306 | var k; 307 | var t; 308 | var currentValue; 309 | var output = []; 310 | 311 | // Convert the input in UCS-2 to Unicode 312 | input = ucs2decode(input); 313 | 314 | // Cache the length 315 | var inputLength = input.length; 316 | 317 | // Initialize the state 318 | n = initialN; 319 | delta = 0; 320 | bias = initialBias; 321 | 322 | // Handle the basic code points 323 | for (j = 0; j < inputLength; ++j) { 324 | currentValue = input[j]; 325 | if (currentValue < 0x80) { 326 | output.push(stringFromCharCode(currentValue)); 327 | } 328 | } 329 | 330 | h = b = output.length; 331 | 332 | // `b` is the number of basic code points in the input, which is also the 333 | // number of code points in the output so far. 334 | 335 | if (b < inputLength) { 336 | output.push(delimiter); 337 | } 338 | 339 | // Main encoding loop: 340 | while (h < inputLength) { 341 | // All non-basic code points < n have been handled already. Find the next 342 | // larger one: 343 | for (m = maxInt, j = 0; j < inputLength; ++j) { 344 | currentValue = input[j]; 345 | if (currentValue >= n && currentValue < m) { 346 | m = currentValue; 347 | } 348 | } 349 | 350 | // Increase `delta` enough to advance the decoder's state to , 351 | // but guard against overflow 352 | var handledCPCount = h + 1; 353 | if (m - n > floor((maxInt - delta) / handledCPCount)) { 354 | error('overflow'); 355 | } 356 | 357 | delta += (m - n) * handledCPCount; 358 | n = m; 359 | 360 | for (j = 0; j < inputLength; ++j) { 361 | currentValue = input[j]; 362 | 363 | if (currentValue < n && ++delta > maxInt) { 364 | error('overflow'); 365 | } 366 | 367 | if (currentValue == n) { 368 | // Represent delta as a generalized variable-length integer 369 | for (q = delta, k = base; /* no condition */; k += base) { 370 | t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias); 371 | if (q < t) { 372 | break; 373 | } 374 | var qMinusT = q - t; 375 | var baseMinusT = base - t; 376 | output.push( 377 | stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0)) 378 | ); 379 | q = floor(qMinusT / baseMinusT); 380 | } 381 | 382 | output.push(stringFromCharCode(digitToBasic(q, 0))); 383 | bias = adapt(delta, handledCPCount, h == b); 384 | delta = 0; 385 | ++h; 386 | } 387 | } 388 | 389 | ++delta; 390 | ++n; 391 | 392 | } 393 | return output.join(''); 394 | } 395 | 396 | /** 397 | * Converts a Punycode string representing a domain name or an email address 398 | * to Unicode. Only the Punycoded parts of the input will be converted, i.e. 399 | * it doesn't matter if you call it on a string that has already been 400 | * converted to Unicode. 401 | * @memberOf punycode 402 | * @param {String} input The Punycoded domain name or email address to 403 | * convert to Unicode. 404 | * @returns {String} The Unicode representation of the given Punycode 405 | * string. 406 | */ 407 | function toUnicode(input) { 408 | return mapDomain(input, function(string) { 409 | return regexPunycode.test(string) 410 | ? decode(string.slice(4).toLowerCase()) 411 | : string; 412 | }); 413 | } 414 | 415 | /** 416 | * Converts a Unicode string representing a domain name or an email address to 417 | * Punycode. Only the non-ASCII parts of the domain name will be converted, 418 | * i.e. it doesn't matter if you call it with a domain that's already in 419 | * ASCII. 420 | * @memberOf punycode 421 | * @param {String} input The domain name or email address to convert, as a 422 | * Unicode string. 423 | * @returns {String} The Punycode representation of the given domain name or 424 | * email address. 425 | */ 426 | function toASCII(input) { 427 | return mapDomain(input, function(string) { 428 | return regexNonASCII.test(string) 429 | ? 'xn--' + encode(string) 430 | : string; 431 | }); 432 | } 433 | 434 | /*--------------------------------------------------------------------------*/ 435 | 436 | /** Define the public API */ 437 | var punycode = { 438 | /** 439 | * A string representing the current Punycode.js version number. 440 | * @memberOf punycode 441 | * @type String 442 | */ 443 | 'version': '2.1.0', 444 | /** 445 | * An object of methods to convert from JavaScript's internal character 446 | * representation (UCS-2) to Unicode code points, and back. 447 | * @see 448 | * @memberOf punycode 449 | * @type Object 450 | */ 451 | 'ucs2': { 452 | 'decode': ucs2decode, 453 | 'encode': ucs2encode 454 | }, 455 | 'decode': decode, 456 | 'encode': encode, 457 | 'toASCII': toASCII, 458 | 'toUnicode': toUnicode 459 | }; 460 | 461 | return punycode; 462 | }()); 463 | -------------------------------------------------------------------------------- /popup/prefs.js.erb: -------------------------------------------------------------------------------- 1 | // prefs.html -:- See LICENSE.txt for copyright and license details. 2 | 3 | var thPrefFunc = (function() { 4 | 5 | var commandKeyInput, closeOnGoCheckbox; 6 | var fontSizeInput; 7 | var sortByTitleButton; 8 | var sortByURLButton; 9 | var sortByPositionButton; 10 | <% if ENV['TARGET'] == 'firefox' %> 11 | var sortByNeglectButton; 12 | <% end %> 13 | var sortByReverseCheckbox; 14 | var controlVisitNCheckbox; 15 | var originalCommandKey; 16 | var isMac; 17 | var prefFields, prefSettings, origPrefSettings; 18 | var prefs; 19 | 20 | const DEFAULT_BASE_FONT_SIZE = 12; 21 | 22 | const CTRL_USER = "ctrl"; 23 | const ALT_USER = "alt"; 24 | const COMMAND_USER = "command"; 25 | const SHIFT_USER = "shift"; 26 | 27 | const CTRL_API = "Ctrl"; 28 | const ALT_API = "Alt"; 29 | const MAC_CTRL_API = "MacCtrl"; 30 | const SHIFT_API = "Shift"; 31 | 32 | const USER_NAMES_FROM_KEYS = { 33 | ",": ",", 34 | ".": ".", 35 | " ": "Space" 36 | }; 37 | 38 | const API_NAMES_FROM_KEYS = { 39 | ",": "Comma", 40 | ".": "Period", 41 | " ": "Space" 42 | }; 43 | 44 | const FUNCTION_KEY_NAMES = ["Home", "End", "PageUp", "PageDown", "Insert", "Delete", 45 | "Up", "Down", "Left", "Right"]; 46 | 47 | const PREF_FIELD_NAMES = ["command_key", "closeOnGo"]; 48 | 49 | function initPrefs() { 50 | FUNCTION_KEY_NAMES.forEach(function(name) { 51 | USER_NAMES_FROM_KEYS[name] = name.toLowerCase(); 52 | API_NAMES_FROM_KEYS[name] = name; 53 | }); 54 | 55 | prefFields = {}; 56 | origPrefSettings = {}; 57 | prefSettings = {}; 58 | for (var prefName of PREF_FIELD_NAMES) { 59 | prefFields[prefName] = document.getElementById(prefName); 60 | if (!prefFields[prefName]) { 61 | throw new Error(`Awp: no field for pref ${prefName}`); 62 | } 63 | } 64 | 65 | document.getElementById("restoreFontSizeButton").addEventListener("click", resetFontSizeToFactory); 66 | 67 | $("button").mouseover(doMouseOver); 68 | $("button").mouseout(doMouseOut); 69 | $("button").mousedown(doMouseDown); 70 | $("button").mouseup(doMouseUp); 71 | // $("#command_key").click(select); 72 | $("#command_key").keypress(handleConfigKeyPress); 73 | 74 | originalCommandKey = ""; 75 | if (browser.commands.update && typeof(browser.commands.update) == 'function') { 76 | $("div#change-shortcut-div").removeClass("hide").addClass("show"); 77 | } else { 78 | //console.log("QQQ: Should not show the set-key\n"); 79 | } 80 | 81 | initFields(); 82 | } 83 | 84 | function dumpError(err, msg) { 85 | if (typeof(err) == "string") { 86 | msg += err; 87 | } else { 88 | msg += err.message; 89 | } 90 | console.log(msg); 91 | } 92 | 93 | function checkFontSizeInput(event) { 94 | let value = event.target.value; 95 | if (value === '') { 96 | alert("size must be a number only"); 97 | event.preventDefault(); 98 | event.stopPropagation(); 99 | return false; 100 | } 101 | let m = /.*[^\d]/.test(value); 102 | if (m) { 103 | alert("size must be a number"); 104 | event.preventDefault(); 105 | event.stopPropagation(); 106 | return false; 107 | } 108 | let numval = parseInt(value); 109 | if (numval < 6) { 110 | alert('sorry, min size of 6'); 111 | event.target.value = 6; 112 | } else if (numval > 36) { 113 | alert('sorry, max size of 36'); 114 | event.target.value = 36; 115 | } 116 | submitChanges(); 117 | } 118 | 119 | 120 | function getCheckSortByGroup() { 121 | if (sortByTitleButton.checked) { 122 | return 'Title'; 123 | } else if (sortByURLButton.checked) { 124 | return 'URL'; 125 | } else if (sortByPositionButton.checked) { 126 | return 'Position'; 127 | <% if ENV['TARGET'] == 'firefox' %> 128 | } else if (sortByNeglectButton.checked) { 129 | return 'Neglect'; 130 | <% end %> 131 | } else { 132 | alert("Hey -- no sort button checked"); 133 | return 'Title'; 134 | } 135 | } 136 | 137 | function checkSortByGroup(event) { 138 | prefSettings['sortBy'] = getCheckSortByGroup(); 139 | submitChanges(); 140 | } 141 | 142 | function getURLForLocalFile(fname) { 143 | <% if ENV['TARGET'] == 'firefox' %> 144 | return browser.extension.getURL("popup/images/restore12.png"); 145 | <% else %> 146 | return chrome.runtime.getURL("popup/images/restore12.png"); 147 | <% end %> 148 | } 149 | 150 | function initFields() { 151 | commandKeyInput = document.getElementById("command_key"); 152 | 153 | closeOnGoCheckbox = document.getElementById("closeOnGo"); 154 | closeOnGoCheckbox.checked = true; 155 | closeOnGoCheckbox.addEventListener('change', submitChanges, false); 156 | 157 | let imageURL = getURLForLocalFile("popup/images/restore12.png"); 158 | ["restoreCloseOnGoImg", "restoreSortByReverseImg", "restoreControlVisitNImg", 159 | "restoreStartupKeyImg", "restoreFontSizeImg"].forEach(function(id) { 160 | document.getElementById(id).setAttribute("src", imageURL); 161 | }); 162 | 163 | let restoreCloseAndGoButton = document.getElementById("restoreCloseOnGoButton"); 164 | restoreCloseAndGoButton.addEventListener('click', handleRestoreCloseOnGoButton, false); 165 | 166 | document.getElementById("restoreSortByReverseButton").addEventListener('click', handleRestoreSortByReverseButton, false); 167 | document.getElementById("restoreControlVisitNButton").addEventListener('click', handleRestoreControlVisitNButton, false); 168 | document.getElementById("restoreStartupKeyButton").addEventListener('click', handleRestoreStartupKeyButton, false); 169 | 170 | sortByReverseCheckbox = document.getElementById("sortByReverse"); 171 | sortByReverseCheckbox.checked = false; 172 | sortByReverseCheckbox.addEventListener('change', submitChanges, false); 173 | 174 | controlVisitNCheckbox = document.getElementById("controlVisitN"); 175 | controlVisitNCheckbox.checked = false; 176 | controlVisitNCheckbox.addEventListener('change', submitChanges, false); 177 | 178 | fontSizeInput = document.getElementById("fontSize"); 179 | fontSizeInput.addEventListener('change', checkFontSizeInput); 180 | 181 | sortByTitleButton = document.getElementById("sortByTitle"); 182 | sortByURLButton = document.getElementById("sortByURL"); 183 | sortByPositionButton = document.getElementById("sortByPosition"); 184 | <% if ENV['TARGET'] == 'firefox' %> 185 | sortByNeglectButton = document.getElementById("sortByNeglect"); 186 | <% end %> 187 | [sortByTitleButton, sortByURLButton, 188 | <% if ENV['TARGET'] == 'firefox' %> 189 | sortByNeglectButton, 190 | <% end %> 191 | sortByPositionButton].forEach(function(e) { 192 | e.addEventListener('change', checkSortByGroup, false); 193 | }); 194 | var gotCommandsOK = function(commands) { 195 | if (commands[0].name == "_execute_browser_action") { 196 | commandKeyInput.value = 197 | prefSettings["_execute_browser_action"] = 198 | origPrefSettings["_execute_browser_action"] = 199 | userStringFromInternalString(commands[0].shortcut); 200 | prefSettings["_execute_browser_action__description"] = 201 | origPrefSettings["_execute_browser_action__description"] = 202 | (commands[0].description || ""); 203 | } 204 | getPrefs(); 205 | }; 206 | var gotCommandsErr = function(err) { 207 | var msg = "Error getting add-on commmands: "; 208 | if (typeof(err) == "string") { 209 | msg += err;; 210 | } else { 211 | msg += err.message; 212 | } 213 | console.log(msg); 214 | getPrefs(); 215 | }; 216 | browser.commands.getAll().then(gotCommandsOK, gotCommandsErr); 217 | } 218 | 219 | function getPrefs() { 220 | let gotPrefsOK = function(prefs) { 221 | if ('prefs' in prefs) { 222 | let innerPrefs = prefs['prefs']; 223 | for (var p in innerPrefs) { 224 | origPrefSettings[p] = innerPrefs[p]; 225 | } 226 | if ('sortBy' in origPrefSettings) { 227 | prefSettings.sortBy = origPrefSettings.sortBy; 228 | } 229 | } 230 | initFieldsWithPrefs(); 231 | getIsMac(); 232 | }; 233 | let gotPrefsErr = function(err) { 234 | dumpError(err, `Error getting prefs`); 235 | prefs = {}; 236 | initFieldsWithPrefs(); 237 | getIsMac(); 238 | }; 239 | browser.storage.local.get().then(gotPrefsOK, gotPrefsErr); 240 | } 241 | 242 | function getIsMac() { 243 | var gotPlatformInfoOK = function(info) { 244 | isMac = info.os == "mac"; 245 | }; 246 | var gotPlatformInfoError = function(err) { 247 | dumpError(err, "Error getting platform info: "); 248 | } 249 | browser.runtime.getPlatformInfo().then(gotPlatformInfoOK, gotPlatformInfoError); 250 | } 251 | 252 | function doMouseOver(eventData) { 253 | if (!eventData.currentTarget.disabled) { 254 | $(this).addClass("highlighted"); 255 | } 256 | } 257 | 258 | function doMouseOut(eventData) { 259 | $(this).removeClass("highlighted"); 260 | $(this).removeClass("pressed"); 261 | } 262 | 263 | function doMouseDown(eventData) { 264 | if (!eventData.currentTarget.disabled) { 265 | $(this).addClass("pressed"); 266 | } 267 | } 268 | 269 | function doMouseUp(eventData) { 270 | $(this).removeClass("pressed"); 271 | } 272 | 273 | function verifyShortcutFromEvent(event) { 274 | let validKeys = isMac ? ["ctrlKey", "metaKey"] : ["ctrlKey", "altKey"]; 275 | let modifiers = isMac ? [CTRL_USER, COMMAND_USER] : [CTRL_USER, ALT_USER]; 276 | let count = validKeys.reduce(function(acc, name) { 277 | return acc + (event[name] ? 1 : 0) }, 0); 278 | if (count == 0 && FUNCTION_KEY_NAMES.indexOf(event.key) == -1) { 279 | console.log(`tabhunter prefs: ${event.key} must have exactly one of the ${modifiers.join(", ")} modifier keys`); 280 | throw new Error("bad modifiers"); 281 | } else if (count > 1) { 282 | //XXX: What about the media keys? 283 | console.log("tabhunter prefs: The startup keybinding must have exactly one of the <" + modifiers.join(", ") + "> modifier keys"); 284 | throw new Error("bad modifiers"); 285 | } 286 | } 287 | 288 | function eventToInternalProperties(event) { 289 | var props = {ctrlKey:false, macCtrlKey: false, 290 | altKey: false, shiftKey: false, key:"" }; 291 | ["key", "altKey", "shiftKey"].forEach(function(p) { props[p] = event[p]; }); 292 | if (event.ctrlKey) { 293 | if (isMac) { 294 | props.macCtrlKey = true; 295 | } else { 296 | props.ctrlKey = true; 297 | } 298 | } 299 | if (event.metaKey && isMac) { 300 | props.ctrlKey = true; 301 | } 302 | var s = []; 303 | ["key", "altKey", "shiftKey", "macCtrlKey", "ctrlKey"].forEach(function(p) { 304 | s.push(p + ":" + (props[p] ? "true" : "false")); 305 | }); 306 | 307 | return props; 308 | } 309 | 310 | function userStringFromInternalString(internalCommand) { 311 | let parts = internalCommand.split("+"); 312 | let newParts = parts.map(function(internalCommandName) { 313 | switch (internalCommandName) { 314 | case ALT_API: 315 | return ALT_USER; 316 | case SHIFT_API: 317 | return SHIFT_USER; 318 | case CTRL_API: 319 | return isMac ? COMMAND_USER : CTRL_USER; 320 | case MAC_CTRL_API: 321 | return CTRL_USER; 322 | default: 323 | if (USER_NAMES_FROM_KEYS[internalCommandName]) { 324 | return USER_NAMES_FROM_KEYS[internalCommandName]; 325 | } else { 326 | return internalCommandName; 327 | } 328 | } 329 | }); 330 | return newParts.join("+"); 331 | } 332 | 333 | // "MacCtrl" is a very un-user-friendly way to refer to the "ctrl" key on osx, 334 | // same with calling the Command key "ctrl". So let's show the users 335 | // user-centered views of the pref values. 336 | 337 | function propertiesToUserAndAPIString(props) { 338 | let s_user = "", s_api = ""; 339 | if (props.altKey) { 340 | s_user = ALT_USER + "+"; 341 | s_api = ALT_API + "+"; 342 | } else if (props.ctrlKey) { 343 | if (isMac) { 344 | s_user = COMMAND_USER + "+"; 345 | s_api = CTRL_API + "+"; 346 | } else { 347 | s_user = CTRL_USER + "+"; 348 | s_api = CTRL_API + "+"; 349 | } 350 | } else if (props.macCtrlKey) { 351 | s_user = CTRL_USER + "+"; 352 | s_api = MAC_CTRL_API + "+"; 353 | } 354 | if (props.shiftKey) { 355 | s_user += SHIFT_USER + "+"; 356 | s_api += SHIFT_API + "+"; 357 | } 358 | var propNames = { 359 | ",": "Comma", 360 | ".": "Period", 361 | " ": "Space" 362 | }; 363 | 364 | if (/^[A-Z]$/.test(props.key)) { 365 | s_user += props.key.toLowerCase(); 366 | s_api += props.key.toUpperCase(); 367 | } else if (/^[0-9a-z]$/.test(props.key)) { 368 | s_user += props.key; 369 | s_api += props.key.toUpperCase(); 370 | } else if (/^F[0-9]+$/.test(props.key) || FUNCTION_KEY_NAMES.indexOf(props.key) >= 0) { 371 | s_user += props.key; 372 | s_api += props.key; 373 | } else if (props.key in USER_NAMES_FROM_KEYS) { 374 | s_user += USER_NAMES_FROM_KEYS[props.key]; 375 | s_api += props.key; 376 | } else { 377 | s_user += props.key; 378 | console.log("tabhunter prefs: Can't support a key sequence of '" + s_user + "'"); 379 | alert("Can't support a key sequence of '" + s_user + "'"); 380 | throw new Error("bad key sequence: " + s_user); 381 | } 382 | return [s_user, s_api]; 383 | } 384 | 385 | function initFieldsWithPrefs() { 386 | if ("_execute_browser_action" in origPrefSettings) { 387 | commandKeyInput.value = origPrefSettings["_execute_browser_action"]; 388 | } else { 389 | commandKeyInput.value = ""; 390 | } 391 | if ("closeOnGo" in origPrefSettings) { 392 | closeOnGoCheckbox.checked = !!origPrefSettings["closeOnGo"]; 393 | } else { 394 | closeOnGoCheckbox.checked = true; 395 | origPrefSettings["closeOnGo"] = true; 396 | } 397 | if ("sortByReverse" in origPrefSettings) { 398 | sortByReverseCheckbox.checked = !!origPrefSettings["sortByReverse"]; 399 | } else { 400 | sortByReverseCheckbox.checked = false; 401 | origPrefSettings["sortByReverse"] = false; 402 | } 403 | if ("controlVisitN" in origPrefSettings) { 404 | controlVisitNCheckbox.checked = !!origPrefSettings["controlVisitN"]; 405 | } else { 406 | controlVisitNCheckbox.checked = false; 407 | origPrefSettings["controlVisitN"] = false; 408 | } 409 | fontSizeInput.value = (('fontSize' in origPrefSettings) ? 410 | origPrefSettings['fontSize'] : DEFAULT_BASE_FONT_SIZE); 411 | if ('sortBy' in origPrefSettings) { 412 | let sortByValue = origPrefSettings['sortBy']; 413 | switch(sortByValue) { 414 | case 'Title': 415 | sortByTitleButton.checked = true; 416 | break; 417 | case 'URL': 418 | sortByURLButton.checked = true; 419 | break; 420 | case 'Position': 421 | sortByPositionButton.checked = true; 422 | break; 423 | <% if ENV['TARGET'] == 'firefox' %> 424 | case 'Neglect': 425 | sortByNeglectButton.checked = true; 426 | break; 427 | <% end %> 428 | default: 429 | //console.log(`tabhunter prefs: ignoring sortBy pref ${sortByValue}`); 430 | sortByTitleButton.checked = true; 431 | } 432 | } else { 433 | sortByTitleButton.checked = true; 434 | } 435 | } 436 | 437 | function resetFontSizeToFactory() { 438 | fontSizeInput.value = DEFAULT_BASE_FONT_SIZE; 439 | submitChanges(); 440 | } 441 | 442 | function handleRestoreCloseOnGoButton(event) { 443 | closeOnGoCheckbox.checked = origPrefSettings["closeOnGo"]; 444 | submitChanges(); 445 | } 446 | 447 | function handleRestoreSortByReverseButton(event) { 448 | sortByReverseCheckbox.checked = origPrefSettings["sortByReverse"]; 449 | submitChanges(); 450 | } 451 | 452 | function handleRestoreControlVisitNButton(event) { 453 | controlVisitNCheckbox.checked = origPrefSettings["controlVisitN"]; 454 | submitChanges(); 455 | } 456 | 457 | function handleRestoreStartupKeyButton(event) { 458 | if ("_execute_browser_action" in origPrefSettings) { 459 | commandKeyInput.value = origPrefSettings["_execute_browser_action"]; 460 | } else { 461 | commandKeyInput.value = ""; 462 | } 463 | updateCommands(); 464 | } 465 | 466 | function submitChanges() { 467 | var innerPrefs = {}; 468 | var prefs = {"prefs": innerPrefs}; 469 | innerPrefs["closeOnGo"] = closeOnGoCheckbox.checked; 470 | innerPrefs["sortByReverse"] = sortByReverseCheckbox.checked; 471 | innerPrefs["controlVisitN"] = controlVisitNCheckbox.checked; 472 | innerPrefs["fontSize"] = fontSizeInput.value; 473 | innerPrefs["sortBy"] = prefSettings['sortBy']; 474 | 475 | let updatePrefErr = function(err) { 476 | dumpError(err, `Error updating prefs`); 477 | }; 478 | browser.storage.local.set(prefs).then(postSubmitChanges). 479 | catch( 480 | function(err) { 481 | dumpError(err, "Error updating _execute_browser_action: "); 482 | }); 483 | } 484 | 485 | function postSubmitChanges() { 486 | if (typeof(thMain) != "undefined") { 487 | thMain.reloadPrefsAndMatches(); 488 | } 489 | } 490 | 491 | function updateCommands() { 492 | let updateCommandOK = function() { 493 | //console.log(`tabhunter: shortcut changed`); 494 | }; 495 | let updateCommandErr = function(err) { 496 | dumpError(err, `Error updating command: ${err}`); 497 | alert(`Error updating command: ${err}`); 498 | }; 499 | browser.commands.update({ name: "_execute_browser_action", 500 | shortcut: prefSettings["_execute_browser_action"], 501 | description: prefSettings["_execute_browser_action__description"] 502 | }).then(updateCommandOK, updateCommandErr); 503 | } 504 | 505 | function handleConfigKeyPress(event) { 506 | var target = event.target; 507 | try { 508 | verifyShortcutFromEvent(event); 509 | let props = eventToInternalProperties(event); 510 | let propertyStrings = propertiesToUserAndAPIString(props); 511 | target.value = propertyStrings[0]; 512 | // Save the value now, use it later when we submit it. 513 | prefSettings["_execute_browser_action"] = propertyStrings[1]; 514 | } catch(ex) { 515 | console.log(`tabhunter prefs: Error: ${ex} \n ${ex}`); 516 | } 517 | event.stopPropagation(); 518 | event.preventDefault(); 519 | updateCommands(); 520 | } 521 | 522 | return { 523 | initPrefs: function() { 524 | initPrefs(); 525 | this.closeOnGoCheckbox_checked = closeOnGoCheckbox.checked; 526 | }, 527 | __bosco__: null 528 | }; 529 | 530 | }); 531 | 532 | var thPref; 533 | $(document).ready(function() { 534 | thPref = thPrefFunc(); 535 | thPref.initPrefs(); 536 | }); 537 | -------------------------------------------------------------------------------- /popup/jquery-3.5.1.slim.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v3.5.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector | (c) JS Foundation and other contributors | jquery.org/license */ 2 | !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(g,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,v=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,y=n.hasOwnProperty,a=y.toString,l=a.call(Object),m={},b=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},w=g.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function C(e,t,n){var r,i,o=(n=n||w).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function T(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector",E=function(e,t){return new E.fn.init(e,t)};function d(e){var t=!!e&&"length"in e&&e.length,n=T(e);return!b(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+R+")"+R+"*"),U=new RegExp(R+"|>"),V=new RegExp(W),X=new RegExp("^"+B+"$"),Q={ID:new RegExp("^#("+B+")"),CLASS:new RegExp("^\\.("+B+")"),TAG:new RegExp("^("+B+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+R+"*(even|odd|(([+-]|)(\\d*)n|)"+R+"*(?:([+-]|)"+R+"*(\\d+)|))"+R+"*\\)|)","i"),bool:new RegExp("^(?:"+I+")$","i"),needsContext:new RegExp("^"+R+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+R+"*((?:-\\d)?\\d*)"+R+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,G=/^(?:input|select|textarea|button)$/i,K=/^h\d$/i,J=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+R+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){C()},ae=xe(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{O.apply(t=P.call(d.childNodes),d.childNodes),t[d.childNodes.length].nodeType}catch(e){O={apply:t.length?function(e,t){q.apply(e,P.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,d=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==d&&9!==d&&11!==d)return n;if(!r&&(C(e),e=e||T,E)){if(11!==d&&(u=Z.exec(t)))if(i=u[1]){if(9===d){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return O.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&p.getElementsByClassName&&e.getElementsByClassName)return O.apply(n,e.getElementsByClassName(i)),n}if(p.qsa&&!k[t+" "]&&(!v||!v.test(t))&&(1!==d||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===d&&(U.test(t)||_.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&p.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=A)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+be(l[o]);c=l.join(",")}try{return O.apply(n,f.querySelectorAll(c)),n}catch(e){k(t,!0)}finally{s===A&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>x.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[A]=!0,e}function ce(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)x.attrHandle[n[r]]=t}function de(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function pe(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in p=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},C=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:d;return r!=T&&9===r.nodeType&&r.documentElement&&(a=(T=r).documentElement,E=!i(T),d!=T&&(n=T.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),p.scope=ce(function(e){return a.appendChild(e).appendChild(T.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),p.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),p.getElementsByTagName=ce(function(e){return e.appendChild(T.createComment("")),!e.getElementsByTagName("*").length}),p.getElementsByClassName=J.test(T.getElementsByClassName),p.getById=ce(function(e){return a.appendChild(e).id=A,!T.getElementsByName||!T.getElementsByName(A).length}),p.getById?(x.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(x.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),x.find.TAG=p.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):p.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},x.find.CLASS=p.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(p.qsa=J.test(T.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+R+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+R+"*(?:value|"+I+")"),e.querySelectorAll("[id~="+A+"-]").length||v.push("~="),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+R+"*name"+R+"*="+R+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+A+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=T.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+R+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(p.matchesSelector=J.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){p.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",W)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=J.test(a.compareDocumentPosition),y=t||J.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!p.sortDetached&&t.compareDocumentPosition(e)===n?e==T||e.ownerDocument==d&&y(d,e)?-1:t==T||t.ownerDocument==d&&y(d,t)?1:u?H(u,e)-H(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==T?-1:t==T?1:i?-1:o?1:u?H(u,e)-H(u,t):0;if(i===o)return de(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?de(a[r],s[r]):a[r]==d?-1:s[r]==d?1:0}),T},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(C(e),p.matchesSelector&&E&&!k[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||p.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){k(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&V.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+R+")"+e+"("+R+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return b(n)?E.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?E.grep(e,function(e){return e===n!==r}):"string"!=typeof n?E.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(E.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||L,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:j.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof E?t[0]:t,E.merge(this,E.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:w,!0)),k.test(r[1])&&E.isPlainObject(t))for(r in t)b(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=w.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):b(e)?void 0!==n.ready?n.ready(e):e(E):E.makeArray(e,this)}).prototype=E.fn,L=E(w);var q=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}E.fn.extend({has:function(e){var t=E(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,pe=/^$|^module$|\/(?:java|ecma)script/i;le=w.createDocumentFragment().appendChild(w.createElement("div")),(ce=w.createElement("input")).setAttribute("type","radio"),ce.setAttribute("checked","checked"),ce.setAttribute("name","t"),le.appendChild(ce),m.checkClone=le.cloneNode(!0).cloneNode(!0).lastChild.checked,le.innerHTML="",m.noCloneChecked=!!le.cloneNode(!0).lastChild.defaultValue,le.innerHTML="",m.option=!!le.lastChild;var he={thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};function ge(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&S(e,t)?E.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var ye=/<|&#?\w+;/;function me(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),d=[],p=0,h=e.length;p\s*$/g;function Le(e,t){return S(e,"table")&&S(11!==t.nodeType?t:t.firstChild,"tr")&&E(e).children("tbody")[0]||e}function je(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n
    ",2===ft.childNodes.length),E.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(m.createHTMLDocument?((r=(t=w.implementation.createHTMLDocument("")).createElement("base")).href=w.location.href,t.head.appendChild(r)):t=w),o=!n&&[],(i=k.exec(e))?[t.createElement(i[1])]:(i=me([e],t,o),o&&o.length&&E(o).remove(),E.merge([],i.childNodes)));var r,i,o},E.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=E.css(e,"position"),c=E(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=E.css(e,"top"),u=E.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),b(t)&&(t=t.call(e,n,E.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},E.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){E.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===E.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===E.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=E(e).offset()).top+=E.css(e,"borderTopWidth",!0),i.left+=E.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-E.css(r,"marginTop",!0),left:t.left-i.left-E.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===E.css(e,"position"))e=e.offsetParent;return e||re})}}),E.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;E.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),E.each(["top","left"],function(e,n){E.cssHooks[n]=Fe(m.pixelPosition,function(e,t){if(t)return t=We(e,n),Ie.test(t)?E(e).position()[n]+"px":t})}),E.each({Height:"height",Width:"width"},function(a,s){E.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){E.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?E.css(e,t,i):E.style(e,t,n,i)},s,n?e:void 0,n)}})}),E.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),E.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){E.fn[n]=function(e,t){return 0