├── .github └── FUNDING.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── PRIVACY.md ├── README.md ├── package-lock.json ├── package.json ├── screenshots ├── context-menu.png ├── open-in-main-window.png ├── open-in-popup-screenshot.png ├── options-screenshot-2.png └── options-screenshot.png ├── src ├── assets │ ├── _locales │ │ ├── en │ │ │ └── messages.json │ │ ├── es │ │ │ └── messages.json │ │ └── ru │ │ │ └── messages.json │ ├── icon.png │ └── icon_white.png ├── background.js ├── configs.js ├── content.js ├── manifest.json ├── options │ ├── icons │ │ ├── donate.svg │ │ ├── github.svg │ │ └── review.svg │ ├── options.css │ ├── options.html │ └── options.js └── viewer │ ├── viewer.css │ ├── viewer.html │ └── viewer.js └── webpack.config.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 7 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 8 | liberapay: # Replace with a single Liberapay username 9 | issuehunt: # Replace with a single IssueHunt username 10 | otechie: # Replace with a single Otechie username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | ko_fi: emvaized 13 | liberapay: emvaized -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.zip 2 | .DS_Store 3 | node_modules/ 4 | dist/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 0.3.6 2 | - Added option to open current page in popup window to main context menu 3 | - Added option to not close current page popup window on focus lost 4 | - Fix opening popup on drag and drop of the selected text 5 | - 'Open in the main window' now works more reliable 6 | 7 | ### 0.3.5 8 | - Added support for images with source in `sourceset` 9 | - Added check to not launch popup window with no found link 10 | - Added padding to options page 11 | 12 | ### 0.3.4 13 | - Add option for images wrapped in link to prefer link or image 14 | - Apply configs immediately, without the need to reload the page 15 | - Improve options page appearance on Firefox 16 | - Add white background to the black icon for better visibility on dark backgrounds, and fix white icon vertical alignment 17 | 18 | ### 0.3.3 19 | - Handle images wrapped inside links, to favor links 20 | - Fix shift+click on image ignoring disabled image viewer 21 | 22 | ### 0.3.2 23 | - Added option to always open by drag and drop under mouse 24 | - Fix for multimonitor usage with secondary on the left 25 | - Disabled fullscreen logic which caused confusion 26 | - Try to filter out manually created window in "Reopen new single tab windows as popup windows" (`tabs` permission was added for this) 27 | - Added white icon for the dark mode 28 | 29 | ### 0.3.1 30 | - Improved window listener on focus lost 31 | - Refactored code for better readability 32 | 33 | ### 0.3 34 | - Added feature to reopen new single tab windows as popup windows [how to use](./README.md#how-to-use-the-new-feature-reopen-new-single-tab-windows-as-popup-windows) 35 | - Improve logic for popup size calculation and autoclose 36 | - Added top center option for popup placement 37 | - Added option for fallback popup window location 38 | - Added ability to open page in main window with Ctrl+Esc 39 | 40 | ### 0.2.1 41 | - Updated extension icon 42 | - Added option for minimal drag distance 43 | - Small options page improvements 44 | 45 | ### 0.2 46 | - Added new option for popup position "Near mouse cursor" 47 | - Added option to close popup with Esc key (disabled by default) 48 | - Fix text selection popup not opened by drag & drop 49 | - Options page design improvements 50 | 51 | ### 0.1.2 52 | - Implement option to use shift+click to open in popup 53 | - Try to preserve state of page when open in main window 54 | - Added missing "all_urls" permission to run on every page automatically 55 | - Updated translations 56 | 57 | ### 0.1.1 58 | - Added more options for popup window location (top right, bottom right, etc) 59 | - Use options page as a popup for toolbar button 60 | - Added footer buttons for the options page 61 | - Fix potential bug on Firefox 62 | 63 | ### 0.1 64 | - Migrated extension to Manifest V3 for Chrome Web Store publish 65 | - Implemented build mechanism for minified release 66 | - Updated extension icon to have more unique design 67 | - Unavailable options in settings are now greyed out 68 | 69 | ### 0.0.6 70 | - Implemented ability to open images in popup window 71 | - Added ability to create popup by drag'n'drop link on empty place 72 | - Slightly update extension icon 73 | - Various code optimizations and bug fixes 74 | 75 | ### 0.0.5 76 | - reverted background script changes because Firefox kept loosing the context menu item 77 | 78 | ### 0.0.4 79 | - optimized code for persistent background script 80 | - minified content script for better page load speed 81 | - don't process window focus events when lost focus 82 | 83 | ### 0.0.3 84 | - added context menu entry for popups to open page in main window 85 | - added context menu entry to search for selected text in popup 86 | 87 | ### 0.0.2 88 | - changed context menu entry label 89 | - removed unused code 90 | - fix to use absolute mouse coordinates instead of relative to window 91 | - added spanish translation 92 | 93 | ### 0.0.1 94 | - initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | ---- 3 | 4 | Copyright (c) 2024 emvaized 5 | 6 | The only restriction is to not publish any extension for browsers or 7 | native application without getting a written permission first. Otherwise: 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | This extension doesn't collect or send any private data. It requires access to currently open page in order to fetch information about the object under cursor when drag and drop event occured or context menu was opened. Such information as mouse coordinates, geometry and link associated with the element under cursor. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open in Popup Window 2 | 3 | [![Mozilla Add-on Version](https://img.shields.io/amo/v/open-in-popup-window?label=version&color=red)](./CHANGELOG.md) 4 | [![Chrome Web Store](https://img.shields.io/chrome-web-store/users/gmnkpkmmkhbgnljljcchnakehlkihhie?label=users&logo=googlechrome&logoColor=white&color=blue)](https://chrome.google.com/webstore/detail/open-in-popup-window/gmnkpkmmkhbgnljljcchnakehlkihhie) 5 | [![Mozilla Add-on](https://img.shields.io/amo/users/open-in-popup-window?color=%23FF6611&label=users&logo=Firefox)](https://addons.mozilla.org/firefox/addon/open-in-popup-window/) 6 | [![Mozilla Add-on Stars](https://img.shields.io/amo/stars/open-in-popup-window)](https://addons.mozilla.org/firefox/addon/open-in-popup-window/) 7 | 8 | This tiny browser extension provides ability to quickly preview links without leaving the current page context. It adds entry in context menu of links, and when clicked, opens new small window at cursor position, with no tab bar and addressbar. 9 | 10 | Features: 11 | - [x] Ability to open images and search selected text in popup window 12 | - [x] Use drag and drop to open in popup window (disabled by default) 13 | - [x] Use Shift + Click to open in popup window (disabled by default) 14 | - [x] Configurable popup height, width and position on screen 15 | - [x] Option to close popup window when origin window regains focus 16 | - [x] New popups can be opened from existing popups 17 | - [x] Automatically reopen new single tab windows as popup windows 18 | 19 | This extension is intended to be used as an analogue of Safari Link Preview, or the new Arc Peek. It's a great way to preview links, which works everywhere and is not affected by the CORS problem like extensions which are using iframe element for link preview. 20 | 21 | Get for Firefox   Get for Chrome 22 | 23 | ## Screenshots 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ## Support project ❤️ 32 | If you really enjoy this project, please consider supporting its further development by making a small donation using one of the ways below! 33 | 34 | Support on Ko-fi   Donate using Liberapay   Donate Bitcoin 35 | 36 | ## FAQ 37 | 38 | #### How to close the popup using keyboard? 39 | - Alt + F4 40 | - Ctrl + W 41 | - Escape key (if option is enabled in extension settings) 42 | 43 | #### How to open page from the popup in the main window? 44 | You can do it in 2 ways: 45 | - Ctrl + Escape hotkey (if enabled in extension settings) 46 | - By right clicking on the page in popup window and selecting "Open page in main window" 47 |
48 | Demonstration 49 | 50 |
51 | 52 | #### In fullscreen mode on Mac OS popup windows do not appear 53 | Due to the specifics of fullscreen mode on Mac OS (fullscreen apps separate in their own Desktop space), popup windows do not appear above the fullcreen window. They either open on the "main" desktop (Firefox), or as a new fullscreen window (Chrome). To use this extension on Mac OS, you would have to open browser not in the fullscreen mode. 54 | 55 | #### How to make popups remain always on top? 56 | Unfortunately, browser extensions currently are [not capable](https://github.com/w3c/webextensions/issues/443) of manually setting "always on top" flag. 57 | 58 | But you can use third-party programs in your system which can do it, for example [PowerToys](https://github.com/microsoft/PowerToys) on Windows. Don't forget to disable autoclosing popup window when it loses focus in the extension settings. 59 | 60 | #### How to use the new feature "Reopen new single tab windows as popup windows"? 61 | 62 | This powerful feature, introduced in version `0.3`, allows to extend extension abilities and open any link anywhere as a popup window. With this option enabled, extension will be waiting in the background for any new windows to open, and once a new window with only one tab opens, it will reopen it as a popup. 63 | 64 | This way it can operate with links, which it couldn't access otherwise, for example: sites on new tab page, in bookmarks panel, links on browser-protected pages (`chrome://`, `about:`) etc. You could create a popup window the following ways: 65 | 66 | - Using right click context menu and selecting "Open in new window" 67 | - Shift + left click in Firefox (+ Ctrl in Chrome) – this will essentially duplicate extension's "Open with shift+click" option 68 | - Shift + Enter in Firefox (+ Ctrl in Chrome) on any focused element, for example search suggestion in address bar 69 | 70 | Popup windows opened this way could not be positioned by mouse cursor location, so an alternative "Fallback popup window location" setting will be used ("Center" by default) 71 | 72 | #### If private windows are not reopened as popup with this option enabled 73 | 74 | You might need to manually allow this extension to run in private windows 75 | 76 | ## Troubleshooting 77 | - ⚠️ If "Open by drag" and "Shift+click to open" options not working, or the popup window always gets positioned in the top left corner no matter the placement settings, please make sure you gave extension all permissions to run on every page you visit! 78 | - It was also previously [reported](https://github.com/emvaized/open-in-popup-window-extension/issues/1#issuecomment-1637067834) that Firefox Multi-Account Containers might interfere with "Search in popup" action, enforcing it's own redirection and preventing popup window from open. If you have a setup like this, I belive there is no easy fix other than choosing another search URL in Open in Popup Window settings 79 | 80 | ## Building 81 | - `npm install` to install all dependencies 82 | - `npm run build` to generate `dist` folder with minimized code of the extension 83 | 84 | ## Plans for future 85 | - [ ] Option to remember popup window size on manual resize (_improssible in Firefox_ – [bug report](https://bugzilla.mozilla.org/show_bug.cgi?id=1762975)) 86 | - [ ] Option to open page in the main window on clicking "Maximize" window button (_improssible in Firefox_ – [bug report](https://bugzilla.mozilla.org/show_bug.cgi?id=1762975)) 87 | 88 | ## Privacy 89 | This extension doesn't collect any private data. It only requires access to currently open page in order to fetch information about the object under cursor when drag and drop event occured or context menu was opened. 90 | 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-in-popup-window-extension", 3 | "description": "This tiny extension provides ability to quickly preview links without leaving the current page context. It adds entry in context menu of links, and when clicked, opens new small window without tab bar and addressbar at cursor position.", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/emvaized/open-in-popup-window-extension.git" 7 | }, 8 | "author": "emvaized", 9 | "license": "MIT", 10 | "bugs": { 11 | "url": "https://github.com/emvaized/open-in-popup-window-extension/issues" 12 | }, 13 | "homepage": "https://github.com/emvaized/open-in-popup-window-extension#readme", 14 | "devDependencies": { 15 | "@mcler/webpack-concat-plugin": "^4.1.6", 16 | "css-minimizer-webpack-plugin": "^7.0.0", 17 | "json-minimizer-webpack-plugin": "^5.0.0", 18 | "terser-webpack-plugin": "^5.3.10", 19 | "webpack": "^5.93.0", 20 | "webpack-cli": "^5.1.4", 21 | "webpack-concat-files-plugin": "^0.5.2" 22 | }, 23 | "dependencies": { 24 | "copy-webpack-plugin": "^12.0.2" 25 | }, 26 | "scripts": { 27 | "test": "echo \"Error: no test specified\" && exit 1", 28 | "build": "webpack --config webpack.config.js" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /screenshots/context-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emvaized/open-in-popup-window-extension/4918ec5cabf1aa6be6168561ae02625b3192a1b4/screenshots/context-menu.png -------------------------------------------------------------------------------- /screenshots/open-in-main-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emvaized/open-in-popup-window-extension/4918ec5cabf1aa6be6168561ae02625b3192a1b4/screenshots/open-in-main-window.png -------------------------------------------------------------------------------- /screenshots/open-in-popup-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emvaized/open-in-popup-window-extension/4918ec5cabf1aa6be6168561ae02625b3192a1b4/screenshots/open-in-popup-screenshot.png -------------------------------------------------------------------------------- /screenshots/options-screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emvaized/open-in-popup-window-extension/4918ec5cabf1aa6be6168561ae02625b3192a1b4/screenshots/options-screenshot-2.png -------------------------------------------------------------------------------- /screenshots/options-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emvaized/open-in-popup-window-extension/4918ec5cabf1aa6be6168561ae02625b3192a1b4/screenshots/options-screenshot.png -------------------------------------------------------------------------------- /src/assets/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "addOptionOpenPageInPopupWindow": { 3 | "message": "Add option to open page in popup window" 4 | }, 5 | "keepOpenPageInPopupWindowOpen": { 6 | "message": "Don't close page popup window on focus lost" 7 | }, 8 | "bottomLeft": { 9 | "message": "Bottom left" 10 | }, 11 | "bottomRight": { 12 | "message": "Bottom right" 13 | }, 14 | "center": { 15 | "message": "Center" 16 | }, 17 | "closeWhenFocusedInitialWindow": { 18 | "message": "Close popup when normal window is focused" 19 | }, 20 | "debugMode": { 21 | "message": "Debug mode" 22 | }, 23 | "donateButton": { 24 | "message": "Support project" 25 | }, 26 | "escKeyClosesPopup": { 27 | "message": "Close popup window with an Escape key" 28 | }, 29 | "escKeyClosesPopupHint": { 30 | "message": "Ctrl + Escape will also open page in the main window" 31 | }, 32 | "extensionDescription": { 33 | "message": "Open any links and images in a popup window" 34 | }, 35 | "fallbackPopupWindowLocation": { 36 | "message": "Fallback option for popup window location" 37 | }, 38 | "generalSettings": { 39 | "message": "General settings" 40 | }, 41 | "githubButton": { 42 | "message": "Source code" 43 | }, 44 | "hideBrowserControls": { 45 | "message": "Hide browser controls (addressbar, tabbar, etc)" 46 | }, 47 | "imageViewer": { 48 | "message": "Image viewer" 49 | }, 50 | "imageWithLinkPreferLink": { 51 | "message": "When image is wrapped in link, prefer link" 52 | }, 53 | "minimalDragDistance": { 54 | "message": "Minimal drag distance (px)" 55 | }, 56 | "mousePosition": { 57 | "message": "Mouse cursor position" 58 | }, 59 | "nearMousePosition": { 60 | "message": "Near mouse cursor" 61 | }, 62 | "openByDragAndDrop": { 63 | "message": "Use drag and drop on empty place to open in popup" 64 | }, 65 | "openByShiftClick": { 66 | "message": "Use Shift + Click to open in popup" 67 | }, 68 | "openDragAndDropUnderMouse": { 69 | "message": "Always open by drag and drop under mouse cursor" 70 | }, 71 | "openInPopupWindow": { 72 | "message": "Open Link in Popup Window" 73 | }, 74 | "openPageInPopupWindow": { 75 | "message": "Open Page in Popup Window" 76 | }, 77 | "openPageInMainWindow": { 78 | "message": "Open Page in Main Window" 79 | }, 80 | "popupHeight": { 81 | "message": "Popup height (px)" 82 | }, 83 | "popupSearchUrl": { 84 | "message": "Search url (%s will be replaced with the search query)" 85 | }, 86 | "popupWidth": { 87 | "message": "Popup width (px)" 88 | }, 89 | "popupWindowLocation": { 90 | "message": "Popup window location" 91 | }, 92 | "popupWindowSize": { 93 | "message": "Popup window size" 94 | }, 95 | "rememberWindowResize": { 96 | "message": "Remember dimensions on manual resize" 97 | }, 98 | "reopenSingleTabWindowAsPopup": { 99 | "message": "Reopen new single tab windows as popup windows" 100 | }, 101 | "reopenSingleTabWindowAsPopupHint": { 102 | "message": "With this option enabled, any new browser window with only one tab will be automatically reopened as a popup window. It allows extension to create popup for links which it usually can't access (for example, bookmarks bar). But it can lead to unwanted behavior if you usually open many browser windows, so use with caution!" 103 | }, 104 | "searchInPopupEnabled": { 105 | "message": "Option to search selection in popup enabled" 106 | }, 107 | "searchInPopupWindow": { 108 | "message": "Search Text in Popup Window" 109 | }, 110 | "settingsTitle": { 111 | "message": "Settings" 112 | }, 113 | "textSelectionHeader": { 114 | "message": "Text selection" 115 | }, 116 | "topCenter": { 117 | "message": "Top center" 118 | }, 119 | "topLeft": { 120 | "message": "Top left" 121 | }, 122 | "topRight": { 123 | "message": "Top right" 124 | }, 125 | "tryFitWindowSizeToImage": { 126 | "message": "Try to fit popup width to image aspect ratio" 127 | }, 128 | "useBuiltInImageViewer": { 129 | "message": "Use browser's built-in image viewer" 130 | }, 131 | "viewInPopupEnabled": { 132 | "message": "Enable option to view images in popup window" 133 | }, 134 | "viewInPopupWindow": { 135 | "message": "View Image in Popup Window" 136 | }, 137 | "writeAReviewButton": { 138 | "message": "Write a review" 139 | } 140 | } -------------------------------------------------------------------------------- /src/assets/_locales/es/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "addOptionOpenPageInPopupWindow": { 3 | "message": "Agregar opción para abrir la página en una ventana emergente" 4 | }, 5 | "keepOpenPageInPopupWindowOpen": { 6 | "message": "No cerrar la ventana emergente de la página al perder el foco" 7 | }, 8 | "bottomLeft": { 9 | "message": "Abajo a la izquierda" 10 | }, 11 | "bottomRight": { 12 | "message": "Abajo a la derecha" 13 | }, 14 | "center": { 15 | "message": "Centro" 16 | }, 17 | "closeWhenFocusedInitialWindow": { 18 | "message": "Cerrar la ventana emergente cuando la ventana normal obtiene el foco" 19 | }, 20 | "debugMode": { 21 | "message": "Depurar" 22 | }, 23 | "donateButton": { 24 | "message": "Proyecto de apoyo" 25 | }, 26 | "escKeyClosesPopup": { 27 | "message": "Cerrar ventana emergente con la tecla Escape" 28 | }, 29 | "escKeyClosesPopupHint": { 30 | "message": "Ctrl + Escape también abrirá la página en la ventana principal" 31 | }, 32 | "extensionDescription": { 33 | "message": "Abrir enlaces e imágenes en una ventana emergente" 34 | }, 35 | "fallbackPopupWindowLocation": { 36 | "message": "Opción alternativa para la ubicación de la ventana emergente" 37 | }, 38 | "generalSettings": { 39 | "message": "Configuración general" 40 | }, 41 | "githubButton": { 42 | "message": "Código fuente" 43 | }, 44 | "hideBrowserControls": { 45 | "message": "Ocultar los controles del navegador (barra de direcciones, barra de pestañas, etc.)" 46 | }, 47 | "imageViewer": { 48 | "message": "Visor de imágenes" 49 | }, 50 | "imageWithLinkPreferLink": { 51 | "message": "Cuando la imagen está envuelta en un enlace, prefiera el enlace" 52 | }, 53 | "minimalDragDistance": { 54 | "message": "Distancia mínima de arrastre (px)" 55 | }, 56 | "mousePosition": { 57 | "message": "Posición del cursor del ratón" 58 | }, 59 | "nearMousePosition": { 60 | "message": "Cerca del cursor" 61 | }, 62 | "openByDragAndDrop": { 63 | "message": "Utilice arrastrar y soltar en un lugar vacío para abrir una ventana emergente" 64 | }, 65 | "openByShiftClick": { 66 | "message": "Utilice Shift + Clic para abrir en ventana emergente" 67 | }, 68 | "openDragAndDropUnderMouse": { 69 | "message": "Abrir siempre arrastrando y soltando bajo el cursor del ratón" 70 | }, 71 | "openInPopupWindow": { 72 | "message": "Abrir enlace en ventana emergente" 73 | }, 74 | "openLinkInMainWindow": { 75 | "message": "Abrir página en la ventana principal" 76 | }, 77 | "openPageInPopupWindow": { 78 | "message": "Abrir página en ventana emergente" 79 | }, 80 | "openPageInMainWindow": { 81 | "message": "Abrir página en la ventana principal" 82 | }, 83 | "popupHeight": { 84 | "message": "Altura emergente (px)" 85 | }, 86 | "popupSearchUrl": { 87 | "message": "URL de búsqueda (%s se reemplazará con la consulta de búsqueda)" 88 | }, 89 | "popupWidth": { 90 | "message": "Ancho de la ventana emergente (px)" 91 | }, 92 | "popupWindowLocation": { 93 | "message": "Ubicación de la ventana emergente" 94 | }, 95 | "popupWindowSize": { 96 | "message": "Tamaño de la ventana emergente" 97 | }, 98 | "reopenSingleTabWindowAsPopup": { 99 | "message": "Reabrir nuevas ventanas de una sola pestaña como ventanas emergentes" 100 | }, 101 | "reopenSingleTabWindowAsPopupHint": { 102 | "message": "Con esta opción activada, cualquier ventana nueva del navegador con una sola pestaña se reabrirá automáticamente como ventana emergente. Permite a la extensión crear ventanas emergentes para enlaces a los que normalmente no puede acceder (por ejemplo, la barra de marcadores). Pero puede conducir a un comportamiento no deseado si sueles abrir muchas ventanas del navegador, ¡así que úsalo con precaución!" 103 | }, 104 | "searchInPopupEnabled": { 105 | "message": "Opción para buscar selección en ventana emergente habilitada" 106 | }, 107 | "searchInPopupWindow": { 108 | "message": "Buscar texto en la ventana emergente" 109 | }, 110 | "settingsTitle": { 111 | "message": "Configuración" 112 | }, 113 | "textSelectionHeader": { 114 | "message": "Selección de texto" 115 | }, 116 | "topCenter": { 117 | "message": "Arriba medio" 118 | }, 119 | "topLeft": { 120 | "message": "Arriba a la izquierda" 121 | }, 122 | "topRight": { 123 | "message": "Arriba a la derecha" 124 | }, 125 | "tryFitWindowSizeToImage": { 126 | "message": "Intente ajustar el ancho de la ventana emergente a la relación de aspecto de la imagen" 127 | }, 128 | "useBuiltInImageViewer": { 129 | "message": "Utilice el visor de imágenes integrado del navegador" 130 | }, 131 | "viewInPopupEnabled": { 132 | "message": "Activar la opción de ver las imágenes en una ventana emergente" 133 | }, 134 | "viewInPopupWindow": { 135 | "message": "Ver imagen en ventana emergente" 136 | }, 137 | "writeAReviewButton": { 138 | "message": "Escribe una reseña" 139 | } 140 | } -------------------------------------------------------------------------------- /src/assets/_locales/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "addOptionOpenPageInPopupWindow": { 3 | "message": "Включить опцию открытия текущей страницы во вспл. окне" 4 | }, 5 | "keepOpenPageInPopupWindowOpen": { 6 | "message": "Не закрывать вспл. окно текущей страницы при потере фокуса" 7 | }, 8 | "bottomLeft": { 9 | "message": "Слева внизу" 10 | }, 11 | "bottomRight": { 12 | "message": "Справа внизу" 13 | }, 14 | "center": { 15 | "message": "Центр экрана" 16 | }, 17 | "closeWhenFocusedInitialWindow": { 18 | "message": "Закрыть всплывающее окно, когда обычное окно получает фокус" 19 | }, 20 | "debugMode": { 21 | "message": "Отладка" 22 | }, 23 | "donateButton": { 24 | "message": "Поддержать проект" 25 | }, 26 | "escKeyClosesPopup": { 27 | "message": "Закрывать всплывающее окно клавишей Escape" 28 | }, 29 | "escKeyClosesPopupHint": { 30 | "message": "Ctrl + Escape также откроет страницу в основном окне" 31 | }, 32 | "extensionDescription": { 33 | "message": "Открывает ссылки и изображения во вплывающем окне" 34 | }, 35 | "fallbackPopupWindowLocation": { 36 | "message": "Запасной вариант расположения всплывающего окна" 37 | }, 38 | "generalSettings": { 39 | "message": "Общие настройки" 40 | }, 41 | "githubButton": { 42 | "message": "Исходный код" 43 | }, 44 | "hideBrowserControls": { 45 | "message": "Прятать элементы управления браузера (панель адреса, вкладок, и тд)" 46 | }, 47 | "imageViewer": { 48 | "message": "Просмотр изображений" 49 | }, 50 | "imageWithLinkPreferLink": { 51 | "message": "Когда изображение вложено в ссылку, предпочитать ссылку" 52 | }, 53 | "minimalDragDistance": { 54 | "message": "Минимальное расстояние перетаскивания (px)" 55 | }, 56 | "mousePosition": { 57 | "message": "Под курсором мыши" 58 | }, 59 | "nearMousePosition": { 60 | "message": "Возле курсора мыши" 61 | }, 62 | "openByDragAndDrop": { 63 | "message": "Использовать перетаскивание на пустое место для октрытия во вспл. окне" 64 | }, 65 | "openByShiftClick": { 66 | "message": "Использовать Shift + клик для открытия во вспл. окне" 67 | }, 68 | "openDragAndDropUnderMouse": { 69 | "message": "Всегда открывать перетаскиванием под курсором мыши" 70 | }, 71 | "openInPopupWindow": { 72 | "message": "Открыть ссылку во вспл. окне" 73 | }, 74 | "openPageInMainWindow": { 75 | "message": "Открыть страницу в основном окне" 76 | }, 77 | "openPageInPopupWindow": { 78 | "message": "Открыть страницу во вспл. окне" 79 | }, 80 | "popupHeight": { 81 | "message": "Высота окна (px)" 82 | }, 83 | "popupSearchUrl": { 84 | "message": "URL поиска (%s будет заменено поисковым запросом)" 85 | }, 86 | "popupWidth": { 87 | "message": "Ширина окна (px)" 88 | }, 89 | "popupWindowLocation": { 90 | "message": "Расположение всплывающего окна" 91 | }, 92 | "popupWindowSize": { 93 | "message": "Размер всплывающего окна" 94 | }, 95 | "reopenSingleTabWindowAsPopup": { 96 | "message": "Переоткрывать новые окна с одной вкладкой в виде всплывающих окон" 97 | }, 98 | "reopenSingleTabWindowAsPopupHint": { 99 | "message": "При включении этой опции любое новое окно браузера с одной вкладкой будет автоматически переоткрыто как всплывающее окно. Это позволит расширению создавать всплывающие окна для ссылок, к которым оно обычно не имеет доступа (например, панель закладок). Но это также может привести к нежелательному поведению, если вы обычно открываете вручную несколько окон браузера, поэтому используйте эту опцию с осторожностью!" 100 | }, 101 | "searchInPopupEnabled": { 102 | "message": "Включить опцию поиска выделения во всплывающем окне" 103 | }, 104 | "searchInPopupWindow": { 105 | "message": "Искать текст во вспл. окне" 106 | }, 107 | "settingsTitle": { 108 | "message": "Настройки" 109 | }, 110 | "textSelectionHeader": { 111 | "message": "Выделение текста" 112 | }, 113 | "topCenter": { 114 | "message": "Центр вверху" 115 | }, 116 | "topLeft": { 117 | "message": "Слева вверху" 118 | }, 119 | "topRight": { 120 | "message": "Справа вверху" 121 | }, 122 | "tryFitWindowSizeToImage": { 123 | "message": "Попытаться подогнать ширину окна под размер изображения" 124 | }, 125 | "useBuiltInImageViewer": { 126 | "message": "Использовать встроенный в браузер просмотрщик изображений" 127 | }, 128 | "viewInPopupEnabled": { 129 | "message": "Включить опцию просмотра изображений во всплывающем окне" 130 | }, 131 | "viewInPopupWindow": { 132 | "message": "Посмотреть изображение во вспл. окне" 133 | }, 134 | "writeAReviewButton": { 135 | "message": "Написать отзыв" 136 | } 137 | } -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emvaized/open-in-popup-window-extension/4918ec5cabf1aa6be6168561ae02625b3192a1b4/src/assets/icon.png -------------------------------------------------------------------------------- /src/assets/icon_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emvaized/open-in-popup-window-extension/4918ec5cabf1aa6be6168561ae02625b3192a1b4/src/assets/icon_white.png -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | let mouseX, mouseY, elementHeight, elementWidth, lastPopupId; 2 | let textSelection, availWidth, availHeight, availLeft; 3 | 4 | chrome.runtime.onMessage.addListener( 5 | function (request, sender, sendResponse) { 6 | if (request.action == 'requestEscPopupWindowClose') { 7 | loadUserConfigs((c) => { 8 | if (configs.escKeyClosesPopup){ 9 | chrome.windows.getCurrent((w)=>{ 10 | if (w.type == 'popup') 11 | chrome.windows.remove(w.id); 12 | }); 13 | } 14 | }); 15 | return; 16 | } 17 | if (request.action == 'requestOpenInMainWindow') { 18 | loadUserConfigs((c) => { 19 | if (configs.escKeyClosesPopup && sender.tab) 20 | moveTabToRegularWindow(sender.tab) 21 | }); 22 | return; 23 | } 24 | 25 | if (request.action == 'updateAspectRatio') { 26 | if (request.aspectRatio && configs.tryFitWindowSizeToImage) { 27 | chrome.windows.get(lastPopupId, function(w){ 28 | if (!w || lastPopupId < 0 || !request.aspectRatio) return; 29 | 30 | if (request.availWidth) { 31 | availWidth = request.availWidth; 32 | availHeight = request.availHeight; 33 | } 34 | 35 | let newWidth = w.height * request.aspectRatio; 36 | if (newWidth > availWidth) 37 | newWidth = availWidth * 0.7; 38 | 39 | let dx = w.left; 40 | if (dx + newWidth > availWidth) 41 | dx = dx - (dx + newWidth - availWidth); 42 | 43 | newWidth = Math.round(newWidth); 44 | dx = Math.round(dx); 45 | chrome.windows.update(lastPopupId, { 46 | 'width': newWidth, 47 | 'left': dx 48 | }); 49 | }) 50 | } 51 | return; 52 | } 53 | 54 | mouseX = request.mouseX; 55 | mouseY = request.mouseY; 56 | elementHeight = request.elementHeight; 57 | elementWidth = request.elementWidth; 58 | textSelection = request.selectedText ?? ''; 59 | availWidth = request.availWidth; 60 | availHeight = request.availHeight; 61 | availLeft = request.availLeft; 62 | 63 | if (request.type == 'drag' || request.type == 'shiftClick') { 64 | loadUserConfigs((cfg) => { 65 | if (request.type == 'drag' && configs.openByDragAndDrop == false) return; 66 | if (request.type == 'shiftClick' && configs.openByShiftClick == false) return; 67 | 68 | const isViewer = request.nodeName == 'IMG' || request.nodeName == 'VIDEO'; 69 | if (isViewer && !cfg.viewInPopupEnabled) return; 70 | openPopupWindowForLink(request.link, isViewer, request.type == 'drag'); 71 | }); 72 | } 73 | } 74 | ); 75 | 76 | const openLinkContextMenuItem = { 77 | "id": "openInPopupWindow", 78 | "title": chrome.i18n.getMessage('openInPopupWindow'), 79 | "contexts": ["link"] 80 | } 81 | const openPageContextMenuItem = { 82 | "id": "openPageInPopupWindow", 83 | "title": chrome.i18n.getMessage('openPageInPopupWindow'), 84 | "contexts": ["page"] 85 | } 86 | const openInMainWindowContextMenuItem = { 87 | "id": "openInMainWindow", 88 | "title": chrome.i18n.getMessage('openPageInMainWindow'), 89 | "visible": false, 90 | "contexts": ["page"] 91 | } 92 | const searchInPopupWindowContextMenuItem = { 93 | "id": "searchInPopupWindow", 94 | "title": chrome.i18n.getMessage('searchInPopupWindow'), 95 | "contexts": ["selection"] 96 | } 97 | const viewImageContextMenuItem = { 98 | "id": "viewInPopupWindow", 99 | "title": chrome.i18n.getMessage('viewInPopupWindow'), 100 | "contexts": ["image"] 101 | } 102 | 103 | chrome.contextMenus.create(openLinkContextMenuItem); 104 | chrome.contextMenus.create(openPageContextMenuItem); 105 | chrome.contextMenus.create(openInMainWindowContextMenuItem); 106 | chrome.contextMenus.create(searchInPopupWindowContextMenuItem); 107 | chrome.contextMenus.create(viewImageContextMenuItem); 108 | 109 | /// Update context menu availability 110 | chrome.windows.onFocusChanged.addListener(function(wId){ 111 | if (wId == undefined || wId < 0) return; /// don't process when window lost focus 112 | chrome.windows.get(wId, {}, 113 | (w) => { if (w) { 114 | chrome.contextMenus.update("openInMainWindow", {"visible": w.type == 'popup'}); 115 | chrome.contextMenus.update("openPageInPopupWindow", {"visible": w.type !== 'popup'}); 116 | } }, 117 | ); 118 | } 119 | ); 120 | 121 | /// Update configs 122 | chrome.storage.onChanged.addListener((changes) => { 123 | if (changes.searchInPopupEnabled) 124 | chrome.contextMenus.update("searchInPopupWindow", {"visible": changes.searchInPopupEnabled.newValue }); 125 | if (changes.viewInPopupEnabled) 126 | chrome.contextMenus.update("viewInPopupWindow", {"visible": changes.viewInPopupEnabled.newValue }); 127 | if (changes.addOptionOpenPageInPopupWindow) 128 | chrome.contextMenus.update("openPageInPopupWindow", {"visible": changes.addOptionOpenPageInPopupWindow.newValue }); 129 | applyUserConfigs(changes); 130 | }); 131 | 132 | /// Reopen new single tab windows as popups 133 | chrome.windows.onCreated.addListener( 134 | (w) => { 135 | loadUserConfigs((c) => { 136 | if (configs.reopenSingleTabWindowAsPopup && w.type == 'normal') 137 | setTimeout(()=> 138 | chrome.tabs.query({windowId: w.id}, (tabs) => { 139 | if (tabs.length == 1){ 140 | const tab = tabs[0]; 141 | if (tab.url !== 'about:home' && tab.url !== 'about:privatebrowsing' && 142 | tab.url !== 'chrome://newtab/' &&tab.url !== 'edge://newtab/' 143 | ) { 144 | openPopupWindowForLink(undefined, false, false, tab.id) 145 | } 146 | } 147 | }) 148 | , 5) 149 | }) 150 | } 151 | ) 152 | 153 | chrome.contextMenus.onClicked.addListener(function(clickData, tab) { 154 | if (clickData.menuItemId == 'openInMainWindow') { 155 | // if (tab) moveTabToRegularWindow(tab) 156 | chrome.tabs.remove(tab.id); 157 | chrome.tabs.create({ url: clickData.pageUrl, active: true }); 158 | return; 159 | } 160 | 161 | if (clickData.menuItemId == 'openPageInPopupWindow') { 162 | if (tab) openPopupWindowForLink(clickData.pageUrl, false, false, undefined, true); 163 | return; 164 | } 165 | 166 | const link = clickData.menuItemId == 'searchInPopupWindow' ? 167 | configs.popupSearchUrl.replace('%s', clickData.selectionText) 168 | : clickData.menuItemId == 'viewInPopupWindow' ? clickData.srcUrl : clickData.linkUrl; 169 | openPopupWindowForLink(link, clickData.menuItemId == 'viewInPopupWindow'); 170 | }); 171 | 172 | function openPopupWindowForLink(link, isViewer = false, isDragEvent, tabId, isCurrentPage = false) { 173 | loadUserConfigs(function(){ 174 | 175 | /* 176 | This logic was created in order to counter MacOS behavior, 177 | where popup windows could not be opened above the fullscreen window. 178 | It should determine MacOS native fullscreen specifically 179 | */ 180 | // let originalWindowIsFullscreen = false; 181 | // chrome.windows.getCurrent( 182 | // function(originWindow){ 183 | // // if (originWindow.type !== 'popup') originWindowId = originWindow.id; 184 | 185 | // /// if original window is fullscreen, unmaximize it (for MacOS) 186 | // if (originWindow.state == 'fullscreen') { 187 | // originalWindowIsFullscreen = true; 188 | // chrome.windows.update(originWindow.id, { 189 | // 'state': 'maximized' 190 | // }); 191 | // } 192 | // }); 193 | 194 | /// calculate popup size 195 | let height, width; 196 | 197 | height = configs.popupHeight ?? 800, width = configs.popupWidth ?? 600; 198 | if (isViewer && configs.tryFitWindowSizeToImage && elementHeight && elementWidth) { 199 | const aspectRatio = elementWidth / elementHeight; 200 | width = height * aspectRatio; 201 | 202 | if (width > availWidth) { 203 | width = availWidth * 0.7; 204 | } 205 | } 206 | height = parseInt(height); width = parseInt(width); 207 | 208 | /// calculate popup position 209 | let dx, dy; 210 | let popupLocation = configs.popupWindowLocation; 211 | if (isDragEvent && configs.openDragAndDropUnderMouse) popupLocation = 'mousePosition'; 212 | 213 | /// try to get current screen size (not supported in Manifest v3) 214 | try { 215 | availWidth = window.screen.width; 216 | availHeight = window.screen.height; 217 | } catch(e){} 218 | if (configs.debugMode) console.log('Initial availLeft: ', availLeft) 219 | if (!availLeft) availLeft = 0; 220 | 221 | function setCenterCoordinates(){ 222 | if (availHeight && availWidth){ 223 | dx = availLeft + availWidth / 2; 224 | dy = availHeight / 2; 225 | } else { 226 | dx = availLeft; dy = 0; 227 | } 228 | dx -= width / 2; 229 | dy -= height / 2; 230 | } 231 | 232 | function setCursorCoordinates(){ 233 | if (mouseX && mouseY){ 234 | dx = mouseX - (width / 2), dy = mouseY - (height / 2); 235 | } else { 236 | /// if no dx stored, switch to fallback 237 | setFallbackPopupLocation(); 238 | } 239 | } 240 | 241 | function setFallbackPopupLocation(){ 242 | setPopupLocation(configs.fallbackPopupWindowLocation ?? 'center'); 243 | } 244 | 245 | function setPopupLocation(preference){ 246 | switch(preference){ 247 | case 'mousePosition': { 248 | /// open at last known mouse position 249 | setCursorCoordinates(); 250 | } break; 251 | case 'nearMousePosition': { 252 | if (!mouseX) { 253 | setFallbackPopupLocation(); 254 | } else { 255 | /// try to open on side near mouse position, where there's enough space 256 | 257 | // const verticalPadding = elementHeight; 258 | const verticalPadding = 15; 259 | const horizontalPadding = 15; 260 | dx = mouseX - (width / 2), dy = mouseY - height - verticalPadding; 261 | 262 | if (dy < 0) dy = mouseY + verticalPadding; 263 | if (dy + height > availHeight) { 264 | dy = mouseY - (height / 2); 265 | dx = mouseX - width - horizontalPadding; 266 | 267 | if (dx < 0) dx = mouseX + horizontalPadding; 268 | if (dx + width > availWidth){ 269 | /// if nothing works, open centered in mouse position 270 | setFallbackPopupLocation(); 271 | } 272 | } 273 | } 274 | } break; 275 | case 'topRight': { 276 | dx = availLeft + availWidth - width, 277 | dy = 0; 278 | } break; 279 | case 'topLeft': { 280 | dx = availLeft, 281 | dy = 0; 282 | } break; 283 | case 'topCenter': { 284 | setCenterCoordinates(); 285 | dy = 0; 286 | } break; 287 | case 'bottomRight': { 288 | dx = availLeft + availWidth - width, 289 | dy = availHeight - height; 290 | } break; 291 | case 'bottomLeft': { 292 | dx = availLeft, 293 | dy = availHeight - height; 294 | } break; 295 | default: { 296 | /// open at center of screen 297 | setCenterCoordinates(); 298 | } break; 299 | } 300 | } 301 | setPopupLocation(popupLocation); 302 | 303 | if (configs.debugMode){ 304 | console.log('~~~'); 305 | console.log('Trying to open a popup window...'); 306 | console.log('window.screen.width: ', window.screen.width); 307 | console.log('window.screen.availWidth: ', window.screen.availWidth); 308 | console.log('window.screenLeft: ', window.screenLeft); 309 | console.log('window.screenX: ', window.screenX); 310 | console.log('window.availLeft: ', window.screen.availLeft); 311 | console.log('availLeft: ', availLeft); 312 | console.log('Selected popup window placement: ', popupLocation); 313 | console.log('Calculated popup window dx: ', dx); 314 | console.log('Checking for dx overflow...'); 315 | } 316 | 317 | /// check for screen overflow 318 | if (!dx) dx = 0; 319 | if (availLeft >= 0 && dx < 0) dx = 0; 320 | if (!dy || dy < 0) dy = 0; 321 | if (dy + height > availHeight) dy = dy - (dy + height - availHeight); 322 | dx = parseInt(dx); dy = parseInt(dy); 323 | if (dx + width > availWidth) dx = dx - (dx + width - availWidth); 324 | 325 | if (configs.debugMode){ 326 | console.log('Calucated dx after checking: ', dx); 327 | console.log('Calucated dy after checking: ', dy); 328 | console.log('End logging ~~~'); 329 | } 330 | 331 | /// create popup window 332 | // setTimeout(function () { 333 | const createParams = { 334 | 'type': 'popup', 335 | 'width': width, 'height': height, 336 | 'top': dy, 'left': dx 337 | }; 338 | 339 | if (tabId) { 340 | createParams.tabId = tabId; 341 | } else { 342 | createParams['url'] = isViewer ? 343 | (configs.useBuiltInImageViewer ? link : 344 | chrome.runtime.getURL('viewer/viewer.html') + '?src=' + link) 345 | : link ?? (textSelection ? 346 | (configs.popupSearchUrl.replace('%s', textSelection)) 347 | : 'about:blank'); 348 | } 349 | 350 | chrome.windows.create(createParams, function (popupWindow) { 351 | /// set coordinates again (workaround for old firefox bug) 352 | chrome.windows.update(popupWindow.id, { 353 | 'top': dy, 'left': dx, 'width': width, 'height': height 354 | }); 355 | 356 | /// close popup on focus normal window 357 | if (configs.closeWhenFocusedInitialWindow && (!isCurrentPage || !configs.keepOpenPageInPopupWindowOpen)){ 358 | function windowFocusListener(wId) { 359 | if (wId > -1) 360 | chrome.windows.get(wId,{}, (w) => { 361 | if (w && w.type == 'normal') { 362 | chrome.windows.remove(popupWindow.id); 363 | chrome.windows.onFocusChanged.removeListener(windowFocusListener); 364 | 365 | // if (originalWindowIsFullscreen) 366 | // chrome.windows.update(parentWindow.id, { 367 | // 'state': 'fullscreen' 368 | // }); 369 | } 370 | }); 371 | } 372 | 373 | setTimeout(function () { 374 | chrome.windows.onFocusChanged.addListener(windowFocusListener); 375 | }, 300); 376 | } 377 | 378 | /* remember dimensions on window resize 379 | [draft until onBoundsChanged is supported in Firefox] 380 | {@link https://bugzilla.mozilla.org/show_bug.cgi?id=1762975} 381 | */ 382 | // if (configs.rememberWindowResize){ 383 | // function resizeListener(w){ 384 | // const newSize = { 385 | // 'popupHeight': w.height, 386 | // 'popupWidth': w.width, 387 | // } 388 | // console.log(newSize); 389 | // chrome.storage.sync.set(configs) 390 | 391 | // } 392 | // function removedListener(wId){ 393 | // if (wId && wId > -1 && wId == popupWindow.id) { 394 | // chrome.windows.onBoundsChanged.removeListener(resizeListener); 395 | // chrome.windows.onRemoved.removeListener(removedListener); 396 | // } 397 | // } 398 | // chrome.windows.onBoundsChanged.addListener(resizeListener); 399 | // chrome.windows.onRemoved.addListener(removedListener); 400 | // } 401 | 402 | /// clear variables 403 | elementHeight = undefined; elementWidth = undefined; 404 | mouseX = undefined; mouseY = undefined; 405 | textSelection = undefined; 406 | lastPopupId = popupWindow.id; 407 | }); 408 | // }, originalWindowIsFullscreen ? 600 : 0) 409 | }); 410 | } 411 | 412 | function moveTabToRegularWindow(tab){ 413 | chrome.windows.getAll( 414 | { windowTypes: ['normal'] }, 415 | function(windows){ 416 | chrome.tabs.move(tab.id, { index: 0, windowId: windows[0].id}, function(t){ 417 | if (t) chrome.tabs.update(t[0].id, { 'active': true }); 418 | }); 419 | } 420 | ); 421 | } -------------------------------------------------------------------------------- /src/configs.js: -------------------------------------------------------------------------------- 1 | const configs = { 2 | 'closeWhenFocusedInitialWindow': true, 3 | 'hideBrowserControls': true, 4 | 'popupHeight': 800, 5 | 'popupWidth': 600, 6 | 'searchInPopupEnabled': true, 7 | 'viewInPopupEnabled': true, 8 | 'popupSearchUrl': 'https://www.google.com/search?q=%s', 9 | 'openByDragAndDrop': false, 10 | 'minimalDragDistance': 50, 11 | 'tryFitWindowSizeToImage': true, 12 | 'useBuiltInImageViewer': false, 13 | 'openByShiftClick': false, 14 | 'escKeyClosesPopup': false, 15 | 'reopenSingleTabWindowAsPopup': false, 16 | 'popupWindowLocation': 'mousePosition', /// possible values: mousePositon,nearMousePosition,center,etc 17 | 'imageWithLinkPreferLink': false, 18 | 'fallbackPopupWindowLocation': 'center', 19 | //, 'rememberWindowResize': false, 20 | 'debugMode': false, 21 | 'openDragAndDropUnderMouse': true, 22 | 'addOptionOpenPageInPopupWindow': true, 23 | 'keepOpenPageInPopupWindowOpen': true, 24 | } 25 | 26 | function loadUserConfigs(callback) { 27 | const keys = Object.keys(configs); 28 | chrome.storage.sync.get( 29 | keys, (cfg)=>{ 30 | if (cfg) applyUserConfigs(cfg, keys); 31 | if (callback) callback(configs); 32 | } 33 | ); 34 | } 35 | 36 | function applyUserConfigs(cfg, keys){ 37 | if (!keys) keys = Object.keys(cfg); 38 | const l = keys.length; 39 | for (let i = 0; i < l; i++) { 40 | const key = keys[i]; 41 | if (cfg[key] !== undefined) configs[key] = cfg[key]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/content.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("contextmenu",(e=>onTrigger(e,'context'))); 2 | chrome.storage.onChanged.addListener((c) => { 3 | loadUserConfigs((c) => setMouseListeners()) 4 | }); 5 | 6 | loadUserConfigs((c) => setMouseListeners()) 7 | 8 | function setMouseListeners(){ 9 | 10 | /* Drag listeners */ 11 | if (configs.openByDragAndDrop){ 12 | document.addEventListener("dragstart",dragStartListener); 13 | document.addEventListener("dragend",dragEndListener); 14 | } else { 15 | document.removeEventListener("dragstart",dragStartListener); 16 | document.removeEventListener("dragend",dragEndListener); 17 | } 18 | 19 | /* Shift+Click */ 20 | if (configs.openByShiftClick){ 21 | document.addEventListener("click",onClickListener); 22 | } else { 23 | document.removeEventListener("click",onClickListener); 24 | } 25 | 26 | /* Escape key to close popup */ 27 | if (configs.escKeyClosesPopup){ 28 | document.addEventListener('keyup', keyUpListener) 29 | } else { 30 | document.removeEventListener('keyup', keyUpListener) 31 | } 32 | } 33 | 34 | let dragStartDx, dragStartDy; 35 | 36 | function dragStartListener(e){ 37 | dragStartDx = e.clientX; dragStartDy = e.clientY; 38 | } 39 | function dragEndListener(e){ 40 | if (e.dataTransfer.dropEffect == 'none'){ 41 | if ( 42 | Math.abs(e.clientX - dragStartDx) > configs.minimalDragDistance || 43 | Math.abs(e.clientY - dragStartDy) > configs.minimalDragDistance 44 | ) onTrigger(e, 'drag') 45 | } 46 | } 47 | 48 | function keyUpListener(e){ 49 | if (e.key == 'Escape'){ 50 | if (e.ctrlKey){ 51 | chrome.runtime.sendMessage({action: 'requestOpenInMainWindow'}) 52 | } else { 53 | chrome.runtime.sendMessage({action: 'requestEscPopupWindowClose'}) 54 | } 55 | } 56 | } 57 | 58 | function onClickListener(e){ 59 | if (e.shiftKey && (e.target.href || e.target.src || e.target.parentNode.href)){ 60 | e.preventDefault(); 61 | onTrigger(e, 'shiftClick'); 62 | } 63 | } 64 | 65 | function onTrigger(e, type){ 66 | const t = e.target, 67 | message = { 68 | mouseX: e.screenX, mouseY: e.screenY, 69 | elementHeight: t.naturalHeight ?? t.clientHeight > 0 ? t.clientHeight : t.offsetHeight, 70 | elementWidth: t.naturalWidth ?? t.clientWidth > 0 ? t.clientWidth : t.offsetWidth, 71 | availHeight: window.screen.availHeight, availWidth: window.screen.availWidth, 72 | selectedText: window.getSelection().toString().trim(), 73 | availLeft: window.screen.availLeft, type: type 74 | } 75 | 76 | let nodeName, link; 77 | if (type == 'drag' || type == 'shiftClick') { 78 | /// Handle IMG wrapped in A 79 | if (t.parentNode && t.parentNode.nodeName == 'A'){ 80 | if (configs.imageWithLinkPreferLink){ 81 | nodeName = t.parentNode.nodeName; 82 | link = t.parentNode.href; 83 | } else { 84 | nodeName = t.nodeName; 85 | link = t.src; 86 | } 87 | } else if (t.childNodes && t.childNodes.length == 1 && t.firstChild.nodeName == 'IMG') { 88 | if (configs.imageWithLinkPreferLink){ 89 | nodeName = t.nodeName; 90 | link = t.href; 91 | } else { 92 | nodeName = t.firstChild.nodeName; 93 | link = t.firstChild.src; 94 | } 95 | } else { 96 | nodeName = t.nodeName; 97 | link = t.src ?? t.href ?? t.parentNode.href; 98 | } 99 | 100 | /// Handle IMG with source in sourceset 101 | if (nodeName == 'IMG' && !link && t.parentNode && t.parentNode.nodeName == 'PICTURE') { 102 | const src = t.parentNode.querySelector('source'); 103 | if (src) link = src.getAttribute('srcset'); 104 | } 105 | 106 | message['nodeName'] = nodeName; 107 | message['link'] = link; 108 | } 109 | 110 | chrome.runtime.sendMessage(message) 111 | } -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Open in Popup Window", 4 | "description": "__MSG_extensionDescription__", 5 | "default_locale": "en", 6 | "version": "0.3.6", 7 | "icons": { 8 | "48": "icon.png", 9 | "96": "icon.png", 10 | "128": "icon.png" 11 | }, 12 | "background": { 13 | "scripts": ["background.js"], 14 | "service_worker": "background.js" 15 | }, 16 | "content_scripts": [ 17 | { 18 | "matches": [ 19 | "" 20 | ], 21 | "js": [ 22 | "content.js" 23 | ], 24 | "run_at": "document_start" 25 | } 26 | ], 27 | "permissions": [ 28 | "", 29 | "contextMenus", 30 | "tabs", 31 | "storage" 32 | ], 33 | "options_ui": { 34 | "page": "options/options.html" 35 | }, 36 | "action": { 37 | "default_icon": "icon.png", 38 | "theme_icons": [{ 39 | "light": "icon_white.png", 40 | "dark": "icon.png", 41 | "size": 32 42 | }], 43 | "default_title": "Open in Popup Window", 44 | "default_popup": "options/options.html" 45 | }, 46 | "browser_specific_settings": { 47 | "gecko": { 48 | "id": "open_in_popup_window@emvaized.dev" 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/options/icons/donate.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/options/icons/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/options/icons/review.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/options/options.css: -------------------------------------------------------------------------------- 1 | body { 2 | min-width: 381px; 3 | padding: 4px; 4 | } 5 | #settingsTitle{ 6 | padding: 0 6px; 7 | } 8 | .option { 9 | padding: 5px 6px; 10 | } 11 | .option, .option label { 12 | cursor: pointer; 13 | } 14 | 15 | .option:hover { 16 | background-color: lightgrey; 17 | transition: background-color 50ms ease-in-out; 18 | border-radius: 4px; 19 | } 20 | 21 | input[type="text"]{ 22 | min-width: 40%; 23 | margin-right: 4px; 24 | } 25 | 26 | hr { width: 100%; opacity: 0.2; } 27 | 28 | .disabled-option { 29 | opacity: 0.5; 30 | transition: opacity 150ms ease-in-out; 31 | pointer-events: none; 32 | } 33 | 34 | #footer-buttons{ 35 | padding: 6px; 36 | margin-top: 8px; 37 | } 38 | #footer-buttons img { 39 | vertical-align: bottom; 40 | } 41 | #footer-buttons button{ 42 | cursor: pointer; 43 | } 44 | 45 | #versionLabel { 46 | color: gray; 47 | } 48 | 49 | 50 | h2{ 51 | margin: 6px 0; 52 | } 53 | 54 | h5{ 55 | color: gray; 56 | margin: 6px; 57 | min-width: fit-content !important; 58 | margin-right: 4px; 59 | font-weight: normal; 60 | } 61 | 62 | div:has(h5){ 63 | display: flex; align-items: center; margin-top: 2px; margin-bottom: 2px; 64 | } 65 | 66 | .hint { 67 | border-radius: 50%; 68 | line-height: 1; 69 | vertical-align: middle; 70 | height: 12px; 71 | width: 12px; 72 | min-width: 12px; 73 | font-size: 13px; 74 | display: inline-block; 75 | text-align: center; 76 | border: 1px solid gray; 77 | color: gray; 78 | } 79 | .hint:hover{ 80 | background: rgba(0,0,0,0.4); 81 | color:white; 82 | } 83 | 84 | .subgroup{ 85 | padding-left: 4px; 86 | margin-left: 8px; 87 | border-left: 1px solid gray; 88 | } 89 | 90 | @media (prefers-color-scheme: dark) { 91 | body { 92 | background: rgb(40,41,44); 93 | color: white; 94 | } 95 | 96 | .option:hover { 97 | background-color: rgb(256, 256, 256, 0.1); 98 | } 99 | } 100 | 101 | @-moz-document url-prefix() { 102 | body { 103 | font-family: sans-serif !important; 104 | /* line-height: 1.0 !important; */ 105 | } 106 | } -------------------------------------------------------------------------------- /src/options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
General settings

