├── .gitignore
├── icons
├── icon16.png
├── icon32.png
├── icon48.png
└── icon512.png
├── package-ignore.txt
├── package.sh
├── README.md
├── content
├── settings
│ ├── settings.css
│ ├── settings.js
│ └── settings.html
├── _shared
│ ├── i18n.js
│ └── browser-polyfill.min.js
├── button
│ ├── panel.html
│ ├── panel.css
│ └── panel.js
└── background
│ └── background.js
├── LICENSE
├── manifest.json
└── _locales
└── en
└── messages.json
/.gitignore:
--------------------------------------------------------------------------------
1 | release.zip
2 |
--------------------------------------------------------------------------------
/icons/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aqt/webext-popup/HEAD/icons/icon16.png
--------------------------------------------------------------------------------
/icons/icon32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aqt/webext-popup/HEAD/icons/icon32.png
--------------------------------------------------------------------------------
/icons/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aqt/webext-popup/HEAD/icons/icon48.png
--------------------------------------------------------------------------------
/icons/icon512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aqt/webext-popup/HEAD/icons/icon512.png
--------------------------------------------------------------------------------
/package-ignore.txt:
--------------------------------------------------------------------------------
1 | icons/icon512.png
2 | package-ignore.txt
3 | package.sh
4 | README.md
5 | release.zip
6 |
--------------------------------------------------------------------------------
/package.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | output="release.zip"
4 |
5 | rm $output &> /dev/null
6 |
7 | blacklist=$(cat package-ignore.txt)
8 |
9 | echo zip -r $output * -x $blacklist
10 | zip -r $output * -x $blacklist
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Popup
2 |
3 | Simple Web Extension to open a page with minimal browser ui.
4 |
5 | ## Install
6 |
7 | Note: This extension has only been tested and verified to work with **Firefox**, but any browser that implements the Web Extensions API properly should work.
8 |
9 | * Firefox: https://addons.mozilla.org/addon/popup/
10 |
11 | You can also download the extension from the [releases](https://github.com/aqt/webext-popup/releases/latest) page (not recommended)
12 |
--------------------------------------------------------------------------------
/content/settings/settings.css:
--------------------------------------------------------------------------------
1 | .col-spacing td {
2 | padding-right: 4px;
3 | }
4 |
5 | #section_rules input[type="number"] {
6 | width: 100%;
7 | }
8 |
9 | input[type="checkbox"] {
10 | vertical-align: middle;
11 | margin: 4px;
12 | }
13 |
14 | legend {
15 | padding: 0 3px;
16 | }
17 |
18 | fieldset:not(:first-child) {
19 | margin-top: 2em;
20 | }
21 |
22 | th {
23 | text-align: left;
24 | }
25 |
26 | .description {
27 | margin-bottom: 1em;
28 | }
29 |
30 | .hidden {
31 | display: none;
32 | }
33 |
--------------------------------------------------------------------------------
/content/_shared/i18n.js:
--------------------------------------------------------------------------------
1 | function DOMLoaded() {
2 | let attr = "data-i18n-message";
3 | let elements = document.querySelectorAll(`[${attr}]`);
4 |
5 | let usePlaceholder = typeof browser === "undefined";
6 |
7 | elements.forEach(el => {
8 | let text = el.getAttribute(attr);
9 | el.textContent = usePlaceholder ? "$"+text : browser.i18n.getMessage(text);
10 | });
11 |
12 | attr = "data-i18n-title";
13 | elements = document.querySelectorAll(`[${attr}]`);
14 |
15 | elements.forEach(el => {
16 | let text = el.getAttribute(attr);
17 | el.title = usePlaceholder ? "$"+text : browser.i18n.getMessage(text);
18 | });
19 |
20 | attr = "data-i18n-placeholder";
21 | elements = document.querySelectorAll(`[${attr}]`);
22 |
23 | elements.forEach(el => {
24 | let text = el.getAttribute(attr);
25 | el.placeholder = usePlaceholder ? "$"+text : browser.i18n.getMessage(text);
26 | });
27 | }
28 |
29 | document.addEventListener("DOMContentLoaded", DOMLoaded);
30 |
--------------------------------------------------------------------------------
/content/button/panel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
21 |
22 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 aqt
4 |
5 | https://github.com/aqt/webext-popup
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "author": "aqt",
4 |
5 | "homepage_url": "https://github.com/aqt/webext-popup",
6 | "name": "__MSG_extensionName__",
7 | "version": "1.5.4",
8 | "description": "__MSG_extensionDescription__",
9 |
10 | "default_locale": "en",
11 |
12 | "icons": {
13 | "16": "icons/icon16.png",
14 | "32": "icons/icon32.png",
15 | "48": "icons/icon48.png"
16 | },
17 |
18 | "permissions": [
19 | "activeTab",
20 | "bookmarks",
21 | "contextMenus",
22 | "storage",
23 | "tabs"
24 | ],
25 |
26 | "background": {
27 | "scripts": [
28 | "content/_shared/browser-polyfill.min.js",
29 | "content/background/background.js"
30 | ]
31 | },
32 |
33 | "browser_action": {
34 | "default_icon": {
35 | "16": "icons/icon16.png",
36 | "32": "icons/icon32.png",
37 | "48": "icons/icon48.png"
38 | },
39 |
40 | "default_title": "__MSG_extensionName__",
41 | "default_popup": "content/button/panel.html"
42 | },
43 |
44 | "applications": {
45 | "gecko": {
46 | "id": "{7345afcc-32b6-4a3d-8e05-189bc954e9e7}"
47 | }
48 | },
49 |
50 | "options_ui": {
51 | "page": "content/settings/settings.html",
52 | "browser_style": true
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/content/button/panel.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | img {
6 | border: 1px solid red;
7 | }
8 |
9 | body {
10 | color: #40505A;
11 | font-family: arial;
12 | font-size: 14px;
13 | margin: 0;
14 | min-width: 250px;
15 | }
16 |
17 | .disabled {
18 | background: #EBEBEB;
19 | }
20 |
21 | .hidden {
22 | display: none;
23 | }
24 |
25 | .wrapper {
26 | display: flex;
27 | flex-direction: column;
28 | min-height: 100vh;
29 | }
30 |
31 | .menu {
32 | flex: 1;
33 | }
34 |
35 | .footer {
36 | text-align: center;
37 | }
38 |
39 | .footer .menu-item {
40 | border-bottom: 0;
41 | border-top: 1px solid #CDCDCD;
42 | }
43 |
44 | ul {
45 | list-style: none;
46 | margin: 0;
47 | padding: 0;
48 | }
49 |
50 | .menu-item {
51 | border-bottom: 1px solid #CDCDCD;
52 | -moz-user-select: none;
53 | user-select: none;
54 | }
55 |
56 | .menu-item > a {
57 | cursor: pointer;
58 | padding: 10px 10px;
59 | width: 100%;
60 | height: 100%;
61 | display: block;
62 | }
63 |
64 | .menu-item:hover {
65 | background: #F5F5F5;
66 | }
67 |
68 | .menu-sub .menu-item {
69 | background: #F5F5F5;
70 | }
71 |
72 | .menu-sub .menu-item:hover {
73 | background: #EBEBEB;
74 | }
75 |
76 | .menu-sub .menu-item > a {
77 | padding-left: 20px;
78 | }
79 |
80 | a.disabled {
81 | cursor: not-allowed;
82 | }
83 |
--------------------------------------------------------------------------------
/content/button/panel.js:
--------------------------------------------------------------------------------
1 | let BUTTON_ACTION = Object.freeze({
2 | "POPUP": "POPUP",
3 | "MENU": "MENU",
4 | });
5 |
6 | let action_setting = "button-action";
7 |
8 | let submenu_container = document.querySelector("#submenu-restore_window-container");
9 | let submenu_menu = document.querySelector("#submenu-restore_window-menu");
10 |
11 | let settings_button = document.querySelector("#open-settings");
12 | settings_button.addEventListener("click", e => browser.runtime.openOptionsPage());
13 |
14 | function convertThisTab() {
15 | browser.tabs.query({ active: true, currentWindow: true }).then(tabs => {
16 | let tab = tabs[0];
17 |
18 | browser.runtime.sendMessage({ "type": "CONVERT_EXISTING", tab });
19 | window.close();
20 | });
21 | }
22 |
23 | function buildMenu() {
24 | function restoreWindow(tab) {
25 | browser.runtime.sendMessage({ "type": "RESTORE_WINDOW", tab });
26 | window.close();
27 | }
28 |
29 | function appendWindowItem(wnd, tab) {
30 | // Firefox 56.0.2 doesn't seem to be able to retrieve tab.title, but can retrieve window.title
31 | let wndTitle = wnd ? wnd.title : undefined;
32 | let title = tab.title || wndTitle || browser.i18n.getMessage("button_menu_item_restore-unknown_title");
33 |
34 | let li = document.createElement("li");
35 | li.classList.add("menu-item");
36 |
37 | let a = document.createElement("a");
38 | a.textContent = title;
39 |
40 | a.addEventListener("click", e => restoreWindow(tab));
41 |
42 | li.appendChild(a);
43 | submenu_menu.appendChild(li);
44 | }
45 |
46 | browser.tabs.query({ windowType: "popup" }).then(tabs => {
47 | if (!tabs.length) {
48 | return;
49 | }
50 |
51 | restore_window.classList.remove("disabled");
52 |
53 | tabs.forEach(tab => {
54 | browser.windows.get(tab.windowId).then(
55 | wnd => appendWindowItem(wnd, tab),
56 | err => appendWindowItem(undefined, tab)
57 | );
58 | });
59 | });
60 | }
61 |
62 | browser.storage.local.get([ action_setting ]).then(settings => {
63 | let action = settings[action_setting];
64 |
65 | switch (action) {
66 | default:
67 | case BUTTON_ACTION.MENU:
68 | buildMenu();
69 | break;
70 |
71 | case BUTTON_ACTION.POPUP:
72 | convertThisTab();
73 | break;
74 | }
75 | });
76 |
77 | open_popup.addEventListener("click", convertThisTab);
78 | restore_window.addEventListener("click", e => submenu_container.classList.toggle("hidden"));
79 |
--------------------------------------------------------------------------------
/content/settings/settings.js:
--------------------------------------------------------------------------------
1 | let singleSettingElements;
2 | let multiSettingElements;
3 |
4 | function DOMLoaded() {
5 | singleSettingElements = [...document.querySelectorAll(`[data-setting]`)];
6 | multiSettingElements = [...document.querySelectorAll(`[data-setting-list]`)];
7 |
8 | button_save.setAttribute("disabled", "disabled");
9 |
10 | loadSettings();
11 | initializeListeners();
12 | }
13 |
14 | function initializeListeners() {
15 | button_save.addEventListener("click", () => {
16 | button_save.setAttribute("disabled", "disabled");
17 | saveSettings();
18 | });
19 |
20 | // Button functionality to add new row to list settings
21 | let addListItemButtons = document.querySelectorAll(`[data-setting-list-item-add]`);
22 | for (let button of addListItemButtons) {
23 | for (let parent = button; parent; parent = parent.parentElement) {
24 | if (parent.tagName.toLowerCase() === "table") {
25 | button.addEventListener("click", e => addNewRowToList(parent));
26 | continue;
27 | }
28 | }
29 | }
30 |
31 | attachChangeListener(singleSettingElements);
32 | }
33 |
34 | function attachChangeListener(elements) {
35 | elements.forEach(element => {
36 | element.addEventListener("change", onChange);
37 | });
38 | }
39 |
40 | function detachChangeListener(elements) {
41 | elements.forEach(element => {
42 | element.removeEventListener("change", onChange);
43 | });
44 | }
45 |
46 | function onChange() {
47 | button_save.removeAttribute("disabled");
48 | }
49 |
50 |
51 | function loadSettings() {
52 | console.log("Loading settings");
53 |
54 | browser.storage.local.get().then(result => {
55 | for (key in result) {
56 | if (key === "version") {
57 | continue;
58 | }
59 |
60 | let value = result[key];
61 | let element = document.querySelector(`#${key}`);
62 |
63 | if (!element) {
64 | console.warn(`No settings element matching saved setting "${key}"`);
65 | continue;
66 | }
67 |
68 | if (value instanceof Array) {
69 | // List setting
70 |
71 | for (let rowObject of value) {
72 | let newRow = addNewRowToList(element);
73 |
74 | for (let col of Object.keys(rowObject)) {
75 | let input = newRow.querySelector(`[data-setting-id="${ col }"]`);
76 |
77 | setValueForInput(input, rowObject[col]);
78 | }
79 | }
80 | } else {
81 | // Single setting
82 | setValueForInput(element, value);
83 | }
84 | }
85 | });
86 | }
87 |
88 | function getValueForInput(element) {
89 | switch(element.type.toLowerCase()) {
90 | default:
91 | return element.value;
92 |
93 | case "checkbox":
94 | return element.checked;
95 | }
96 |
97 | return undefined;
98 | }
99 |
100 | function setValueForInput(element, value) {
101 | switch(element.type.toLowerCase()) {
102 | default:
103 | console.log(`Using default for settings element of type "${element.type}"`, element);
104 | element.value = value;
105 | break;
106 |
107 | case "number":
108 | case "select-one":
109 | case "text":
110 | element.value = value;
111 | break;
112 |
113 | case "checkbox":
114 | element.checked = value;
115 | break;
116 | }
117 | }
118 |
119 | function saveSettings() {
120 | console.log("Saving settings");
121 |
122 | let batch = {};
123 |
124 | singleSettingElements.forEach(element => {
125 | let val = getValueForInput(element);
126 |
127 | if (val !== undefined) {
128 | batch[element.id] = val;
129 | }
130 | });
131 |
132 | multiSettingElements.forEach(element => {
133 | let val = [];
134 |
135 | let listItems = element.querySelectorAll("[data-setting-list-item]");
136 |
137 | for (let row of listItems) {
138 | let item = {};
139 |
140 | let subkeys = row.querySelectorAll("[data-setting-id]");
141 |
142 | for (let sk of subkeys) {
143 | let sk_id = sk.getAttribute("data-setting-id");
144 | let sk_value = getValueForInput(sk);
145 |
146 | if (sk_value !== undefined) {
147 | item[sk_id] = sk_value;
148 | }
149 | }
150 |
151 | val.push(item);
152 | }
153 |
154 | batch[element.id] = val;
155 | });
156 |
157 | browser.storage.local.set(batch);
158 | }
159 |
160 | function addNewRowToList(table) {
161 | let template = table.querySelector("[data-setting-list-item-template]");
162 |
163 | if (!template) {
164 | console.warn("No template found for table:", table);
165 | return;
166 | }
167 |
168 | let templateClone = template.cloneNode(true);
169 | templateClone.classList.remove("hidden");
170 | templateClone.removeAttribute("data-setting-list-item-template");
171 | templateClone.setAttribute("data-setting-list-item", "");
172 |
173 | let deleteButton = templateClone.querySelector("[data-setting-list-item-delete]");
174 | deleteButton.addEventListener("click", e => { e.target.parentElement.parentElement.remove(); onChange(); });
175 |
176 | attachChangeListener(templateClone.querySelectorAll("[data-setting-id]"));
177 |
178 | let row = table.insertRow(table.rows.length - 1); // Subtract due to add button
179 | row.replaceWith(templateClone);
180 |
181 | return templateClone;
182 | }
183 |
184 | document.addEventListener("DOMContentLoaded", DOMLoaded);
185 |
--------------------------------------------------------------------------------
/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionName": {
3 | "message": "PopUp",
4 | "description": "Title of the extension"
5 | },
6 |
7 | "extensionDescription": {
8 | "message": "Create a popup of pages, with minimal browser UI",
9 | "description": "Description of the extension"
10 | },
11 |
12 |
13 | "menu_item_bookmark_popup": {
14 | "message": "Open as a popup",
15 | "description": "Menu item for bookmarks"
16 | },
17 |
18 | "menu_item_link_popup": {
19 | "message": "Open as a popup",
20 | "description": "Menu item for links"
21 | },
22 |
23 | "menu_item_page_popup": {
24 | "message": "Open as a popup",
25 | "description": "Menu item for pages"
26 | },
27 |
28 | "menu_item_page_restore": {
29 | "message": "Restore to window",
30 | "description": "Menu item for pages"
31 | },
32 |
33 | "menu_item_tab_popup": {
34 | "message": "Move to a new popup",
35 | "description": "Menu item for tabs"
36 | },
37 |
38 |
39 | "settings_title": {
40 | "message": "Settings",
41 | "description": "Settings page title"
42 | },
43 |
44 | "settings_context_menus": {
45 | "message": "Context menus",
46 | "description": "Settings section title for context menus"
47 | },
48 |
49 | "settings_context_menus_bookmarks": {
50 | "message": "Show on bookmarks",
51 | "description": "Settings label for context menus on Bookmarks"
52 | },
53 |
54 | "settings_context_menus_links": {
55 | "message": "Show on links",
56 | "description": "Settings label for context menus on Links"
57 | },
58 |
59 | "settings_context_menus_pages": {
60 | "message": "Show on pages",
61 | "description": "Settings label for context menus on Pages"
62 | },
63 |
64 | "settings_context_menus_tabs": {
65 | "message": "Show on tabs",
66 | "description": "Settings label for context menus on Tabs"
67 | },
68 |
69 | "settings_button": {
70 | "message": "Button",
71 | "description": "Settings section title for button action"
72 | },
73 |
74 | "settings_button_action": {
75 | "message": "Button action",
76 | "description": "Settings label for button action"
77 | },
78 |
79 | "settings_button_action_menu": {
80 | "message": "Menu",
81 | "description": "Settings label for button action type"
82 | },
83 |
84 | "settings_button_action_popup": {
85 | "message": "Create popup of current tab",
86 | "description": "Settings label for button action"
87 | },
88 |
89 | "settings_popup_position": {
90 | "message": "Popup rules",
91 | "description": "Settings label for popup position"
92 | },
93 |
94 | "settings_popup_position_description": {
95 | "message": "If several rules are matching, only the first applies. Fields left empty will use the default value if enabled",
96 | "description": "Settings description for default popup rule"
97 | },
98 |
99 | "settings_popup_rule_applies_format": {
100 | "message": "Single domain\nwww.google.com\n\nMultiple domains\ngoogle.com,www.google.com\n\nDomain + all subdomains\ngoogle.com,*.google.com\n\nRegExp\n/[\\/\\.](google|youtube)\\.com/i",
101 | "description": "Settings description for popup rules"
102 | },
103 |
104 | "settings_popup_position_enabled": {
105 | "message": "Enabled",
106 | "description": "Settings for enable/disable toggle"
107 | },
108 |
109 | "settings_popup_position_type": {
110 | "message": "Applies to (?)",
111 | "description": "Settings header for applies-type"
112 | },
113 |
114 | "settings_popup_position_type_domain": {
115 | "message": "Domain",
116 | "description": "Settings value for applies-type domain"
117 | },
118 |
119 | "settings_popup_position_type_url": {
120 | "message": "URL",
121 | "description": "Settings value for applies-type URL"
122 | },
123 |
124 | "settings_popup_position_x": {
125 | "message": "X",
126 | "description": "Settings header for popup position column"
127 | },
128 |
129 | "settings_popup_position_y": {
130 | "message": "Y",
131 | "description": "Settings header for popup position column"
132 | },
133 |
134 | "settings_popup_position_default_enabled": {
135 | "message": "Defaults enabled",
136 | "description": "Settings header for popup position column"
137 | },
138 |
139 | "settings_popup_position_default_x": {
140 | "message": "Default X",
141 | "description": "Settings header for popup position column"
142 | },
143 |
144 | "settings_popup_position_default_y": {
145 | "message": "Default Y",
146 | "description": "Settings header for popup position column"
147 | },
148 |
149 | "settings_popup_position_default_width": {
150 | "message": "Default width",
151 | "description": "Settings header for popup position column"
152 | },
153 |
154 | "settings_popup_position_default_height": {
155 | "message": "Default height",
156 | "description": "Settings header for popup position column"
157 | },
158 |
159 | "settings_popup_position_width": {
160 | "message": "Width",
161 | "description": "Settings header for popup position column"
162 | },
163 |
164 | "settings_popup_position_height": {
165 | "message": "Height",
166 | "description": "Settings header for popup position column"
167 | },
168 |
169 | "settings_popup_position_autopopup": {
170 | "message": "Auto-Popup",
171 | "description": "Settings header for popup position column"
172 | },
173 |
174 | "settings_popup_position_domain_description": {
175 | "message": "Entries without a subdomain (e.g. www) also apply to all of its subdomains. Multiple domains can be used by separating the entries with a comma (,). Example value: github.com,www.google.com",
176 | "description": "Settings description for popup position column"
177 | },
178 |
179 | "settings_workaround": {
180 | "message": "Workarounds",
181 | "description": "Settings header for workarounds"
182 | },
183 |
184 | "settings_workaround_force_position": {
185 | "message": "Force position/size - FF < 61 or Resist fingerprinting",
186 | "description": "Settings item for a bug workaround"
187 | },
188 |
189 | "settings_workaround_popup_size": {
190 | "message": "Add or subtract pixels to position/size - Invisible window borders on win 10",
191 | "description": "Settings item for a bug workaround"
192 | },
193 |
194 | "button_menu_item_open": {
195 | "message": "Open this tab as a popup",
196 | "description": "Item in button menu for converting current tab"
197 | },
198 |
199 | "button_menu_item_restore": {
200 | "message": "Restore to this window...",
201 | "description": "Item in button menu for restoring tab"
202 | },
203 |
204 | "button_menu_item_settings": {
205 | "message": "Open settings",
206 | "description": "Item in button menu for opening settings"
207 | },
208 |
209 | "button_menu_item_restore_unknown_title": {
210 | "message": "Unknown title",
211 | "description": "Error text for item in button menu for restoring tabs"
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/content/settings/settings.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
--------------------------------------------------------------------------------
/content/_shared/browser-polyfill.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}})("undefined"==typeof globalThis?"undefined"==typeof self?this:self:globalThis,function(a){"use strict";if("undefined"==typeof browser||Object.getPrototypeOf(browser)!==Object.prototype){if("object"!=typeof chrome||!chrome||!chrome.runtime||!chrome.runtime.id)throw new Error("This script should only be loaded in a browser extension.");a.exports=(a=>{const b={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},get:{minArgs:1,maxArgs:1},getChildren:{minArgs:1,maxArgs:1},getRecent:{minArgs:1,maxArgs:1},getSubTree:{minArgs:1,maxArgs:1},getTree:{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:{disable:{minArgs:0,maxArgs:1,fallbackToNoCallback:!0},enable:{minArgs:0,maxArgs:1,fallbackToNoCallback:!0},getBadgeBackgroundColor:{minArgs:1,maxArgs:1},getBadgeText:{minArgs:1,maxArgs:1},getPopup:{minArgs:1,maxArgs:1},getTitle:{minArgs:1,maxArgs:1},openPopup:{minArgs:0,maxArgs:0},setBadgeBackgroundColor:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setBadgeText:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setIcon:{minArgs:1,maxArgs:1},setPopup:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setTitle:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},browsingData:{remove:{minArgs:2,maxArgs:2},removeCache:{minArgs:1,maxArgs:1},removeCookies:{minArgs:1,maxArgs:1},removeDownloads:{minArgs:1,maxArgs:1},removeFormData:{minArgs:1,maxArgs:1},removeHistory:{minArgs:1,maxArgs:1},removeLocalStorage:{minArgs:1,maxArgs:1},removePasswords:{minArgs:1,maxArgs:1},removePluginData:{minArgs:1,maxArgs:1},settings:{minArgs:0,maxArgs:0}},commands:{getAll:{minArgs:0,maxArgs:0}},contextMenus:{remove:{minArgs:1,maxArgs:1},removeAll:{minArgs:0,maxArgs:0},update:{minArgs:2,maxArgs:2}},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,singleCallbackArg:!1}},panels:{create:{minArgs:3,maxArgs:3,singleCallbackArg:!0}}},downloads:{cancel:{minArgs:1,maxArgs:1},download:{minArgs:1,maxArgs:1},erase:{minArgs:1,maxArgs:1},getFileIcon:{minArgs:1,maxArgs:2},open:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},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,fallbackToNoCallback:!0}},extension:{isAllowedFileSchemeAccess:{minArgs:0,maxArgs:0},isAllowedIncognitoAccess:{minArgs:0,maxArgs:0}},history:{addUrl:{minArgs:1,maxArgs:1},deleteAll:{minArgs:0,maxArgs:0},deleteRange:{minArgs:1,maxArgs:1},deleteUrl:{minArgs:1,maxArgs:1},getVisits:{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},setEnabled:{minArgs:2,maxArgs:2},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},getTitle:{minArgs:1,maxArgs:1},hide:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setIcon:{minArgs:1,maxArgs:1},setPopup:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setTitle:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},show:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},permissions:{contains:{minArgs:1,maxArgs:1},getAll:{minArgs:0,maxArgs:0},remove:{minArgs:1,maxArgs:1},request:{minArgs:1,maxArgs:1}},runtime:{getBackgroundPage:{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}},sessions:{getDevices:{minArgs:0,maxArgs:1},getRecentlyClosed:{minArgs:0,maxArgs:1},restore:{minArgs:0,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:{captureVisibleTab:{minArgs:0,maxArgs:2},create:{minArgs:1,maxArgs:1},detectLanguage:{minArgs:0,maxArgs:1},discard:{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},query:{minArgs:1,maxArgs:1},reload:{minArgs:0,maxArgs:2},remove:{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}},topSites:{get:{minArgs:0,maxArgs:0}},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(b).length)throw new Error("api-metadata.json has not been included in browser-polyfill");class c extends WeakMap{constructor(a,b=void 0){super(b),this.createItem=a}get(a){return this.has(a)||this.set(a,this.createItem(a)),super.get(a)}}const d=a=>a&&"object"==typeof a&&"function"==typeof a.then,e=(b,c)=>(...d)=>{a.runtime.lastError?b.reject(a.runtime.lastError):c.singleCallbackArg||1>=d.length&&!1!==c.singleCallbackArg?b.resolve(d[0]):b.resolve(d)},f=a=>1==a?"argument":"arguments",g=(a,b)=>function(c,...d){if(d.lengthb.maxArgs)throw new Error(`Expected at most ${b.maxArgs} ${f(b.maxArgs)} for ${a}(), got ${d.length}`);return new Promise((f,g)=>{if(b.fallbackToNoCallback)try{c[a](...d,e({resolve:f,reject:g},b))}catch(e){console.warn(`${a} API method doesn't seem to support the callback parameter, `+"falling back to call it without a callback: ",e),c[a](...d),b.fallbackToNoCallback=!1,b.noCallback=!0,f()}else b.noCallback?(c[a](...d),f()):c[a](...d,e({resolve:f,reject:g},b))})},h=(a,b,c)=>new Proxy(b,{apply(b,d,e){return c.call(d,a,...e)}});let i=Function.call.bind(Object.prototype.hasOwnProperty);const j=(a,b={},c={})=>{let d=Object.create(null),e={has(b,c){return c in a||c in d},get(e,f,k){if(f in d)return d[f];if(!(f in a))return;let l=a[f];if("function"==typeof l){if("function"==typeof b[f])l=h(a,a[f],b[f]);else if(i(c,f)){let b=g(f,c[f]);l=h(a,a[f],b)}else l=l.bind(a);}else if("object"==typeof l&&null!==l&&(i(b,f)||i(c,f)))l=j(l,b[f],c[f]);else if(i(c,"*"))l=j(l,b[f],c["*"]);else return Object.defineProperty(d,f,{configurable:!0,enumerable:!0,get(){return a[f]},set(b){a[f]=b}}),l;return d[f]=l,l},set(b,c,e,f){return c in d?d[c]=e:a[c]=e,!0},defineProperty(a,b,c){return Reflect.defineProperty(d,b,c)},deleteProperty(a,b){return Reflect.deleteProperty(d,b)}},f=Object.create(a);return new Proxy(f,e)},k=a=>({addListener(b,c,...d){b.addListener(a.get(c),...d)},hasListener(b,c){return b.hasListener(a.get(c))},removeListener(b,c){b.removeListener(a.get(c))}});let l=!1;const m=new c(a=>"function"==typeof a?function(b,c,e){let f,g,h=!1,i=new Promise(a=>{f=function(b){l||(console.warn("Returning a Promise is the preferred way to send a reply from an onMessage/onMessageExternal listener, as the sendResponse will be removed from the specs (See https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage)",new Error().stack),l=!0),h=!0,a(b)}});try{g=a(b,c,f)}catch(a){g=Promise.reject(a)}const j=!0!==g&&d(g);if(!0!==g&&!j&&!h)return!1;const k=a=>{a.then(a=>{e(a)},a=>{let b;b=a&&(a instanceof Error||"string"==typeof a.message)?a.message:"An unexpected error occurred",e({__mozWebExtensionPolyfillReject__:!0,message:b})}).catch(a=>{console.error("Failed to send onMessage rejected reply",a)})};return j?k(g):k(i),!0}:a),n=({reject:b,resolve:c},d)=>{a.runtime.lastError?a.runtime.lastError.message==="The message port closed before a response was received."?c():b(a.runtime.lastError):d&&d.__mozWebExtensionPolyfillReject__?b(new Error(d.message)):c(d)},o=(a,b,c,...d)=>{if(d.lengthb.maxArgs)throw new Error(`Expected at most ${b.maxArgs} ${f(b.maxArgs)} for ${a}(), got ${d.length}`);return new Promise((a,b)=>{const e=n.bind(null,{resolve:a,reject:b});d.push(e),c.sendMessage(...d)})},p={runtime:{onMessage:k(m),onMessageExternal:k(m),sendMessage:o.bind(null,"sendMessage",{minArgs:1,maxArgs:3})},tabs:{sendMessage:o.bind(null,"sendMessage",{minArgs:2,maxArgs:3})}},q={clear:{minArgs:1,maxArgs:1},get:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}};return b.privacy={network:{"*":q},services:{"*":q},websites:{"*":q}},j(a,p,b)})(chrome)}else a.exports=browser});
2 |
3 | // webextension-polyfill v.0.6.0 (https://github.com/mozilla/webextension-polyfill)
4 |
5 | /* This Source Code Form is subject to the terms of the Mozilla Public
6 | * License, v. 2.0. If a copy of the MPL was not distributed with this
7 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 |
--------------------------------------------------------------------------------
/content/background/background.js:
--------------------------------------------------------------------------------
1 | let _addonSettings;
2 | let _dynamicMenuItems = [];
3 |
4 | const Notification = Object.freeze({
5 | CONVERT_EXISTING: "CONVERT_EXISTING",
6 | RESTORE_WINDOW: "RESTORE_WINDOW",
7 | });
8 |
9 | const WindowType = Object.freeze({
10 | NORMAL: "normal",
11 | POPUP: "popup"
12 | });
13 |
14 | const ContextMenuType = Object.freeze({
15 | BOOKMARK_POPUP: "bookmark-popup",
16 | LINK_POPUP: "link-popup",
17 | PAGE_POPUP: "page-popup",
18 | PAGE_RESTORE: "Page-restore",
19 | TAB_POPUP: "tab-popup",
20 | });
21 |
22 | const SettingsKey = Object.freeze({
23 | BUTTON_ACTION: "button-action",
24 | MENU_ITEM_BOOKMARK: "menu-item_bookmark",
25 | MENU_ITEM_LINK: "menu-item_link",
26 | MENU_ITEM_PAGE: "menu-item_page",
27 | MENU_ITEM_TAB: "menu-item_tab",
28 | POPUP_POSITION: "popup-position",
29 | POPUP_POSITION_DEFAULTS_ENABLED: "popup-position_default_enabled",
30 | POPUP_POSITION_HEIGHT_DEFAULT: "popup-position_height_default",
31 | POPUP_POSITION_WIDTH_DEFAULT: "popup-position_width_default",
32 | POPUP_POSITION_X_DEFAULT: "popup-position_x_default",
33 | POPUP_POSITION_Y_DEFAULT: "popup-position_y_default",
34 | RULES: "rules",
35 | VERSION: "version",
36 | WORKAROUND_FORCE_POSITION: "workaround_force-position",
37 | WORKAROUND_OFFSET_SIZE: "workaround_popup-size",
38 | WORKAROUND_OFFSET_SIZE_X: "workaround_popup-size_x",
39 | WORKAROUND_OFFSET_SIZE_Y: "workaround_popup-size_y",
40 | WORKAROUND_OFFSET_SIZE_WIDTH: "workaround_popup-size_width",
41 | WORKAROUND_OFFSET_SIZE_HEIGHT: "workaround_popup-size_height",
42 | });
43 |
44 | function main() {
45 | // Apply current settings
46 | browser.storage.local.get().then(settings => actOnSettings(settings));
47 |
48 | addListeners();
49 | }
50 |
51 | function addListeners() {
52 | // Set default settings and (eventually) handle migration for new versions
53 | browser.runtime.onInstalled.addListener(details => {
54 | console.log("New install/update, creating default settings");
55 |
56 | browser.storage.local.get().then(settings => {
57 | let version;
58 |
59 | if (details.reason === "install") {
60 | version = "new";
61 | } else {
62 | if (settings.hasOwnProperty("version")) {
63 | version = settings[SettingsKey.VERSION];
64 | } else {
65 | version = "none";
66 | }
67 | }
68 |
69 | migrateSettings(settings, version);
70 | });
71 | });
72 |
73 | // Apply settings after changes
74 | browser.storage.onChanged.addListener((changes, area) => {
75 | let settings = _addonSettings;
76 |
77 | for (key in changes) {
78 | settings[key] = changes[key].newValue;
79 | }
80 |
81 | actOnSettings(settings);
82 | });
83 |
84 | // Handle context menu items
85 | browser.contextMenus.onClicked.addListener((info, tab) => {
86 | if (info.parentMenuItemId === ContextMenuType.PAGE_RESTORE) {
87 | browser.windows.get(info.menuItemId*1).then(wnd => restoreTab(tab, wnd));
88 | return;
89 | }
90 |
91 | switch (info.menuItemId) {
92 | default:
93 | console.warn("Unhandled menu item", info, tab);
94 | break;
95 | case ContextMenuType.BOOKMARK_POPUP:
96 | browser.bookmarks.get(info.bookmarkId).then(arr => {
97 | if (arr.length !== 1) {
98 | console.error(`Unhandled number of bookmarks of id:${info.bookmarkId} !?`);
99 | return;
100 | }
101 |
102 | open_popup({ "url": arr[0].url });
103 | });
104 | break;
105 | case ContextMenuType.LINK_POPUP:
106 | open_popup({ "url": info.linkUrl });
107 | break;
108 | case ContextMenuType.PAGE_POPUP:
109 | case ContextMenuType.TAB_POPUP:
110 | open_popup({ "tab": tab });
111 | break;
112 | case ContextMenuType.PAGE_RESTORE:
113 | // If firefox doesn't make changes, the only way to get here is if there is no submenu of windows
114 | browser.windows.getAll({ windowTypes: [ WindowType.NORMAL ] }).then(windows => {
115 | if (windows.length > 0) {
116 | restoreTab(tab, windows[0])
117 | } else {
118 | // No `type` allowed in browser.windows.update, ugly workaround...
119 | browser.windows.create().then(wnd =>
120 | browser.tabs.move(tab.id, { windowId: wnd.id, index: -1 }).then(newTab =>
121 | browser.tabs.remove(wnd.tabs[0].id)
122 | )
123 | );
124 | }
125 | });
126 |
127 | break;
128 | }
129 | });
130 |
131 | // Receive messages from other scripts
132 | browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
133 | switch(message.type) {
134 | default:
135 | console.warn("Unhandled message!", message);
136 | break;
137 |
138 | case Notification.RESTORE_WINDOW:
139 | browser.windows.getCurrent().then(wnd => restoreTab(message.tab, wnd));
140 | break;
141 |
142 | case Notification.CONVERT_EXISTING:
143 | open_popup({ "tab": message.tab });
144 | break;
145 | }
146 | });
147 |
148 |
149 | // Listen for tab url changes
150 | try {
151 | // FF 88+
152 | browser.tabs.onUpdated.addListener(handleUpdatedTab, {
153 | properties: ["url"],
154 | });
155 |
156 | // FF 87-
157 | browser.tabs.onUpdated.addListener(handleUpdatedTab, {
158 | properties: ["status"],
159 | });
160 | } catch (e) {
161 | // Fallback due to second parameter added in version 61
162 | browser.tabs.onUpdated.addListener(handleUpdatedTab);
163 | }
164 | }
165 |
166 | function migrateSettings(settings, version) {
167 | switch (version) {
168 | default: return;
169 |
170 | case "new":
171 | // New install, only generate defaults
172 | settings[SettingsKey.POPUP_POSITION_X_DEFAULT] = "";
173 | settings[SettingsKey.POPUP_POSITION_Y_DEFAULT] = "";
174 | settings[SettingsKey.POPUP_POSITION_WIDTH_DEFAULT] = "";
175 | settings[SettingsKey.POPUP_POSITION_HEIGHT_DEFAULT] = "";
176 | settings[SettingsKey.MENU_ITEM_TAB] = true;
177 | settings[SettingsKey.MENU_ITEM_LINK] = true;
178 | settings[SettingsKey.MENU_ITEM_PAGE] = true;
179 | settings[SettingsKey.MENU_ITEM_BOOKMARK] = true;
180 | settings[SettingsKey.BUTTON_ACTION] = "MENU";
181 | settings[SettingsKey.POPUP_POSITION_DEFAULTS_ENABLED] = true;
182 | settings[SettingsKey.WORKAROUND_FORCE_POSITION] = false;
183 | settings[SettingsKey.WORKAROUND_OFFSET_SIZE] = false
184 | break;
185 |
186 | // INTENTIONAL FALLTHROUGH FOR ALL CASES BELOW
187 | case "none":
188 | // Update from old version prior to settings migration system, "version 1"
189 | if (settings.hasOwnProperty(SettingsKey.POPUP_POSITION)) {
190 | settings[SettingsKey.POPUP_POSITION].map(rule => {
191 | rule["appliestype"] = "DOMAIN"
192 | rule["search"] = rule["domain"].split(",").map(d => d+",*."+d).join(",");
193 | delete rule["domain"];
194 | });
195 |
196 | settings[SettingsKey.RULES] = settings[SettingsKey.POPUP_POSITION];
197 | delete settings[SettingsKey.POPUP_POSITION];
198 |
199 | browser.storage.local.remove("popup-position");
200 | }
201 | case "2":
202 | settings[SettingsKey.WORKAROUND_FORCE_POSITION] = true;
203 | settings[SettingsKey.POPUP_POSITION_DEFAULTS_ENABLED] = true;
204 |
205 | if (settings.hasOwnProperty(SettingsKey.RULES)) {
206 | for (let rule of settings[SettingsKey.RULES]) {
207 | rule["enabled"] = true;
208 | }
209 | }
210 | // case "3":
211 | }
212 |
213 | console.log("Settings migration, from version:"+ version);
214 |
215 | settings[SettingsKey.VERSION] = "3";
216 |
217 | browser.storage.local.set(settings);
218 | }
219 |
220 | function handleUpdatedTab(tabId, changeInfo, tab) {
221 | if (!changeInfo.hasOwnProperty("url")) {
222 | return;
223 | }
224 |
225 | browser.windows.get(tab.windowId).then(wnd => {
226 | if (wnd.type === WindowType.POPUP) {
227 | return;
228 | }
229 |
230 | let rule = getMatchingRule(changeInfo.url);
231 |
232 | if (rule && rule["autopopup"]) {
233 | open_popup({ "tab": tab }, rule);
234 | }
235 | });
236 | }
237 |
238 | function restoreTab(tab, wnd) {
239 | function cleanup(err, w, t) {
240 | browser.tabs.get(tab.id).then(newTab => {
241 | if (tab.windowId === newTab.windowId) {
242 | console.error("Error restoring tab: ", err);
243 | } else {
244 | // Cleanup necessary until unknown firefox version before 62
245 | browser.windows.remove(tab.windowId)
246 | }
247 | });
248 | }
249 |
250 | if (tab.windowId === wnd.id) {
251 | browser.windows.create({ tabId: tab.id }).then(() => cleanup(undefined, wnd, tab)).catch(err => cleanup(err, wnd, tab));
252 | } else {
253 | browser.tabs.move(tab.id, { windowId: wnd.id, index: -1 }).catch(err => cleanup(err, wnd, tab));
254 | }
255 | }
256 |
257 | function open_popup(settings, rule) {
258 | let data = { "type": WindowType.POPUP };
259 |
260 | let url = "";
261 |
262 | if (settings.hasOwnProperty("url")) {
263 | data.url = settings.url;
264 | url = settings.url;
265 | } else if (settings.hasOwnProperty("tab")) {
266 | data.tabId = settings.tab.id;
267 | url = settings.tab.url;
268 | } else {
269 | console.warn("Unknown popup type", settings);
270 | return;
271 | }
272 |
273 | if (typeof rule === "undefined") {
274 | rule = getMatchingRule(url);
275 | }
276 |
277 | let x="", y="", w="", h="";
278 | let ox=0, oy=0, ow=0, oh=0;
279 |
280 | if (rule) {
281 | x = rule.x;
282 | y = rule.y;
283 | w = rule.width;
284 | h = rule.height;
285 | }
286 |
287 | if (_addonSettings[SettingsKey.POPUP_POSITION_DEFAULTS_ENABLED]) {
288 | if (x === "") {
289 | x = _addonSettings[SettingsKey.POPUP_POSITION_X_DEFAULT] || "";
290 | }
291 |
292 | if (y === "") {
293 | y = _addonSettings[SettingsKey.POPUP_POSITION_Y_DEFAULT] || "";
294 | }
295 |
296 | if (w === "") {
297 | w = _addonSettings[SettingsKey.POPUP_POSITION_WIDTH_DEFAULT] || "";
298 | }
299 |
300 | if (h === "") {
301 | h = _addonSettings[SettingsKey.POPUP_POSITION_HEIGHT_DEFAULT] || "";
302 | }
303 | }
304 |
305 | if (_addonSettings[SettingsKey.WORKAROUND_OFFSET_SIZE]) {
306 | ox = (_addonSettings[SettingsKey.WORKAROUND_OFFSET_SIZE_X] || 0) * 1;
307 | oy = (_addonSettings[SettingsKey.WORKAROUND_OFFSET_SIZE_Y] || 0) * 1;
308 | ow = (_addonSettings[SettingsKey.WORKAROUND_OFFSET_SIZE_WIDTH] || 0) * 1;
309 | oh = (_addonSettings[SettingsKey.WORKAROUND_OFFSET_SIZE_HEIGHT] || 0) * 1;
310 | }
311 |
312 | if (x !== "") {
313 | data.left = x * 1 + ox;
314 | }
315 |
316 | if (y !== "") {
317 | data.top = y * 1 + oy;
318 | }
319 |
320 | if (w !== "") {
321 | data.width = w * 1 + ow;
322 | }
323 |
324 | if (h !== "") {
325 | data.height = h * 1 + oh;
326 | }
327 |
328 | try {
329 | browser.windows.create(data).then(wnd => {
330 | // Try to update properties if they were ignored in create
331 |
332 | if (!_addonSettings[SettingsKey.WORKAROUND_FORCE_POSITION]) {
333 | return;
334 | }
335 |
336 | let performUpdate = false;
337 | let retryData = {};
338 |
339 | if (data.left !== wnd.left || data.top !== wnd.top) {
340 | performUpdate = true;
341 | retryData["left"] = data.left;
342 | retryData["top"] = data.top;
343 | }
344 |
345 | if (data.width !== wnd.width || data.height !== wnd.height) {
346 | performUpdate = true;
347 | retryData["width"] = data.width;
348 | retryData["height"] = data.height;
349 | }
350 |
351 | if (performUpdate) {
352 | browser.windows.update(wnd.id, retryData);
353 | }
354 | });
355 | } catch(e) {
356 | console.error("Cannot open popup", e);
357 | }
358 | }
359 |
360 | function modifyPageContextMenu(windowId) {
361 | if (windowId == browser.windows.WINDOW_ID_NONE) {
362 | return;
363 | }
364 |
365 | browser.windows.get(windowId).then(wnd => {
366 | switch (wnd.type.toLowerCase()) {
367 | case WindowType.NORMAL:
368 | // Firefox versions before 63 does not support `visible`, and even rejects the entire update
369 | browser.contextMenus.update(ContextMenuType.PAGE_POPUP, { enabled: true }).then(() =>
370 | browser.contextMenus.update(ContextMenuType.PAGE_POPUP, { visible: true })
371 | );
372 | browser.contextMenus.update(ContextMenuType.PAGE_RESTORE, { enabled: false }).then(() =>
373 | browser.contextMenus.update(ContextMenuType.PAGE_RESTORE, { visible: false })
374 | );
375 |
376 | break;
377 | case WindowType.POPUP:
378 | browser.contextMenus.update(ContextMenuType.PAGE_POPUP, { enabled: false }).then(() =>
379 | browser.contextMenus.update(ContextMenuType.PAGE_POPUP, { visible: false })
380 | );
381 | browser.contextMenus.update(ContextMenuType.PAGE_RESTORE, { enabled: true }).then(() =>
382 | browser.contextMenus.update(ContextMenuType.PAGE_RESTORE, { visible: true })
383 | );
384 |
385 | break;
386 | }
387 | });
388 | }
389 |
390 | function popuplateWindowList(info, tab) {
391 | if (~info.menuIds.indexOf(ContextMenuType.PAGE_RESTORE)) {
392 | _dynamicMenuItems.forEach(windowId => browser.contextMenus.remove(windowId));
393 |
394 | _dynamicMenuItems = [];
395 |
396 | browser.windows.getAll({ windowTypes: [ WindowType.NORMAL ] }).then(windows => {
397 | if (windows.length > 1) {
398 | windows.forEach(w => {
399 | _dynamicMenuItems.push(w.id + "");
400 |
401 | browser.contextMenus.create({
402 | id: w.id + "",
403 | title: w.title,
404 | parentId: ContextMenuType.PAGE_RESTORE
405 | });
406 | });
407 |
408 | browser.contextMenus.refresh();
409 | }
410 | });
411 | }
412 | }
413 |
414 | function escapeForRegex(str) {
415 | let wildcardReplacement = "\u0006\u0015\u0000";
416 | let wildcardRegex = new RegExp(wildcardReplacement, "g")
417 |
418 | str = str.replace(/\*/g, wildcardReplacement);
419 | str = str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
420 | str = "^" + str.replace(wildcardRegex, ".*") + "$";
421 | str = str.replace(/,/g, "|");
422 |
423 | return str;
424 | }
425 |
426 | function getMatchingRule(url) {
427 | let a = document.createElement("a");
428 | a.setAttribute("href", url);
429 |
430 | if (typeof _addonSettings[SettingsKey.RULES] === "undefined") {
431 | return undefined;
432 | }
433 |
434 | for (let rule of _addonSettings[SettingsKey.RULES]) {
435 | if (!rule["enabled"]) {
436 | continue;
437 | }
438 |
439 | let regex;
440 |
441 | if (rule["search"].charAt(0) === "/") {
442 | let tmpSearch = rule["search"].substr(1);
443 | let idx = tmpSearch.lastIndexOf("/");
444 |
445 | if (!~idx) {
446 | continue;
447 | }
448 |
449 | let flags = tmpSearch.slice(idx + 1);
450 |
451 | regex = new RegExp(tmpSearch.slice(0, idx), flags);
452 | } else {
453 | regex = escapeForRegex(rule["search"]);
454 | }
455 |
456 | if (rule["appliestype"] === "DOMAIN") {
457 | let origin = a.hostname;
458 |
459 | let includePort = ~rule["search"].indexOf(":");
460 |
461 | if (includePort && a.port) {
462 | origin += ":" + a.port;
463 | }
464 |
465 | if (~origin.search(regex)) {
466 | return rule;
467 | }
468 | } else {
469 | if (~url.search(regex)) {
470 | return rule;
471 | }
472 | }
473 | }
474 |
475 | return undefined;
476 | }
477 |
478 | function actOnSettings(settings) {
479 | console.log("Acting on new settings");
480 |
481 | _addonSettings = settings;
482 |
483 | browser.contextMenus.removeAll();
484 |
485 | if (settings[SettingsKey.MENU_ITEM_TAB]) {
486 | try {
487 | browser.contextMenus.create({
488 | id: ContextMenuType.TAB_POPUP,
489 | title: browser.i18n.getMessage("menu_item_tab_popup"),
490 | contexts: [ "tab" ],
491 | });
492 | } catch (e) {
493 | console.warn("Feature unavailable: Tab context menus");
494 | }
495 | }
496 |
497 | if (settings[SettingsKey.MENU_ITEM_LINK]) {
498 | try {
499 | browser.contextMenus.create({
500 | id: ContextMenuType.LINK_POPUP,
501 | title: browser.i18n.getMessage("menu_item_link_popup"),
502 | contexts: [ "link" ],
503 | });
504 | } catch (e) {
505 | console.warn("Feature unavailable: Link context menus");
506 | }
507 | }
508 |
509 | if (settings[SettingsKey.MENU_ITEM_BOOKMARK]) {
510 | try {
511 | browser.contextMenus.create({
512 | id: ContextMenuType.BOOKMARK_POPUP,
513 | title: browser.i18n.getMessage("menu_item_bookmark_popup"),
514 | contexts: [ "bookmark" ],
515 | });
516 | } catch (e) {
517 | console.warn("Feature unavailable: Bookmark context menus");
518 | }
519 | }
520 |
521 | if (settings[SettingsKey.MENU_ITEM_PAGE]) {
522 | browser.windows.onFocusChanged.addListener(modifyPageContextMenu);
523 |
524 | if (browser.contextMenus.hasOwnProperty("onShown")) {
525 | browser.contextMenus.onShown.addListener(popuplateWindowList);
526 | }
527 |
528 | browser.contextMenus.create({
529 | id: ContextMenuType.PAGE_POPUP,
530 | title: browser.i18n.getMessage("menu_item_page_popup"),
531 | contexts: [ "page" ],
532 | });
533 |
534 | browser.contextMenus.create({
535 | id: ContextMenuType.PAGE_RESTORE,
536 | title: browser.i18n.getMessage("menu_item_page_restore"),
537 | contexts: [ "page" ],
538 | });
539 |
540 | // Firefox versions before 63 does not support `visible`, and even rejects the entire update
541 | browser.contextMenus.update(ContextMenuType.PAGE_POPUP, { enabled: false }).then(() =>
542 | browser.contextMenus.update(ContextMenuType.PAGE_POPUP, { visible: false })
543 | );
544 | browser.contextMenus.update(ContextMenuType.PAGE_RESTORE, { enabled: false }).then(() =>
545 | browser.contextMenus.update(ContextMenuType.PAGE_RESTORE, { visible: false })
546 | );
547 | } else {
548 | browser.windows.onFocusChanged.removeListener(modifyPageContextMenu);
549 |
550 | if (browser.contextMenus.hasOwnProperty("onShown")) {
551 | browser.contextMenus.onShown.removeListener(popuplateWindowList);
552 | }
553 | }
554 | }
555 |
556 | main();
557 |
--------------------------------------------------------------------------------