├── .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 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 21 | 27 | 28 | 29 |
19 | 20 | 22 | 26 |
30 |
31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 41 | 44 | 45 | 46 | 49 | 52 | 53 | 54 | 57 | 60 | 61 | 62 | 65 | 68 | 69 | 70 |
39 | 40 | 42 | 43 |
47 | 48 | 50 | 51 |
55 | 56 | 58 | 59 |
63 | 64 | 66 | 67 |
71 |
72 | 73 | 74 |
75 | 76 | 77 | 78 | 79 | 80 | 83 | 86 | 87 | 88 | 91 | 99 | 100 | 101 |
81 | 82 | 84 | 85 |
89 | 90 | 92 | 93 |
94 | 95 | 96 | 97 | 98 |
102 |
103 | 104 |
105 | 106 | 107 |
108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 123 | 126 | 129 | 132 | 135 | 136 | 137 |
121 | 122 | 124 | 125 | 127 | 128 | 130 | 131 | 133 | 134 |
138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 156 | 157 | 163 | 164 | 167 | 170 | 173 | 176 | 179 | 180 | 183 | 184 | 187 | 188 | 189 | 190 | 193 | 194 | 195 |
191 | 192 |
196 |
197 | 198 |
199 | 200 | 201 |
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 | --------------------------------------------------------------------------------