13 | 14 |
15 | 25 |
26 | 27 |
28 | 36 |
37 | 38 |
39 | 40 |
41 | 42 | 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 |
Popup window size

80 | 81 |
82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 | 90 | 93 | 94 |
Image viewer

95 | 96 |
97 | 98 |
99 | 100 |
101 |
102 | 103 |
104 | 105 |
106 | 107 |
108 |
109 | 110 |
Text selection

111 | 112 |
113 | 114 |
115 | 116 |
117 |
118 | 119 |
120 |
121 | 122 |
Open page in popup window

123 | 124 |
125 | 126 |
127 | 128 |
129 |
130 | 131 |
132 |
133 | 134 |
135 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /src/options/options.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", init); 2 | 3 | function init(){ 4 | loadUserConfigs(function(userConfigs){ 5 | const keys = Object.keys(configs); 6 | 7 | for (let i = 0, l = keys.length; i < l; i++) { 8 | const key = keys[i]; 9 | 10 | /// set corresponing input value 11 | let input = document.getElementById(key.toString()); 12 | 13 | /// Set input value 14 | if (input !== null && input !== undefined) { 15 | if (input.type == 'checkbox') { 16 | if ((userConfigs[key] !== null && userConfigs[key] == true) || (userConfigs[key] == null && configs[key] == true)) 17 | input.setAttribute('checked', 0); 18 | else input.removeAttribute('checked', 0); 19 | } else if (input.tagName == 'SELECT') { 20 | let options = input.querySelectorAll('option'); 21 | if (options) 22 | options.forEach(function (option) { 23 | let selectedValue = userConfigs[key] ?? configs[key]; 24 | if (option.value == selectedValue) option.setAttribute('selected', true); 25 | 26 | try { 27 | if (chrome.i18n.getMessage(option.innerHTML) != '') 28 | option.innerHTML = chrome.i18n.getMessage(option.innerHTML); 29 | else if (chrome.i18n.getMessage(option['value']) != '') 30 | option.innerHTML = chrome.i18n.getMessage(option['value']); 31 | } catch (e) { } 32 | 33 | }); 34 | } else { 35 | input.setAttribute('value', userConfigs[key] ?? configs[key]); 36 | } 37 | 38 | /// Set translated label for input 39 | let translatedLabel = chrome.i18n.getMessage(key); 40 | translatedLabel = translatedLabel 41 | .replace('Shift','Shift') 42 | .replace('Escape','Escape'); 43 | if (!input.parentNode.innerHTML.includes(translatedLabel)) { 44 | if (input.type == 'checkbox'){ 45 | input.parentNode.innerHTML += ' ' + translatedLabel; 46 | } else { 47 | input.parentNode.innerHTML = translatedLabel + ' ' + input.parentNode.innerHTML; 48 | } 49 | } 50 | 51 | /// Check if needs hint tooltip 52 | const hintMark = document.querySelector(`.option:has(#${key}) .hint`); 53 | if (hintMark) { 54 | const hintText = chrome.i18n.getMessage(key + 'Hint'); 55 | if (hintText) hintMark.title = hintText; 56 | } 57 | 58 | input = document.querySelector('#' + key.toString()); 59 | 60 | /// Set event listener 61 | input.addEventListener("input", function (e) { 62 | let id = input.getAttribute('id'); 63 | let inputValue = input.getAttribute('type') == 'checkbox' ? input.checked : input.value; 64 | configs[id] = inputValue; 65 | 66 | saveAllSettings(); 67 | updateDisabledOptions(); 68 | }); 69 | } 70 | } 71 | updateDisabledOptions(); 72 | setFooterButtons(); 73 | setVersionLabel(); 74 | }); 75 | 76 | setTranslatedLabels(); 77 | } 78 | 79 | function setTranslatedLabels(){ 80 | /// Set translations 81 | // document.getElementById('settingsTitle').innerText = chrome.i18n.getMessage('settingsTitle'); 82 | document.getElementById('donateButton').innerHTML += chrome.i18n.getMessage('donateButton'); 83 | document.getElementById('githubButton').innerHTML += chrome.i18n.getMessage('githubButton'); 84 | document.getElementById('writeAReviewButton').innerHTML += chrome.i18n.getMessage('writeAReviewButton'); 85 | document.getElementById('textSelectionHeader').innerText = chrome.i18n.getMessage('textSelectionHeader'); 86 | document.getElementById('imageViewer').innerText = chrome.i18n.getMessage('imageViewer'); 87 | document.getElementById('popupWindowSize').innerText = chrome.i18n.getMessage('popupWindowSize'); 88 | document.getElementById('generalSettings').innerText = chrome.i18n.getMessage('generalSettings'); 89 | document.getElementById('openPageInPopupWindowHeader').innerText = chrome.i18n.getMessage('openPageInPopupWindow'); 90 | } 91 | 92 | function updateDisabledOptions() { 93 | /// Grey out unavailable optoins 94 | document.getElementById("popupSearchUrl").parentNode.className = document.getElementById("searchInPopupEnabled").checked ? 'enabled-option' : 'disabled-option'; 95 | document.getElementById("tryFitWindowSizeToImage").parentNode.className = document.getElementById("useBuiltInImageViewer").checked ? 'enabled-option' : 'disabled-option'; 96 | document.getElementById("tryFitWindowSizeToImage").parentNode.className = document.getElementById("viewInPopupEnabled").checked ? 'enabled-option' : 'disabled-option'; 97 | document.getElementById("useBuiltInImageViewer").parentNode.className = document.getElementById("viewInPopupEnabled").checked ? 'enabled-option' : 'disabled-option'; 98 | document.getElementById("minimalDragDistance").parentNode.className = document.getElementById("openByDragAndDrop").checked ? 'enabled-option' : 'disabled-option'; 99 | document.getElementById("openDragAndDropUnderMouse").parentNode.className = document.getElementById("openByDragAndDrop").checked ? 'enabled-option' : 'disabled-option'; 100 | document.getElementById("keepOpenPageInPopupWindowOpen").parentNode.className = document.getElementById("addOptionOpenPageInPopupWindow").checked ? 'enabled-option' : 'disabled-option'; 101 | document.getElementById("fallbackPopupWindowLocation").parentNode.className = 102 | document.getElementById("popupWindowLocation").value == "mousePosition" || 103 | document.getElementById("popupWindowLocation").value == "nearMousePosition" 104 | ? 'enabled-option' : 'disabled-option'; 105 | } 106 | 107 | function setFooterButtons(){ 108 | document.querySelector("#donateButton").addEventListener("click", function (val) { 109 | window.open('https://github.com/emvaized/open-in-popup-window-extension?tab=readme-ov-file#support-project-%EF%B8%8F', '_blank'); 110 | }); 111 | 112 | document.querySelector("#githubButton").addEventListener("click", function (val) { 113 | window.open('https://github.com/emvaized/open-in-popup-window-extension', '_blank'); 114 | }); 115 | document.querySelector("#writeAReviewButton").addEventListener("click", function (val) { 116 | 117 | const isFirefox = navigator.userAgent.indexOf("Firefox") > -1; 118 | window.open(isFirefox ? 'https://addons.mozilla.org/firefox/addon/open-in-popup-window/' : 'https://chrome.google.com/webstore/detail/open-in-popup-window/gmnkpkmmkhbgnljljcchnakehlkihhie/reviews', '_blank'); 119 | }); 120 | } 121 | 122 | function setVersionLabel() { 123 | const label = document.getElementById('versionLabel'); 124 | const manifestData = chrome.runtime.getManifest(); 125 | label.innerHTML = 'v' + manifestData.version; 126 | label.title = 'Release notes'; 127 | label.onclick = function () { 128 | window.open('https://github.com/emvaized/open-in-popup-window-extension/blob/main/CHANGELOG.md') 129 | } 130 | } 131 | 132 | function saveAllSettings(){ 133 | chrome.storage.sync.set(configs) 134 | } -------------------------------------------------------------------------------- /src/viewer/viewer.css: -------------------------------------------------------------------------------- 1 | body { 2 | max-height: 100%; overflow: hidden; 3 | } 4 | 5 | .backgroundPattern { 6 | position: fixed; 7 | top: 0; 8 | left: 0; 9 | width: 100%; 10 | height: 100vh; 11 | z-index: -1; 12 | opacity: 0.1; 13 | } 14 | 15 | .backgroundPattern::before { 16 | content: ""; 17 | position: absolute; 18 | width: 100%; 19 | height: 100%; 20 | background: repeating-linear-gradient( 21 | 0deg, #000 0, #000 25px, 22 | #fff 25px, #fff 50px); 23 | } 24 | 25 | .backgroundPattern::after { 26 | content: ""; 27 | position: absolute; 28 | width: 100%; 29 | height: 100%; 30 | background: repeating-linear-gradient( 31 | 90deg, #000 0, #000 25px, 32 | #fff 25px, #fff 50px); 33 | mix-blend-mode: difference; 34 | } 35 | 36 | #mouseListener { 37 | height: 100%; width: 100%; position: fixed; top: 0; bottom: 0; left: 0; right: 0; 38 | } 39 | 40 | #wrapper:not(.noTransition) { 41 | transition: transform 300ms ease; 42 | } 43 | 44 | #image { 45 | width: 100%; height: 100%; 46 | transform-origin: 0% 0%; 47 | } 48 | 49 | @media (prefers-color-scheme: dark) { 50 | body { 51 | background: black; 52 | } 53 | } -------------------------------------------------------------------------------- /src/viewer/viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Image Viewer 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/viewer/viewer.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", init); 2 | 3 | let dxToShow = 0, dyToShow = 0, scale = 1.0, image, stepsCounter = 0, mouseListener; 4 | const minScale = 0.1, maxScale = 50.0, initialScale = 1.0, initialDx = 0, initialDy = 0, transitionDuration = 300; 5 | let rotationWrapper, scaleSlider, rotationStepsCounter = 0; 6 | const rotationSteps = [0, 90, 180, 270, 360], scaleSteps = [1.0, 2.0, 4.0]; 7 | let mirroredX = false, mirroredY = false; 8 | 9 | function init(){ 10 | const imageUrl = window.location.href.split('?src=')[1]; 11 | if (!imageUrl) return; 12 | 13 | image = document.getElementById('image'); 14 | image.src = imageUrl; 15 | document.title = imageUrl; 16 | mouseListener = document.getElementById('mouseListener'); 17 | rotationWrapper = document.getElementById('wrapper'); 18 | 19 | /// add mouse listeners 20 | setImageListeners(); 21 | 22 | /// update window size on image load (in case we got it wrong) 23 | image.onload = function(){ 24 | const aspectRatio = (image.naturalWidth ?? image.clientWidth) / (image.naturalHeight ?? image.clientHeight), 25 | toolbarHeight = window.outerHeight - window.innerHeight, 26 | toolbarWidth = window.outerWidth - window.innerWidth, 27 | availHeight = window.screen.availHeight, availWidth = window.screen.availWidth; 28 | 29 | chrome.runtime.sendMessage({ 30 | action: 'updateAspectRatio', aspectRatio: aspectRatio, 31 | toolbarHeight: toolbarHeight, toolbarWidth: toolbarWidth, 32 | availHeight: availHeight, availWidth: availWidth 33 | }); 34 | } 35 | } 36 | 37 | function setImageListeners(){ 38 | /// scale on wheel 39 | mouseListener.addEventListener('wheel', imageWheelListener, { passive: false }); 40 | /// move on pad down 41 | mouseListener.addEventListener('mousedown', function (e) { 42 | e.preventDefault(); 43 | evt = e || window.event; 44 | if ("buttons" in evt) { 45 | if (evt.button == 1) { 46 | /// Middle click to close view 47 | closeView(); 48 | } else if (evt.button == 0) { 49 | /// Left button 50 | panMouseDownListener(e) 51 | } 52 | // else if (evt.button == 2) { 53 | // /// Right button 54 | // rotateMouseDownListener(e) 55 | // } 56 | } 57 | }); 58 | /// Double click to scale up listener 59 | mouseListener.addEventListener('dblclick', function (e) { 60 | evt = e || window.event; 61 | if ("buttons" in evt) { 62 | if (evt.button == 0) { 63 | 64 | /// take the scale into account with the offset 65 | let xs = (e.clientX - dxToShow) / scale, 66 | ys = (e.clientY - dyToShow) / scale; 67 | 68 | let scaleValueWithinSteps = false; 69 | scaleSteps.forEach(function (step) { 70 | if (scale == initialScale * step) scaleValueWithinSteps = true; 71 | }) 72 | 73 | if (scaleValueWithinSteps) { 74 | if (stepsCounter == scaleSteps.length - 1) { 75 | stepsCounter = 0; 76 | scale = initialScale; 77 | dxToShow = initialDx; 78 | dyToShow = initialDy; 79 | } 80 | else { 81 | stepsCounter += 1; 82 | scale = initialScale * scaleSteps[stepsCounter]; 83 | /// reverse the offset amount with the new scale 84 | dxToShow = e.clientX - xs * scale; 85 | dyToShow = e.clientY - ys * scale; 86 | } 87 | image.style.transform = `translate(${dxToShow}px,${dyToShow}px) scale(${scale})`; 88 | } else { 89 | /// Return image to initial scale 90 | scale = initialScale; 91 | stepsCounter = 0; 92 | rotationStepsCounter = 0; 93 | rotationWrapper.style.transform = 'rotate(0deg)'; 94 | 95 | dxToShow = 0; dyToShow = 0; 96 | image.style.transform = 'translate(0,0)'; 97 | } 98 | 99 | if (image.style.transition == '') 100 | image.style.transition = `transform ${transitionDuration}ms ease-in-out, scale ${transitionDuration}ms ease-in-out`; 101 | // image.style.scale = scale; 102 | 103 | setTimeout(function(){ 104 | image.style.transition = ''; 105 | }, transitionDuration) 106 | } 107 | } 108 | }); 109 | } 110 | 111 | function panMouseDownListener(e) { 112 | e.preventDefault(); 113 | 114 | image.style.cursor = 'grabbing'; 115 | document.body.style.cursor = 'move'; 116 | image.style.transition = ''; 117 | 118 | function mouseMoveListener(e) { 119 | dxToShow = dxToShow + e.movementX; 120 | dyToShow = dyToShow + e.movementY; 121 | 122 | image.style.transform = `translate(${dxToShow}px, ${dyToShow}px) scale(${scale})`; 123 | } 124 | 125 | document.addEventListener('mousemove', mouseMoveListener); 126 | document.addEventListener('mouseup', function () { 127 | document.body.style.cursor = 'unset'; 128 | image.style.cursor = 'grab'; 129 | document.removeEventListener('mousemove', mouseMoveListener); 130 | }); 131 | } 132 | 133 | function imageWheelListener(e) { 134 | e.preventDefault(); 135 | 136 | /// take the scale into account with the offset 137 | const xs = (e.clientX - dxToShow) / scale, 138 | ys = (e.clientY - dyToShow) / scale; 139 | 140 | const wheelDelta = e.wheelDeltaY ?? -e.deltaY; 141 | scale += wheelDelta / 300; 142 | 143 | if (scale < minScale) scale = minScale; 144 | if (scale > maxScale) scale = maxScale; 145 | 146 | scale = parseFloat(scale); 147 | 148 | /// reverse the offset amount with the new scale 149 | dxToShow = e.clientX - xs * scale; 150 | dyToShow = e.clientY - ys * scale; 151 | 152 | // image.style.transition = ''; 153 | image.style.transform = `translate(${dxToShow}px, ${dyToShow}px) scale(${scale})`; 154 | } 155 | 156 | function closeView() { 157 | window.close() 158 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const TerserPlugin = require("terser-webpack-plugin"); 3 | const CopyPlugin = require("copy-webpack-plugin"); 4 | const ConcatPlugin = require('@mcler/webpack-concat-plugin'); 5 | const JsonMinimizerPlugin = require("json-minimizer-webpack-plugin"); 6 | const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); 7 | 8 | module.exports = { 9 | /// background script 10 | entry: { 11 | index: "./src/content.js" 12 | }, 13 | output: { 14 | path: path.resolve(__dirname, 'dist'), 15 | filename: "[name].js" 16 | }, 17 | plugins: [ 18 | /// content scripts 19 | new ConcatPlugin({ 20 | name: 'content', 21 | outputPath: './', 22 | fileName: '[name].js', 23 | filesToConcat: [ 24 | "./src/configs.js", 25 | "./src/content.js", 26 | ] 27 | }), 28 | new ConcatPlugin({ 29 | name: 'background', 30 | outputPath: './', 31 | fileName: '[name].js', 32 | filesToConcat: [ 33 | "./src/configs.js", 34 | "./src/background.js", 35 | ] 36 | }), 37 | /// static files 38 | new CopyPlugin({ 39 | patterns: [ 40 | { from: "src/manifest.json", to: "manifest.json" }, 41 | { from: "src/assets/_locales", to: "_locales" }, 42 | { from: "src/assets/icon.png", to: "icon.png" }, 43 | { from: "src/assets/icon_white.png", to: "icon_white.png" }, 44 | { from: "src/options", to: "options" }, 45 | { from: "src/viewer", to: "viewer" }, 46 | /// additional dependencies for the options page 47 | { from: "src/configs.js", to: "configs.js" }, 48 | 49 | ], 50 | }), 51 | ], 52 | mode: 'production', 53 | optimization: { 54 | minimize: true, 55 | minimizer: [ 56 | new TerserPlugin(), 57 | new CssMinimizerPlugin(), 58 | new JsonMinimizerPlugin(), 59 | ], 60 | }, 61 | }; --------------------------------------------------------------------------------