├── _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 |
40 |
62 |
76 |
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 |
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