├── README.md ├── _locales ├── bg │ └── messages.json ├── de │ └── messages.json ├── en │ └── messages.json ├── nl │ └── messages.json └── ru │ └── messages.json ├── background.js ├── changelog-ru.md ├── changelog.md ├── content.js ├── global.js ├── i18n.js ├── icon16-off.png ├── icon16.png ├── icon24-off.png ├── icon24.png ├── license.txt ├── make.bat ├── make.sh ├── manifest.json ├── options.html └── options.js /README.md: -------------------------------------------------------------------------------- 1 | WebExtensions port of Right Links extension for Firefox/SeaMonkey (see Right_Links#31). 2 | 3 | ## Issues 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |
DescriptionPartStatusSeverityFirefox bug
Browser behavior for “open link in new tab” (browser.tabs.insertRelatedAfterCurrent & Co)UXNo API
browser.tabs.create({ openerTabId: … }) in Firefox 57+
Major
Ability to handle clicks on bookmarks and history itemsCore functionalityNo APIMajor
Ability to handle clicks on internal restricted pages (about:…, chrome://…, especially on about:newtab)Core functionalityNo APIMajor
Ability to handle clicks on restricted webpages (like addons.mozilla.org)Core functionality 40 | May be configured (at your own risk) in about:config: 41 |
privacy.resistFingerprinting.block_mozAddonManager = true 42 |
extensions.webextensions.restrictedDomains = "" (empty string, or remove some domains as you like) 43 |
Major
Ability to simulate click on JavaScript-links (e.g. with `window.open()` inside)Core functionalityNo API, new tabs/windows will be blockedMajor
Ability to programmatically open context menuUXNo APIMajor
Ability to send HTTP refererCore functionalityNo APIMajor
Support for canvas images (ability to open data:… and blob:… URIs)Core functionalityForbidden
Works from background script: URL.createObjectURL(Blob)
Minor
Configurable keyboard shortcuts (note: also not possible to assign Ctrl+Alt+X)UXNo API
browser.commands.update() in Firefox 60+
MinorBug 1421811, bug 1303384
-------------------------------------------------------------------------------- /_locales/bg/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Right Links WE" 4 | }, 5 | "extensionDescription": { 6 | "message": "Right Links - Щракнете, за да отворите връзка в нов раздел (за тъчпади и др.)" 7 | }, 8 | "buttonTooltip": { 9 | "message": "Right Links WE: " 10 | }, 11 | "tooltipEnabled": { 12 | "message": "Включено" 13 | }, 14 | "tooltipDisabled": { 15 | "message": "Изключено" 16 | }, 17 | "options": { 18 | "message": "Options" 19 | }, 20 | "updateNotice": { 21 | "message": "Some features are not implementable anymore due to WebExtensions restrictions, for Firefox 56 and older please use Right Links (GitHub)." 22 | }, 23 | "enabled": { 24 | "message": "Включено" 25 | }, 26 | "keyboardShortcut": { 27 | "message": "Keyboard shortcut:" 28 | }, 29 | "longLeftClick": { 30 | "message": "Дълъг ляв бутон" 31 | }, 32 | "loadInBackground": { 33 | "message": "Зареждай връзките на на заден фон" 34 | }, 35 | "loadInDiscardedTab": { 36 | "message": "Don't load until selected" 37 | }, 38 | "loadIn": { 39 | "message": "Load in" 40 | }, 41 | "newTab": { 42 | "message": "new tab" 43 | }, 44 | "newWindow": { 45 | "message": "new window" 46 | }, 47 | "currentTab": { 48 | "message": "current tab" 49 | }, 50 | "rightClick": { 51 | "message": "Right-click" 52 | }, 53 | "enabledOnImages": { 54 | "message": "Прихващай кликове върху изображения" 55 | }, 56 | "canvasImages": { 57 | "message": "Canvas images" 58 | }, 59 | "useBlob": { 60 | "message": "Use blob:…" 61 | }, 62 | "sizeLimit": { 63 | "message": "Size limit (px):" 64 | }, 65 | "contextMenuTimeout": { 66 | "message": "Context menu timeout (ms): " 67 | }, 68 | "longLeftClickTimeout": { 69 | "message": "Long left-click timeout (ms): " 70 | }, 71 | "blacklist": { 72 | "message": "Disable on following sites:" 73 | }, 74 | "mouseMoveTolerance": { 75 | "message": "Mouse move tolerance (for mouse gestures, in px): " 76 | }, 77 | "debug": { 78 | "message": "Debug mode" 79 | } 80 | } -------------------------------------------------------------------------------- /_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Right Links WE" 4 | }, 5 | "extensionDescription": { 6 | "message": "Klicken Sie mit der rechten Maustaste, um Verweise auf neuen Tabs zu öffnen (für Touchpads usw.)" 7 | }, 8 | "buttonTooltip": { 9 | "message": "Right Links WE: " 10 | }, 11 | "tooltipEnabled": { 12 | "message": "Aktiviert" 13 | }, 14 | "tooltipDisabled": { 15 | "message": "Deaktiviert" 16 | }, 17 | "options": { 18 | "message": "Options" 19 | }, 20 | "updateNotice": { 21 | "message": "Some features are not implementable anymore due to WebExtensions restrictions, for Firefox 56 and older please use Right Links (GitHub)." 22 | }, 23 | "enabled": { 24 | "message": "Aktiviert" 25 | }, 26 | "keyboardShortcut": { 27 | "message": "Keyboard shortcut:" 28 | }, 29 | "longLeftClick": { 30 | "message": "Langer Linksklick" 31 | }, 32 | "loadInBackground": { 33 | "message": "Verweise im Hintergrund laden" 34 | }, 35 | "loadInDiscardedTab": { 36 | "message": "Don't load until selected" 37 | }, 38 | "loadIn": { 39 | "message": "Load in" 40 | }, 41 | "newTab": { 42 | "message": "new tab" 43 | }, 44 | "newWindow": { 45 | "message": "new window" 46 | }, 47 | "currentTab": { 48 | "message": "current tab" 49 | }, 50 | "rightClick": { 51 | "message": "Right-click" 52 | }, 53 | "enabledOnImages": { 54 | "message": "Klicks auf Grafiken anwenden" 55 | }, 56 | "canvasImages": { 57 | "message": "Canvas images" 58 | }, 59 | "useBlob": { 60 | "message": "Use blob:…" 61 | }, 62 | "sizeLimit": { 63 | "message": "Size limit (px):" 64 | }, 65 | "contextMenuTimeout": { 66 | "message": "Context menu timeout (ms): " 67 | }, 68 | "longLeftClickTimeout": { 69 | "message": "Long left-click timeout (ms): " 70 | }, 71 | "blacklist": { 72 | "message": "Disable on following sites:" 73 | }, 74 | "mouseMoveTolerance": { 75 | "message": "Mouse move tolerance (for mouse gestures, in px): " 76 | }, 77 | "debug": { 78 | "message": "Debug mode" 79 | } 80 | } -------------------------------------------------------------------------------- /_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Right Links WE" 4 | }, 5 | "extensionDescription": { 6 | "message": "Right-click to open links in new tabs (for touchpads etc.)" 7 | }, 8 | "buttonTooltip": { 9 | "message": "Right Links WE: " 10 | }, 11 | "tooltipEnabled": { 12 | "message": "Enabled" 13 | }, 14 | "tooltipDisabled": { 15 | "message": "Disabled" 16 | }, 17 | "options": { 18 | "message": "Options" 19 | }, 20 | "updateNotice": { 21 | "message": "Some features are not implementable anymore due to WebExtensions restrictions, for Firefox 56 and older please use Right Links (GitHub)." 22 | }, 23 | "enabled": { 24 | "message": "Enabled" 25 | }, 26 | "keyboardShortcut": { 27 | "message": "Keyboard shortcut:" 28 | }, 29 | "longLeftClick": { 30 | "message": "Long left-click" 31 | }, 32 | "loadInBackground": { 33 | "message": "Open links in background" 34 | }, 35 | "loadInDiscardedTab": { 36 | "message": "Don't load until selected" 37 | }, 38 | "loadIn": { 39 | "message": "Open in" 40 | }, 41 | "newTab": { 42 | "message": "new tab" 43 | }, 44 | "newWindow": { 45 | "message": "new window" 46 | }, 47 | "currentTab": { 48 | "message": "current tab" 49 | }, 50 | "rightClick": { 51 | "message": "Right-click" 52 | }, 53 | "enabledOnImages": { 54 | "message": "Handle clicks on images" 55 | }, 56 | "canvasImages": { 57 | "message": "Canvas images" 58 | }, 59 | "useBlob": { 60 | "message": "Use blob:…" 61 | }, 62 | "sizeLimit": { 63 | "message": "Size limit (px):" 64 | }, 65 | "contextMenuTimeout": { 66 | "message": "Context menu timeout (ms): " 67 | }, 68 | "longLeftClickTimeout": { 69 | "message": "Long left-click timeout (ms): " 70 | }, 71 | "blacklist": { 72 | "message": "Disable on following sites:" 73 | }, 74 | "mouseMoveTolerance": { 75 | "message": "Mouse move tolerance (for mouse gestures, in px): " 76 | }, 77 | "debug": { 78 | "message": "Debug mode" 79 | } 80 | } -------------------------------------------------------------------------------- /_locales/nl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Right Links WE" 4 | }, 5 | "extensionDescription": { 6 | "message": "Rechtsklik om koppelingen te openen op nieuwe tabbladen (voor touchpads e.d.)" 7 | }, 8 | "buttonTooltip": { 9 | "message": "Right Links WE: " 10 | }, 11 | "tooltipEnabled": { 12 | "message": "Ingeschakeld" 13 | }, 14 | "tooltipDisabled": { 15 | "message": "Uitgeschakeld" 16 | }, 17 | "options": { 18 | "message": "Opties" 19 | }, 20 | "updateNotice": { 21 | "message": "Enkele functies zijn niet meer te implementeren wegens WebExtensions-beperkingen. In Firefox 56 en ouder kunt u beter de volgende add-on gebruiken: Right Links (GitHub)." 22 | }, 23 | "enabled": { 24 | "message": "Ingeschakeld" 25 | }, 26 | "keyboardShortcut": { 27 | "message": "Sneltoets:" 28 | }, 29 | "longLeftClick": { 30 | "message": "Linkermuisknop ingedrukt houden" 31 | }, 32 | "loadInBackground": { 33 | "message": "Koppelingen op achtergrond openen" 34 | }, 35 | "loadInDiscardedTab": { 36 | "message": "Pas laden na aanklikken" 37 | }, 38 | "loadIn": { 39 | "message": "Openen op/in" 40 | }, 41 | "newTab": { 42 | "message": "nieuw tabblad" 43 | }, 44 | "newWindow": { 45 | "message": "nieuw venster" 46 | }, 47 | "currentTab": { 48 | "message": "huidig tabblad" 49 | }, 50 | "rightClick": { 51 | "message": "Rechtsklikken" 52 | }, 53 | "enabledOnImages": { 54 | "message": "Klikken op afbeeldingen" 55 | }, 56 | "canvasImages": { 57 | "message": "Canvasafbeeldingen" 58 | }, 59 | "useBlob": { 60 | "message": "Blob gebruiken:…" 61 | }, 62 | "sizeLimit": { 63 | "message": "Afmetingen beperken (in px):" 64 | }, 65 | "contextMenuTimeout": { 66 | "message": "Rechtermuisknopmenutime-out (in ms): " 67 | }, 68 | "longLeftClickTimeout": { 69 | "message": "Time-out bij ingedrukt houden van linkermuisknop (in ms): " 70 | }, 71 | "blacklist": { 72 | "message": "Uitschakelen op de volgende sites:" 73 | }, 74 | "mouseMoveTolerance": { 75 | "message": "Cursor-verplaatstolerantie (van toepassing op muisgebaren, in px): " 76 | }, 77 | "debug": { 78 | "message": "Foutopsporingsmodus" 79 | } 80 | } -------------------------------------------------------------------------------- /_locales/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Right Links WE" 4 | }, 5 | "extensionDescription": { 6 | "message": "Позволяет открывать ссылки в новых вкладках по клику правой кнопкой мыши (для тачпадов и т.п.)" 7 | }, 8 | "buttonTooltip": { 9 | "message": "Right Links WE: " 10 | }, 11 | "tooltipEnabled": { 12 | "message": "Включено" 13 | }, 14 | "tooltipDisabled": { 15 | "message": "Выключено" 16 | }, 17 | "options": { 18 | "message": "Настройки" 19 | }, 20 | "updateNotice": { 21 | "message": "Некоторые возможности теперь невозможно реализовать из-за ограничений WebExtensions, для Firefox 56 и более старых, пожалуйста, используйте Right Links (GitHub)." 22 | }, 23 | "enabled": { 24 | "message": "Включено" 25 | }, 26 | "keyboardShortcut": { 27 | "message": "Сочетание клавиш:" 28 | }, 29 | "longLeftClick": { 30 | "message": "Долгий клик левой кнопкой мыши" 31 | }, 32 | "loadInBackground": { 33 | "message": "Открывать ссылки в фоне" 34 | }, 35 | "loadInDiscardedTab": { 36 | "message": "Загружать только после переключения на вкладку" 37 | }, 38 | "loadIn": { 39 | "message": "Открывать в" 40 | }, 41 | "newTab": { 42 | "message": "новой вкладке" 43 | }, 44 | "newWindow": { 45 | "message": "новом окне" 46 | }, 47 | "currentTab": { 48 | "message": "текущей вкладке" 49 | }, 50 | "rightClick": { 51 | "message": "Клик правой кнопкой мыши" 52 | }, 53 | "enabledOnImages": { 54 | "message": "Обрабатывать клики по изображениям" 55 | }, 56 | "canvasImages": { 57 | "message": "Canvas изображения" 58 | }, 59 | "useBlob": { 60 | "message": "Использовать blob:…" 61 | }, 62 | "sizeLimit": { 63 | "message": "Ограничение размеров (в пикселях):" 64 | }, 65 | "contextMenuTimeout": { 66 | "message": "Задержка перед контекстным меню (мс): " 67 | }, 68 | "longLeftClickTimeout": { 69 | "message": "Задержка перед долгим кликом левой кнопкой мыши (мс): " 70 | }, 71 | "blacklist": { 72 | "message": "Отключить на следующих сайтах:" 73 | }, 74 | "mouseMoveTolerance": { 75 | "message": "Чувствительность к перемещениям мыши (для жестов мышью, в пикселях): " 76 | }, 77 | "debug": { 78 | "message": "Отладочный режим" 79 | } 80 | } -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | readPrefs(init); 2 | 3 | function init() { 4 | initOnce(); 5 | if(!prefs.enabled) 6 | return updateState(); 7 | browser.runtime.onMessage.addListener(onMessageFromContent); 8 | if(prefs.updateNotice) setTimeout(function() { 9 | browser.storage.local.set({ 10 | updateNotice: false 11 | }); 12 | browser.runtime.openOptionsPage(); 13 | }, 500); 14 | return updateState(); 15 | } 16 | function initOnce() { 17 | initOnce = function() {}; 18 | setTimeout(function() { 19 | if("getBrowserInfo" in browser.runtime) 20 | browser.runtime.getBrowserInfo().then(createMenus); 21 | else 22 | createMenus(); 23 | }, 50); 24 | updateHotkey(); 25 | } 26 | function destroy() { 27 | browser.runtime.onMessage.removeListener(onMessageFromContent); 28 | updateState(); 29 | } 30 | function toggle(enable) { 31 | if(enable) 32 | init(); 33 | else 34 | destroy(); 35 | updateMenus(); 36 | } 37 | function onPrefChanged(key, newVal) { 38 | prefs[key] = newVal; 39 | if(key == "enabled") 40 | toggle(newVal); 41 | else if( 42 | key == "enabledLeft" 43 | || key == "loadInBackgroundLeft" 44 | || key == "enabledRight" 45 | || key == "loadInBackgroundRight" 46 | ) 47 | updateMenus(); 48 | else if(key == "toggleKey") 49 | updateHotkey(250); 50 | } 51 | function updateState() { 52 | setTimeout(setState, 0, prefs.enabled); 53 | } 54 | function setState(enabled) { 55 | _log("setState(" + enabled + ")"); 56 | var key = enabled ? "" : "-off"; 57 | browser.browserAction.setIcon({ 58 | path: { 59 | 16: "icon16" + key + ".png", 60 | 24: "icon24" + key + ".png" 61 | } 62 | }); 63 | browser.browserAction.setTitle({ 64 | title: browser.i18n.getMessage("buttonTooltip") 65 | + browser.i18n.getMessage(enabled ? "tooltipEnabled" : "tooltipDisabled") 66 | }); 67 | } 68 | 69 | browser.browserAction.onClicked.addListener(function() { 70 | _log("browserAction.onClicked"); 71 | browser.storage.local.set({ 72 | enabled: !prefs.enabled 73 | }); 74 | }); 75 | 76 | function createMenus(brInfo) { 77 | var hasManageExt = brInfo && brInfo.name == "Firefox" && parseFloat(brInfo.version) >= 62; 78 | 79 | // Note: browser.contextMenus.ACTION_MENU_TOP_LEVEL_LIMIT == 6 80 | var item = browser.contextMenus.create.bind(browser.contextMenus); 81 | item({ 82 | id: "enabledLeft", 83 | title: browser.i18n.getMessage("longLeftClick"), 84 | type: "checkbox", 85 | contexts: ["browser_action"] 86 | }); 87 | item({ 88 | id: "loadInBackgroundLeft", 89 | title: browser.i18n.getMessage("loadInBackground"), 90 | type: "checkbox", 91 | contexts: ["browser_action"] 92 | }); 93 | 94 | hasManageExt && item({ 95 | id: "enabledSeparator", 96 | type: "separator", 97 | contexts: ["browser_action"] 98 | }); 99 | 100 | item({ 101 | id: "enabledRight", 102 | title: browser.i18n.getMessage("rightClick"), 103 | type: "checkbox", 104 | contexts: ["browser_action"] 105 | }); 106 | item({ 107 | id: "loadInBackgroundRight", 108 | title: browser.i18n.getMessage("loadInBackground"), 109 | type: "checkbox", 110 | contexts: ["browser_action"] 111 | }); 112 | 113 | !hasManageExt && item({ 114 | id: "optionsSeparator", 115 | type: "separator", 116 | contexts: ["browser_action"] 117 | }); 118 | !hasManageExt && item({ 119 | id: "options", 120 | title: browser.i18n.getMessage("options"), 121 | icons: { 122 | "16": "icon16.png", 123 | "24": "icon24.png" 124 | }, 125 | contexts: ["browser_action"] 126 | }); 127 | 128 | browser.contextMenus.onClicked.addListener(function(info, tab) { 129 | var miId = info.menuItemId; 130 | if(miId == "options") { 131 | _log("contextMenus.onClicked(): " + miId); 132 | browser.runtime.openOptionsPage(); 133 | } 134 | else if( 135 | miId == "enabledLeft" 136 | || miId == "loadInBackgroundLeft" 137 | || miId == "enabledRight" 138 | || miId == "loadInBackgroundRight" 139 | ) { 140 | _log("contextMenus.onClicked(): " + miId + " -> " + info.checked); 141 | browser.storage.local.set({ 142 | [miId]: info.checked 143 | }); 144 | } 145 | }); 146 | 147 | setTimeout(updateMenus, 50); 148 | } 149 | function updateMenus() { 150 | var update = browser.contextMenus.update.bind(browser.contextMenus); 151 | update("enabledLeft", { 152 | checked: prefs.enabledLeft, 153 | enabled: prefs.enabled 154 | }); 155 | update("loadInBackgroundLeft", { 156 | checked: prefs.loadInBackgroundLeft, 157 | enabled: prefs.enabled && prefs.enabledLeft 158 | }); 159 | update("enabledRight", { 160 | checked: prefs.enabledRight, 161 | enabled: prefs.enabled 162 | }); 163 | update("loadInBackgroundRight", { 164 | checked: prefs.loadInBackgroundRight, 165 | enabled: prefs.enabled && prefs.enabledRight 166 | }); 167 | } 168 | function updateHotkey(delay = 0) { 169 | if(!("update" in browser.commands)) // Firefox 60+ 170 | return; 171 | if(updateHotkey.timer || 0) 172 | return; 173 | updateHotkey.timer = setTimeout(function() { 174 | updateHotkey.timer = 0; 175 | function feedback(err) { 176 | browser.runtime.sendMessage({ 177 | action: "shortcutValidation", 178 | error: err || "" 179 | }); 180 | } 181 | if(!prefs.toggleKey) { 182 | browser.commands.reset("_execute_browser_action"); 183 | feedback(); 184 | return; 185 | } 186 | try { 187 | browser.commands.update({ 188 | name: "_execute_browser_action", 189 | shortcut: prefs.toggleKey 190 | }).then(function() { 191 | feedback(); 192 | }); 193 | } 194 | catch(e) { 195 | feedback("" + e); 196 | } 197 | }, delay); 198 | } 199 | 200 | function onMessageFromContent(msg, sender, sendResponse) { 201 | if(msg.action == "openURI") { 202 | if(msg.uri instanceof Blob) // Should be converted here to prevent security errors 203 | var uri = msg.uri = URL.createObjectURL(msg.uri); 204 | if(msg.loadIn == 1) 205 | openURIInWindow(sender.tab, msg); 206 | else { 207 | platformInfo(function() { 208 | openURIInTab(sender.tab, msg); 209 | }); 210 | } 211 | if(uri) setTimeout(function() { 212 | _log("Cleanup: URL.revokeObjectURL()"); 213 | URL.revokeObjectURL(uri); 214 | }, 100); 215 | } 216 | } 217 | function platformInfo(callback) { 218 | platformInfo.os = "win"; 219 | browser.runtime.getPlatformInfo().then(function(pi) { 220 | _log("platformInfo(): OS: " + pi.os); 221 | platformInfo = function(callback) { 222 | callback(); 223 | }; 224 | platformInfo.os = pi.os; 225 | callback(); 226 | }, callback); 227 | } 228 | function openURIInTab(sourceTab, data) { 229 | _log("openURIInTab(), inBG: " + data.inBG + ", URI: " + data.uri); 230 | var opts = { 231 | url: data.uri, 232 | active: !data.inBG, 233 | openerTabId: sourceTab.id 234 | }; 235 | if(platformInfo.os == "mac") // To not break popup windows in all OS 236 | opts.windowId = sourceTab.windowId; 237 | if(data.inBG && data.discarded) { 238 | opts.discarded = true; 239 | if(data.title) 240 | opts.title = data.title; 241 | } 242 | try { 243 | browser.tabs.create(opts).catch(function(e) { 244 | if((e + "").indexOf("Opener tab must be in the same window") == -1) { 245 | notifyError(e); 246 | return; 247 | } 248 | _log("openURIInTab(): will try without openerTabId"); 249 | delete opts.openerTabId; 250 | browser.tabs.create(opts) 251 | .then(function(tab) { 252 | browser.windows.update(tab.windowId, { 253 | focused: true 254 | }); 255 | }) 256 | .catch(notifyError); 257 | }); 258 | } 259 | catch(e) { 260 | // Type error for parameter createProperties (Property "openerTabId" is unsupported by Firefox) for tabs.create. 261 | if((e + "").indexOf('"openerTabId" is unsupported') == -1) { 262 | notifyError(e); 263 | throw e; 264 | } 265 | _log("openURIInTab(): openerTabId property not supported, will use workaround"); 266 | delete opts.openerTabId; 267 | opts.index = sourceTab.index + 1; 268 | browser.tabs.create(opts).catch(notifyError); 269 | } 270 | } 271 | function openURIInWindow(sourceTab, data) { 272 | _log("openURIInWindow(), inBG: " + data.inBG + ", URI: " + data.uri); 273 | var opts = { 274 | url: data.uri, 275 | focused: !data.inBG 276 | }; 277 | try { 278 | browser.windows.create(opts).catch(notifyError); 279 | } 280 | catch(e) { 281 | // Type error for parameter createData (Property "focused" is unsupported by Firefox) for windows.create. 282 | if((e + "").indexOf('"focused" is unsupported') == -1) 283 | throw e; 284 | _log("openURIInWindow(): focused property not supported"); 285 | delete opts.focused; 286 | browser.windows.create(opts).catch(notifyError); 287 | } 288 | } 289 | function notifyError(err) { 290 | browser.notifications.create({ 291 | type: "basic", 292 | iconUrl: browser.extension.getURL("icon24-off.png"), 293 | title: browser.i18n.getMessage("extensionName"), 294 | message: "" + err 295 | }); 296 | } -------------------------------------------------------------------------------- /changelog-ru.md: -------------------------------------------------------------------------------- 1 | #### Right Links WE: История изменений 2 | 3 | `+` – добавлено
4 | `-` – удалено
5 | `x` – исправлено
6 | `*` – улучшено
7 | 8 | ##### master/HEAD 9 | `+` Добавлен перевод на нидерландский (спасибо Vistaus: #26).
10 | 11 | ##### 0.5b12 (2021-03-14) 12 | `x` Исправлено открытие ссылок без `.textContent`, например, с изображением вместо текста ссылки (ошибка в реализации открытия в выгруженных вкладках, см. #22).
13 | 14 | ##### 0.5b11 (2021-03-14) 15 | `x` Исправлены настройки по умолчанию (ошибочно изменены в процессе реализации открытия в выгруженных вкладках, см. #22).
16 | `+` Добавлен разделитель между настройками долгого левой клика левой и правой кнопкой мыши (только если существует встроенный пункт «Управление расширением»).
17 | 18 | ##### 0.5b10 (2021-03-13) 19 | `+` Добавлена возможность открывать ссылки в выгруженных вкладках в Firefox 63+ (#22).
20 | `x` Исправлено открытие вкладок из неактивного окна в MacOS (#23, спасибо asleepysamurai).
21 | 22 | ##### 0.5b9 (2019-05-26) 23 | `+` Добавлены раздельные черные списки для левой и правой кнопок мыши для возможности отключения обработки кликов на заданных пользователем сайтах.
24 | `x` Исправлена обработка кликов правой кнопкой мыши в Firefox 68+: теперь используется событие «auxclick».
25 | `*` Добавлено отключение вложенной настройки для canvas при снятии галочки «Canvas изображения».
26 | `x` Исправлено открытие вкладок из окна без панели вкладок: вкладка будет открыта в другом окне.
27 | 28 | ##### 0.5b8 (2019-01-27) 29 | `x` Исправлена работа настройки «Обрабатывать клики по изображениям».
30 | `*` Добавлено отключение дочерних настроек в случае отключения родительской настройки.
31 | 32 | ##### 0.5b7 (2019-01-20) 33 | `*` Внутренние улучшения: используется отдельный скрипт global.js для общего кода, упрощена локализация.
34 | `*` Улучшено меню настроек (у кнопки на панели инструментов).
35 | `*` Логически перегруппированы настройки.
36 | `+` Добавлена поддержка canvas-изображения (спасибо Dumby).
37 | `+` Добавлена (настраиваемая) горячая клавиша F2 для переключения Right Links WE.
38 | 39 | ##### 0.5b6 (2017-12-15) 40 | `+` Добавлены галочки в контекстное меню кнопки: левый/правый клик + открывать в фоне.
41 | `x` Исправлена установка значения по умолчанию для настройки «Включено» в content-скрипте (не работало для новых установок без снятия/установки этой галочки).
42 | 43 | ##### 0.5b5 (2017-11-26) 44 | `x` Исправлена задержка перед долгим кликом левой кнопкой мыши.
45 | `x` Исправлена обработка фреймов и вкладок, восстанавливаемых «по запросу» (теперь content-скрипт автоматически загружается во все вкладки и фреймы).
46 | 47 | ##### 0.5b4 (2017-11-19) 48 | `x` Исправлено обнаружение перемещения мыши.
49 | `x` Исправлено подавление контекстного меню на Linux (#2).
50 | 51 | ##### 0.5b3 (2017-11-17) 52 | `-` Удалена устаревшая замена для браузеров без поддержки openerTabId.
53 | `*` Внутренние улучшения.
54 | `*` Более надежный способ предотвращения открытия контекстного меню.
55 | 56 | ##### 0.5b2 (2017-11-15) 57 | `*` Обновлена страница настроек и переводы.
58 | 59 | ##### 0.5b1 (2017-11-13) 60 | `*` Портировано на WebExtensions.
61 | 62 | ##### 0.1a1 (2017-10-10) 63 | `*` Первый черновой вариант на WebExtensions.
-------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | #### Right Links WE: Changelog 2 | 3 | `+` – added
4 | `-` – deleted
5 | `x` – fixed
6 | `*` – improved
7 | 8 | ##### master/HEAD 9 | `+` Added Dutch translation (thanks to Vistaus: #26).
10 | 11 | ##### 0.5b12 (2021-03-14) 12 | `x` Correctly open links without `.textContent`, e.g. with image instead of link text (bug in discarded tab implementation, see #22).
13 | 14 | ##### 0.5b11 (2021-03-14) 15 | `x` Fixed default options (changed by mistake in discarded tab implementation, see #22).
16 | `+` Added separator between long left- and right-click options (only if exists built-in “Manage Extension” menu item).
17 | 18 | ##### 0.5b10 (2021-03-13) 19 | `+` Added ability to open link in discarded (unloaded) tab in Firefox 63+ (#22).
20 | `x` Correctly open tabs from non-focused window in MacOS (#23, thanks to asleepysamurai).
21 | 22 | ##### 0.5b9 (2019-05-26) 23 | `+` Added separate blacklists for left-click and right-click to disable click handling on user-defined sites.
24 | `x` Fixed right-click support in Firefox 68+: now used “auxclick” event.
25 | `*` Disable sub-option for canvas, if not checked “Canvas images”.
26 | `x` Fixed ability to open tabs from popup window (without tab bar): tab will be opened in another window.
27 | 28 | ##### 0.5b8 (2019-01-27) 29 | `x` Fixed “Handle clicks on images” option.
30 | `*` Disable sub-options in case of disabled parent option.
31 | 32 | ##### 0.5b7 (2019-01-20) 33 | `*` Internal tweaks: used separate global.js script for shared code, simplified localization.
34 | `*` Improved options menu (on toolbar button).
35 | `*` Logically re-grouped options.
36 | `+` Implemented support for canvas images (thanks to Dumby).
37 | `+` Added (configurable) F2 hotkey to toggle Right Links WE.
38 | 39 | ##### 0.5b6 (2017-12-15) 40 | `+` Added checkboxes in toolbar button context menu: left/right-click + load in background.
41 | `x` Correctly set default value for “Enabled” preference in content script (failed to load on new installs without unchecking/checking of that checkbox).
42 | 43 | ##### 0.5b5 (2017-11-26) 44 | `x` Fixed long left-click timeout.
45 | `x` Fixed handling of frames and restored “on demand” tabs (now content script will be automatically loaded into all tabs and frames).
46 | 47 | ##### 0.5b4 (2017-11-19) 48 | `x` Fixed detection of mouse moving.
49 | `x` Correctly prevent context menu on Linux (#2).
50 | 51 | ##### 0.5b3 (2017-11-17) 52 | `-` Removed obsolete extended replacement for browsers without openerTabId support.
53 | `*` Internal tweaks.
54 | `*` More robust way to prevent context menu.
55 | 56 | ##### 0.5b2 (2017-11-15) 57 | `*` Tweak options page + update localizations.
58 | 59 | ##### 0.5b1 (2017-11-13) 60 | `*` WebExtensions port.
61 | 62 | ##### 0.1a1 (2017-10-10) 63 | `*` First WebExtensions draft.
-------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | var flags = { 2 | executed: false, 3 | canceled: false, 4 | stopClick: false, 5 | stopMouseUp: false, 6 | stopContextMenu: false 7 | }; 8 | 9 | readPrefs(init); 10 | 11 | function init() { 12 | prefs.enabled && listenClicks(true); 13 | } 14 | function destroy() { 15 | listenClicks(false); 16 | moveData && cancel(); 17 | } 18 | function onUnload(e) { 19 | var doc = e.target; 20 | var win = doc && doc.defaultView; 21 | if(win != window) 22 | return; 23 | _log("onUnload(): " + (doc && doc.location)); 24 | destroy(); 25 | } 26 | function toggle(enable) { 27 | if(enable) 28 | init(); 29 | else 30 | destroy(); 31 | } 32 | function onPrefChanged(key, newVal) { 33 | prefs[key] = newVal; 34 | if(key == "enabled") 35 | toggle(newVal); 36 | else if(key == "blacklistLeft") 37 | blacklist.left = null; 38 | else if(key == "blacklistRight") 39 | blacklist.right = null; 40 | } 41 | 42 | var auxClickHandler; 43 | function listenClicks(on) { 44 | auxClickHandler = on; 45 | listen(on, { 46 | mousedown: onMouseDown, 47 | mouseup: onMouseUp, 48 | click: onClick, 49 | auxclick: onAuxClick, 50 | contextmenu: onContextMenu, 51 | unload: onUnload 52 | }); 53 | } 54 | var delayedTimer = 0; 55 | var cleanupTimer = 0; 56 | function onMouseDown(e) { 57 | if(!enabledFor(e)) 58 | return; 59 | 60 | flags.executed = false; 61 | flags.canceled = false; 62 | resetFlags(); 63 | 64 | var trg = e.originalTarget || e.target; 65 | var it = getItem(trg); 66 | _log("onMouseDown() -> getItem(): " + it); 67 | if(!it) 68 | return; 69 | if(blacklist.check(e)) { 70 | _log("onMouseDown() -> blacklisted site"); 71 | return; 72 | } 73 | 74 | if(isRight(e)) // For Linux with "contextmenu" event right after "mousedown" 75 | flags.stopContextMenu = true; 76 | 77 | var delay = isLeft(e) ? prefs.longLeftClickTimeout : prefs.showContextMenuTimeout; 78 | if(delay <= 0) 79 | return; 80 | 81 | moveHandlers(e); 82 | 83 | clearTimeout(delayedTimer); 84 | clearTimeout(cleanupTimer); 85 | delayedTimer = setTimeout(function() { 86 | flags.executed = true; 87 | if(!it.ownerDocument || !it.ownerDocument.location) // Page already unloaded 88 | return; 89 | if(isLeft(e)) { 90 | _log("onMouseDown() -> delayedTimer -> openURIItem()"); 91 | openURIItem(e, trg, it, prefs.loadInBackgroundLeft, prefs.loadInLeft, prefs.loadInDiscardedLeft); 92 | flags.stopMouseUp = flags.stopClick = true; 93 | } 94 | else { 95 | _log("onMouseDown() -> delayedTimer -> showContextMenu():"); 96 | showContextMenu(trg, e); 97 | } 98 | }, delay); 99 | } 100 | function onMouseUp(e) { 101 | if("_rightLinksIgnore" in e) 102 | return; 103 | 104 | if(flags.stopMouseUp) { 105 | flags.stopMouseUp = false; 106 | stopEvent(e); 107 | } 108 | 109 | moveHandlers(false); 110 | clearTimeout(delayedTimer); 111 | clearTimeout(cleanupTimer); 112 | cleanupTimer = setTimeout(function() { 113 | flags.stopContextMenu = false; 114 | }, 100); 115 | } 116 | function onClick(e) { 117 | if(!enabledFor(e)) 118 | return; 119 | 120 | if(flags.stopClick) { 121 | flags.stopClick = false; 122 | stopEvent(e); 123 | } 124 | 125 | if(flags.executed || flags.canceled) 126 | return; 127 | 128 | if(isLeft(e)) { 129 | clearTimeout(delayedTimer); 130 | return; 131 | } 132 | 133 | var isClick = e.type == "click"; 134 | if(isClick && auxClickHandler) { // Firefox 67 and older 135 | _log("Remove auxclick listener"); 136 | listen(false, { auxclick: onAuxClick }); 137 | auxClickHandler = false; 138 | } 139 | 140 | var trg = e.originalTarget || e.target; 141 | var it = getItem(trg); 142 | var fn = isClick ? "onClick()" : "onAuxClick()"; 143 | _log(fn + " -> getItem(): " + it); 144 | if(!it) { 145 | flags.stopContextMenu = false; 146 | return; 147 | } 148 | if(blacklist.check(e)) { 149 | _log(fn + " -> blacklisted site"); 150 | return; 151 | } 152 | flags.executed = true; 153 | openURIItem(e, trg, it, prefs.loadInBackgroundRight, prefs.loadInRight, prefs.loadInDiscardedRight); 154 | } 155 | function onAuxClick(e) { // Will called after onClick() 156 | if(!enabledFor(e)) 157 | return; 158 | onClick(e); 159 | } 160 | function onContextMenu(e) { 161 | if(flags.stopContextMenu) 162 | stopEvent(e); 163 | } 164 | function onMouseMove(e) { 165 | var md = moveData; 166 | if(!md.enabled) 167 | return; 168 | var x = e.screenX; 169 | var y = e.screenY; 170 | md.dist += Math.sqrt( 171 | Math.pow(md.x - x, 2) + 172 | Math.pow(md.y - y, 2) 173 | ); 174 | if(md.dist >= prefs.disallowMousemoveDist) { 175 | cancel(e); 176 | return; 177 | } 178 | md.x = x; 179 | md.y = y; 180 | } 181 | 182 | function isLeft(e) { 183 | return e.button == 0; 184 | } 185 | function isRight(e) { 186 | return e.button == 2; 187 | } 188 | function enabledFor(e) { 189 | if("_rightLinksIgnore" in e || e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) 190 | return false; 191 | return prefs.enabledLeft && isLeft(e) 192 | || prefs.enabledRight && isRight(e); 193 | } 194 | 195 | var blacklist = { 196 | left: null, 197 | right: null, 198 | patterns: function(e) { 199 | var [key, pref] = isLeft(e) ? ["left", "blacklistLeft"] : ["right", "blacklistRight"]; 200 | return this[key] || (this[key] = this.parsePatterns(prefs[pref])); 201 | }, 202 | check: function(e) { 203 | var curURI = e.view.location.href; 204 | _log("blacklist.check(): " + curURI); 205 | for(var pattern of this.patterns(e)) 206 | if(pattern.test(curURI)) 207 | return true; 208 | return false; 209 | }, 210 | parsePatterns: function(data) { 211 | var patterns = []; 212 | for(var str of data.split(/[\r\n]+/)) { 213 | str = str.trim(); 214 | if(/^\/(.+)\/(i?)$/.test(str)) { 215 | var pattern = RegExp.$1; 216 | var flags = RegExp.$2; 217 | } 218 | else { 219 | var pattern = "^" + str 220 | .replace(/[\\\/.^$+?|()\[\]{}]/g, "\\$&") // Escape special symbols 221 | .replace(/\*/g, ".*") 222 | + "$"; 223 | var flags = "i"; 224 | } 225 | try { 226 | patterns.push(new RegExp(pattern, flags)); 227 | } 228 | catch(e) { 229 | _err( 230 | "blacklist.parsePatterns(): Invalid regular expression:\n" 231 | + str + "\n-> " + pattern + "\n" + e 232 | ); 233 | } 234 | } 235 | prefs.debug && _log( 236 | "parsePatterns():" + (data 237 | ? "\n" + data + "\nPatterns:\n" + patterns.join("\n") 238 | : " (no patterns)" 239 | ) 240 | ); 241 | return patterns; 242 | } 243 | }; 244 | 245 | function trim(s) { 246 | return s ? ("" + s).trim() : ""; 247 | } 248 | function openURIItem(e, trg, it, inBG, loadIn, discarded) { 249 | var uri = getItemURI(it); 250 | if( 251 | uri == "data:," 252 | && it instanceof HTMLCanvasElement 253 | && "toBlob" in it 254 | && "URL" in window 255 | && "createObjectURL" in URL 256 | ) { 257 | it.toBlob(function(blob) { 258 | openURIIn(blob, inBG, loadIn, discarded); 259 | }); 260 | return; 261 | } 262 | if(isVoidURI(uri) || isDummyURI(it, uri)) { 263 | _log("openURIItem() -> void or dummy URI"); 264 | mouseEvents(trg, ["mousedown", "mouseup", "click"], e, { 265 | ctrlKey: true 266 | }); 267 | return; 268 | } 269 | if(isJSURI(uri)) { 270 | _log("openURIItem() -> javascript:... URI"); 271 | loadJSURI(trg, uri); 272 | return; 273 | } 274 | if(loadIn == 2) { 275 | _log("openURIItem() -> load in current tab"); 276 | loadURI(trg, uri); 277 | return; 278 | } 279 | var title = trim(it.textContent) || trim(it.title) || trim(it.alt) || ""; 280 | if(!title && it.children.length == 1) { 281 | var img = it.children[0]; 282 | if(img.localName.toLowerCase() == "img") 283 | title = trim(img.title) || trim(img.alt) || ""; 284 | } 285 | openURIIn(uri, inBG, loadIn, discarded, title); 286 | } 287 | function openURIIn(uri, inBG, loadIn, discarded, title) { 288 | browser.runtime.sendMessage({ 289 | action: "openURI", 290 | uri: uri, 291 | inBG: inBG, 292 | discarded: discarded, 293 | title: title, 294 | loadIn: loadIn 295 | }).then(function onResponse() {}, _err); 296 | } 297 | function loadJSURI(trg, uri) { 298 | var win = trg.ownerDocument.defaultView; 299 | new win.Function("location = " + JSON.stringify(uri))(); 300 | } 301 | function loadURI(trg, uri) { 302 | blinkNode(trg, true); 303 | trg.ownerDocument.location = uri; 304 | } 305 | function showContextMenu(trg, origEvt) { 306 | _log("showContextMenu()"); 307 | var events = ["mouseup", "contextmenu"]; 308 | //if(simulateMousedown) 309 | // events.unshift("mousedown"); 310 | flags.stopContextMenu = false; 311 | mouseEvents(trg, events, origEvt, {}); // Actually doesn't work... 312 | blinkNode(trg); 313 | } 314 | 315 | function listenMove(on) { 316 | listen(on, { 317 | mousemove: onMouseMove, 318 | wheel: cancel, 319 | dragstart: cancel 320 | }); 321 | } 322 | var moveData = null; 323 | function moveHandlers(e) { 324 | if(!e == !moveData) 325 | return; 326 | if(!e) { 327 | moveData = null; 328 | listenMove(false); 329 | return; 330 | } 331 | var dist = prefs.disallowMousemoveDist; 332 | moveData = { 333 | enabled: dist >= 0, 334 | dist: 0, 335 | x: e.screenX, 336 | y: e.screenY 337 | }; 338 | listenMove(true); 339 | } 340 | function resetFlags() { 341 | flags.stopMouseUp = false; 342 | flags.stopClick = false; 343 | flags.stopContextMenu = false; 344 | } 345 | function cancel(e) { 346 | _log("cancel()" + (e ? " " + e.type : "")); 347 | flags.canceled = true; 348 | resetFlags(); 349 | clearTimeout(delayedTimer); 350 | moveHandlers(false); 351 | } 352 | function listen(add, o) { 353 | var fn = add ? addEventListener : removeEventListener; 354 | for(var type in o) 355 | fn(type, o[type], true); 356 | } 357 | function mouseEvents(trg, evtTypes, origEvt, opts) { 358 | for(var evtType of evtTypes) 359 | mouseEvent(trg, evtType, origEvt, opts); 360 | } 361 | function mouseEvent(trg, evtType, origEvt, opts) { 362 | //~ note: doesn't work as expected 363 | var evt = new MouseEvent(evtType, { 364 | bubbles: true, 365 | cancelable: true, 366 | view: origEvt.view, 367 | detail: 1, 368 | screenX: origEvt.screenX, 369 | screenY: origEvt.screenY, 370 | clientX: origEvt.clientX, 371 | clientY: origEvt.clientY, 372 | ctrlKey: opts.ctrlKey || false, 373 | altKey: opts.altKey || false, 374 | shiftKey: opts.shiftKey || false, 375 | metaKey: opts.metaKey || false, 376 | button: opts.button || 0, 377 | relatedTarget: null 378 | }); 379 | evt._rightLinksIgnore = true; 380 | return trg.dispatchEvent(evt); 381 | } 382 | function stopEvent(e) { 383 | e.preventDefault(); 384 | e.stopPropagation(); 385 | e.stopImmediatePropagation(); 386 | } 387 | 388 | function getItem(trg) { 389 | if(!trg.localName) // trg === document 390 | return null; 391 | return getLink(trg) 392 | || prefs.enabledOnImages && getImg(trg); 393 | } 394 | function getLink(it) { 395 | const docNode = Node.DOCUMENT_NODE; // 9 396 | const eltNode = Node.ELEMENT_NODE; // 1 397 | for(; it && it.nodeType != docNode; it = it.parentNode) { 398 | // https://bugzilla.mozilla.org/show_bug.cgi?id=266932 399 | // https://bug266932.bugzilla.mozilla.org/attachment.cgi?id=206815 400 | // It's strange to see another link in Status Bar 401 | // and other browsers (Opera, Safari, Google Chrome) will open "top level" link. 402 | // And IE... IE won't open XML (it's important!) testcase. :D 403 | // Also this seems like bug of left-click handler. 404 | // So, let's open link, which user see in Status Bar. 405 | if( 406 | ( 407 | it instanceof HTMLAnchorElement 408 | || it instanceof HTMLAreaElement 409 | || it instanceof HTMLLinkElement 410 | ) && it.hasAttribute("href") 411 | || it.nodeType == eltNode && it.hasAttributeNS("http://www.w3.org/1999/xlink", "href") 412 | ) 413 | return it; 414 | } 415 | return null; 416 | } 417 | function getImg(it) { 418 | var itln = it.localName.toLowerCase(); 419 | if(itln == "_moz_generated_content_before") { // Alt-text 420 | it = it.parentNode; 421 | itln = it.localName.toLowerCase(); 422 | } 423 | if( 424 | (itln == "img" || itln == "image") 425 | && it.hasAttribute("src") 426 | && it.src != it.ownerDocument.documentURI // Ignore image documents 427 | || ( 428 | it instanceof HTMLCanvasElement 429 | && prefs.enabledOnCanvasImages 430 | && Math.max(it.width, it.height) < (prefs.canvasImagesSizeLimit || Infinity) 431 | ) 432 | ) 433 | return it; 434 | return null; 435 | } 436 | function getItemURI(it) { 437 | return getLinkURI(it) 438 | || it.src || it.getAttribute("src") 439 | || it instanceof HTMLCanvasElement 440 | //&& (prefs.canvasImagesUseBlob ? "data:," : it.toDataURL()); 441 | && "data:,"; 442 | } 443 | function getLinkURI(it) { 444 | const ns = "http://www.w3.org/1999/xlink"; 445 | if(it.hasAttributeNS(ns, "href")) { 446 | var url = it.getAttributeNS(ns, "href"); 447 | if(it.baseURI) 448 | return new URL(url, it.baseURI).href; 449 | return url; 450 | } 451 | return it.href || it.getAttribute("href"); 452 | } 453 | function isJSURI(uri) { 454 | return /^javascript:/i.test(uri); 455 | } 456 | function isVoidURI(uri) { 457 | return isJSURI(uri) && /^javascript: *(?:|\/\/|void *(?: +0|\( *0 *\))) *;? *$/i.test( 458 | uri.replace(/(?:\s|%20)+/g, " ") 459 | ); 460 | } 461 | function isDummyURI(it, uri) { 462 | var doc = it.ownerDocument; 463 | var loc = doc.documentURI.replace(/#.*$/, ""); 464 | if(!uri.startsWith(loc)) 465 | return false; 466 | var hash = uri.substr(loc.length); 467 | if(!hash && it.hasAttribute && it.hasAttribute("href") && !it.getAttribute("href")) // 468 | return true; 469 | if(hash.charAt(0) != "#") 470 | return false; 471 | var anchor = hash.substr(1); 472 | if(!anchor) // 473 | return true; 474 | if(anchor.charAt(0) == "!") // site.com/#!... links on JavaScript-based sites like http://twitter.com/ 475 | return false; 476 | return !doc.getElementById(anchor) && !doc.getElementsByName(anchor).length; 477 | } 478 | function blinkNode(node, hl) { 479 | var stl = node.hasAttribute("style") && node.getAttribute("style"); 480 | function s(p, v) { 481 | node.style.setProperty(p, v, "important"); 482 | } 483 | if(hl) { 484 | s("outline", "1px solid"); 485 | s("outline-offset", "0"); 486 | s("transition", "outline 100ms ease-in-out"); 487 | } 488 | else { 489 | s("opacity", "0.1"); 490 | s("transition", "opacity 120ms ease-in-out"); 491 | } 492 | setTimeout(function() { 493 | if(stl === false) 494 | node.removeAttribute("style"); 495 | else 496 | node.setAttribute("style", stl); 497 | }, 270); 498 | } -------------------------------------------------------------------------------- /global.js: -------------------------------------------------------------------------------- 1 | const LOG_PREFIX = "[Right Links WE] "; 2 | 3 | var prefs = { // Defaults 4 | updateNotice: true, 5 | debug: false, 6 | enabled: true, 7 | enabledLeft: true, 8 | enabledRight: true, 9 | loadInBackgroundLeft: false, 10 | loadInBackgroundRight: true, 11 | loadInDiscardedLeft: false, 12 | loadInDiscardedRight: false, 13 | loadInLeft: 0, 14 | loadInRight: 0, 15 | blacklistLeft: "", 16 | blacklistRight: "", 17 | enabledOnImages: true, 18 | enabledOnCanvasImages: true, 19 | canvasImagesSizeLimit: 0, 20 | canvasImagesUseBlob: true, 21 | showContextMenuTimeout: 500, 22 | longLeftClickTimeout: 500, 23 | disallowMousemoveDist: 14, 24 | toggleKey: "F2" 25 | }; 26 | 27 | function readPrefs(callback) { 28 | browser.storage.local.get().then(function(o) { 29 | browser.storage.onChanged.addListener(function(changes, area) { 30 | if(area == "local") for(var key in changes) 31 | _onPrefChanged(key, changes[key].newValue); 32 | }); 33 | Object.assign(prefs, o); 34 | callback(); 35 | 36 | for(var key in o) 37 | return; // Prefs already saved 38 | setTimeout(function() { // Pseudo async 39 | browser.storage.local.set(prefs); 40 | }, 5000); 41 | }, _err); 42 | } 43 | function _onPrefChanged(key, newVal) { 44 | prefs[key] = newVal; 45 | onPrefChanged(key, newVal); 46 | } 47 | function onPrefChanged(key, newVal) { 48 | } 49 | 50 | function ts() { 51 | var d = new Date(); 52 | var ms = d.getMilliseconds(); 53 | return d.toTimeString().replace(/^.*\d+:(\d+:\d+).*$/, "$1") + ":" + "000".substr(("" + ms).length) + ms + " "; 54 | } 55 | function _log(s) { 56 | if(prefs.debug) 57 | console.log(LOG_PREFIX + ts() + s); 58 | } 59 | function _err(s) { 60 | console.error(LOG_PREFIX + ts() + s); 61 | } -------------------------------------------------------------------------------- /i18n.js: -------------------------------------------------------------------------------- 1 | // Based on code from https://github.com/piroor/webextensions-lib-l10n 2 | (function i18n() { 3 | function localize(s) { 4 | return s.replace(/__MSG_(.+?)__/g, function(s, key) { 5 | return browser.i18n.getMessage(key) || s; 6 | }); 7 | } 8 | function xPath(expr) { 9 | return document.evaluate(expr, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); 10 | } 11 | var texts = xPath('descendant::text()[contains(self::text(), "__MSG_")]'); 12 | for(var i = 0, l = texts.snapshotLength; i < l; ++i) { 13 | var text = texts.snapshotItem(i); 14 | text.nodeValue = localize(text.nodeValue); 15 | } 16 | var attrs = xPath('descendant::*/attribute::*[contains(., "__MSG_")]'); 17 | for(var i = 0, l = attrs.snapshotLength; i < l; ++i) { 18 | var attr = attrs.snapshotItem(i); 19 | attr.value = localize(attr.value); 20 | } 21 | })(); -------------------------------------------------------------------------------- /icon16-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infocatcher/Right_Links_WE/a9391fed10140c4fae943652adb7bdc921e42d76/icon16-off.png -------------------------------------------------------------------------------- /icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infocatcher/Right_Links_WE/a9391fed10140c4fae943652adb7bdc921e42d76/icon16.png -------------------------------------------------------------------------------- /icon24-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infocatcher/Right_Links_WE/a9391fed10140c4fae943652adb7bdc921e42d76/icon24-off.png -------------------------------------------------------------------------------- /icon24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infocatcher/Right_Links_WE/a9391fed10140c4fae943652adb7bdc921e42d76/icon24.png -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | This Source Code Form is subject to the terms of the Mozilla Public 2 | License, v. 2.0. If a copy of the MPL was not distributed with this 3 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | Right Links WE extension for Firefox 6 | https://github.com/Infocatcher/Right_Links_WE 7 | (c) Infocatcher 2017-2021 8 | 9 | Based on code from Right Links extension for Firefox and SeaMonkey 10 | https://github.com/Infocatcher/Right_Links 11 | (c) Infocatcher 2007-2017 -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set _out=right_links_we-latest.xpi 3 | set _out_tmp=%~d0\~%_out%.tmp 4 | set _7zip="%COMMANDER_PATH%\arch\7-Zip-4.65\7z.exe" 5 | set _winRar="%COMMANDER_PATH%\arch\WinRAR\WinRAR.exe" 6 | title Make %_out% 7 | 8 | if not exist %_7zip% set _7zip="%ProgramFiles%\7-Zip\7z.exe" 9 | if not exist %_winRar% set _winRar="%ProgramFiles%\WinRAR\WinRAR.exe" 10 | 11 | if not exist %_7zip% ( 12 | title Error - Make %_out% 13 | echo 7-Zip not found! 14 | if not exist %_winRar% ( 15 | echo WinRAR not found! 16 | pause 17 | exit /b 18 | ) 19 | ) 20 | 21 | cd /d "%~dp0" 22 | 23 | if exist %_out_tmp% ( 24 | title Error - Make %_out% 25 | echo ============================================= 26 | echo Something went wrong, please remove or rename 27 | echo %_out_tmp% 28 | echo ============================================= 29 | pause 30 | exit /b 31 | ) 32 | 33 | :: Test for write access 34 | type nul > %_out_tmp% 35 | if not exist %_out_tmp% ( 36 | echo =^> %_out_tmp% isn't writable 37 | echo ==^> will use %temp% 38 | set _out_tmp="%temp%\~%_out%.tmp" 39 | ) else ( 40 | del %_out_tmp% 41 | ) 42 | 43 | set _files=_locales *.js *.json *.png *.html license* 44 | if exist %_7zip% ( 45 | echo =^> %_7zip% 46 | %_7zip% a -tzip -mx9 -mfb=258 -mpass=15 -- %_out_tmp% %_files% 47 | ) else ( 48 | echo =^> %_winRar% 49 | %_winRar% a -afzip -m5 -r -- %_out_tmp% %_files% 50 | ) 51 | 52 | if not exist %_out_tmp% echo Error: %_out_tmp% not found! & pause & exit /b 53 | 54 | if not exist %_out% goto skipCompare 55 | fc /l /lb2 /t %_out% %_out_tmp% 56 | if %ErrorLevel% == 0 ( 57 | del %_out_tmp% 58 | echo =============== 59 | echo ==^> No changes! 60 | echo =============== 61 | if not "%1" == "nodelay" ping -n 3 127.0.0.1 > nul 62 | exit /b 63 | ) 64 | :skipCompare 65 | 66 | echo ==^> Changed, update... 67 | move /y %_out_tmp% %_out% -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PROG=false 4 | XPI=right_links_we-latest.xpi 5 | FILES='_locales *.js *.json *.png *.html license*' 6 | 7 | for z in 7za 7z zip 8 | do 9 | if command -v $z >/dev/null 2>&1 10 | then 11 | PROG=$z 12 | break 13 | fi 14 | done 15 | 16 | if test $PROG = false 17 | then 18 | echo "usable program not found, abort." 19 | exit 1 20 | else 21 | echo "use $PROG to create xpi." 22 | fi 23 | 24 | if test $PROG = zip 25 | then 26 | $PROG -9r $XPI $FILES 27 | else 28 | $PROG a -tzip -mm=Deflate -mx9 -mfb258 -mpass=15 $XPI $FILES 29 | fi -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "version": "0.5b12", 4 | "name": "__MSG_extensionName__", 5 | "description": "__MSG_extensionDescription__", 6 | "author": "Infocatcher", 7 | "default_locale": "en", 8 | "applications": { 9 | "gecko": { 10 | "id": "{B5F5E8D3-AE31-49A1-AC42-78B7B1CC5CDC}" 11 | } 12 | }, 13 | 14 | "background": { 15 | "scripts": ["global.js", "background.js"] 16 | }, 17 | "content_scripts": [{ 18 | "matches": [""], 19 | "js": ["global.js", "content.js"], 20 | "run_at": "document_start", 21 | "all_frames": true 22 | }], 23 | "permissions": [ 24 | "", 25 | "activeTab", 26 | "tabs", 27 | "contextMenus", 28 | "storage", 29 | "notifications" 30 | ], 31 | "options_ui": { 32 | "page": "options.html", 33 | "browser_style": true 34 | }, 35 | "browser_action": { 36 | "browser_style": true, 37 | "default_title": "__MSG_extensionName__", 38 | "default_icon": { 39 | "16": "icon16.png", 40 | "24": "icon24.png" 41 | } 42 | }, 43 | "commands": { 44 | "_execute_browser_action": { 45 | "description": "__MSG_extensionName__" 46 | } 47 | }, 48 | "icons": { 49 | "16": "icon16.png", 50 | "24": "icon24.png" 51 | } 52 | } -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 |
16 |
17 |
18 |
19 | 20 | (?) 21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | 37 |
38 |
39 |
40 | 43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | 59 |
60 |
61 |
62 | 65 |
66 |
67 |
68 |
69 |
70 |
71 | 72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | function init() { 2 | $("updateNotice").innerHTML = browser.i18n.getMessage("updateNotice"); 3 | $("blacklistLeft").placeholder = $("blacklistRight").placeholder = [ 4 | "https://example.com/*", 5 | "/^https?://example(?:\\.\\w+){1,2}/something//i" 6 | ].join("\n"); 7 | readPrefs(loadOptions); 8 | addEventListener("input", saveOption); 9 | addEventListener("unload", destroy, { once: true }); 10 | browser.runtime.onMessage.addListener(onMessageFromBG); 11 | } 12 | function destroy() { 13 | removeEventListener("input", saveOption); 14 | browser.runtime.onMessage.removeListener(onMessageFromBG); 15 | } 16 | function loadOptions() { 17 | for(var id in prefs) 18 | loadOption(id, prefs[id]); 19 | checkSubItems(); 20 | validateKey(); 21 | } 22 | function onPrefChanged(key, newVal) { 23 | loadOption(key, newVal); 24 | } 25 | function loadOption(id, val) { 26 | var node = $(id); 27 | node && setValue(node, val); 28 | } 29 | function saveOption(e) { 30 | var node = e.target; 31 | var id = node.id; 32 | if(!(id in prefs)) 33 | return; 34 | (save.prefs || (save.prefs = {}))[id] = getValue(node); 35 | if(!save.timer) 36 | save.timer = setTimeout(save, Date.now() - (save.last || 0) < 1000 ? 400 : 20); 37 | if(id == "enabledLeft" || id == "enabledRight") 38 | disableSection(node), disableSubSection($("loadInBackground" + id.match(/[LR].+$/)[0])); 39 | else if(id == "enabledOnImages") 40 | disableSection(node), disableSubSection($("enabledOnCanvasImages")); 41 | else if(id == "enabledOnCanvasImages" || id == "loadInBackgroundLeft" || id == "loadInBackgroundRight") 42 | disableSubSection(node); 43 | else if(id == "toggleKey") 44 | validateKey(); 45 | } 46 | function checkSubItems() { 47 | disableSection($("enabledLeft")); 48 | disableSection($("enabledRight")); 49 | disableSection($("enabledOnImages")); 50 | disableSubSection($("enabledOnCanvasImages")); 51 | disableSubSection($("loadInBackgroundLeft")); 52 | disableSubSection($("loadInBackgroundRight")); 53 | } 54 | function disableSection(ch) { 55 | var dis = !ch.checked; 56 | for(var sub of ch.closest("section.group").querySelectorAll("section.sub")) { 57 | sub.classList.toggle("disabled", dis); 58 | for(var it of sub.querySelectorAll("input, textarea, select")) 59 | it.disabled = dis; 60 | } 61 | } 62 | function disableSubSection(ch) { 63 | if(!ch.closest("section.group").classList.contains("disabled")) 64 | disableSection(ch); 65 | } 66 | function onMessageFromBG(msg, sender, sendResponse) { 67 | if(msg.action != "shortcutValidation") 68 | return; 69 | var inp = $("toggleKey"); 70 | inp.title = msg.error; 71 | inp.classList.toggle("error", msg.error); 72 | } 73 | function validateKey() { 74 | var inp = $("toggleKey"); 75 | var key = inp.value; 76 | browser.commands.getAll().then(function(cmds) { 77 | for(var cmd of cmds) { 78 | if(cmd.name == "_execute_browser_action") { 79 | var err = cmd.shortcut != key; 80 | inp.classList.toggle("error", err); 81 | inp.title = err ? cmd.shortcut : ""; 82 | } 83 | } 84 | }); 85 | } 86 | function save() { 87 | _log("Save: " + JSON.stringify(save.prefs)); 88 | browser.storage.local.set(save.prefs); 89 | save.prefs = {}; 90 | save.timer = 0; 91 | save.last = Date.now(); 92 | } 93 | function getValue(node) { 94 | return node.localName == "select" || node.type == "number" 95 | ? +node.value 96 | : node.type == "checkbox" 97 | ? node.checked 98 | : node.value; 99 | } 100 | function setValue(node, val) { 101 | if(node.type == "checkbox") 102 | node.checked = val; 103 | else 104 | node.value = val; 105 | } 106 | function $(id) { 107 | return document.getElementById(id); 108 | } 109 | 110 | init(); --------------------------------------------------------------------------------