├── .github └── FUNDING.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── PRIVACY.md ├── README.md ├── package-lock.json ├── package.json ├── screenshots ├── banner.png ├── screenshot.png ├── screenshot_2.png └── screenshot_3.png ├── src ├── _locales │ ├── de │ │ └── messages.json │ ├── en │ │ └── messages.json │ ├── es │ │ └── messages.json │ ├── fr │ │ └── messages.json │ ├── nl │ │ └── messages.json │ ├── pt │ │ └── messages.json │ ├── ru │ │ └── messages.json │ ├── tr │ │ └── messages.json │ ├── uk │ │ └── messages.json │ └── zh │ │ └── messages.json ├── assets │ └── icons │ │ ├── button-icons │ │ ├── bold.svg │ │ ├── calendar.svg │ │ ├── call.svg │ │ ├── clear.svg │ │ ├── copy.svg │ │ ├── cut.svg │ │ ├── dictionary.svg │ │ ├── email.svg │ │ ├── extend-selection.svg │ │ ├── highlighter.svg │ │ ├── italic.svg │ │ ├── link.svg │ │ ├── location.svg │ │ ├── map.svg │ │ ├── marker.svg │ │ ├── open.svg │ │ ├── paste.svg │ │ ├── quote.svg │ │ ├── search.svg │ │ ├── strike.svg │ │ ├── time.svg │ │ └── translate.svg │ │ ├── donate.svg │ │ ├── github.svg │ │ └── logo-new.png ├── data │ ├── configs.js │ ├── currencies.js │ ├── keywords.js │ ├── search-urls.js │ ├── tooltip-icons.js │ └── variables.js ├── functions │ ├── background.js │ ├── clipboard-functions.js │ ├── color-functions.js │ ├── css-path-for-node.js │ ├── currencies-functions.js │ ├── hover-buttons-functions.js │ ├── locale-functions.js │ ├── marker-functions.js │ ├── selection-functions.js │ ├── text-functions.js │ └── tooltip-functions.js ├── index.css ├── index.js ├── manifest.json ├── options │ ├── icons │ │ ├── border-color.svg │ │ ├── highlighter.svg │ │ ├── network.svg │ │ ├── palette.svg │ │ ├── search.svg │ │ ├── select-end.svg │ │ ├── select-start.svg │ │ ├── settings.svg │ │ ├── split-screen.svg │ │ ├── sync.svg │ │ └── text-field.svg │ ├── options.css │ ├── options.html │ ├── options.js │ ├── test-page.html │ └── test-page.js ├── popup │ ├── popup.css │ ├── popup.html │ └── popup.js └── ui │ ├── buttons │ ├── basic-buttons.js │ ├── contextual-buttons.js │ └── hover-buttons │ │ ├── calendar-button.js │ │ ├── collapse-button.js │ │ ├── dictionary-button.js │ │ ├── marker.js │ │ ├── search-button.js │ │ └── translate-button.js │ ├── selection-handles.js │ └── tooltip.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 | .DS_Store 2 | node_modules/ 3 | dist/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | ---- 3 | 4 | Copyright (c) 2021 Max Tsyba 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 | Selecton requires permission to run on any website in order to detect text selection and show popup depending on selection's contents. 2 | 3 | Selecton does not collect or send any personal data. The only stored information is user's configs, which are stored locally and do not contain any personal information. 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ### SelectON — Customizable pop-up on text selection 3 | 4 | [![Changelog](https://img.shields.io/chrome-web-store/v/pemdbnndbdpbelmfcddaihdihdfmnadi?label=version&color=yellow)](./CHANGELOG.md) 5 | [![Chrome Web Store](https://img.shields.io/chrome-web-store/users/pemdbnndbdpbelmfcddaihdihdfmnadi?label=users&logo=googlechrome&logoColor=white&color=blue)](https://chrome.google.com/webstore/detail/selection-actions/pemdbnndbdpbelmfcddaihdihdfmnadi) 6 | [![Mozilla Add-on](https://img.shields.io/amo/users/selection-actions?color=%23FF6611&label=users&logo=Firefox)](https://addons.mozilla.org/firefox/addon/selection-actions/) 7 | [![Chrome Web Store](https://img.shields.io/chrome-web-store/rating/pemdbnndbdpbelmfcddaihdihdfmnadi)](https://chrome.google.com/webstore/detail/selecton/pemdbnndbdpbelmfcddaihdihdfmnadi/reviews) 8 | ![Created](https://img.shields.io/github/created-at/emvaized/selecton-extension?color=purple&label=created) 9 | [![Support project](https://shields.io/badge/Ko--fi-Donate-ff5f5f?logo=Ko-Fi&style=for-the-badgeKo-fi)](https://ko-fi.com/emvaized) 10 | 11 | 12 | 13 | ### Features 14 | * Copy or search any text in one click - especially useful for laptops 15 | * Customizable appearance for tooltip and custom text selection color 16 | * Currency converter (supports 30+ currencies & 10+ crypto currencies) 17 | * Basic units converter (metric/imperial) and timezones conversion 18 | * Contextual buttons depending on selected text, such as 'Open link', 'Translate', 'Show on map', CSS color preview and more 19 | * Smart selection - automatic snapping of text selection by words, so you'll never lose that last letter again 20 | * Selection handles, which allow to quickly edit text selection 21 | * Live translation of the selected text on hovering the "Translate" button 22 | * Dictionary button, which fetches definition from Wikipedia on hover 23 | * Highlighter button, which allows to highlight specific text on page and quickly find it later 24 | 25 | Get for Firefox   Get for Chrome 26 | 27 | 28 | ## Screenshots 29 | | ![Screenshot 1](./screenshots/screenshot.png) | 30 | |-| 31 |
32 | More screenshots 33 | 34 | | ![Screenshot 3](./screenshots/screenshot_3.png) | 35 | |-| 36 | | ![Screenshot 2](./screenshots/screenshot_2.png) | 37 | |-| 38 | 39 |
40 | 41 | 42 | ## FAQ 43 | 44 | Moved to the Wiki page – [read here](https://github.com/emvaized/selecton-extension/wiki/FAQ-(Frequently-Asked-Questions)) 45 | 46 | ## Donate 47 | If you really enjoy this project, please consider supporting its further development by making a small donation using one of the ways below! 48 | 49 | Support on Ko-fi   Donate using Liberapay   Donate Bitcoin 50 | 51 | ## Currency conversion 52 | In order to make extension more autonomous, currency rates are set to be updated every 2 weeks, and at the moment of conversion data may not be 100% accurate. Currency conversion output is intended to be used only for a quick estimation. You can decrease update interval in extension's settings if needed, but minimal value for now is 7 days to not cause too much load on API servers. Currency rates are fetched from fawazahmed0's [currency-api](https://github.com/fawazahmed0/exchange-api/blob/main/README.md). 53 | 54 |
55 | List of the supported currencies 56 | 57 | ``` 58 | AUD — Australian Dollar 59 | BGN — Bulgarian Lev 60 | BRL — Brazilian real 61 | CAD — Canadian Dollar 62 | CHF — Swiss Franc 63 | CNY — Chinese Yuan 64 | CRC — Costa Rican Colon 65 | CZK — Czech Koruna 66 | DKK — Danish Krone 67 | EUR — Euro 68 | GBP — British Pound 69 | HKD — Hong Kong dollar 70 | ILS — Israeli New Sheqel 71 | INR — Indian Rupee 72 | IRR — Iranian Rial 73 | JPY — Japanese Yen 74 | KPW — North Korean Won 75 | KRW — South Korean Won 76 | KZT — Kazakhstani Tenge 77 | MNT — Mongolian Tugrik 78 | MXN — Mexican Peso 79 | NGN — Nigerian Naira 80 | NOK — Norwegian krone 81 | PLN — Polish złoty 82 | RUB — Russian Ruble 83 | SAR — Saudi Riyal 84 | SEK — Swedish Krona 85 | TRY — Turkish Lira 86 | UAH — Ukrainian Hryvnia 87 | USD — United States Dollar 88 | VND — Vietnamese Dong 89 | ZAR — Rand 90 | 91 | Crypto: 92 | BTC — Bitcoin 93 | ETH — Etherium 94 | LTC — Litecoin 95 | ADA — Cardano 96 | BCH — Bitcoin Cash 97 | XRP — Ripple 98 | ZEC — Zcash 99 | XMR — Monero 100 | ZCL — ZClassic 101 | DOGE — Dogecoin 102 | IOTA (MIOTA) 103 | EOS 104 | ``` 105 |
106 | 107 | ## Contribution 108 | You can make SelectON better without even knowing how to code: 109 | - Provide translation for your language: [Base English file](./src/assets/_locales/en/messages.json) 110 | - Add your currency to the list of supported currencies: [Currencies list](./src/data/currencies.js) 111 | - SelectON relies on looking for keywords in the selected text. Enhance them with keywords for your language: [Keywords](./src/data/keywords.js) 112 | 113 | Make your changes, and then create pull request here on GitHub so I can merge it. 114 | Also, you can always write me an [email](mailto:maximtsyba@gmail.com) to share your ideas and suggestions. 115 | 116 | **Some ideas for future releases** 117 | 118 | - [ ] Advanced buttons editor, which allows to quickly turn on/off buttons and change reorder them with drag'n'drop 119 | - [ ] Cloud sync of settings using browser account sync 120 | - [ ] Ability to turn on/off background blur for tooltip and hover panels 121 | - [ ] Make separate tab for markers in the extension popup, with separate category for markers for currently open page 122 | 123 | ## Building 124 | - `npm install` to install all dependencies 125 | - `npm run build` to generate `dist` folder with minimized code of the extension 126 | 127 | ## Links to my other browser extensions 128 | * [Circle Mouse Gestures](https://github.com/emvaized/circle-mouse-gestures) – better mouse gestures, with visual representation of all available actions 129 | * [Google Search Tweaks](https://github.com/emvaized/google-tiles-extension) – set of tweaks for Google search page to make it easier to use 130 | * [Open in Popup Window](https://github.com/emvaized/open-in-popup-window-extension) – quickly open any links and images in a small popup window with no browser controls 131 | * [Linkover](https://github.com/emvaized/linkover-extension) – load info about any link on mouse hover or on a long click -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@mcler/webpack-concat-plugin": "^4.1.6", 4 | "css-minimizer-webpack-plugin": "^7.0.0", 5 | "json-minimizer-webpack-plugin": "^5.0.0", 6 | "terser-webpack-plugin": "^5.3.10", 7 | "webpack": "^5.93.0", 8 | "webpack-cli": "^5.1.4", 9 | "webpack-concat-files-plugin": "^0.5.2" 10 | }, 11 | "dependencies": { 12 | "copy-webpack-plugin": "^12.0.2" 13 | }, 14 | "scripts": { 15 | "build": "webpack --config webpack.config.js" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /screenshots/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emvaized/selecton-extension/3533a144f70a52b1538190dc40f0585fcd8db582/screenshots/banner.png -------------------------------------------------------------------------------- /screenshots/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emvaized/selecton-extension/3533a144f70a52b1538190dc40f0585fcd8db582/screenshots/screenshot.png -------------------------------------------------------------------------------- /screenshots/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emvaized/selecton-extension/3533a144f70a52b1538190dc40f0585fcd8db582/screenshots/screenshot_2.png -------------------------------------------------------------------------------- /screenshots/screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emvaized/selecton-extension/3533a144f70a52b1538190dc40f0585fcd8db582/screenshots/screenshot_3.png -------------------------------------------------------------------------------- /src/_locales/zh/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "actionButtonsHeader": { 3 | "message": "動作按鈕" 4 | }, 5 | "addActionButtonsForTextFields": { 6 | "message": "為文字欄位使用特殊面板" 7 | }, 8 | "addButtonIcons": { 9 | "message": "顯示按鈕圖示" 10 | }, 11 | "addButtonToCopyLinkToText": { 12 | "message": "新增按鈕以將連結複製到所選文字" 13 | }, 14 | "addCalendarButton": { 15 | "message": "為日期添加日曆按鈕" 16 | }, 17 | "addClearButton": { 18 | "message": "點擊非空欄位時顯示「清除」按鈕" 19 | }, 20 | "addColorPreviewButton": { 21 | "message": "新增顏色預覽按鈕 (用於#ffffff等CSS顏色)" 22 | }, 23 | "addDragHandles": { 24 | "message": "新增選擇拖動手把" 25 | }, 26 | "addExtendSelectionButton": { 27 | "message": "将按钮添加到扩展当前文本选择" 28 | }, 29 | "addFontFormatButtons": { 30 | "message": "新增字體格式按鈕 (斜體、粗體)" 31 | }, 32 | "addMarkerButton": { 33 | "message": "新增「突出顯示」按鈕" 34 | }, 35 | "addNewSearchOption": { 36 | "message": "新增新的搜尋選項" 37 | }, 38 | "addOpenLinks": { 39 | "message": "新增按鈕以打開連結" 40 | }, 41 | "addPasteButton": { 42 | "message": "點擊文字欄位時顯示「貼上」按鈕" 43 | }, 44 | "addPasteOnlyEmptyField": { 45 | "message": "…僅當欄位為空時" 46 | }, 47 | "addPhoneButton": { 48 | "message": "新增電話號碼按鈕" 49 | }, 50 | "addQuoteButton": { 51 | "message": "新增按鈕以引用選取的內容" 52 | }, 53 | "addScaleUpEffect": { 54 | "message": "在顯示上使用放大效果" 55 | }, 56 | "addTooltipShadow": { 57 | "message": "新增工具提示陰影" 58 | }, 59 | "allChangesSavedAutomatically": { 60 | "message": "所有設定都會自動儲存" 61 | }, 62 | "animationDuration": { 63 | "message": "動畫時長 (毫秒)" 64 | }, 65 | "appearanceHeader": { 66 | "message": "外觀" 67 | }, 68 | "applyConfigsImmediately": { 69 | "message": "立即套用配置" 70 | }, 71 | "behaviorHeader": { 72 | "message": "表現" 73 | }, 74 | "boldLabel": { 75 | "message": "大膽的" 76 | }, 77 | "borderRadius": { 78 | "message": "邊界半徑" 79 | }, 80 | "buttonsStyle": { 81 | "message": "按鈕樣式" 82 | }, 83 | "buyMeCoffee": { 84 | "message": "支援專案" 85 | }, 86 | "callLabel": { 87 | "message": "稱呼" 88 | }, 89 | "changeTextSelectionColor": { 90 | "message": "自訂文字選擇" 91 | }, 92 | "chooseFileFirst": { 93 | "message": "首先選擇檔案" 94 | }, 95 | "circle": { 96 | "message": "圓形" 97 | }, 98 | "clearLabel": { 99 | "message": "清除" 100 | }, 101 | "collapseAsSecondPanel": { 102 | "message": "始终将折叠的按钮显示为静态面板" 103 | }, 104 | "collapseButtons": { 105 | "message": "隱藏「更多」按鈕下的超額按鈕" 106 | }, 107 | "contextualButtonsHeader": { 108 | "message": "按鈕" 109 | }, 110 | "convertCurrencies": { 111 | "message": "轉換貨幣" 112 | }, 113 | "convertMetrics": { 114 | "message": "轉換度量" 115 | }, 116 | "convertResultClickAction": { 117 | "message": "點擊結果時的動作" 118 | }, 119 | "convertTime": { 120 | "message": "轉換時間" 121 | }, 122 | "convertToCurrency": { 123 | "message": "要轉換為的貨幣 (info)" 124 | }, 125 | "convertToCurrencyHint": { 126 | "message": "檢視「程式碼」 這裡 以供參考" 127 | }, 128 | "convertionHeader": { 129 | "message": "轉換" 130 | }, 131 | "copyLabel": { 132 | "message": "複製" 133 | }, 134 | "correctTooltipPositionByMoreButtonWidth": { 135 | "message": "按「更多」按鈕的寬度調整面板" 136 | }, 137 | "customIcon": { 138 | "message": "自訂圖示" 139 | }, 140 | "customIconUrl": { 141 | "message": "自訂圖示網址" 142 | }, 143 | "customSearchButtonsHeader": { 144 | "message": "搜尋按鈕" 145 | }, 146 | "customSearchOptionsDisplay": { 147 | "message": "顯示自訂搜尋選項的樣式" 148 | }, 149 | "customSearchTooltip": { 150 | "message": "自訂搜尋選項" 151 | }, 152 | "customSearchTooltipHint": { 153 | "message": "將滑鼠懸停在“搜尋”按鈕上時顯示。 使用 %s 作為搜尋查詢的佔位符,%w 是目前域的佔位符(用於網站搜尋)" 154 | }, 155 | "customSearchUrl": { 156 | "message": "自訂搜尋網址
(使用 %s 作為查詢的佔位符)" 157 | }, 158 | "cutLabel": { 159 | "message": "剪下" 160 | }, 161 | "dayAgo": { 162 | "message": "天前" 163 | }, 164 | "daysAgo": { 165 | "message": "$DAYS$ 天前", 166 | "placeholders": { 167 | "days": { 168 | "content": "$1" 169 | } 170 | } 171 | }, 172 | "debugMode": { 173 | "message": "除錯模式" 174 | }, 175 | "delayToRevealHoverPanels": { 176 | "message": "懸停時顯示面板的延遲 (毫秒)" 177 | }, 178 | "delayToRevealSearchTooltip": { 179 | "message": "顯示面板前的延遲 (毫秒)" 180 | }, 181 | "delayToRevealTranslateTooltip": { 182 | "message": "自動翻譯前的延遲 (毫秒)" 183 | }, 184 | "deleteLabel": { 185 | "message": "刪除" 186 | }, 187 | "dictionaryButtonRemark": { 188 | "message": "在懸停時尋找維基百科定義" 189 | }, 190 | "dictionaryButtonResponseCharsAmount": { 191 | "message": "結果顯示的最大字元數" 192 | }, 193 | "dictionaryButtonWordsAmount": { 194 | "message": "顯示字典按鈕的單字" 195 | }, 196 | "dictionaryLabel": { 197 | "message": "字典" 198 | }, 199 | "disableForBetterPerformance": { 200 | "message": "停用以獲得更好的效能" 201 | }, 202 | "disableWordSnapForCode": { 203 | "message": "停用程式碼的單字捕捉" 204 | }, 205 | "disableWordSnappingOnCtrlKey": { 206 | "message": "如果按下 CTRL 鍵,則不要按單字選擇" 207 | }, 208 | "dontSnapTextfieldSelection": { 209 | "message": "不要在文字欄位中捕捉選擇" 210 | }, 211 | "dragHandleStyle": { 212 | "message": "拖動手把樣式" 213 | }, 214 | "draggableTooltip": { 215 | "message": "面板可以透過箭頭拖動" 216 | }, 217 | "email": { 218 | "message": "電子郵件" 219 | }, 220 | "enabled": { 221 | "message": "啟用" 222 | }, 223 | "excludedDomains": { 224 | "message": "排除的網域" 225 | }, 226 | "export": { 227 | "message": "匯出" 228 | }, 229 | "exportImportSettings": { 230 | "message": "匯出/匯入設定" 231 | }, 232 | "exportSettingsLabel": { 233 | "message": "匯出設定" 234 | }, 235 | "exportSettingsNote": { 236 | "message": "如果匯出按鈕不起作用,請嘗試使用右上角的按鈕在新頁籤中打開此頁面" 237 | }, 238 | "extendSelection": { 239 | "message": "扩展选区" 240 | }, 241 | "extendSelectionTooltip": { 242 | "message": "在元素树中将文本选择扩展一级" 243 | }, 244 | "extensionDescription": { 245 | "message": "帶有文字選擇操作的可自訂彈出視窗" 246 | }, 247 | "fallbackExportLabel": { 248 | "message": "匯出,另存為 .json 檔案" 249 | }, 250 | "floatingOffscreenTooltip": { 251 | "message": "滾動到螢幕外時的浮動面板" 252 | }, 253 | "fontSize": { 254 | "message": "字型大小" 255 | }, 256 | "fullOpacityOnHover": { 257 | "message": "懸停時完全不透明" 258 | }, 259 | "gmail": { 260 | "message": "Gmail 新電子郵件" 261 | }, 262 | "hideOnKeypress": { 263 | "message": "按鍵隱藏" 264 | }, 265 | "hideOnScroll": { 266 | "message": "在滾動條上隱藏工具提示" 267 | }, 268 | "hideTooltipOnActionButtonClick": { 269 | "message": "單擊操作按鈕時隱藏面板" 270 | }, 271 | "hideTooltipWhenCursorMovesAway": { 272 | "message": "游標移開時隱藏工具提示" 273 | }, 274 | "hideTranslateButtonForUserLanguage": { 275 | "message": "隱藏使用者語言的翻譯按鈕" 276 | }, 277 | "horizontalLayout": { 278 | "message": "水平的" 279 | }, 280 | "hoverCustomSearchStyle": { 281 | "message": "在搜尋按鈕懸停時顯示" 282 | }, 283 | "iconlabel": { 284 | "message": "圖示 + 文字" 285 | }, 286 | "imperial": { 287 | "message": "英製" 288 | }, 289 | "import": { 290 | "message": "匯入" 291 | }, 292 | "importAlert": { 293 | "message": "此動作將覆蓋目前的 Selecton 設定。 你確定嗎?" 294 | }, 295 | "importSettingsLabel": { 296 | "message": "匯入設定" 297 | }, 298 | "inDay": { 299 | "message": "天內" 300 | }, 301 | "inDays": { 302 | "message": "在 $DAYS$ 天內", 303 | "placeholders": { 304 | "days": { 305 | "content": "$1" 306 | } 307 | } 308 | }, 309 | "inMonth": { 310 | "message": "月內" 311 | }, 312 | "inMonths": { 313 | "message": "在 $MONTHS$ 月內", 314 | "placeholders": { 315 | "months": { 316 | "content": "$1" 317 | } 318 | } 319 | }, 320 | "inYear": { 321 | "message": "年內" 322 | }, 323 | "inYears": { 324 | "message": "在 $YEARS$ 年內", 325 | "placeholders": { 326 | "years": { 327 | "content": "$1" 328 | } 329 | } 330 | }, 331 | "invertColorOnDarkWebsite": { 332 | "message": "在深色網站上反轉顏色" 333 | }, 334 | "italicLabel": { 335 | "message": "斜體" 336 | }, 337 | "languageToTranslate": { 338 | "message": "使用者的語言 (ISO 639-1 code)" 339 | }, 340 | "leftClickBackgroundTab": { 341 | "message": "左鍵點擊打開連結作為背景頁籤" 342 | }, 343 | "linesCount": { 344 | "message": "行" 345 | }, 346 | "linkToTextDescription": { 347 | "message": "複製指向所選文字的頁面連結 (僅在 Chrome 90+ 中打開)" 348 | }, 349 | "linkToTextLabel": { 350 | "message": "複製連結" 351 | }, 352 | "liveTranslation": { 353 | "message": "按鈕懸停時的即時翻譯 (Google 翻譯)" 354 | }, 355 | "mailto": { 356 | "message": "電子郵件用戶端" 357 | }, 358 | "markedLabel": { 359 | "message": "突出顯示" 360 | }, 361 | "markerLabel": { 362 | "message": "突出顯示" 363 | }, 364 | "markersLabel": { 365 | "message": "突出顯示" 366 | }, 367 | "maxIconsInRow": { 368 | "message": "列中的圖示數量" 369 | }, 370 | "maxMarkerPagesToStore": { 371 | "message": "儲存突顯的最大頁數" 372 | }, 373 | "maxTooltipButtonsToShow": { 374 | "message": "要在工具提示中顯示的按鈕數" 375 | }, 376 | "metric": { 377 | "message": "公制" 378 | }, 379 | "middleClickHidesTooltip": { 380 | "message": "中鍵點擊 (在後台打開) 隱藏面板" 381 | }, 382 | "monthAgo": { 383 | "message": "個月前" 384 | }, 385 | "monthsAgo": { 386 | "message": "$MONTHS$ 幾個月前", 387 | "placeholders": { 388 | "months": { 389 | "content": "$1" 390 | } 391 | } 392 | }, 393 | "moveDownLabel": { 394 | "message": "下移" 395 | }, 396 | "moveDownTooltipEffect": { 397 | "message": "下移" 398 | }, 399 | "moveUpLabel": { 400 | "message": "上移" 401 | }, 402 | "moveUpTooltipEffect": { 403 | "message": "上移" 404 | }, 405 | "noDefinitionFound": { 406 | "message": "未找到定義" 407 | }, 408 | "noTooltipEffect": { 409 | "message": "沒有效果" 410 | }, 411 | "noTranslationFound": { 412 | "message": "沒有找到翻譯" 413 | }, 414 | "onlyicon": { 415 | "message": "僅圖示" 416 | }, 417 | "onlylabel": { 418 | "message": "僅文字" 419 | }, 420 | "openInNewTab": { 421 | "message": "在新分頁中打開" 422 | }, 423 | "openLinkLabel": { 424 | "message": "打開" 425 | }, 426 | "overCursor": { 427 | "message": "游標上方" 428 | }, 429 | "panelCustomSearchStyle": { 430 | "message": "在主面板中顯示" 431 | }, 432 | "pasteLabel": { 433 | "message": "貼上" 434 | }, 435 | "performSimpleMathOperations": { 436 | "message": "執行簡單的數學計算" 437 | }, 438 | "preferCurrencySymbol": { 439 | "message": "首選貨幣符號而不是程式碼" 440 | }, 441 | "preferredMapsService": { 442 | "message": "首選地圖服務" 443 | }, 444 | "preferredMetricsSystem": { 445 | "message": "首選計量單位" 446 | }, 447 | "preferredNewEmailMethod": { 448 | "message": "建立新電子郵件的首選方法" 449 | }, 450 | "preferredSearchEngine": { 451 | "message": "搜尋引擎" 452 | }, 453 | "preferredTranslateService": { 454 | "message": "翻譯服務" 455 | }, 456 | "quote": { 457 | "message": "引用" 458 | }, 459 | "quoteButtonRemark": { 460 | "message": "(當頁面上找到支援的文字欄位時)" 461 | }, 462 | "recentMarkersLabel": { 463 | "message": "最近突顯" 464 | }, 465 | "recreateTooltipAfterScroll": { 466 | "message": "滾動或調整視窗大小後重新建立工具提示" 467 | }, 468 | "removeCustomIcon": { 469 | "message": "移除自訂圖示" 470 | }, 471 | "removeSelectionOnActionButtonClick": { 472 | "message": "選擇動作後刪除文字選擇" 473 | }, 474 | "resetDefaults": { 475 | "message": "復原預設值" 476 | }, 477 | "reverseTooltipButtonsOrder": { 478 | "message": "反轉按鈕的次序" 479 | }, 480 | "scaleUpFromBottomTooltipEffect": { 481 | "message": "縱向擴展 (從底部)" 482 | }, 483 | "scaleUpTooltipEffect": { 484 | "message": "縱向擴展" 485 | }, 486 | "searchLabel": { 487 | "message": "搜尋" 488 | }, 489 | "searchingDefinitions": { 490 | "message": "搜尋定義" 491 | }, 492 | "secondaryTooltipEnabled": { 493 | "message": "已啟用工具提示" 494 | }, 495 | "secondaryTooltipIconSize": { 496 | "message": "圖示大小 (px)" 497 | }, 498 | "secondaryTooltipLayout": { 499 | "message": "按鈕佈局" 500 | }, 501 | "selectionCenter": { 502 | "message": "選擇中心" 503 | }, 504 | "selectionHeader": { 505 | "message": "文字選擇" 506 | }, 507 | "selectonSettings": { 508 | "message": "SelectON 設定" 509 | }, 510 | "shadowOpacity": { 511 | "message": "陰影不透明度" 512 | }, 513 | "shiftTooltipWhenWebsiteHasOwn": { 514 | "message": "當網站顯示自己的文字選擇時,嘗試向上移動工具提示" 515 | }, 516 | "shouldOverrideWebsiteSelectionColor": { 517 | "message": "覆蓋頁面的突出顯示顏色" 518 | }, 519 | "showButtonBorders": { 520 | "message": "顯示按鈕分隔符號" 521 | }, 522 | "showButtonLabelOnHover": { 523 | "message": "懸停時顯示按鈕標籤" 524 | }, 525 | "showDictionaryButton": { 526 | "message": "顯示字典按鈕" 527 | }, 528 | "showDotForHoverButtons": { 529 | "message": "在具有懸停事件的按鈕上顯示點" 530 | }, 531 | "showEmailButton": { 532 | "message": "新增電子郵件按鈕" 533 | }, 534 | "showInfoPanel": { 535 | "message": "顯示資訊面板 (單字和符號計數)" 536 | }, 537 | "showOnMap": { 538 | "message": "在地圖上顯示" 539 | }, 540 | "showOnMapButtonEnabled": { 541 | "message": "新增按鈕以顯示在地圖上" 542 | }, 543 | "showSecondaryTooltipTitleOnHover": { 544 | "message": "懸停時顯示標題" 545 | }, 546 | "showStatsOnCopyButtonHover": { 547 | "message": "在複製按鈕懸停時顯示統計資訊" 548 | }, 549 | "showTooltipArrow": { 550 | "message": "顯示工具提示箭頭" 551 | }, 552 | "showTranslateButton": { 553 | "message": "顯示翻譯按鈕" 554 | }, 555 | "showTranslateIfLanguageUnknown": { 556 | "message": "如果未檢測到語言,則顯示翻譯按鈕" 557 | }, 558 | "showUnconvertedValue": { 559 | "message": "顯示轉換前的原始值" 560 | }, 561 | "showUpdateNotification": { 562 | "message": "顯示更新通知" 563 | }, 564 | "snapSelectionToWord": { 565 | "message": "按單字對齊文字選擇" 566 | }, 567 | "strikeLabel": { 568 | "message": "擊打" 569 | }, 570 | "symbolsCount": { 571 | "message": "符號" 572 | }, 573 | "testPageButton": { 574 | "message": "測試設定" 575 | }, 576 | "textFieldsHeader": { 577 | "message": "文字欄位" 578 | }, 579 | "textSelectionBackground": { 580 | "message": "選擇顏色" 581 | }, 582 | "textSelectionBackgroundOpacity": { 583 | "message": "背景不透明度" 584 | }, 585 | "textSelectionColor": { 586 | "message": "文字顏色" 587 | }, 588 | "today": { 589 | "message": "今天" 590 | }, 591 | "tooltipBackground": { 592 | "message": "背景顏色" 593 | }, 594 | "tooltipInvertedBackground": { 595 | "message": "背景顏色 (深色頁面)" 596 | }, 597 | "tooltipOpacity": { 598 | "message": "工具提示不透明度" 599 | }, 600 | "tooltipPosition": { 601 | "message": "工具提示的水平對齊" 602 | }, 603 | "tooltipRevealEffect": { 604 | "message": "工具提示顯示效果" 605 | }, 606 | "translateLabel": { 607 | "message": "翻譯" 608 | }, 609 | "translating": { 610 | "message": "正在翻譯" 611 | }, 612 | "triangle": { 613 | "message": "三角形" 614 | }, 615 | "updateNotificationMessage": { 616 | "message": "點擊此處查看新功能" 617 | }, 618 | "updateNotificationTitle": { 619 | "message": "SelectON 已更新為 $VERSION$", 620 | "placeholders": { 621 | "version": { 622 | "content": "$1", 623 | "example": "1.0.0" 624 | } 625 | } 626 | }, 627 | "updatePageToSeeChanges": { 628 | "message": "重新整理頁面以查看更改" 629 | }, 630 | "updateRatesEveryDays": { 631 | "message": "費率更新間隔 (天)" 632 | }, 633 | "useCustomStyle": { 634 | "message": "為工具提示使用自訂樣式" 635 | }, 636 | "useIconFromGoogle": { 637 | "message": "使用來自 Google 的圖示" 638 | }, 639 | "verticalLayout": { 640 | "message": "垂直的" 641 | }, 642 | "verticalLayoutTooltip": { 643 | "message": "使用垂直工具提示佈局 (試驗性)" 644 | }, 645 | "verticalSecondaryTooltip": { 646 | "message": "顯示為垂直清單 (停用水平)" 647 | }, 648 | "visitGithub": { 649 | "message": "訪問 GitHub 頁面" 650 | }, 651 | "whatsNew": { 652 | "message": "有什麼新功能" 653 | }, 654 | "wordSnappingBlacklist": { 655 | "message": "要排除的網域" 656 | }, 657 | "wordsCount": { 658 | "message": "文字" 659 | }, 660 | "writeAReview": { 661 | "message": "寫評論" 662 | }, 663 | "yearAgo": { 664 | "message": "年前" 665 | }, 666 | "yearsAgo": { 667 | "message": "$YEARS$ 年前", 668 | "placeholders": { 669 | "years": { 670 | "content": "$1" 671 | } 672 | } 673 | } 674 | } -------------------------------------------------------------------------------- /src/assets/icons/button-icons/bold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/calendar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/call.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/clear.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/cut.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/dictionary.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/email.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/extend-selection.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/highlighter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/italic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/location.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/map.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/marker.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/paste.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/quote.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/strike.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/time.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/button-icons/translate.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/donate.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/logo-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emvaized/selecton-extension/3533a144f70a52b1538190dc40f0585fcd8db582/src/assets/icons/logo-new.png -------------------------------------------------------------------------------- /src/data/configs.js: -------------------------------------------------------------------------------- 1 | const configs = { 2 | 'convertToCurrency': 'USD', 3 | 'hideOnScroll': true, 4 | 'convertMetrics': true, 5 | 'addOpenLinks': true, 6 | 'addPasteButton': true, 7 | 'addExtendSelectionButton': true, 8 | 'convertCurrencies': true, 9 | 'convertTime': true, 10 | 'performSimpleMathOperations': false, 11 | 'preferredMetricsSystem': 'metric', 12 | 'showTranslateButton': true, 13 | 'languageToTranslate': navigator.language || navigator.userLanguage || 'en', 14 | 'ratesLastFetchedDate': '', 15 | 'useCustomStyle': false, 16 | 'tooltipBackground': '#333232', 17 | 'tooltipInvertedBackground': '#bfbfbf', 18 | 'tooltipOpacity': 1.0, 19 | 'addTooltipShadow': false, 20 | 'shadowOpacity': 0.5, 21 | 'borderRadius': 4, 22 | 'changeTextSelectionColor': false, 23 | 'textSelectionBackground': '#338FFF', 24 | 'textSelectionColor': '#ffffff', 25 | 'shiftTooltipWhenWebsiteHasOwn': false, 26 | 'addActionButtonsForTextFields': false, 27 | 'removeSelectionOnActionButtonClick': true, 28 | 'hideTooltipOnActionButtonClick': true, 29 | 'draggableTooltip': true, 30 | 'enabled': true, 31 | 'preferredSearchEngine': 'google', 32 | 'hideOnKeypress': true, 33 | 'middleClickHidesTooltip': false, 34 | 'showOnMapButtonEnabled': true, 35 | 'showEmailButton': true, 36 | 'preferredNewEmailMethod': 'mailto', 37 | 'customSearchUrl': '', 38 | 'preferredMapsService': 'google', 39 | 'addColorPreviewButton': true, 40 | 'secondaryTooltipEnabled': true, 41 | 'secondaryTooltipIconSize': 16, 42 | 'showSecondaryTooltipTitleOnHover': false, 43 | 'excludedDomains': '', 44 | 'addPhoneButton': true, 45 | 'showUnconvertedValue': false, 46 | 'debugMode': false, 47 | 'buttonsStyle': 'onlylabel', /// Possible: onlylabel, onlyicon, iconlabel 48 | 'addDragHandles': true, 49 | 'snapSelectionToWord': true, 50 | 'preferCurrencySymbol': false, 51 | 'shouldOverrideWebsiteSelectionColor': false, 52 | 'disableWordSnappingOnCtrlKey': true, 53 | 'showButtonLabelOnHover': true, 54 | 'animationDuration': 200, 55 | 'wordSnappingBlacklist': '', 56 | 'disableWordSnapForCode': false, 57 | 'dontSnapTextfieldSelection': true, 58 | 'tooltipRevealEffect': 'moveUpTooltipEffect', /// Possible values: 'moveUpTooltipEffect', 'moveDownTooltipEffect', 'scaleUpTooltipEffect', 'scaleUpFromBottomTooltipEffect', 'noTooltipEffect' 59 | 'textSelectionBackgroundOpacity': 1.0, 60 | 'updateRatesEveryDays': 18, 61 | 'fontSize': 12.5, 62 | 'maxIconsInRow': 5, 63 | 'secondaryTooltipLayout': 'verticalLayout', /// Possible values: 'horizontalLayout', 'verticalLayout' 64 | 'liveTranslation': true, 65 | // 'reverseTooltipButtonsOrder': false, 66 | 'recreateTooltipAfterScroll': false, 67 | 'applyConfigsImmediately': false, 68 | 'invertColorOnDarkWebsite': true, 69 | 'addPasteOnlyEmptyField': true, 70 | 'hideTranslateButtonForUserLanguage': true, 71 | 'showTranslateIfLanguageUnknown': true, 72 | 'fullOpacityOnHover': true, 73 | 'preferredTranslateService': 'google', 74 | 'tooltipPosition': 'selectionCenter', /// Possible values: 'selectionCenter', 'overCursor' 75 | 'floatingOffscreenTooltip': false, 76 | 'showUpdateNotification': true, 77 | 'convertResultClickAction': 'search', /// Possible values: 'copy', 'search' 78 | 'delayToRevealSearchTooltip': 350, 79 | 'delayToRevealTranslateTooltip': 550, 80 | 'delayToRevealHoverPanels': 700, 81 | 'showDictionaryButton': true, 82 | 'showDotForHoverButtons': true, 83 | 'collapseButtons': true, 84 | 'addMarkerButton': true, 85 | 'showStatsOnCopyButtonHover': true, 86 | 'addFontFormatButtons': true, 87 | 'showButtonBorders': true, 88 | 'addClearButton': true, 89 | 'leftClickBackgroundTab': false, 90 | 'showTooltipArrow': true, 91 | 'verticalLayoutTooltip': false, 92 | 'addButtonToCopyLinkToText': true, 93 | 'addCalendarButton': true, 94 | 'hideTooltipWhenCursorMovesAway': false, 95 | 'showInfoPanel': true, 96 | 'maxTooltipButtonsToShow': 3, 97 | 'maxMarkerPagesToStore': 10, 98 | 'dictionaryButtonWordsAmount': 1, 99 | 'dictionaryButtonResponseCharsAmount': 300, 100 | 'correctTooltipPositionByMoreButtonWidth': true, 101 | 'addQuoteButton': true, 102 | 'dragHandleStyle': 'circle', /// possible values: circle, triangle, 103 | 'customSearchOptionsDisplay': 'hoverCustomSearchStyle', /// Possible values: 'hoverCustomSearchStyle', 'panelCustomSearchStyle' 104 | 'collapseAsSecondPanel': false, 105 | 'translateSingleWordsImmediately': false, 106 | 'customSearchButtons': [ 107 | { 108 | 'url': 'https://www.youtube.com/results?search_query=%s', 109 | 'title': 'YouTube', 110 | // 'icon': 'https://icons-for-free.com/iconfiles/png/512/video+youtube+icon-1320192294490006733.png', 111 | 'enabled': true 112 | }, 113 | { 114 | 'url': 'https://open.spotify.com/search/%s', 115 | 'title': 'Spotify', 116 | // 'icon': 'https://image.flaticon.com/icons/png/512/2111/2111624.png', 117 | 'enabled': true 118 | }, 119 | { 120 | 'url': 'https://aliexpress.ru/wholesale?catId=&SearchText=%s', 121 | 'title': 'Ali (ru)', 122 | 'icon': 'https://img.icons8.com/color/452/aliexpress.png', 123 | 'enabled': true 124 | }, 125 | { 126 | 'url': 'https://www.aliexpress.com/wholesale?SearchText=%s', 127 | 'title': 'Ali (en)', 128 | // 'icon': 'https://img.icons8.com/color/452/aliexpress.png', 129 | 'enabled': false 130 | }, 131 | { 132 | 'url': 'https://www.amazon.com/s?k=%s', 133 | 'title': 'Amazon', 134 | 'icon': 'https://mapleleafdeals.com/wp-content/uploads/2020/08/amazon.png', 135 | 'enabled': true 136 | }, 137 | { 138 | 'url': 'https://wikipedia.org/w/index.php?search=%s', 139 | 'title': 'Wikipedia', 140 | // 'icon': 'https://pngimg.com/uploads/wikipedia/wikipedia_PNG16.png', 141 | 'enabled': false 142 | }, 143 | { 144 | 'url': 'https://www.imdb.com/find?s=alt&q=%s', 145 | 'title': 'IMDB', 146 | // 'icon': 'https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/171_Imdb_logo_logos-512.png', 147 | 'enabled': false 148 | }, 149 | { 150 | 'url': 'https://google.com/search?q=site:%w %s', 151 | 'title': 'Search on website', 152 | 'enabled': false 153 | }, 154 | ] 155 | }; -------------------------------------------------------------------------------- /src/data/currencies.js: -------------------------------------------------------------------------------- 1 | /// List of currencies with various keywords to look for 2 | /// 'rate' should be provided in comparison to United States Dollars (USD) 3 | /// 4 | /// New rates will be downloaded automatically with from network by looking for each currency key in server response 5 | /// Period of update specified in configs.updateRatesEveryDays 6 | 7 | /** URLs for loading currency rates 8 | * Currencies are fetched in {@link fetchCurrencyRates} in src/function/background.js 9 | * {@link urlToLoadCurrencyRates}. 10 | * {@link urlToLoadCryptoCurrencies}. 11 | */ 12 | 13 | const currenciesList = { 14 | "AUD": { currencyName: "Australian Dollar", currencySymbol: "A$", rate: 1.29009, currencyKeywords: ['australian dollar', 'австралийских доллар'] }, 15 | "BGN": { currencyName: "Bulgarian Lev", currencySymbol: "лв", rate: 1.640562 }, 16 | "BRL": { currencyName: "Brazilian real", currencySymbol: "R$", rate: 5.616101 }, 17 | "BTC": { currencyName: "Bitcoin", rate: 0.000018, currencyKeywords: ['bitcoins', 'биткоин'] }, 18 | "BYN": { currencyName: "Belarussian Ruble", rate: 2.596137, currencyKeywords: ['белорусских рублей'] }, 19 | "CAD": { currencyName: "Canadian Dollar", currencySymbol: "C$", rate: 1.269384, currencyKeywords: ['canadian dollar', 'канадских доллар'] }, 20 | "CHF": { currencyName: "Swiss Franc", currencySymbol: "CHF", rate: 0.926525 }, 21 | "CNY": { currencyName: "Chinese Yuan", currencySymbol: "¥", rate: 6.497301, currencyKeywords: ['yuan', 'юаней'] }, 22 | "CRC": { currencyName: "Costa Rican Colon", currencySymbol: "₡", rate: 610.339772 }, 23 | "CZK": { currencyName: "Czech Koruna", currencySymbol: "Kč", rate: 21.936455 }, 24 | "DKK": { currencyName: "Danish Krone", currencySymbol: " kr", rate: 6.229502 }, 25 | "EUR": { currencyName: "Euro", currencySymbol: "€", rate: 0.8378, currencyKeywords: ['euro', 'евро'], }, 26 | "GBP": { currencyName: "British Pound", currencySymbol: "£", rate: 0.721124, currencyKeywords: ['фунтов стерлингов', 'british pound'], }, 27 | "HKD": { currencyName: "Hong Kong dollar", currencySymbol: "HK$", rate: 7.765632 }, 28 | "HUF": { currencyName: "Hungarian forint", rate: 316.005504 }, 29 | "IDR": { currencyName: "Indonesian Rupiah", currencySymbol: "Rp", rate: 15711.86182839, currencyKeywords: ['Rp', 'Rupiah'] }, 30 | "ILS": { currencyName: "Israeli New Sheqel", currencySymbol: "₪", rate: 3.310401 }, 31 | "INR": { currencyName: "Indian Rupee", currencySymbol: "₹", rate: 72.452006, currencyKeywords: ['rupees', 'рупий'], }, 32 | "IRR": { currencyName: "Iranian Rial", currencySymbol: "﷼", rate: 42105.017329 }, 33 | "JPY": { currencyName: "Japanese Yen", currencySymbol: "¥", rate: 109.188027, currencyKeywords: [' yen', ' йен'] }, 34 | "KRW": { currencyName: "South Korean Won", currencySymbol: "₩", rate: 1193.057307 }, 35 | "KPW": { currencyName: "North Korean Won", currencySymbol: "₩", rate: 900.00022 }, 36 | "KZT": { currencyName: "Kazakhstani Tenge", currencySymbol: "₸", rate: 418.821319, currencyKeywords: ['тенге'] }, 37 | "MNT": { currencyName: "Mongolian Tugrik", currencySymbol: "₮", rate: 2849.930035 }, 38 | "MXN": { currencyName: "Mexican Peso", currencySymbol: "peso", rate: 20.655212, currencyKeywords: ['peso', 'песо'] }, 39 | "MYR": { currencyName: "Malaysian Ringgit", currencySymbol: "RM", rate: 4.208613, currencyKeywords: ['myr'] }, 40 | "NGN": { currencyName: "Nigerian Naira", currencySymbol: "₦", rate: 410.317377 }, 41 | "NOK": { currencyName: "Norwegian Krone", currencySymbol: " kr", rate: 8.51191 }, 42 | "PHP": { currencyName: "Philippine Peso", currencySymbol: "₱", rate: 56.012, currencyKeywords: ['pesos', 'php'], searchInText: false}, 43 | "PLN": { currencyName: "Polish złoty", currencySymbol: "zł", rate: 3.845051 }, 44 | "RON": { currencyName: "Romanian leu", currencySymbol: "leu", rate: 5.058587 }, 45 | "RUB": { currencyName: "Russian Ruble", currencySymbol: "₽", rate: 72.880818, currencyKeywords: ['rubles', 'рублей', 'руб', ' р.'] }, 46 | "SAR": { currencyName: "Saudi Riyal", currencySymbol: "﷼", rate: 3.750694 }, 47 | "SEK": { currencyName: "Swedish Krona", currencySymbol: " kr", rate: 8.514027 }, 48 | "THB": { currencyName: "Thai Baht", currencySymbol: "฿", rate: 34.700854, currencyKeywords: ['THB', 'Baht', 'baht'] }, 49 | "TRY": { currencyName: "Turkish Lira", currencySymbol: "₺", rate: 0.14 }, 50 | "TWD": { currencyName: "New Taiwan dollar", currencySymbol: "NT$", rate: 31.99368752 }, 51 | "UAH": { currencyName: "Ukrainian Hryvnia", currencySymbol: "₴", rate: 27.852288, currencyKeywords: ['hryvnia', 'гривен', 'грн'] }, 52 | "USD": { currencyName: "United States Dollar", currencySymbol: "$", rate: 1, currencyKeywords: ['dollar', 'dolar', 'доллар'] }, 53 | "VND": { currencyName: "Vietnamese Dong", currencySymbol: "₫", rate: 23054.385489 }, 54 | "ZAR": { currencyName: "Rand", rate: 14.856969 }, 55 | 56 | /// Crypto 57 | "ETH": { currencyName: "Ethereum", rate: 0.0003208, crypto: true }, 58 | "LTC": { currencyName: "Litecoin", rate: 0.006242, crypto: true }, 59 | "ADA": { currencyName: "Cardano", rate: 0.4492, crypto: true }, 60 | "MIOTA": { currencyName: "MIOTA", rate: 0.7418, crypto: true }, 61 | "EOS": { currencyName: "EOS", rate: 0.2336, crypto: true }, 62 | "BCH": { currencyName: "BCH", rate: 0.001843, crypto: true }, 63 | "XRP": { currencyName: "XRP", rate: 1.016, crypto: true }, 64 | "ZEC": { currencyName: "ZEC", rate: 0.008209, crypto: true }, 65 | "XMR": { currencyName: "XMR", rate: 0.004037, crypto: true }, 66 | "ZCL": { currencyName: "XMR", rate: 7.348, crypto: true }, 67 | "DOGE": { currencyName: "DOGE", rate: 4.537, crypto: true }, 68 | } 69 | -------------------------------------------------------------------------------- /src/data/keywords.js: -------------------------------------------------------------------------------- 1 | /// Look for these words to find that selected text is address, in order to show "Show on map" button 2 | const addressKeywords = [ 3 | /// English keywords 4 | ' street', 5 | 'broadway', 6 | ' st.', 7 | 'str.', 8 | ' city', 9 | 10 | /// Russian 11 | 'ул. ', 12 | 'пр. ', 13 | 'улица ', 14 | 'переулок ', 15 | 'город ', 16 | 'проспект ', 17 | 'жк ', 18 | 'трц ', 19 | 20 | /// Ukrainian 21 | 'вулиця ', 22 | 'вул.', 23 | 'м. ', 24 | 'місто ', 25 | 'трк ', 26 | 27 | /// Belorussian 28 | 'вуліца ', 29 | 'горад ', 30 | 'праспект ', 31 | 32 | /// Spanish 33 | 'calle ', 34 | 'ciudad ', 35 | 36 | /// French 37 | 'ville ', 38 | ' rue', 39 | 40 | /// German 41 | 'straße', 42 | 'strasse', 43 | ' stadt', 44 | ]; 45 | 46 | 47 | /// Literal multipliers for numeric values 48 | /// With the help of these, "2 thousand" will be converted to "2000" 49 | const thousandMultipliers = [ 50 | 'thousand', 51 | 'тысяч', 52 | 'тыс', 53 | ]; 54 | 55 | const millionMultipliers = [ 56 | 'million', 57 | 'millón', 58 | 'millones', 59 | 'млн', 60 | 'миллион', 61 | 'мільйон', 62 | ]; 63 | 64 | const billionMultipliers = [ 65 | 'billion', 66 | 'milliard', 67 | 'mil millones', 68 | 'млрд', 69 | 'миллиард', 70 | 'більйон', 71 | 'мільярд', 72 | ]; 73 | 74 | 75 | /// Unit conversion units 76 | /// Each key is a keyword, which will be searched for in the selected text 77 | /// 'ratio' is the ratio to multiply, in order to get the value in 'covertsTo' 78 | /// Temperature units provide "convertFunction" instead - code will look for this if selected value contains "°" 79 | const convertionUnits = { 80 | "inch": { 81 | "convertsTo": "cm", 82 | "ratio": 2.54, 83 | "type": "imperial", 84 | "variations": [ 85 | "pouces", /// fr 86 | "pulgadas", /// sp 87 | "дюймов", /// ru 88 | "дюйма", 89 | ] 90 | }, 91 | "feet": { 92 | "convertsTo": "m", 93 | "ratio": 0.3048, 94 | "type": "imperial", 95 | "variations": [ 96 | " ft", 97 | " foot", 98 | "pieds", /// fr 99 | "pies", /// sp 100 | ' фута', 101 | "футов" 102 | ] 103 | }, 104 | "pound": { 105 | "convertsTo": "kg", 106 | "ratio": 0.453592, 107 | "variations": [ 108 | " lb", 109 | "lbs", 110 | " libras", /// sp 111 | " livres", /// fr 112 | " фунтов", /// ru 113 | " фунта", 114 | ] 115 | }, 116 | "mph": { 117 | "convertsTo": "km/h", 118 | "ratio": 1.60934, 119 | }, 120 | " mile": { 121 | "convertsTo": "km", 122 | "ratio": 1.60934, 123 | "variations": [ 124 | 'millas', /// sp 125 | 'milles', /// fr 126 | /// rus 127 | ' миль', 128 | ' мили', 129 | ], 130 | }, 131 | "yard": { 132 | "convertsTo": "m", 133 | "variations": [ 134 | ' yd', 135 | ' ярдов', 136 | ], 137 | "ratio": 0.9144, 138 | }, 139 | " oz": { 140 | "convertsTo": "gr", 141 | "ratio": 28.3495, 142 | "variations": [ 143 | 'oz.', 144 | ' унций', 145 | ' унции', 146 | ' унция', 147 | ], 148 | }, 149 | " qt": { 150 | "convertsTo": "L", 151 | "ratio": 0.95, 152 | "variations": [ 153 | ' quarts', 154 | ], 155 | }, 156 | " gal": { 157 | "convertsTo": "L", 158 | "ratio": 4.54609, 159 | "variations": [ 160 | ' gallon', 161 | ' галлон', 162 | ' галон', 163 | ], 164 | }, 165 | "°F": { 166 | "convertsTo": "°C", 167 | "convertFunction": function (value) { 168 | if (configs.preferredMetricsSystem == 'metric') 169 | return (value - 32) * (5 / 9); 170 | return (value * 9 / 5) + 32; 171 | }, 172 | }, 173 | "°K": { 174 | "convertsTo": "°C", 175 | "convertFunction": function (value) { 176 | return value - 273.15; 177 | }, 178 | }, 179 | }; 180 | 181 | /// Unit conversion units when preferred system is imprerial 182 | const imprerialConvertionUnits = { 183 | "cm": { 184 | "convertsTo": "inch", 185 | "ratio": 2.54, 186 | "variations": [ 187 | "см", /// ru 188 | ] 189 | }, 190 | "meter": { 191 | "convertsTo": "ft.", 192 | "ratio": 0.3048, 193 | "variations": [ 194 | " m.", 195 | " metros", // sp 196 | " mètres", // fr 197 | ] 198 | }, 199 | "kg": { 200 | "convertsTo": "lbs", 201 | "ratio": 0.453592, 202 | "variations": [ 203 | " kilogram", 204 | ] 205 | }, 206 | "km/h": { 207 | "convertsTo": "mph", 208 | "ratio": 1.60934, 209 | }, 210 | "km": { 211 | "convertsTo": "miles", 212 | "ratio": 1.60934, 213 | "variations": [ 214 | 'killometer', 215 | 'kilometr', 216 | 'kilómetros', 217 | ], 218 | }, 219 | " gr": { 220 | "convertsTo": "oz", 221 | "ratio": 28.3495, 222 | "variations": [ 223 | ' gramm', 224 | ' gramos', 225 | ], 226 | }, 227 | " liters": { 228 | "convertsTo": "gal", 229 | "ratio": 4.54609, 230 | }, 231 | "°C": { 232 | "convertsTo": "°F", 233 | "convertFunction": function (value) { 234 | return (value * 9 / 5) + 32; 235 | }, 236 | }, 237 | "°K": { 238 | "convertsTo": "°F", 239 | "convertFunction": function (value) { 240 | return value * (9 / 5) - 459.67; 241 | }, 242 | }, 243 | }; 244 | 245 | /// Convert timezones 246 | const timeZoneKeywords = { 247 | 'GMT': 'GMT', 248 | 'UTC': 'UTC', 249 | 'WET': 'UTC', 250 | 'AKST': '-0900', 251 | 'PST': '-0800', 252 | 'PDT': '-0700', 253 | 'MST': '-0700', 254 | 'MDT': '-0600', 255 | 'CST': '-0600', 256 | 'EST': '-0500', 257 | 'AST': '-0400', 258 | 'EDT': '-0400', 259 | 'NST': '-0330', 260 | 'HAST': '-1000', 261 | 'AEST': '+1000', 262 | 'CET': '+0100', 263 | 'WAT': '+0100', 264 | 'BST': '+0100', 265 | 'MET': '+0100', 266 | 'CEST': '+0200', 267 | 'EET': '+0200', 268 | 'EEST': '+0200', 269 | 'EET': '+0200', 270 | 'CAT': '+0200', 271 | 'MSK': '+0300', 272 | 'EAT': '+0300', 273 | 'IST': '+0530', 274 | 'AWST': '+0800', 275 | 'JST': '+0900', 276 | 'KST': '+0900', 277 | 'ACST': '+0930', 278 | 279 | /// Russian keywords 280 | 'по Московскому времени': '+0300', 281 | 'по московскому времени': '+0300', 282 | 'по Москве': '+0300', 283 | 'по центральноевропейскому времени': '+0100', 284 | 'по европейскому времени': '+0100', 285 | 'по тихоокеанскому времени': '-0800', 286 | 'по Гринвичу': 'GMT', 287 | }; 288 | 289 | 290 | /// Those will be ignored when looking for URL in selected text 291 | /// So that, for example, when selected "somefile.txt" - it won't be recognized as a website for "Open link" button 292 | const filetypesToIgnoreAsDomains = [ 293 | "txt", 294 | "zip", 295 | "rar", 296 | "7z", 297 | "mp3", 298 | "mp4", 299 | "png", 300 | "jpg", 301 | "gif", 302 | "wav", 303 | "exe", 304 | "cfg", 305 | "ini", 306 | "js", 307 | "html", 308 | "css", 309 | "log", 310 | "php", 311 | ]; 312 | 313 | /* 314 | Search for these keywords to detect if selected text looks like code (in order to disable word snapping) 315 | Another possible solution is to use regex: 316 | const codeRegex = /[;{}()\[\]]|\b(?:function|var|let|const|if|else|for|while|return|switch|case|break)\b|=[^=]|\+\+|--|\+[^+]|-[^-]|\*|\/[^/]|%|&&|\|\||\b\d+\b|["'`].*?["'`]|\/\/.*?$|\/\*.*?\*\//; 317 | */ 318 | const codeMarkers = [ 319 | 'const ', 320 | 'var ', 321 | 'let ', 322 | 'async ', 323 | 'await ', 324 | '/>', 325 | '{', 326 | '}', 327 | '()', 328 | ' = ', 329 | `='`, 330 | `="`, 331 | `('`, 332 | `("`, 333 | `": "`, 334 | '//', 335 | '/*', 336 | ]; 337 | 338 | /// Search for these keywords to detect dates in the selected text 339 | /// For each language, number of keywords should match the whole amount (12 for 'month', 7 for 'weekday' etc.) 340 | const dateKeywords = { 341 | 'month': [ 342 | 'jan', 343 | 'feb', 344 | 'mar', 345 | 'apr', 346 | 'may', 347 | 'june', 348 | 'july', 349 | 'aug', 350 | 'sept', 351 | 'oct', 352 | 'nov', 353 | 'dec', 354 | ///russian 355 | 'янв', 356 | 'фев', 357 | 'март', 358 | 'апр', 359 | 'мая', 360 | 'июн', 361 | 'июл', 362 | 'авг', 363 | 'сен.', 364 | 'окт.', 365 | 'ноя.', 366 | 'дек.', 367 | ///esp 368 | 'enero', 369 | 'feb.', 370 | 'marzo', 371 | 'abr.', 372 | 'mayo', 373 | 'jun.', 374 | 'jul.', 375 | 'agosto', 376 | 'set.', 377 | 'oct.', 378 | 'nov.', 379 | 'dic.', 380 | ], 381 | 'weekday': [ 382 | 'monday', 383 | 'tuesday', 384 | 'wednesday', 385 | 'thursday', 386 | 'friday', 387 | 'saturday', 388 | 'sunday', 389 | ///rus 390 | 'понедельник', 391 | 'вторник', 392 | 'среда', 393 | 'четверг', 394 | 'пятница', 395 | 'суббота', 396 | 'воскресенье', 397 | ///es 398 | 'lunes', 399 | 'martes', 400 | 'miércoles', 401 | 'jueves', 402 | 'viernes', 403 | 'sábado', 404 | 'domingo', 405 | ], 406 | 'tomorrow': [ 407 | 'tomorrow', 408 | 'завтра', 409 | 'mañana', 410 | 'demain', 411 | ], 412 | }; 413 | -------------------------------------------------------------------------------- /src/data/search-urls.js: -------------------------------------------------------------------------------- 1 | function returnSearchUrl(query, shouldEncode = true) { 2 | let encodedQuery = query; 3 | if (shouldEncode) 4 | encodedQuery = encodeURI(query); 5 | 6 | encodedQuery = encodedQuery.replaceAll('&', '%26').replaceAll('+', '%2B'); 7 | 8 | switch (configs.preferredSearchEngine) { 9 | case 'google': return `https://www.google.com/search?q=${encodedQuery}`; break; 10 | case 'duckduckgo': return `https://duckduckgo.com/?q=${encodedQuery}`; break; 11 | case 'bing': return `https://www.bing.com/search?q=${encodedQuery}`; break; 12 | case 'yandex': return `https://yandex.ru/search/?text=${encodedQuery}`; break; 13 | case 'baidu': return `http://www.baidu.com/s?wd=${encodedQuery}`; break; 14 | case 'yahoo': return `https://search.yahoo.com/search?p=${encodedQuery}`; break; 15 | case 'custom': return configs.customSearchUrl.replaceAll('%s', encodedQuery); break; 16 | } 17 | } 18 | 19 | function returnNewEmailUrl(query, shouldEncode = true) { 20 | let encodedQuery = query; 21 | 22 | if (shouldEncode) 23 | encodedQuery = encodeURI(query); 24 | 25 | encodedQuery = encodedQuery.replaceAll('&', '%26').replaceAll('+', '%2B'); 26 | 27 | switch (configs.preferredNewEmailMethod) { 28 | case 'mailto': return `mailto:${query}`; break; 29 | case 'gmail': return `https://mail.google.com/mail/?view=cm&fs=1&to=${encodedQuery}`; break; 30 | case 'yahoo': return `https://compose.mail.yahoo.com/?to=${encodedQuery}`; break; 31 | case 'outlook': return `https://outlook.com/?path=/mail/action/compose&to=${encodedQuery}`; break; 32 | } 33 | } 34 | 35 | function returnShowOnMapUrl(query, shouldEncode = true) { 36 | let encodedQuery = query; 37 | 38 | if (shouldEncode) 39 | encodedQuery = encodeURI(query); 40 | 41 | encodedQuery = encodedQuery.replaceAll('&', '%26').replaceAll('+', '%2B'); 42 | 43 | switch (configs.preferredMapsService) { 44 | case 'google': return `https://www.google.com/maps/place/${encodedQuery}`; break; 45 | case '2gis': return `https://2gis.ua/search/${encodedQuery}`; break; 46 | case '2gisRU': return `https://2gis.ru/search/${encodedQuery}`; break; 47 | case '2gisUA': return `https://2gis.ua/search/${encodedQuery}`; break; 48 | case 'yandexmaps': return `https://yandex.ru/maps/geo/${encodedQuery}`; break; 49 | case 'waze': return `https://www.waze.com/ru/live-map?q=${encodedQuery}`; break; 50 | case 'mapquest': return `https://www.mapquest.com/search/results?query=${encodedQuery}`; break; 51 | } 52 | } 53 | 54 | function returnTranslateUrl(query, languageToTranslateTo) { 55 | let textToPass = encodeURI(query.trim()); 56 | 57 | switch (configs.preferredTranslateService) { 58 | case 'google': return `https://translate.google.com/?sl=auto&tl=${languageToTranslateTo ?? configs.languageToTranslate}&text=${textToPass}`; break; 59 | case 'yandex': return `https://translate.yandex.ru/?lang=auto-${languageToTranslateTo ?? configs.languageToTranslate}&text=${textToPass}`; break; 60 | case 'bing': return `https://www.bing.com/translator?from=auto&to=${languageToTranslateTo ?? configs.languageToTranslate}&text=${textToPass}`; break; 61 | case 'deepl': return `https://www.deepl.com/translator#auto/${languageToTranslateTo ?? configs.languageToTranslate}/${textToPass}`; break; 62 | } 63 | } -------------------------------------------------------------------------------- /src/data/tooltip-icons.js: -------------------------------------------------------------------------------- 1 | const searchButtonIcon = chrome.runtime.getURL('assets/icons/button-icons/search.svg'); 2 | const copyButtonIcon = chrome.runtime.getURL('assets/icons/button-icons/copy.svg'); 3 | const translateButtonIcon = chrome.runtime.getURL('assets/icons/button-icons/translate.svg'); 4 | const dictionaryButtonIcon = chrome.runtime.getURL('assets/icons/button-icons/dictionary.svg'); 5 | const openLinkButtonIcon = chrome.runtime.getURL('assets/icons/button-icons/open.svg'); 6 | const cutButtonIcon = chrome.runtime.getURL('assets/icons/button-icons/cut.svg'); 7 | const pasteButtonIcon = chrome.runtime.getURL('assets/icons/button-icons/paste.svg'); 8 | const mapButtonIcon = chrome.runtime.getURL('assets/icons/button-icons/map.svg'); 9 | const emailButtonIcon = chrome.runtime.getURL('assets/icons/button-icons/email.svg'); 10 | const phoneIcon = chrome.runtime.getURL('assets/icons/button-icons/call.svg'); 11 | const boldTextIcon = chrome.runtime.getURL('assets/icons/button-icons/bold.svg'); 12 | const italicTextIcon = chrome.runtime.getURL('assets/icons/button-icons/italic.svg'); 13 | const strikeTextIcon = chrome.runtime.getURL('assets/icons/button-icons/strike.svg'); 14 | const clockIcon = chrome.runtime.getURL('assets/icons/button-icons/time.svg'); 15 | // const markerIcon = chrome.runtime.getURL('assets/icons/button-icons/marker.svg'); 16 | const markerIcon = chrome.runtime.getURL('assets/icons/button-icons/highlighter.svg'); 17 | const clearIcon = chrome.runtime.getURL('assets/icons/button-icons/clear.svg'); 18 | const linkIcon = chrome.runtime.getURL('assets/icons/button-icons/link.svg'); 19 | const calendarIcon = chrome.runtime.getURL('assets/icons/button-icons/calendar.svg'); 20 | const quoteIcon = chrome.runtime.getURL('assets/icons/button-icons/quote.svg'); 21 | const extendSelectionIcon = chrome.runtime.getURL('assets/icons/button-icons/extend-selection.svg'); -------------------------------------------------------------------------------- /src/data/variables.js: -------------------------------------------------------------------------------- 1 | /// Currently non user-configurable settings 2 | var convertWhenOnlyFewWordsSelected = true; 3 | var wordsLimitToProccessText = 5; 4 | var secondaryColor = 'lightBlue'; 5 | var linkSymbolsToShow = 20; 6 | var selectionHandleLineHeight = 21; 7 | 8 | /// Button labels – translations assigned in code 9 | var copyLabel = 'Copy'; 10 | var searchLabel = 'Search'; 11 | var openLinkLabel = 'Open'; 12 | var translateLabel = 'Translate'; 13 | var showOnMapLabel = 'Show on map'; 14 | var cutLabel = 'Cut'; 15 | var pasteLabel = 'Paste'; 16 | var dictionaryLabel = 'Dictionary'; 17 | var markerLabel = 'Highlight'; 18 | var italicLabel = 'Italic'; 19 | var boldLabel = 'Bold'; 20 | var strikeLabel = 'Strike'; 21 | var clearLabel = 'Clear'; 22 | 23 | /// Dynammically assigned variables 24 | var selection, selectedText; 25 | var tooltip, secondaryTooltip, arrow, infoPanel, searchButton, copyButton, verticalSecondaryTooltip; 26 | var tooltipIsShown = false, dontShowTooltip = false, isDraggingTooltip = false, isDraggingDragHandle = false, isDarkTooltip = true; 27 | var draggingHandleIndex, lastMouseUpEvent, ratesLastFetchedDate; 28 | var firstButtonBorderRadius = '3px 0px 0px 3px', lastButtonBorderRadius = '0px 3px 3px 0px', onlyButtonBorderRadius = '3px'; 29 | var browserLanguage, browserCurrency, browserMetricSystem; 30 | var tooltipOnBottom = false, configsWereLoaded = false, currencyRatesWereLoaded = false, isTextFieldFocused = false; 31 | var isTextFieldEmpty = true, domainIsBlacklistedForSnapping, selectedTextIsCode, addButtonIcons; 32 | var timerToRecreateOverlays, delayToRecreateOverlays = 150; 33 | var floatingTooltipTop = false, floatingTooltipBottom = false; -------------------------------------------------------------------------------- /src/functions/background.js: -------------------------------------------------------------------------------- 1 | /// Listener to open url in new tab 2 | chrome.runtime.onMessage.addListener( 3 | function (request, sender, sendResponse) { 4 | 5 | /// Open url in new tab next to current one 6 | if (request.type == 'selecton-open-new-tab') { 7 | chrome.tabs.create({ 8 | url: request.url, active: request.focused, index: sender.tab.index + 1 9 | }); 10 | return true; 11 | } else if (request.type == 'selecton-no-clipboard-permission-message') { 12 | displayNotification('Clipboard access was not granted', 'Could not paste to this field without clipboard access'); 13 | return true; 14 | } else if (request.type == 'check_currencies') { 15 | fetchCurrencyRates(request.debugMode, request.currenciesList); 16 | } else if (request.type == 'background_fetch') { 17 | backgroundFetch(request.url, function(result){ 18 | sendResponse(result); 19 | }); 20 | return true; 21 | } 22 | } 23 | ); 24 | 25 | /// Show notification on extension update 26 | chrome.runtime.onInstalled.addListener(function (details) { 27 | if (details.reason == 'update' && !details.temporary) { 28 | // show notification on extension update 29 | let shouldShowNotification = true; 30 | const storageKey = 'showUpdateNotification'; 31 | 32 | chrome.storage.local.get([storageKey], function (val) { 33 | if (val[storageKey] !== null && val[storageKey] !== undefined) 34 | shouldShowNotification = val[storageKey]; 35 | 36 | if (shouldShowNotification) { 37 | // get manifest for new version number 38 | const manifest = chrome.runtime.getManifest(); 39 | 40 | // show update notification and open changelog on click 41 | displayNotification( 42 | chrome.i18n.getMessage('updateNotificationTitle', manifest.version), 43 | chrome.i18n.getMessage('updateNotificationMessage'), 44 | "https://github.com/emvaized/selecton-extension/blob/master/CHANGELOG.md" 45 | ); 46 | } 47 | }); 48 | } 49 | }); 50 | 51 | /** 52 | * displays a browser notification 53 | * opens an URL on click if specified 54 | **/ 55 | function displayNotification(title, message, link, image) { 56 | chrome.notifications.create({ 57 | "type": "basic", 58 | "iconUrl": image ?? "./assets/icons/logo-new.png", 59 | "title": title, 60 | "message": message, 61 | }, function (notificationId) { 62 | // if an URL is specified register an onclick listener 63 | if (link) 64 | chrome.notifications.onClicked.addListener(function handleNotificationClick(id) { 65 | if (id === notificationId) { 66 | chrome.tabs.create({ 67 | url: link, 68 | active: true 69 | }); 70 | // remove event listener 71 | chrome.notifications.onClicked.removeListener(handleNotificationClick); 72 | } 73 | }); 74 | }); 75 | } 76 | 77 | 78 | /// Load currencies rates in background 79 | const urlToLoadCurrencyRates = 'https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json'; 80 | const urlToLoadCryptoCurrencies = 'https://min-api.cryptocompare.com/data/price?fsym=USD'; 81 | let isUpdating = false; 82 | 83 | async function fetchCurrencyRates(debugMode, currenciesList) { 84 | if (!currenciesList) return; 85 | if (isUpdating) return; 86 | if (debugMode) console.log('Selecton needs to update currency rates...'); 87 | if (debugMode) console.log(currenciesList); 88 | isUpdating = true; 89 | 90 | let urlToFetch = urlToLoadCurrencyRates; 91 | 92 | let today = new Date(); 93 | const offset = today.getTimezoneOffset() 94 | today = new Date(today.getTime() - (offset * 60 * 1000)) 95 | today = today.toISOString().split('T')[0]; 96 | 97 | const ratesObject = {}; 98 | 99 | try { 100 | /// Fetch regular currencies 101 | const response = await fetch(urlToFetch,{ 102 | method: "GET", 103 | // mode: "cors" 104 | }) 105 | 106 | if (!response.ok) throw new Error(`An error has occured: ${response.status}`); 107 | const jsonObj = await response.json(); 108 | const date = jsonObj['date']; 109 | 110 | if (!date) { 111 | if (debugMode) console.log('Error while fetching currency rates from network'); 112 | return; 113 | } 114 | 115 | const val = jsonObj['usd']; 116 | const cryptoCurrencies = []; 117 | 118 | let keys = Object.keys(currenciesList); 119 | for (let i = 0, l = keys.length; i < l; i++) { 120 | let key = keys[i], lowerCaseKey = keys[i].toLowerCase(); 121 | 122 | if (currenciesList[key]['crypto'] == true) 123 | cryptoCurrencies.push(key); 124 | 125 | try { 126 | if (val[lowerCaseKey] == null || val[lowerCaseKey] == undefined) continue; 127 | currenciesList[key]['rate'] = val[lowerCaseKey]; 128 | ratesObject[key] = val[lowerCaseKey]; 129 | } catch (e) { 130 | if (debugMode) console.log(e); 131 | } 132 | } 133 | 134 | if (debugMode) console.log('Fetched regular currencies successfully'); 135 | 136 | /// Fetch crypto currencies 137 | urlToFetch = urlToLoadCryptoCurrencies; 138 | 139 | const listOfParams = cryptoCurrencies.join(','); 140 | urlToFetch += '&tsyms=' + listOfParams; 141 | 142 | if (listOfParams.length > 0) 143 | try { 144 | // await fetchCryptoRates(urlToFetch, ratesObject); 145 | const cryptoResponse = await fetch(urlToFetch); 146 | if (!cryptoResponse.ok) throw new Error(`An error has occured: ${cryptoResponse.status}`); 147 | const cryptoVal = await cryptoResponse.json(); 148 | 149 | for (let i = 0, l = cryptoCurrencies.length; i < l; i++) { 150 | try { 151 | let currency = cryptoCurrencies[i]; 152 | if (cryptoVal[currency] == null || cryptoVal[currency] == undefined) continue; 153 | currenciesList[currency]['rate'] = cryptoVal[currency]; 154 | ratesObject[currency] = cryptoVal[currency]; 155 | } catch (e) { console.log(e); } 156 | } 157 | 158 | if (debugMode) console.log('Fetched crypto currencies successfully'); 159 | } catch (e) { if (debugMode) console.log('Failed to fetch crypto currencies: ' + e.toString()); } 160 | 161 | /// Save rates to memory 162 | if (Object.keys(ratesObject).length > 0) 163 | chrome.storage.local.set({ 164 | //'ratesLastFetchedDate': date, 165 | 'ratesLastFetchedDate': today, 166 | 'rates': ratesObject 167 | }); 168 | 169 | if (debugMode) { 170 | console.log('Updated currency rates from network:'); 171 | console.log(ratesObject); 172 | console.log('Saved date of last rates fetch:'); 173 | console.log(date); 174 | } 175 | 176 | 177 | } catch (error) { 178 | if (debugMode) { 179 | console.log('Error while loading currencies from network:'); 180 | console.log(error); 181 | } 182 | } 183 | 184 | isUpdating = false; 185 | } 186 | 187 | async function backgroundFetch(url, callback){ 188 | try { 189 | const response = await fetch(url); 190 | if (!response.ok) { 191 | throw new Error(`HTTP error! Status: ${response.status}`); 192 | } 193 | const data = await response.json(); 194 | callback(data); 195 | } catch (error) { 196 | console.error('Fetch error:', error); 197 | } 198 | } -------------------------------------------------------------------------------- /src/functions/clipboard-functions.js: -------------------------------------------------------------------------------- 1 | function getCurrentClipboard() { 2 | const activeElemenet = document.activeElement; 3 | 4 | let clipboardContent; 5 | const input = document.createElement('input'); 6 | input.setAttribute('style', 'position: fixed; top: 0px; left: 0px; opacity: 0;') 7 | document.body.appendChild(input); 8 | input.focus(); 9 | document.execCommand('paste'); 10 | clipboardContent = input.value; 11 | document.execCommand('undo'); 12 | input.blur(); 13 | document.body.removeChild(input); 14 | activeElemenet.focus(); 15 | 16 | return clipboardContent; 17 | } 18 | 19 | function copyManuallyToClipboard(text) { 20 | try { 21 | const input = document.createElement('input'); 22 | input.setAttribute('style', `position: fixed; top: 0px; left: 0px; opacity: 0.0;`) 23 | document.body.appendChild(input); 24 | input.value = text; 25 | input.focus(); 26 | input.select(); 27 | document.execCommand('Copy'); 28 | // document.body.removeChild(input); 29 | input.remove(); 30 | } catch (e) { 31 | navigator.clipboard.writeText(text); 32 | } 33 | } -------------------------------------------------------------------------------- /src/functions/color-functions.js: -------------------------------------------------------------------------------- 1 | /// Returns white for dark background, and black for bright 2 | function getTextColorForBackground(color) { 3 | var c = hexToRgb(color); 4 | 5 | // var d = 0; 6 | var luminance = 7 | (0.299 * c.red + 0.587 * c.green + 0.114 * c.blue) / 255; 8 | if (luminance > 0.5) { 9 | /// bright color - black font 10 | isDarkTooltip = false; 11 | // d = 0; // bright colors - black font 12 | } 13 | else { 14 | /// dark color - white font 15 | // d = 255; 16 | isDarkTooltip = true; 17 | } 18 | 19 | // return rgbToHex(d, d, d); 20 | //return rgbToHex(d, d, d); 21 | } 22 | 23 | function hexToRgb(hex) { 24 | var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 25 | return result ? { 26 | red: parseInt(result[1], 16), 27 | green: parseInt(result[2], 16), 28 | blue: parseInt(result[3], 16) 29 | } : null; 30 | } 31 | 32 | function rgbToHex(r, g, b) { 33 | return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); 34 | } -------------------------------------------------------------------------------- /src/functions/css-path-for-node.js: -------------------------------------------------------------------------------- 1 | /// Retrieve CSS path for passed node 2 | /// Source: https://gist.github.com/asfaltboy/8aea7435b888164e8563 3 | 4 | /* 5 | * Copyright (C) 2015 Pavel Savshenko 6 | * Copyright (C) 2011 Google Inc. All rights reserved. 7 | * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. 8 | * Copyright (C) 2008 Matt Lilek 9 | * Copyright (C) 2009 Joseph Pecoraro 10 | * 11 | * Redistribution and use in source and binary forms, with or without 12 | * modification, are permitted provided that the following conditions 13 | * are met: 14 | * 15 | * 1. Redistributions of source code must retain the above copyright 16 | * notice, this list of conditions and the following disclaimer. 17 | * 2. Redistributions in binary form must reproduce the above copyright 18 | * notice, this list of conditions and the following disclaimer in the 19 | * documentation and/or other materials provided with the distribution. 20 | * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of 21 | * its contributors may be used to endorse or promote products derived 22 | * from this software without specific prior written permission. 23 | * 24 | * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY 25 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 26 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 28 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 30 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 31 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | */ 35 | 36 | var UTILS = {}; 37 | UTILS.cssPath = function (node, optimized) { 38 | if (node.nodeType !== Node.ELEMENT_NODE) 39 | return ""; 40 | var steps = []; 41 | var contextNode = node; 42 | while (contextNode) { 43 | var step = UTILS._cssPathStep(contextNode, !!optimized, contextNode === node); 44 | if (!step) 45 | break; // Error - bail out early. 46 | steps.push(step); 47 | if (step.optimized) 48 | break; 49 | contextNode = contextNode.parentNode; 50 | } 51 | steps.reverse(); 52 | return steps.join(" > "); 53 | } 54 | UTILS._cssPathStep = function (node, optimized, isTargetNode) { 55 | if (node.nodeType !== Node.ELEMENT_NODE) 56 | return null; 57 | 58 | var id = node.getAttribute("id"); 59 | if (optimized) { 60 | if (id) 61 | return new UTILS.DOMNodePathStep(idSelector(id), true); 62 | var nodeNameLower = node.nodeName.toLowerCase(); 63 | if (nodeNameLower === "body" || nodeNameLower === "head" || nodeNameLower === "html") 64 | return new UTILS.DOMNodePathStep(node.nodeName.toLowerCase(), true); 65 | } 66 | var nodeName = node.nodeName.toLowerCase(); 67 | 68 | if (id) 69 | return new UTILS.DOMNodePathStep(nodeName.toLowerCase() + idSelector(id), true); 70 | var parent = node.parentNode; 71 | if (!parent || parent.nodeType === Node.DOCUMENT_NODE) 72 | return new UTILS.DOMNodePathStep(nodeName.toLowerCase(), true); 73 | 74 | /** 75 | * @param {UTILS.DOMNode} node 76 | * @return {Array.} 77 | */ 78 | function prefixedElementClassNames(node) { 79 | var classAttribute = node.getAttribute("class"); 80 | if (!classAttribute) 81 | return []; 82 | 83 | return classAttribute.split(/\s+/g).filter(Boolean).map(function (name) { 84 | // The prefix is required to store "__proto__" in a object-based map. 85 | return "$" + name; 86 | }); 87 | } 88 | 89 | /** 90 | * @param {string} id 91 | * @return {string} 92 | */ 93 | function idSelector(id) { 94 | return "#" + escapeIdentifierIfNeeded(id); 95 | } 96 | 97 | /** 98 | * @param {string} ident 99 | * @return {string} 100 | */ 101 | function escapeIdentifierIfNeeded(ident) { 102 | if (isCSSIdentifier(ident)) 103 | return ident; 104 | var shouldEscapeFirst = /^(?:[0-9]|-[0-9-]?)/.test(ident); 105 | var lastIndex = ident.length - 1; 106 | return ident.replace(/./g, function (c, i) { 107 | return ((shouldEscapeFirst && i === 0) || !isCSSIdentChar(c)) ? escapeAsciiChar(c, i === lastIndex) : c; 108 | }); 109 | } 110 | 111 | /** 112 | * @param {string} c 113 | * @param {boolean} isLast 114 | * @return {string} 115 | */ 116 | function escapeAsciiChar(c, isLast) { 117 | return "\\" + toHexByte(c) + (isLast ? "" : " "); 118 | } 119 | 120 | /** 121 | * @param {string} c 122 | */ 123 | function toHexByte(c) { 124 | var hexByte = c.charCodeAt(0).toString(16); 125 | if (hexByte.length === 1) 126 | hexByte = "0" + hexByte; 127 | return hexByte; 128 | } 129 | 130 | /** 131 | * @param {string} c 132 | * @return {boolean} 133 | */ 134 | function isCSSIdentChar(c) { 135 | if (/[a-zA-Z0-9_-]/.test(c)) 136 | return true; 137 | return c.charCodeAt(0) >= 0xA0; 138 | } 139 | 140 | /** 141 | * @param {string} value 142 | * @return {boolean} 143 | */ 144 | function isCSSIdentifier(value) { 145 | return /^-?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value); 146 | } 147 | 148 | var prefixedOwnClassNamesArray = prefixedElementClassNames(node); 149 | var needsClassNames = false; 150 | var needsNthChild = false; 151 | var ownIndex = -1; 152 | var siblings = parent.children; 153 | for (var i = 0; (ownIndex === -1 || !needsNthChild) && i < siblings.length; ++i) { 154 | var sibling = siblings[i]; 155 | if (sibling === node) { 156 | ownIndex = i; 157 | continue; 158 | } 159 | if (needsNthChild) 160 | continue; 161 | if (sibling.nodeName.toLowerCase() !== nodeName.toLowerCase()) 162 | continue; 163 | 164 | needsClassNames = true; 165 | var ownClassNames = prefixedOwnClassNamesArray; 166 | var ownClassNameCount = 0; 167 | for (var name in ownClassNames) 168 | ++ownClassNameCount; 169 | if (ownClassNameCount === 0) { 170 | needsNthChild = true; 171 | continue; 172 | } 173 | var siblingClassNamesArray = prefixedElementClassNames(sibling); 174 | for (var j = 0; j < siblingClassNamesArray.length; ++j) { 175 | var siblingClass = siblingClassNamesArray[j]; 176 | if (ownClassNames.indexOf(siblingClass)) 177 | continue; 178 | delete ownClassNames[siblingClass]; 179 | if (!--ownClassNameCount) { 180 | needsNthChild = true; 181 | break; 182 | } 183 | } 184 | } 185 | 186 | var result = nodeName.toLowerCase(); 187 | if (isTargetNode && nodeName.toLowerCase() === "input" && node.getAttribute("type") && !node.getAttribute("id") && !node.getAttribute("class")) 188 | result += "[type=\"" + node.getAttribute("type") + "\"]"; 189 | if (needsNthChild) { 190 | result += ":nth-child(" + (ownIndex + 1) + ")"; 191 | } else if (needsClassNames) { 192 | for (var prefixedName in prefixedOwnClassNamesArray) 193 | // for (var prefixedName in prefixedOwnClassNamesArray.keySet()) 194 | result += "." + escapeIdentifierIfNeeded(prefixedOwnClassNamesArray[prefixedName].substr(1)); 195 | } 196 | 197 | return new UTILS.DOMNodePathStep(result, false); 198 | } 199 | 200 | /** 201 | * @constructor 202 | * @param {string} value 203 | * @param {boolean} optimized 204 | */ 205 | UTILS.DOMNodePathStep = function (value, optimized) { 206 | this.value = value; 207 | this.optimized = optimized || false; 208 | } 209 | 210 | UTILS.DOMNodePathStep.prototype = { 211 | /** 212 | * @return {string} 213 | */ 214 | toString: function () { 215 | return this.value; 216 | } 217 | } -------------------------------------------------------------------------------- /src/functions/currencies-functions.js: -------------------------------------------------------------------------------- 1 | // URLs and requests moved to background.js 2 | 3 | function fetchCurrencyRates() { 4 | chrome.runtime.sendMessage({ type: 'check_currencies', currenciesList: currenciesList, debugMode: configs.debugMode }); 5 | } 6 | 7 | function loadCurrencyRatesFromMemory() { 8 | if (currencyRatesWereLoaded) return; 9 | 10 | chrome.storage.local.get('rates', function (val) { 11 | if (val == null || val == undefined || val == {}) return; 12 | 13 | const loadedRates = val['rates']; 14 | const keys = Object.keys(currenciesList); 15 | 16 | for (let i = 0, l = keys.length; i < l; i++) { 17 | try { 18 | let key = keys[i]; 19 | let rate = loadedRates[key]; 20 | if (rate !== undefined && rate !== null) 21 | currenciesList[key]['rate'] = rate; 22 | } catch (e) { } 23 | } 24 | 25 | currencyRatesWereLoaded = true; 26 | 27 | if (configs.debugMode) { 28 | console.log('Selecton currency rates loaded from memory:'); 29 | console.log(loadedRates); 30 | } 31 | }); 32 | } -------------------------------------------------------------------------------- /src/functions/hover-buttons-functions.js: -------------------------------------------------------------------------------- 1 | /// Functions used for buttons with on-hover functionality 2 | /// First of all, Live translate and Dictionary buttons 3 | 4 | function addAstrixToHoverButton(button) { 5 | if (configs.showDotForHoverButtons == false) return; 6 | 7 | /// add astrix indicator when hover enabled 8 | const astrix = document.createElement('span'); 9 | astrix.className = 'selecton-hover-button-indicator'; 10 | button.appendChild(astrix); 11 | return astrix; 12 | } 13 | 14 | function showHoverIndicator(indicator) { 15 | if (configs.showDotForHoverButtons == false) return; 16 | indicator.style.opacity = 0.3; 17 | } 18 | 19 | function hideHoverIndicator(indicator) { 20 | if (configs.showDotForHoverButtons == false) return; 21 | indicator.style.opacity = 0; 22 | } 23 | 24 | 25 | function createHoverPanelForButton(button, initialHtml, onHoverCallback, reverseOrder = false, revealAfterDelay = true, pinOnClick = false, unknownHeight = true, staticPanelMode = false) { 26 | let timerToRemovePanel, timeoutToRevealPanel; 27 | let hoverIndicator = revealAfterDelay ? addAstrixToHoverButton(button) : undefined; 28 | const staticPanelVerticalShift = configs.showInfoPanel ? '107%' : '112%'; 29 | 30 | /// Set panel 31 | let panel = document.createElement('div'); 32 | panel.className = 'hover-vertical-tooltip selecton-entity'; 33 | panel.style.borderRadius = `${configs.useCustomStyle ? configs.borderRadius : 3}px`; 34 | panel.style.opacity = 0; 35 | panel.style.visibility = 'collapse'; 36 | panel.style.width = '0px'; 37 | // panel.style.display = 'none'; 38 | panel.style.pointerEvents = 'none'; 39 | panel.style.width = 'max-content'; 40 | 41 | if (initialHtml) 42 | panel.innerHTML = initialHtml; 43 | 44 | if (tooltipOnBottom) 45 | panel.style.top = staticPanelMode ? staticPanelVerticalShift : '125%'; 46 | else 47 | panel.style.bottom = staticPanelMode ? staticPanelVerticalShift : '125%'; 48 | 49 | if (reverseOrder) { 50 | /// specially for the Search button 51 | panel.style.left = '0px'; 52 | } else { 53 | panel.style.right = '0px'; 54 | } 55 | 56 | /// Add panel shadow 57 | if (configs.addTooltipShadow) { 58 | panel.style.boxShadow = `0 1px 5px rgba(0,0,0,${configs.shadowOpacity / 1.5})`; 59 | } 60 | 61 | /// Checks to execute after panel was added to the DOM 62 | let dxTransformValue = configs.verticalLayoutTooltip ? '2px' : 63 | (reverseOrder ? '-2px' : '2px'); 64 | let panelOnBottom = false; 65 | 66 | setTimeout(function () { 67 | // if (!tooltipIsShown) return; 68 | if (!panel.isConnected) return; 69 | 70 | /// Check if panel will go off-screen 71 | if (!configs.verticalLayoutTooltip) { 72 | if (tooltipOnBottom) { 73 | panelOnBottom = true; 74 | movePanelToBottom(panel, button); 75 | } else { 76 | panelOnBottom = checkHoverPanelToOverflowOnTop(panel, button); 77 | } 78 | 79 | /// Clip content on edge for better looking animation 80 | if (unknownHeight && button) 81 | button.classList.add(panelOnBottom ? 'button-with-bottom-hover-panel' : 'button-with-top-hover-panel'); 82 | 83 | /// If button is not alone in the tooltip, and located in the start, align hover panel to the left 84 | // if (!reverseOrder) { 85 | // if (!button.classList.contains('button-with-border') && button.parentNode.children.length > 1) { 86 | // panel.style.left = '0px'; 87 | // panel.style.right = 'unset'; 88 | // dxTransformValue = '-2px'; 89 | // } 90 | // } 91 | } 92 | 93 | /// Set initial transform position for panel 94 | panel.style.transform = configs.verticalLayoutTooltip ? `translate(-100%, 0)` : `translate(${dxTransformValue}, ${panelOnBottom ? -100 : 100}%)`; 95 | }, configs.animationDuration); 96 | 97 | /// Show panel initially 98 | if (staticPanelMode) 99 | setTimeout(()=>{ 100 | if (button) button.classList.toggle('highlighted-popup-button'); 101 | revealHoverPanel(panel, dxTransformValue); 102 | if (revealAfterDelay) hideHoverIndicator(hoverIndicator); 103 | if (onHoverCallback) onHoverCallback(); 104 | }, configs.animationDuration) 105 | 106 | /// Set mouse listeners 107 | if (button) { 108 | const delayToRevealOnHover = revealAfterDelay ? (configs.delayToRevealHoverPanels ?? 700) : 3; 109 | let panelIsPinned = false; 110 | 111 | if (pinOnClick) 112 | button.addEventListener('mousedown', function (e) { 113 | e.stopPropagation(); 114 | panelIsPinned = !panelIsPinned; 115 | button.classList.toggle('highlighted-popup-button'); 116 | }); 117 | 118 | button.addEventListener('mouseover', function () { 119 | try { 120 | clearTimeout(timerToRemovePanel); 121 | clearTimeout(timeoutToRevealPanel); 122 | } catch (e) { } 123 | timerToRemovePanel = null; 124 | 125 | timeoutToRevealPanel = setTimeout(function () { 126 | revealHoverPanel(panel, dxTransformValue); 127 | if (revealAfterDelay) hideHoverIndicator(hoverIndicator); 128 | if (onHoverCallback) onHoverCallback(); 129 | }, delayToRevealOnHover); 130 | }); 131 | 132 | button.addEventListener('mouseout', function () { 133 | if (panelIsPinned) return; 134 | clearTimeout(timeoutToRevealPanel); 135 | 136 | timerToRemovePanel = setTimeout(function () { 137 | if (!panel) return; 138 | 139 | hideHoverPanel(panel, dxTransformValue, panelOnBottom); 140 | if (revealAfterDelay) showHoverIndicator(hoverIndicator); 141 | }, 150); 142 | }); 143 | 144 | button.addEventListener('mousedown', function (e) { 145 | try { 146 | clearTimeout(timeoutToRevealPanel); 147 | } catch (e) { } 148 | }) 149 | } 150 | 151 | function checkHoverPanelToOverflowOnTop(panel, button) { 152 | /// check to hover panel overflow on screen top 153 | try { 154 | if (panel.getBoundingClientRect().top < 0) { 155 | movePanelToBottom(panel, button); 156 | return true; 157 | } else return false; 158 | } catch (e) { return false; } 159 | } 160 | 161 | function movePanelToBottom(panel, button) { 162 | panel.style.bottom = 'unset'; 163 | panel.style.top = staticPanelMode ? staticPanelVerticalShift : '125%'; 164 | 165 | if (button) 166 | button.classList.add('higher-z-index'); 167 | else if (panel.parentNode) 168 | panel.parentNode.classList.add('higher-z-index'); 169 | } 170 | 171 | function checkHoverPanelHorizontalOverflow(panel) { 172 | try { 173 | const panRect = panel.getBoundingClientRect(); 174 | /// check overflow on right screen edge 175 | const rightOverflow = window.innerWidth - panRect.left - (panRect.width * 2); 176 | if (rightOverflow < 0) { 177 | if (configs.verticalLayoutTooltip){ 178 | panel.style.transform = 'translate(-215%, 0)'; 179 | } 180 | return true; 181 | } else if(panRect.left < 0){ 182 | /// check overflow on left screen edge 183 | panel.style.right = `${panRect.left}px`; 184 | } else return false; 185 | } catch (e) { return false; } 186 | } 187 | 188 | 189 | function revealHoverPanel(panel, dxTransformValue) { 190 | if (panel.style.opacity > 0) return; 191 | panel.style.width = 'max-content'; 192 | panel.style.visibility = 'visible'; 193 | 194 | setTimeout(function () { 195 | panel.style.opacity = 1; 196 | panel.style.transform = `translate(${dxTransformValue},0)`; 197 | 198 | // if (configs.verticalLayoutTooltip || staticPanelMode) 199 | checkHoverPanelHorizontalOverflow(panel); 200 | 201 | setTimeout(function () { 202 | if (!panel || !tooltipIsShown) return; 203 | panel.style.pointerEvents = 'all'; 204 | }, configs.animationDuration); 205 | }, 3); 206 | } 207 | 208 | function hideHoverPanel(panel, dxTransformValue, panelOnBottom) { 209 | panel.style.transform = configs.verticalLayoutTooltip ? `translate(-100%, 0)` : `translate(${dxTransformValue}, ${panelOnBottom ? -100 : 100}%)`; 210 | panel.style.opacity = 0.0; 211 | panel.style.pointerEvents = 'none'; 212 | 213 | setTimeout(function () { 214 | if (!panel || !tooltipIsShown) return; 215 | panel.style.width = '0'; 216 | panel.style.visibility = 'collapse'; 217 | // panel.style.display = 'none'; 218 | 219 | }, configs.animationDuration); 220 | } 221 | 222 | return panel; 223 | } -------------------------------------------------------------------------------- /src/functions/locale-functions.js: -------------------------------------------------------------------------------- 1 | function setDefaultLocales() { 2 | 3 | /// Set default currency and language according to browser's locale 4 | let browserLocale = navigator.language || navigator.userLanguage; 5 | let browserCountry; 6 | 7 | if (configs.debugMode) { 8 | console.log('Browser locale is: ' + browserLocale); 9 | console.log('Configuring default locale settings...'); 10 | } 11 | 12 | browserLocale.replaceAll(' ', ''); 13 | if (browserLocale.includes('-')) { 14 | let parts = browserLocale.split('-'); 15 | browserLanguage = parts[0]; 16 | browserCountry = parts[1]; 17 | } else { 18 | browserLanguage = browserLocale; 19 | browserCountry = browserLocale.toUpperCase(); 20 | } 21 | 22 | if (browserCountry !== null && browserCountry !== undefined && browserCountry !== '') { 23 | 24 | /// Set default metric system 25 | if (browserCountry == 'US') 26 | browserMetricSystem = 'imperial'; 27 | else browserMetricSystem = 'metric'; 28 | 29 | /// Set default currency 30 | // Object.keys(currenciesList).forEach(function (key) { 31 | // if (key.includes(browserCountry)) browserCurrency = key; 32 | // }); 33 | 34 | let keys = Object.keys(currenciesList); 35 | for (let i = 0, l = keys.length; i < l; i++) { 36 | let key = keys[i]; 37 | if (key.includes(browserCountry)) browserCurrency = key; 38 | } 39 | 40 | if (configs.debugMode) { 41 | console.log(`Default browser language: ${browserLanguage}`); 42 | console.log(`Default browser metrics: ${browserMetricSystem}`); 43 | console.log(`Default browser currency: ${browserCurrency}`); 44 | console.log('Saved default locales to memory'); 45 | } 46 | } 47 | 48 | /// Save measured locales to memory 49 | chrome.storage.local.set({ 50 | 'convertToCurrency': browserCurrency || 'USD', 51 | 'languageToTranslate': browserLanguage || 'en', 52 | 'preferredMetricsSystem': browserMetricSystem || 'metric', 53 | }); 54 | } -------------------------------------------------------------------------------- /src/functions/marker-functions.js: -------------------------------------------------------------------------------- 1 | const markers = []; 2 | 3 | function createSelectionHighlightSpan(bg, fg, marker, scrollbarHint) { 4 | let span = document.createElement("span"); 5 | span.style.backgroundColor = bg ?? "yellow"; 6 | span.style.color = fg ?? "black"; 7 | span.style.position = 'relative'; 8 | span.className = 'selecton-marker-highlight'; 9 | 10 | if (configs.debugMode) 11 | setTimeout(function () { 12 | console.log('created text marker:'); 13 | console.log(span); 14 | }, 3); 15 | 16 | /// add delete button, shown on hover 17 | setTimeout(function () { 18 | let deleteButton = document.createElement('div'); 19 | deleteButton.className = 'marker-highlight-delete'; 20 | deleteButton.textContent = '✕'; 21 | deleteButton.title = chrome.i18n.getMessage('deleteLabel'); 22 | span.appendChild(deleteButton); 23 | 24 | if (marker.timeAdded) { 25 | let date = new Date(); 26 | date.setTime(marker.timeAdded); 27 | span.title = chrome.i18n.getMessage('markedLabel') + ' ' + date.toLocaleString(); 28 | } 29 | 30 | deleteButton.onclick = function () { 31 | try { 32 | /// remove scrollbar indicator 33 | scrollbarHint.remove(); 34 | 35 | /// remove highlight 36 | span.outerHTML = span.innerHTML; 37 | 38 | /// remove data 39 | const indexOfMarker = markers.indexOf(marker); 40 | if (indexOfMarker > -1) { 41 | markers.splice(indexOfMarker, 1); 42 | saveAllMarkers(); 43 | } 44 | 45 | } catch (e) { if (configs.debugMode) console.log(e); } 46 | } 47 | }, 100); 48 | 49 | return span; 50 | } 51 | 52 | function markTextSelection(bg, fg, text, restoredMarker) { 53 | /// Add hint next to the scrollbar 54 | const selectionRect = restoredMarker ? {} : getSelectionRectDimensions(); 55 | const minHintHeight = 10; 56 | let stringToSave = text; 57 | 58 | let scrollbarHint = document.createElement('div'); 59 | scrollbarHint.className = 'marker-scrollbar-hint'; 60 | scrollbarHint.style.backgroundColor = bg ?? "yellow"; 61 | 62 | let dyForHint = restoredMarker ? restoredMarker.hintDy : ((selectionRect.dy + window.scrollY) * window.innerHeight) / document.body.scrollHeight; 63 | if (dyForHint < 5) dyForHint = 5; 64 | if (dyForHint > window.innerHeight - 5) dyForHint = window.innerHeight - 5; 65 | scrollbarHint.style.top = `${dyForHint}px`; 66 | 67 | let hintHeight = restoredMarker ? restoredMarker.hintHeight : (selectionRect.height * window.innerHeight) / document.body.scrollHeight; 68 | if (hintHeight < minHintHeight) hintHeight = minHintHeight; 69 | scrollbarHint.style.height = `${hintHeight}px`; 70 | 71 | const markersOnTheSameHeight = markers.filter(marker => marker['hintDy'] === dyForHint); 72 | const markersOnTheSameHeightLength = markersOnTheSameHeight.length; 73 | 74 | /// Shift to the left when already added marker on this level 75 | let shift; 76 | if (markersOnTheSameHeightLength !== 0) { 77 | shift = 100 * markersOnTheSameHeightLength; 78 | scrollbarHint.style.transform = `translate(-${shift + 5}%, 0)`; 79 | } 80 | 81 | /// Add on-hover text display 82 | let hoverHint = document.createElement('span'); 83 | hoverHint.innerText = text; 84 | hoverHint.className = 'marker-scrollbar-tooltip'; 85 | hoverHint.style.maxWidth = `${window.innerWidth * 0.3}px`; 86 | hoverHint.style.maxHeight = `${window.innerHeight * 0.6}px`; 87 | scrollbarHint.appendChild(hoverHint); 88 | document.body.appendChild(scrollbarHint); 89 | 90 | /// Check if hover hint overflows 91 | const hoverHintRect = hoverHint.getBoundingClientRect(); 92 | if (hoverHintRect.top < 0) { 93 | hoverHint.classList.add('marker-scrollbar-tooltip-bottom'); 94 | } 95 | 96 | /// Store marker 97 | let containerSelector, range; 98 | if (restoredMarker) { 99 | containerSelector = restoredMarker.startContainer; 100 | } else { 101 | range = selection.getRangeAt(0); 102 | // let node = range.commonAncestorContainer; 103 | let node = range.startContainer; 104 | let containerElement = node.nodeType == 1 ? node : node.parentNode; 105 | 106 | if (range.startContainer != range.endContainer) { 107 | /// TODO: Cut text selection if it exceeds it's parent node 108 | 109 | let stringToCheck = ''; 110 | let firstNodeInnerHtml = containerElement.innerHTML; 111 | let indexToBreakText; 112 | 113 | for (let i = 0, l = text.length; i < l; i++) { 114 | stringToCheck += text[i]; 115 | 116 | if (firstNodeInnerHtml.includes(stringToCheck)) { 117 | continue; 118 | } else { 119 | indexToBreakText = i; 120 | break; 121 | } 122 | } 123 | 124 | if (indexToBreakText && indexToBreakText > 0) { 125 | stringToSave = stringToSave.substr(0, indexToBreakText); 126 | } 127 | 128 | /// limit the selection range 129 | range.setEnd(containerElement, 1); 130 | } 131 | containerSelector = getNodeSelector(containerElement); 132 | 133 | } 134 | 135 | let marker = restoredMarker ?? { 136 | 'hintDy': dyForHint, 137 | 'hintHeight': hintHeight, 138 | 'startContainer': containerSelector, 139 | 'background': bg ?? "yellow", 140 | 'foreground': fg ?? "black", 141 | // 'text': text, 142 | 'text': stringToSave, 143 | // 'timeAdded': restoredMarker ? (restoredMarker.timeAdded ?? '') : new Date().toISOString() 144 | 'timeAdded': restoredMarker ? (restoredMarker.timeAdded ?? '') : new Date().getTime() 145 | }; 146 | markers.push(marker); 147 | 148 | /// Add highlight to the marked text 149 | let span = createSelectionHighlightSpan(bg, fg, marker, scrollbarHint); 150 | 151 | if (restoredMarker) { 152 | const element = document.querySelector(restoredMarker.startContainer); 153 | if (!element) return; 154 | const innerHtml = element.innerHTML; 155 | let index = innerHtml.indexOf(text); 156 | // if (index == 0) index = 1; 157 | 158 | if (index !== undefined && index !== null && index > -1) { 159 | span.innerHTML = innerHtml.substring(index, index + text.length); 160 | element.innerHTML = innerHtml.substring(0, index); 161 | element.appendChild(span); 162 | element.insertAdjacentHTML('beforeend', innerHtml.substring(index + text.length, innerHtml.length)); 163 | } 164 | } else { 165 | const selectionContents = range.extractContents(); 166 | span.appendChild(selectionContents); 167 | range.insertNode(span); 168 | range.detach(); 169 | } 170 | 171 | /// Add on-click listener for the scrollbar hint 172 | scrollbarHint.onmousedown = function () { 173 | span.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'smooth' }); 174 | } 175 | } 176 | 177 | 178 | function getNodeSelector(el) { 179 | return UTILS.cssPath(el); 180 | 181 | // let names = []; 182 | // do { 183 | // let index = 0; 184 | // var cursorElement = el; 185 | // while (cursorElement !== null) { 186 | // ++index; 187 | // cursorElement = cursorElement.previousElementSibling; 188 | // }; 189 | // if (el.tagName !== undefined) 190 | // // names.unshift(el.tagName + ":nth-child(" + index + ")"); 191 | // names.unshift(el.tagName + ":nth-of-type(" + index + ")"); 192 | // el = el.parentElement; 193 | // } while (el !== null && el !== undefined); 194 | 195 | // return names.join(" > "); 196 | } 197 | 198 | function saveAllMarkers() { 199 | setTimeout(function () { 200 | if (markers) 201 | try { 202 | // chrome.storage.local.remove('websiteMarkers'); 203 | chrome.storage.local.get(['websiteMarkers'], function (value) { 204 | let existingMap = value['websiteMarkers']; 205 | if (!existingMap) existingMap = {}; 206 | 207 | if (markers.length <= 0) 208 | delete existingMap[window.location.href]; 209 | else { 210 | let markerKeys = Object.keys(existingMap); 211 | if (markerKeys.length == (configs.maxMarkerPagesToStore ?? 10)) 212 | delete existingMap[markerKeys[0]]; 213 | 214 | if (!existingMap[window.location.href]) existingMap[window.location.href] = {}; 215 | 216 | if (document.title) 217 | existingMap[window.location.href]['title'] = document.title; 218 | 219 | existingMap[window.location.href]['timeUpdated'] = new Date().getTime(); 220 | existingMap[window.location.href]['markers'] = markers; 221 | } 222 | 223 | chrome.storage.local.set({ 'websiteMarkers': existingMap }); 224 | }); 225 | } catch (e) { 226 | console.log(e); 227 | } 228 | }, 5); 229 | } 230 | 231 | function restoreMarkers() { 232 | if (configs.debugMode) { 233 | console.log('--------'); 234 | console.log('Searching for markers on current page...'); 235 | } 236 | 237 | chrome.storage.local.get(['websiteMarkers'], function (value) { 238 | 239 | if (configs.debugMode) { 240 | console.log('restored markers:'); 241 | console.log(value); 242 | } 243 | 244 | if (value['websiteMarkers'] && value['websiteMarkers'][window.location.href]) { 245 | let markersForCurrentPage = value['websiteMarkers'][window.location.href]['markers']; 246 | 247 | if (markersForCurrentPage) { 248 | 249 | if (configs.debugMode) { 250 | console.log('Found markers for current page:'); 251 | console.log(markersForCurrentPage); 252 | } 253 | 254 | const markersLength = markersForCurrentPage.length; 255 | 256 | if (markersLength && markersLength > 0) 257 | for (let i = 0; i < markersLength; i++) { 258 | const marker = markersForCurrentPage[i]; 259 | 260 | try { 261 | markTextSelection(marker.background, marker.foreground, marker.text, marker); 262 | } catch (e) { if (configs.debugMode) console.log(e); } 263 | } 264 | } 265 | } 266 | }); 267 | } 268 | 269 | function initMarkersRestore() { 270 | 271 | function init() { 272 | try { 273 | restoreMarkers(); 274 | } catch (e) { 275 | console.log(e); 276 | } 277 | 278 | /// scroll to marker command receiver 279 | try { 280 | chrome.runtime.onMessage.addListener(request => { 281 | // console.log("Message from the background script:"); 282 | // console.log(request.greeting); 283 | // return Promise.resolve({ response: "Hi from content script" }); 284 | 285 | if (request.command.includes('selecton-scroll-to-marker-message')) { 286 | const selectedHintDy = parseInt(request.command.split(':')[1]); 287 | if (!selectedHintDy || isNaN(selectedHintDy)) return; 288 | 289 | console.log('selectedHintDy'); 290 | console.log(selectedHintDy); 291 | 292 | const dyToScroll = selectedHintDy * document.body.scrollHeight / window.innerHeight; 293 | window.scrollTo(0, dyToScroll - (window.innerHeight / 2)); 294 | 295 | // hintDy = ((selectionRect.dy + window.scrollY) * window.innerHeight) / document.body.scrollHeight 296 | } 297 | }); 298 | 299 | 300 | } catch (e) { console.log(e); } 301 | } 302 | 303 | if (document.readyState === "complete" || document.readyState === 'interactive') { 304 | init(); 305 | } else document.addEventListener('DOMContentLoaded', init); 306 | 307 | } -------------------------------------------------------------------------------- /src/functions/text-functions.js: -------------------------------------------------------------------------------- 1 | const convertTime12to24 = (time12h) => { 2 | const [time, modifier] = time12h.split(' '); 3 | 4 | let [hours, minutes] = time.split(':'); 5 | 6 | if (hours === '12') { 7 | hours = '00'; 8 | } 9 | 10 | if (modifier === 'PM') { 11 | hours = parseInt(hours, 10) + 12; 12 | if (hours > 24 || hours < 0) return time12h; 13 | } else if (modifier !== 'AM') return time12h; 14 | 15 | if (isNaN(hours)) return time12h; 16 | 17 | return `${hours}:${minutes ?? '00'}`; 18 | } 19 | 20 | const convertTime24to12 = (time24h) => { 21 | 22 | let [hours, minutes] = time24h.split(':'); 23 | 24 | if (hours === '00') { 25 | hours = '12'; 26 | } 27 | 28 | let modifier = 'AM'; 29 | hours = parseInt(hours, 10); 30 | 31 | if (hours > 12) { 32 | hours = hours - 12; 33 | modifier = 'PM'; 34 | } 35 | 36 | if (isNaN(hours)) return time24h; 37 | 38 | return `${hours}:${minutes ?? '00'} ${modifier}`; 39 | } 40 | 41 | function splitNumberInGroups(stringNumber) { 42 | const parts = stringNumber.split('.'); 43 | parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, " "); 44 | if (parts[1] == '00') parts[1] = ''; /// Remove empty .00 on end 45 | return parts[1] == '' ? parts[0] : parts.join('.'); 46 | } 47 | 48 | function returnDomainFromUrl(url, firstLetterIsCapital = true) { 49 | if (url == null || url == undefined || url == '') return ''; 50 | 51 | try { 52 | let domainContent = url.split('.'); 53 | let titleText; 54 | 55 | if (domainContent.length == 2) { 56 | titleText = domainContent[0]; 57 | } else if (domainContent.length == 3) { 58 | if (domainContent[1].includes('/')) 59 | titleText = domainContent[0]; 60 | else 61 | titleText = domainContent[1]; 62 | } else { 63 | titleText = url.textContent.split('/')[2].split('.')[0]; 64 | } 65 | titleText = titleText.replaceAll('https://', ''); 66 | 67 | if (titleText == null || titleText == undefined) return ''; 68 | 69 | return firstLetterIsCapital == false ? titleText : titleText.charAt(0).toUpperCase() + titleText.slice(1); 70 | } catch (error) { 71 | return ''; 72 | } 73 | } 74 | 75 | 76 | /// Math calculation of selected string (potentially unsafe) 77 | function calculateString(fn) { 78 | return new Function('return ' + fn)(); 79 | } 80 | 81 | function extractNumber(str) { 82 | return parseFloat(str.replace(/[^\d\.]*/g, '')); 83 | } 84 | 85 | function isStringNumeric(n) { 86 | return !isNaN(parseFloat(n)) && isFinite(n); 87 | } 88 | 89 | function extractAmountFromSelectedText(selectedText) { 90 | let amount; 91 | 92 | try { 93 | let extracted = extractNumber(selectedText); 94 | if (extracted !== null && extracted !== undefined && extracted !== '' && !isNaN(extracted) && extracted !== 0 && extracted !== 0.0) 95 | amount = extracted; 96 | } catch (e) { } 97 | 98 | // let words = selectedText.split(' '); 99 | 100 | // for (i in words) { 101 | // let word = words[i].replaceAll('$', ''); 102 | // try { 103 | // if (isStringNumeric(word)) { 104 | // amount = extractNumber(word); 105 | // if (amount !== null && amount !== undefined && amount !== '' && amount !== NaN && amount !== 0 && amount !== 0.0) break; 106 | // } 107 | // } catch (e) { } 108 | // } 109 | 110 | return amount; 111 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* Tooltip */ 2 | .selection-tooltip { 3 | all: revert; 4 | background: var(--selecton-background-color); 5 | opacity: 0; 6 | display: inline-block; 7 | position: absolute; 8 | padding: var(--selecton-tooltip-inner-padding); 9 | color: white; 10 | border-radius: var(--selecton-border-radius); 11 | font-stretch: condensed; 12 | text-decoration: none; 13 | z-index: 100001 !important; 14 | /* max-height: 24px !important; */ 15 | margin: 0px !important; 16 | line-height: 1.0 !important; 17 | box-sizing: unset !important; 18 | border: 0.25px solid var(--selecton-outline-color); 19 | min-width: fit-content !important; 20 | white-space: nowrap !important; 21 | /* backdrop-filter: blur(5px) !important; */ 22 | } 23 | 24 | /* Background blur experiments */ 25 | /* .selection-tooltip::before { 26 | content: ''; 27 | position: absolute; 28 | top: 0; bottom: 0; left: 0; right: 0; 29 | backdrop-filter: blur(5px) !important; 30 | } 31 | 32 | .hover-vertical-tooltip { 33 | backdrop-filter: blur(5px) !important; 34 | }*/ 35 | 36 | 37 | /* Tooltip arrow */ 38 | .selection-tooltip-arrow { 39 | z-index: 100000 !important; 40 | height: 15px; 41 | width: 25px; 42 | overflow: hidden; 43 | position: absolute; 44 | background: none !important; 45 | box-shadow: none !important; 46 | left: 50%; 47 | top: 100%; 48 | transform: translate(-12.5px, 0px); 49 | } 50 | 51 | .arrow-on-bottom { 52 | top: unset; 53 | bottom: 100%; 54 | transform: rotate(180deg) translate(12.5px, 0px); 55 | } 56 | 57 | /* .selection-tooltip-arrow-child { */ 58 | .selection-tooltip-arrow::after { 59 | content: ""; 60 | position: absolute; 61 | left: 25%; 62 | bottom: 50%; 63 | width: 15px; 64 | height: 15px; 65 | transform: rotate(45deg); 66 | border-bottom: 0.25px solid var(--selecton-outline-color); 67 | border-right: 0.25px solid var(--selecton-outline-color); 68 | background: var(--selecton-background-color); 69 | 70 | /* apply clip-path for future blurred tooltip option */ 71 | /* clip-path: polygon(0% 100%, 100% 0%, 100% 100%) !important; */ 72 | } 73 | 74 | /* Buttons */ 75 | .selection-popup-button { 76 | all: revert; 77 | font: status-bar !important; 78 | font-weight: 500 !important; 79 | letter-spacing: 0 !important; 80 | color: var(--selection-button-foreground) !important; 81 | background-color: transparent !important; 82 | cursor: pointer; 83 | border: none !important; 84 | box-shadow: none !important; 85 | padding: var(--selecton-button-padding) !important; 86 | /* vertical-align: top !important; */ 87 | font-family: Arial, Helvetica, sans-serif !important; 88 | height: 100% !important; 89 | min-height: 100% !important; 90 | margin: 0px !important; 91 | transition: background-color 25ms ease-out !important; 92 | text-transform: none !important; 93 | outline: none !important; 94 | text-align: center !important; 95 | font-size: var(--selecton-font-size) !important; 96 | line-height: 1.2 !important; 97 | position: relative; 98 | vertical-align: top !important; 99 | } 100 | 101 | .tooltip-with-icons .selection-popup-button span { 102 | /* vertical-align: middle !important; */ 103 | } 104 | 105 | .color-highlight { 106 | color: var(--selecton-secondary-color) !important; 107 | } 108 | 109 | .selection-popup-button:hover, 110 | .highlighted-popup-button { 111 | all: revert; 112 | background-color: var(--selection-button-background-hover) !important; 113 | background-clip: padding-box !important; 114 | cursor: pointer; 115 | color: white !important; 116 | transition: background-color 25ms ease-out !important; 117 | position: relative; 118 | } 119 | 120 | .link-button { 121 | text-decoration: none !important; 122 | height: 100% !important; 123 | display: inline-block !important; 124 | } 125 | 126 | .link-button .selecton-button-img-icon { 127 | vertical-align: middle !important; 128 | } 129 | 130 | 131 | /* Clip content in buttons with hover panels */ 132 | .button-with-top-hover-panel { 133 | clip-path: polygon(-2000% -2000%, 2000% -2000%, 2000% 100%, -2000% 100%) !important; 134 | } 135 | 136 | .button-with-bottom-hover-panel { 137 | clip-path: polygon(-2000% 0%, 2000% 0%, 2000% 2000%, -2000% 2000%) !important; 138 | } 139 | 140 | .hover-vertical-tooltip { 141 | all: revert; 142 | transition: transform var(--selecton-anim-duration) ease-out, opacity var(--selecton-anim-duration) ease-out !important; 143 | opacity: 0.0; 144 | /* z-index: 100001 !important; */ 145 | z-index: -1; 146 | position: absolute; 147 | margin: 0px !important; 148 | padding: 6px !important; 149 | line-height: 1.0 !important; 150 | border: 0.25px solid var(--selecton-outline-color); 151 | background: var(--selecton-background-color) !important; 152 | font-size: var(--selecton-font-size) !important; 153 | max-width: 400%; 154 | pointer-events: none; 155 | color: var(--selection-button-foreground); 156 | white-space: normal !important; 157 | } 158 | 159 | .no-padding-tooltip { 160 | padding: 0px !important; 161 | } 162 | 163 | .default-padding-tooltip { 164 | padding: 2px !important; 165 | } 166 | 167 | /* Color accent for fetch results panel (translate, dictionary) */ 168 | .selecton-live-translation { 169 | color: var(--selecton-secondary-color) !important; 170 | } 171 | 172 | /* Search tooltip with custom search options */ 173 | .custom-search-image-button { 174 | all: revert; 175 | padding: 6px 8px; 176 | vertical-align: middle !important; 177 | height: max-content; 178 | font-size: var(--selecton-font-size) !important; 179 | font-family: Arial, Helvetica, sans-serif !important; 180 | color: var(--selection-button-foreground); 181 | text-decoration: none !important; 182 | } 183 | 184 | .custom-search-image-button:hover { 185 | cursor: pointer; 186 | background-color: var(--selection-button-background-hover) !important; 187 | color: white; 188 | } 189 | 190 | .custom-search-image-button span { 191 | display: inline; 192 | vertical-align: top !important; 193 | opacity: 0.75; 194 | padding: 2px 5px !important; 195 | } 196 | 197 | .custom-search-image-button img { 198 | all: revert; 199 | filter: none !important; 200 | } 201 | 202 | .selection-popup-color-preview-circle { 203 | max-width: 9px; 204 | max-height: 9px; 205 | padding: 0px 7.5px !important; 206 | border-radius: 50%; 207 | border: solid 1px var(--selection-button-background-hover); 208 | display: inline; 209 | } 210 | 211 | .selection-tooltip .button-with-border:not(:first-child) { 212 | border-left: var(--selecton-button-border-left) !important; 213 | } 214 | 215 | .hover-vertical-tooltip .button-with-border:not(:first-child) { 216 | border-left: var(--selecton-button-border-left) !important; 217 | } 218 | 219 | /* Highter z-index for search tooltip on bottom */ 220 | .higher-z-index { 221 | z-index: 100002 !important; 222 | } 223 | 224 | .selection-tooltip-draghandle { 225 | position: fixed; 226 | z-index: 99998; 227 | left: 0px; 228 | top: 0px; 229 | opacity: 0; 230 | /* background: var(--selecton-background-color) !important; */ 231 | } 232 | 233 | .selection-tooltip-draghandle-line { 234 | background: var(--selecton-background-color); 235 | opacity: 0.7; 236 | position: absolute; 237 | border-radius: 4px; 238 | transition: height 200ms ease; 239 | } 240 | 241 | .selection-tooltip-draghandle-circle { 242 | border: 0.25px solid var(--selecton-outline-color); 243 | z-index: 99998; 244 | border-radius: 50%; 245 | background: var(--selecton-background-color); 246 | position: relative; 247 | height: var(--selecton-handle-circle-radius); 248 | width: var(--selecton-handle-circle-radius); 249 | } 250 | 251 | .draghandle-triangle { 252 | border-radius: 4px; 253 | transform: translate(-50%) rotate(45deg); 254 | } 255 | 256 | .selecton-button-img-icon { 257 | all: revert; 258 | fill: white; 259 | vertical-align: top !important; 260 | display: unset !important; 261 | transform: translate(0, -1px); 262 | height: var(--selecton-button-icon-height); 263 | max-height: var(--selecton-button-icon-height) !important; 264 | filter: var(--selecton-button-icon-invert) !important; 265 | width: unset !important; 266 | background-color: transparent; 267 | width: var(--selecton-button-icon-height) !important; 268 | background-repeat: no-repeat !important; 269 | background-size: contain !important; 270 | } 271 | 272 | .no-filter-icon { 273 | filter: none !important; 274 | opacity: 1.0 !important; 275 | } 276 | 277 | .selecton-search-tooltip-icon { 278 | filter: none; 279 | width: var(--selecton-search-tooltip-icon-size); 280 | height: var(--selecton-search-tooltip-icon-size) !important; 281 | max-height: var(--selecton-search-tooltip-icon-size); 282 | vertical-align: top !important; 283 | } 284 | 285 | .selecton-hover-button-indicator { 286 | opacity: 0.3; 287 | position: absolute; 288 | right: 4px !important; 289 | top: 2px !important; 290 | width: 4px !important; 291 | height: 4px !important; 292 | transform: translate(0px, 0.5px); 293 | margin: 0 auto !important; 294 | background-color: var(--selection-button-foreground) !important; 295 | border-radius: 50% !important; 296 | transition: opacity 100ms ease !important; 297 | pointer-events: none; 298 | } 299 | .selecton-more-button-child-count{ 300 | background: transparent !important; 301 | color: var(--selection-button-foreground) !important; 302 | font-size: xx-small !important; 303 | top: -1px !important; 304 | opacity: 0.4; 305 | } 306 | 307 | /* Marker */ 308 | .marker-scrollbar-hint { 309 | position: fixed; 310 | right: 1px; 311 | outline: 1px solid lightgrey; 312 | width: 15px !important; 313 | cursor: pointer; 314 | transition: width 200ms ease; 315 | z-index: 2147483646 !important; 316 | opacity: 0.55; 317 | } 318 | 319 | .marker-scrollbar-hint:hover { 320 | opacity: 1.0; 321 | } 322 | 323 | .marker-scrollbar-hint .marker-scrollbar-tooltip { 324 | visibility: hidden; 325 | width: 200px; 326 | background-color: #555 !important; 327 | color: white !important; 328 | text-align: center; 329 | padding: 5px 3px; 330 | border-radius: 6px; 331 | padding: 12px !important; 332 | pointer-events: none !important; 333 | position: absolute; 334 | z-index: 2147483647 !important; 335 | bottom: 0; 336 | right: 120%; 337 | width: max-content !important; 338 | text-align: start !important; 339 | font-size: 12px !important; 340 | opacity: 0; 341 | transition: opacity 250ms, visibility 250ms !important; 342 | } 343 | 344 | .marker-scrollbar-tooltip-bottom { 345 | bottom: unset !important; 346 | top: 0px !important; 347 | } 348 | 349 | .marker-scrollbar-hint:hover .marker-scrollbar-tooltip { 350 | visibility: visible; 351 | opacity: 0.8; 352 | transition-delay: 100ms; 353 | } 354 | 355 | /* Marker delete button */ 356 | .marker-highlight-delete { 357 | position: absolute; 358 | top: -20px; 359 | right: -20px; 360 | background: #555; 361 | color: white; 362 | border-radius: 2px; 363 | border: 1px solid lightgray; 364 | z-index: 999999999999 !important; 365 | opacity: 0; 366 | font-size: 14px; 367 | transition: opacity 200ms; 368 | padding: 0px 4px; 369 | cursor: pointer; 370 | } 371 | 372 | .selecton-marker-highlight:hover .marker-highlight-delete { 373 | opacity: 0.85; 374 | } 375 | 376 | .marker-highlight-delete:hover { 377 | background: red; 378 | } 379 | 380 | 381 | /* Vertical layout for tooltip */ 382 | .selection-tooltip.vertical-layout-tooltip { 383 | max-height: unset !important; 384 | /* min-width: 140px !important; */ 385 | } 386 | 387 | .selection-tooltip.reversed-order { 388 | display: flex !important; 389 | flex-direction: column-reverse !important; 390 | } 391 | 392 | .selection-tooltip.reversed-order .selection-popup-button { 393 | flex: 0 0 auto !important; 394 | } 395 | 396 | .selection-tooltip.vertical-layout-tooltip .selection-popup-button { 397 | display: block !important; 398 | clip-path: none !important; 399 | width: 100% !important; 400 | width: -moz-available !important; 401 | width: -webkit-fill-available !important; 402 | width: fill-available !important; 403 | } 404 | 405 | .vertical-layout-tooltip .hover-vertical-tooltip .selection-popup-button { 406 | border-radius: unset !important; 407 | } 408 | 409 | .selection-tooltip.vertical-layout-tooltip .selection-popup-button img { 410 | float: left; 411 | } 412 | 413 | .vertical-layout-tooltip .selecton-hover-button-indicator { 414 | top: 50% !important; 415 | right: 2px !important; 416 | } 417 | 418 | .selection-tooltip.vertical-layout-tooltip .hover-vertical-tooltip { 419 | left: 101% !important; 420 | top: -2px !important; 421 | height: fit-content !important; 422 | max-height: fit-content !important; 423 | } 424 | 425 | .selection-tooltip.vertical-layout-tooltip .button-with-border:not(:first-child) { 426 | border-left: none !important; 427 | border-right: none !important; 428 | border-top: 1px solid var(--selection-button-background-hover) !important; 429 | } 430 | 431 | .selection-tooltip.vertical-layout-tooltip.reversed-order > .button-with-border { 432 | border-top: unset !important; 433 | border-bottom: 1px solid var(--selection-button-background-hover) !important; 434 | } 435 | 436 | 437 | /* Info panel */ 438 | .selecton-info-panel { 439 | text-align: center !important; 440 | font: status-bar !important; 441 | font-size: 9px !important; 442 | font-weight: 500 !important; 443 | letter-spacing: 0 !important; 444 | /* color: rgba(256, 256, 256, 0.7); */ 445 | color: var(--selecton-info-panel-color) !important; 446 | border-bottom: 1px solid var(--selection-button-background-hover) !important; 447 | padding: 0 0 2px 0 !important; 448 | } 449 | 450 | .info-panel-on-bottom { 451 | border-bottom: unset !important; 452 | border-top: 1px solid var(--selection-button-background-hover) !important; 453 | padding: 2px 0 0 0 !important; 454 | } -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "SelectON — selection popup. Copy & search", 4 | "version": "3.9.6", 5 | "author": "emvaized", 6 | "description": "__MSG_extensionDescription__", 7 | "default_locale": "en", 8 | "icons": { 9 | "48": "assets/icons/logo-new.png", 10 | "96": "assets/icons/logo-new.png" 11 | }, 12 | "web_accessible_resources": [ 13 | { 14 | "resources": [ 15 | "assets/icons/button-icons/*.svg" 16 | ], 17 | "matches": [""] 18 | } 19 | ], 20 | "browser_specific_settings": { 21 | "gecko": { 22 | "id": "selection_action@emvaized.com" 23 | } 24 | }, 25 | "background": { 26 | "scripts": ["background.js"], 27 | "service_worker": "background.js" 28 | }, 29 | "content_scripts": [ 30 | { 31 | "matches": [ 32 | "" 33 | ], 34 | "js": [ 35 | "content.js" 36 | ], 37 | "css": [ 38 | "index.css" 39 | ], 40 | "run_at": "document_start", 41 | "all_frames": true 42 | } 43 | ], 44 | "options_ui": { 45 | "page": "options/options.html", 46 | "open_in_tab": false 47 | }, 48 | "action": { 49 | "default_icon": "assets/icons/logo-new.png", 50 | "default_title": "Selecton", 51 | "default_popup": "popup/popup.html" 52 | }, 53 | "permissions": [ 54 | "storage", 55 | "notifications", 56 | "clipboardRead" 57 | ], 58 | "host_permissions": [ 59 | "https://translate.googleapis.com/*", 60 | "https://api.exchangerate.host/*", 61 | "https://min-api.cryptocompare.com/data/*", 62 | "https://*.wikipedia.org/w/*" 63 | ] 64 | } -------------------------------------------------------------------------------- /src/options/icons/border-color.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/options/icons/highlighter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/options/icons/network.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/options/icons/palette.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/options/icons/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/options/icons/select-end.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/options/icons/select-start.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/options/icons/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/options/icons/split-screen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/options/icons/sync.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/options/icons/text-field.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/options/options.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 9px; 3 | font-size: 14px; 4 | } 5 | 6 | .option { 7 | padding: 4px; 8 | } 9 | 10 | .option:hover { 11 | background-color: rgb(0, 0, 0, 0.1); 12 | border-radius: 4px; 13 | transition: all 200ms ease-in-out; 14 | } 15 | 16 | label { 17 | cursor: pointer; 18 | } 19 | 20 | hr { 21 | opacity: 0.2; 22 | color: grey; 23 | } 24 | 25 | #customSearchUrl { 26 | max-width: 100%; 27 | } 28 | 29 | .option input { 30 | max-width: 65px; 31 | margin-right: 6px; 32 | } 33 | 34 | .option select { 35 | margin-right: 6px; 36 | } 37 | 38 | input[type="checkbox"] { 39 | cursor: pointer; 40 | } 41 | 42 | .disabled-option { 43 | opacity: 0.5; 44 | transition: opacity 150ms ease-in-out; 45 | pointer-events: none; 46 | } 47 | 48 | .enabled-option { 49 | opacity: 1.0; 50 | transition: opacity 150ms ease-in-out; 51 | } 52 | 53 | .visible-option { 54 | opacity: 1.0; 55 | } 56 | 57 | .hidden-option { 58 | display: none; 59 | pointer-events: none; 60 | } 61 | 62 | h3 { 63 | color: grey; 64 | padding-top: 10px; 65 | } 66 | 67 | h4 { 68 | color: grey; 69 | opacity: 0.8; 70 | font-weight: 500; 71 | } 72 | 73 | #footer-buttons button { 74 | margin: 3px !important; 75 | width: 100%; 76 | text-align: center; 77 | cursor: pointer; 78 | } 79 | 80 | #customSearchButtonsContainer { 81 | margin-top: 15px; 82 | } 83 | 84 | select { 85 | margin: 5px 0px; 86 | cursor: pointer; 87 | } 88 | 89 | /* Collapsible settings headers */ 90 | /* Style the button that is used to open and close the collapsible content */ 91 | .collapsible-header { 92 | background-color: #eee; 93 | color: #444; 94 | cursor: pointer; 95 | padding: 18px 6px 18px 40px; 96 | border: none; 97 | text-align: left; 98 | outline: none; 99 | font-size: 15px; 100 | border-radius: 3px; 101 | position: relative; 102 | 103 | margin: 12px 0px; 104 | } 105 | 106 | /* Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) */ 107 | .active, .collapsible-header:hover { 108 | background-color: #ccc; 109 | } 110 | 111 | /* Style the collapsible content. Note: hidden by default */ 112 | .collapsible-content { 113 | padding-left: 8px; 114 | max-height: 0; 115 | overflow: hidden; 116 | transition: max-height 0.2s ease-out; 117 | border-left: 1px solid lightgray; 118 | } 119 | 120 | #selecton-version { 121 | text-align: center; 122 | } 123 | 124 | #additional-settings-block { 125 | width: 100%; 126 | } 127 | 128 | #customSearchTooltipHint, #markerHintHeader { 129 | margin: 0px 0px 15px; 130 | } 131 | 132 | .custom-search-option-url-input { 133 | display: inline; min-width: 100%; max-width: 100%; 134 | } 135 | 136 | .custom-search-option-move-button { 137 | max-width: 25px; padding: 1px; align-items: center 138 | } 139 | 140 | .custom-search-option-url-label { 141 | display: inline; opacity: 0.5; 142 | } 143 | 144 | .custom-search-option-icon-input { 145 | min-width: 100%; max-width: 100%; margin-top: 3px !important; 146 | } 147 | 148 | /* Collapsible header arrow */ 149 | .collapsible-header:after { 150 | content: '❮'; 151 | font-size: 18px; 152 | font-weight: 50; 153 | float: right; 154 | opacity: 0.5; 155 | transform: translate(-7px, -2px) rotate(-90deg); 156 | transition: transform 150ms ease; 157 | } 158 | 159 | .active:after { 160 | transform: translate(-7px, -2px) rotate(-270deg); 161 | } 162 | 163 | 164 | /* Markers section */ 165 | .marker-website-tile { 166 | cursor: pointer; 167 | position: relative !important; 168 | border: 1px solid lightgray; 169 | border-radius: 3px; 170 | margin: 3px 0px; 171 | } 172 | 173 | .marker-website-favicon { 174 | display: inline; margin-right: 5px; width: 15px; height: 15px; vertical-align: middle !important; 175 | } 176 | 177 | .markers-counter-circle { 178 | position: absolute; 179 | bottom: 1px; 180 | right: 1px; 181 | background: rgba(85, 85, 85, 0.438); color: white; 182 | font-size: 14px; 183 | padding: 0px 6px; 184 | border-bottom-right-radius: 3px; 185 | } 186 | 187 | .marker-tile { 188 | position: relative; 189 | padding: 6px 0px; 190 | overflow-y: auto; 191 | max-height: 60px; cursor: pointer; 192 | } 193 | 194 | .marker-color-preview { 195 | max-width: 4px; max-height: 4px; padding: 0px 8.5px; margin-right: 5px; margin-left: 2px; border-radius: 2px; 196 | border: 1px solid lightgray; 197 | display: inline; 198 | } 199 | 200 | /* Marker delete button */ 201 | .marker-highlight-delete { 202 | position: absolute; 203 | top: 1px; 204 | right: 1px; 205 | background: #555; 206 | color: white; 207 | /* border-radius: 50%; */ 208 | /* border: 1px solid lightgray; */ 209 | border-top-right-radius: 3px; 210 | z-index: 999999999999; 211 | opacity: 0; 212 | font-size: 14px; 213 | transition: opacity 200ms; 214 | padding: 0px 4px; 215 | cursor: pointer; 216 | } 217 | 218 | .marker-tile:hover .marker-highlight-delete { 219 | opacity: 0.85; 220 | } 221 | 222 | .marker-highlight-delete:hover { 223 | background: red; 224 | } 225 | 226 | .header-icon { 227 | margin-right: 8px; 228 | opacity: 0.55; 229 | position: absolute; 230 | left: 8px; 231 | margin: auto; 232 | bottom: 0; 233 | top: -2px; 234 | } 235 | 236 | /* 'What's new' button color */ 237 | /* #selecton-version a { color: #55b5ff !important; text-decoration: none; } */ 238 | a { 239 | color: #55b5ff; 240 | } 241 | a:not(:hover) { 242 | text-decoration: none; 243 | } 244 | 245 | 246 | @media (min-width: 600px) { 247 | #footer-buttons { text-align: center; } 248 | #footer-buttons button { width: unset; display: inline; } 249 | #allChangesSavedAutomaticallyHeader { text-align: center; } 250 | } 251 | 252 | @-moz-document url-prefix() { 253 | body { font-family: sans-serif !important; line-height: 1.0 !important; } 254 | h3 {font-size: 18px; font-weight: 600;} 255 | } 256 | 257 | 258 | @media not all and (min-resolution:.001dpcm){ @supports (-webkit-appearance:none) { 259 | .collapsible-header { 260 | background-color: rgba(211,211,211, 0.6); 261 | cursor: auto; 262 | border-radius: 4px; 263 | padding: 16px 6px 16px 12px; 264 | } 265 | 266 | .custom-search-option-move-button{ 267 | max-width: 40px; width: 40px; padding: 2.5px; 268 | } 269 | 270 | .active, .collapsible-header:hover { 271 | background-color: rgba(211,211,211, 0.6); 272 | } 273 | 274 | h4 { 275 | color: black; 276 | opacity: 0.6; 277 | } 278 | 279 | button { 280 | padding: 6px; cursor: auto; 281 | } 282 | 283 | button img { display: none; } 284 | 285 | .option, label { cursor: auto; } 286 | 287 | .option:hover { 288 | background-color: unset; 289 | } 290 | 291 | hr { 292 | opacity: 0.5; 293 | } 294 | }} 295 | 296 | @media (prefers-color-scheme: dark) { 297 | body { 298 | background: rgb(40,41,44); 299 | color: white; 300 | } 301 | 302 | .collapsible-header { 303 | background-color: rgba(238, 238, 238, 0.528); 304 | color: white; 305 | } 306 | 307 | .collapsible-header:hover { 308 | background-color: rgba(204, 204, 204, 0.503); 309 | } 310 | 311 | .collapsible-content { 312 | border-left: 1px solid gray; 313 | } 314 | 315 | .option:hover { 316 | background-color: rgb(256, 256, 256, 0.1); 317 | } 318 | 319 | .header-icon { 320 | filter: invert(100%); 321 | } 322 | 323 | h4 { 324 | color: lightgray; 325 | opacity: 0.75; 326 | } 327 | 328 | hr { 329 | color: lightgray; 330 | } 331 | } -------------------------------------------------------------------------------- /src/options/test-page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Test page — Selecton 9 | 10 | 11 | 40 | 41 | 42 |
43 | 44 |

Selecton test page

45 |
46 | 47 |

You can test your current configs here. 48 | It may be needed to reload page to apply new configs (unless 'Apply configs immediately' is 49 | checked in settings)

50 |
51 | 52 |

Dummy text

53 |

54 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin consequat vel turpis quis venenatis. Sed vitae nisl vel nunc rhoncus feugiat a vitae 55 | augue. Sed 56 | interdum eget arcu ultricies lobortis. Vivamus sodales et dolor ac vestibulum. Aliquam lobortis malesuada nisi nec sodales. Morbi tempus neque efficitur 57 | nunc 58 | eleifend, ac consectetur purus fringilla. Pellentesque vehicula nisl ultricies, venenatis libero non, vestibulum nisi. Vestibulum in risus et nisi 59 | bibendum 60 | porttitor. 61 |

62 |

63 | Phasellus bibendum euismod erat vitae semper. Donec pellentesque pretium massa eget fermentum. Nulla ut velit augue. In hac habitasse platea dictumst. 64 | Suspendisse potenti. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Pellentesque fringilla varius risus, 65 | sit amet 66 | consectetur libero finibus non. Sed fringilla, urna at lobortis mattis, urna mauris porttitor metus, in pulvinar dui mi ut magna. Nam nec felis id mi 67 | maximus 68 | pellentesque. 69 |

70 |

71 | Cras pharetra ex ultrices massa semper gravida. Morbi fermentum, lacus iaculis vehicula commodo, eros orci ultricies magna, eu gravida lorem nulla id 72 | dui. Proin 73 | sed condimentum nunc. Sed porttitor ullamcorper ante eu volutpat. Nullam fringilla aliquam nulla. Praesent non ipsum non ipsum tempor egestas. Etiam id 74 | purus 75 | vel sem efficitur laoreet. Vestibulum ut auctor mi. Mauris bibendum, nisi et scelerisque efficitur, urna quam efficitur nisi, non mollis dui libero quis 76 | tortor. 77 | Suspendisse efficitur nisl vel magna accumsan, vitae ultrices quam suscipit. Ut malesuada tortor nec sem vulputate, sit amet placerat orci viverra. 78 | Phasellus 79 | turpis sem, sagittis a nulla non, rhoncus fringilla turpis. Nam nec cursus velit, quis molestie elit. Aenean eget leo aliquet, tincidunt est sed, 80 | interdum nibh. 81 | Quisque nec sodales nibh. Cras id nibh aliquam, tempus risus nec, ornare urna. 82 |

83 |

84 | Donec elementum pellentesque euismod. Aliquam commodo urna in purus lacinia pulvinar a vel metus. Ut iaculis ante nibh, id viverra lectus placerat 85 | vitae. 86 | Quisque congue nibh urna, vitae interdum justo varius malesuada. Proin tristique sem dictum massa auctor eleifend. Aenean auctor vitae risus id viverra. 87 | Nunc ut 88 | sapien non nibh pretium rhoncus ac sed ante. Vivamus sagittis quam lectus, quis vehicula urna tincidunt id. Cras eu nisl eget nisi dignissim maximus eu 89 | nec leo. 90 | Quisque neque dolor, imperdiet sit amet aliquam sit amet, vestibulum quis justo. Pellentesque non lectus quis diam tincidunt facilisis ut id neque. 91 | Fusce 92 | dignissim egestas neque rhoncus tincidunt. Proin lacus tellus, mattis id justo vitae, porta tempus nibh. 93 |

94 | 95 |

Measure units

96 |
    97 |
  • Mass: 205lbs — 93kg
  • 98 |
  • Temperature: 102.2°F — 39°C
  • 99 |
  • Speed: 62.14mph — 100km/h
  • 100 |
  • Distance: 60 miles — 96.56 km
  • 101 |
  • Volume: 2.5 gal — 11.3652 liters
  • 102 |
  • Volume: 4 qt — 3.79 L
  • 103 |
104 | 105 |

Currencies

106 |
    107 |
  • 71,52 AUD
  • 108 |
  • $71,52
  • 109 |
  • 248 dollars
  • 110 |
  • 3334,29 RUB
  • 111 |
  • 2 million euros
  • 112 |
113 | 114 |

Links

115 |
    116 |
  • https://github.com/emvaized/selecton-extension
  • 117 |
  • google.com
  • 118 |
  • r/kde
  • 119 |
120 | 121 |

Various buttons

122 |
    123 |
  • #3590FF
  • 124 |
  • johndoe@example.com
  • 125 |
  • 25 May
  • 126 |
127 | 128 |

Text field

129 |
130 | 131 |
132 |
133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /src/options/test-page.js: -------------------------------------------------------------------------------- 1 | document.getElementById('text-input').oninput = function (e) { 2 | document.getElementById('text-input-result').innerHTML = e.target.value; 3 | } -------------------------------------------------------------------------------- /src/popup/popup.css: -------------------------------------------------------------------------------- 1 | #selecton-settings-label { 2 | display: inline; font-size: 18px;vertical-align: middle; color: grey; 3 | } 4 | 5 | .circleButton { 6 | opacity: 0.8; 7 | border-radius: 50%; 8 | padding: 3px; 9 | margin-left: 3px; 10 | } 11 | 12 | .float { 13 | float: right; 14 | } 15 | 16 | .circleButton:hover{ 17 | background-color: lightGrey; 18 | cursor: pointer; 19 | } 20 | 21 | #popup-header { 22 | padding: 6px; border-bottom: 1px solid lightGrey; vertical-align: bottom; 23 | height: 25px; 24 | } 25 | 26 | #headerLogo { 27 | display: inline; vertical-align: middle; 28 | } 29 | 30 | iframe { 31 | display: block; margin: 0px !important; width: 100%; 32 | height: 525px; 33 | min-width: 400px; 34 | } 35 | 36 | @-moz-document url-prefix() { 37 | body { font-family: sans-serif !important; } 38 | iframe { min-width: 365px; } 39 | #headerLogo {vertical-align: bottom;} 40 | } 41 | 42 | 43 | @media not all and (min-resolution:.001dpcm){ @supports (-webkit-appearance:none) { 44 | #selecton-settings-label { 45 | color: black; opacity: 0.65; 46 | } 47 | }} 48 | 49 | @media (prefers-color-scheme: dark) { 50 | body { 51 | background: rgb(40,41,44); 52 | color: white; 53 | } 54 | 55 | #selecton-settings-label { 56 | color: lightgray; 57 | } 58 | 59 | #openSettingsInTabButton { 60 | filter: invert(100%); 61 | } 62 | 63 | #popup-header { 64 | border-bottom: 1px solid gray; 65 | } 66 | } -------------------------------------------------------------------------------- /src/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/popup/popup.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | document.getElementById('selecton-settings-label').innerHTML = chrome.i18n.getMessage("selectonSettings") ?? 'Selecton settings'; 3 | 4 | let openSettingsInTabButton = document.getElementById('openSettingsInTabButton'); 5 | openSettingsInTabButton.setAttribute('title', chrome.i18n.getMessage("openInNewTab") ?? 'Open in new tab'); 6 | openSettingsInTabButton.addEventListener('mouseup', function (e) { 7 | if (e.button == 0) { 8 | chrome.runtime.openOptionsPage(); 9 | window.close(); 10 | } else if (e.button == 1) { 11 | window.open(chrome.runtime.getURL('options/options.html')); 12 | } 13 | }); 14 | 15 | }); -------------------------------------------------------------------------------- /src/ui/buttons/basic-buttons.js: -------------------------------------------------------------------------------- 1 | 2 | function addBasicTooltipButtons(layout) { 3 | // TODO: Provide option to use regular butttons instead; add text format buttons as one button 4 | if (layout == 'textfield') { 5 | const textField = document.activeElement; 6 | const isContentEditable = textField.getAttribute('contenteditable') !== null; 7 | 8 | if (selection.toString() !== '') { 9 | try { 10 | /// Add a cut button 11 | addBasicTooltipButton(cutLabel, cutButtonIcon, function () { 12 | document.execCommand('cut'); 13 | }, true); 14 | 15 | /// Add copy button 16 | copyButton = addBasicTooltipButton(copyLabel, copyButtonIcon, function () { 17 | try { 18 | textField.focus(); 19 | document.execCommand('copy'); 20 | removeSelectionOnPage(); 21 | } catch (e) { console.log(e); } 22 | }); 23 | 24 | /// Add paste button 25 | addBasicTooltipButton(pasteLabel, pasteButtonIcon, function () { 26 | textField.focus(); 27 | if (isContentEditable) { 28 | /// TODO: Rewrite this in order to ask for clipboardRead permission first 29 | 30 | // chrome.permissions.request({ 31 | // permissions: ['clipboardRead'], 32 | // }, (granted) => { 33 | // if (granted) { 34 | let currentClipboardContent = getCurrentClipboard(); 35 | if (currentClipboardContent !== null && currentClipboardContent !== undefined && currentClipboardContent != '') 36 | document.execCommand("insertHTML", false, currentClipboardContent); 37 | // } else { 38 | // chrome.runtime.sendMessage({ type: 'selecton-no-clipboard-permission-message' }); 39 | // } 40 | // }); 41 | 42 | } else 43 | document.execCommand('paste'); 44 | 45 | removeSelectionOnPage(); 46 | hideTooltip(); 47 | }); 48 | 49 | if (configs.addFontFormatButtons) { 50 | 51 | /// Italic button 52 | addBasicTooltipButton(italicLabel, italicTextIcon, function () { 53 | textField.focus(); 54 | document.execCommand(isContentEditable ? "insertHTML" : "insertText", false, '' + selectedText + ''); 55 | hideTooltip(); 56 | }); 57 | 58 | /// Bold button 59 | addBasicTooltipButton(boldLabel, boldTextIcon, function () { 60 | textField.focus(); 61 | document.execCommand(isContentEditable ? "insertHTML" : "insertText", false, '' + selectedText + ''); 62 | hideTooltip(); 63 | }); 64 | 65 | /// Strikethrough button 66 | addBasicTooltipButton(strikeLabel, strikeTextIcon, function () { 67 | textField.focus(); 68 | document.execCommand(isContentEditable ? "insertHTML" : "insertText", false, '' + selectedText + ''); 69 | hideTooltip(); 70 | }); 71 | } 72 | 73 | if (configs.collapseButtons) 74 | try { 75 | collapseButtons(); 76 | } catch (e) { if (configs.debugMode) console.log(e); } 77 | 78 | setCopyButtonTitle(copyButton); 79 | 80 | } catch (e) { if (configs.debugMode) console.log(e) } 81 | 82 | } else { 83 | if (configs.addPasteButton) 84 | try { 85 | /// Add paste button 86 | addBasicTooltipButton(pasteLabel, pasteButtonIcon, function (e) { 87 | textField.focus(); 88 | 89 | if (textField.getAttribute('contenteditable') !== null) { 90 | let currentClipboardContent = getCurrentClipboard(); 91 | 92 | if (currentClipboardContent !== null && currentClipboardContent !== undefined && currentClipboardContent != '') 93 | document.execCommand("insertHTML", false, currentClipboardContent); 94 | } else 95 | document.execCommand('paste'); 96 | 97 | removeSelectionOnPage(); 98 | // hideTooltip(); 99 | }, true); 100 | 101 | } catch (e) { if (configs.debugMode) console.log(e); } 102 | 103 | /// Add 'clear' button 104 | if (configs.addClearButton && isTextFieldEmpty == false) 105 | addBasicTooltipButton(clearLabel, clearIcon, function (e) { 106 | removeSelectionOnPage(); 107 | textField.focus(); 108 | 109 | if (textField.getAttribute('contenteditable') !== null) 110 | textField.innerHTML = ''; 111 | else { 112 | textField.value = ''; 113 | } 114 | }); 115 | } 116 | 117 | setBorderRadiusForSideButtons(tooltip); 118 | 119 | } else { 120 | /// Add search button 121 | // let selectedText = selection.toString(); 122 | searchButton = addLinkTooltipButton(searchLabel, searchButtonIcon, returnSearchUrl(selectedText.trim()), true); 123 | 124 | /// Populate panel with custom search buttons, when enabled 125 | if (configs.customSearchOptionsDisplay == 'panelCustomSearchStyle') { 126 | if (configs.customSearchButtons) 127 | for (var i = 0, l = configs.customSearchButtons.length; i < l; i++) { 128 | const item = configs.customSearchButtons[i]; 129 | 130 | const url = item['url'].replace('%s', selectedText); 131 | const optionEnabled = item['enabled']; 132 | const domain = url.split('/')[2]; 133 | const title = item['title'] ?? domain; 134 | const icon = item['icon'] ?? 'https://www.google.com/s2/favicons?domain=' + domain; 135 | 136 | if (optionEnabled) { 137 | let b = addLinkTooltipButton(title ?? url, icon, url); 138 | b.classList.add('custom-search-image-button') 139 | } 140 | } 141 | } 142 | 143 | /// Add copy button 144 | /// TODO: Add option to copy plain text 145 | copyButton = addBasicTooltipButton(copyLabel, copyButtonIcon, function () { 146 | document.execCommand('copy'); 147 | // removeSelectionOnPage(); 148 | // if (configs.hideTooltipOnActionButtonClick){ 149 | // hideDragHandles(); 150 | // hideTooltip(); 151 | // } 152 | }); 153 | } 154 | } -------------------------------------------------------------------------------- /src/ui/buttons/hover-buttons/calendar-button.js: -------------------------------------------------------------------------------- 1 | function checkToAddCalendarButton(text) { 2 | let weekday, day, month, year, time; 3 | let mayBeDate = false, showDateInsteadOfWeekday = false; 4 | 5 | const words = text.toLowerCase().split(' '); 6 | const todayDate = new Date(); 7 | 8 | loop: 9 | for (let i = 0, n = words.length; i < n; i++) { 10 | const word = words[i]; 11 | 12 | /// month 13 | for (j in dateKeywords.month) { 14 | const mon = dateKeywords.month[j]; 15 | if (word.includes(mon)) { 16 | month = dateKeywords.month[j % 12]; 17 | // mayBeDate = true; 18 | continue loop; 19 | } 20 | } 21 | 22 | /// weekday 23 | for (j in dateKeywords.weekday) { 24 | const weekd = dateKeywords.weekday[j]; 25 | if (word.includes(weekd)) { 26 | weekday = dateKeywords.weekday[j % 7]; 27 | mayBeDate = true; 28 | continue loop; 29 | } 30 | } 31 | 32 | /// check for 'tomorrow' keywords 33 | for (j in dateKeywords.tomorrow) { 34 | const tomorrowKeyword = dateKeywords.tomorrow[j]; 35 | if (word.includes(tomorrowKeyword)) { 36 | const tomorrow = new Date(new Date().getTime() + (24 * 60 * 60 * 1000)); 37 | weekday = dateKeywords.weekday[tomorrow.getDay()]; 38 | day = tomorrow.getDate(); 39 | month = dateKeywords.month[todayDate.getMonth()]; 40 | mayBeDate = true; 41 | showDateInsteadOfWeekday = true; 42 | continue loop; 43 | } 44 | } 45 | 46 | const wordLength = word.length; 47 | const wordIsNumeric = isStringNumeric(word); 48 | 49 | /// check for day of month 50 | if (wordIsNumeric && wordLength >= 1 && wordLength < 3) { 51 | /// don't use if it's time in 12-hour format 52 | const nextWord = words[i + 1]; 53 | if (nextWord && (nextWord.toLowerCase() == 'am' || nextWord.toLowerCase() == 'pm')) 54 | continue; 55 | 56 | if (day && !year) year = word; 57 | else { 58 | day = word; 59 | mayBeDate = true; 60 | } 61 | continue; 62 | } 63 | 64 | /// check for year 65 | if (wordIsNumeric && wordLength == 4) { 66 | year = word; 67 | if (month) mayBeDate = true; 68 | continue; 69 | } 70 | 71 | /// check for time 72 | if (word.includes(':')) { 73 | time = word; 74 | if (time.split(':').length == 2) time += ':00'; 75 | continue; 76 | } 77 | } 78 | 79 | if (!mayBeDate) { 80 | /// If no success, try to parse string as date 81 | if (text.includes('/')) { 82 | const parsedDate = new Date(text); 83 | if (isNaN(parsedDate)) return; 84 | addCalendarButtonFromDate(parsedDate, todayDate, showDateInsteadOfWeekday); 85 | return; 86 | } else if (text.includes('.')) { 87 | const parts = text.split('.'); 88 | const partsLength = parts.length; 89 | 90 | if (partsLength >= 2 && partsLength <= 3) { 91 | let d = parts[0], m = parts[1], y = partsLength < 3 ? todayDate.getFullYear() : parts[2]; 92 | if (d == '' || m == '') return; 93 | if (parseInt(d) == 0) return; 94 | if (y.length < 2) return; 95 | 96 | const parsedDate = new Date(`${y}/${m}/${d}`); 97 | if (isNaN(parsedDate)) return; 98 | addCalendarButtonFromDate(parsedDate, todayDate, showDateInsteadOfWeekday); 99 | } 100 | 101 | return; 102 | } else return; 103 | } 104 | 105 | /// fill non found data 106 | if (!year) year = todayDate.getFullYear(); 107 | if (!month && !day && weekday) { 108 | let date = new Date(), daytoset = dateKeywords.weekday.indexOf(weekday); 109 | let currentDay = date.getDay(); 110 | let distance = (daytoset + 7 - currentDay) % 7; 111 | date.setDate(date.getDate() + distance); 112 | month = dateKeywords.month[date.getMonth()]; 113 | day = date.getDate() + 1; 114 | // showDateInsteadOfWeekday = true; 115 | } 116 | 117 | /// gather collected data 118 | /// format: Wed, 09 Aug 1995 00:00:00 GMT' 119 | let dateString = ''; 120 | if (weekday) dateString += `${weekday}, `; 121 | if (day) dateString += `${day} `; 122 | if (month) dateString += `${month} `; 123 | if (year) dateString += `${year} `; 124 | if (time) dateString += `${time} `; 125 | 126 | const returnedDate = new Date(Date.parse(dateString)); 127 | if (isNaN(returnedDate)) return; 128 | 129 | addCalendarButtonFromDate(returnedDate, todayDate, showDateInsteadOfWeekday, time); 130 | } 131 | 132 | 133 | function addCalendarButtonFromDate(date, todayDate, showDateInsteadOfWeekday, time) { 134 | /// get difference in days 135 | const diffTime = todayDate - date; 136 | const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)) * -1; 137 | let buttonLabel; 138 | 139 | if (showDateInsteadOfWeekday) { 140 | buttonLabel = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) 141 | } else { 142 | if (diffDays <= -360) { 143 | const years = -1 * Math.ceil(diffDays / 360); 144 | buttonLabel = years == 1 ? chrome.i18n.getMessage('yearAgo') : chrome.i18n.getMessage('yearsAgo', `${years}`); 145 | } else if (diffDays < -30 && diffDays > -360) { 146 | const months = -1 * Math.ceil(diffDays / 30); 147 | buttonLabel = months == 1 ? chrome.i18n.getMessage('monthAgo') : chrome.i18n.getMessage('monthsAgo', `${months}`); 148 | } else if (diffDays < 0 && diffDays >= -30) { 149 | const days = -1 * diffDays; 150 | buttonLabel = days == 1 ? chrome.i18n.getMessage('dayAgo') : chrome.i18n.getMessage('daysAgo', `${days}`); 151 | } else if (diffDays == 0) { 152 | buttonLabel = chrome.i18n.getMessage('today'); 153 | } else if (diffDays > 0 && diffDays < 30) { 154 | buttonLabel = diffDays == 1 ? chrome.i18n.getMessage('inDay') : chrome.i18n.getMessage('inDays', `${diffDays}`); 155 | } else if (diffDays >= 29 && diffDays < 360) { 156 | const months = Math.floor(diffDays / 30); 157 | buttonLabel = months == 1 ? chrome.i18n.getMessage('inMonth') : chrome.i18n.getMessage('inMonths', `${months}`); 158 | } else if (diffDays >= 360) { 159 | const years = Math.floor(diffDays / 360); 160 | buttonLabel = years == 1 ? chrome.i18n.getMessage('inYear') : chrome.i18n.getMessage('inYears', `${years}`); 161 | } else { 162 | buttonLabel = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) 163 | } 164 | } 165 | 166 | /// If specific time is provided, create event – otherwise open day in calendar 167 | let calendarLink; 168 | if (time) { 169 | let dateString = date.toISOString().replaceAll(':', '').replaceAll('-', ''); 170 | calendarLink = `https://calendar.google.com/calendar/u/0/r/eventedit?&dates=${dateString}/${dateString}&sf=true`; 171 | } else { 172 | calendarLink = `https://calendar.google.com/calendar/u/0/r/day/${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; 173 | } 174 | const dateButton = addLinkTooltipButton(buttonLabel, calendarIcon, calendarLink); 175 | dateButton.title = date.toLocaleDateString(); 176 | dateButton.classList.add('color-highlight'); 177 | 178 | if (configs.buttonsStyle == 'onlyicon') dateButton.innerHTML += ' ' + buttonLabel; 179 | } -------------------------------------------------------------------------------- /src/ui/buttons/hover-buttons/collapse-button.js: -------------------------------------------------------------------------------- 1 | function collapseButtons() { 2 | const maxButtons = configs.maxTooltipButtonsToShow ?? 3; 3 | const buttonsCount = tooltip.children.length - 1; /// subtract the arrow 4 | 5 | if (buttonsCount > maxButtons) { 6 | 7 | let collapsedButtonsPanel, moreButton; 8 | if (configs.collapseAsSecondPanel){ 9 | /// Show as secondary panel 10 | collapsedButtonsPanel = createHoverPanelForButton(undefined, undefined, undefined, false, false, true, false, true); 11 | collapsedButtonsPanel.style.right = '2px'; 12 | 13 | setTimeout(()=> { 14 | collapsedButtonsPanel.style.transform = returnTooltipRevealTransform(false, false); 15 | collapsedButtonsPanel.style.zIndex = '-1'; 16 | if(tooltip) tooltip.appendChild(collapsedButtonsPanel); 17 | }, 2) 18 | 19 | } else { 20 | /// Create 'more' button 21 | moreButton = document.createElement('button'); 22 | moreButton.setAttribute('class', configs.showButtonBorders ? 'selection-popup-button button-with-border' : 'selection-popup-button'); 23 | moreButton.classList.add('more-button'); 24 | moreButton.innerText = configs.verticalLayoutTooltip ? '⋯' : '⋮'; 25 | 26 | /// Show as hover button 27 | tooltip.appendChild(moreButton); 28 | collapsedButtonsPanel = createHoverPanelForButton(moreButton, undefined, undefined, false, true, true, false); 29 | 30 | /// Show buttons count 31 | const buttonsCountSpan = moreButton.querySelector('.selecton-hover-button-indicator'); 32 | if (buttonsCountSpan){ 33 | buttonsCountSpan.innerText = buttonsCount - maxButtons; 34 | buttonsCountSpan.classList.add('selecton-more-button-child-count') 35 | } 36 | } 37 | 38 | collapsedButtonsPanel.style.maxWidth = 'unset'; 39 | collapsedButtonsPanel.style.zIndex = '2'; 40 | collapsedButtonsPanel.classList.add('default-padding-tooltip'); 41 | 42 | /// Append buttons to panel 43 | for (let i = buttonsCount; i > maxButtons; i--) { 44 | const button = tooltip.children[i]; 45 | // if (!configs.verticalLayoutTooltip && i == (maxButtons * 1) + 1) button.classList.remove('button-with-border'); 46 | 47 | // collapsedButtonsPanel.prepend(button); 48 | if (configs.verticalLayoutTooltip) 49 | collapsedButtonsPanel.prepend(button); 50 | else { 51 | collapsedButtonsPanel.appendChild(button); 52 | } 53 | } 54 | 55 | if (!configs.collapseAsSecondPanel) 56 | moreButton.appendChild(collapsedButtonsPanel); 57 | setBorderRadiusForSideButtons(collapsedButtonsPanel, false); 58 | } 59 | } -------------------------------------------------------------------------------- /src/ui/buttons/hover-buttons/dictionary-button.js: -------------------------------------------------------------------------------- 1 | function addDictionaryButton(selectionLength) { 2 | try { 3 | 4 | const locale = configs.languageToTranslate; 5 | const wikiUrl = 'https://' + 6 | (locale ? locale + '.' : '') + 7 | `wikipedia.org/w/index.php?search=${encodeURIComponent(selectedText)}`; 8 | // const wikiButton = addBasicTooltipButton(dictionaryLabel, dictionaryButtonIcon, wikiUrl); 9 | const wikiButton = addLinkTooltipButton(dictionaryLabel, dictionaryButtonIcon, wikiUrl); 10 | // wikiButton.setAttribute('id', 'selecton-translate-button'); 11 | 12 | /// set fetch on hover listener 13 | if (selectionLength < 500) { 14 | setLiveWikiButton(selectedText, wikiButton); 15 | } 16 | 17 | } catch (e) { 18 | if (configs.debugMode) 19 | console.log(e); 20 | } 21 | } 22 | 23 | 24 | function setLiveWikiButton(word, wikiButton) { 25 | let fetched = false; 26 | let definitionPanel = createHoverPanelForButton(wikiButton, `${chrome.i18n.getMessage("searchingDefinitions") ?? 'Searching'}...`, onShow); 27 | wikiButton.appendChild(definitionPanel); 28 | 29 | function onShow() { 30 | if (fetched == false) { 31 | /// Fetch definition from Wikipedia 32 | fetched = true; 33 | fetchDefinition(word, definitionPanel, wikiButton); 34 | } 35 | } 36 | } 37 | 38 | async function fetchDefinition(text, definitionPanel, wikiButton) { 39 | let resultDefinition; 40 | let nothingFoundLabel = chrome.i18n.getMessage("noDefinitionFound") ?? 'No definition found'; 41 | let locale = configs.languageToTranslate; 42 | let textToSearch = encodeURIComponent(text.replaceAll(' ', '_')); 43 | 44 | fetchFromWikipedia(locale, (res) => { 45 | if (resultDefinition == null || resultDefinition == undefined || resultDefinition == '') { 46 | /// try to fetch from english Wiki 47 | locale = 'en'; 48 | fetchFromWikipedia('en', (res2)=>{ 49 | if (resultDefinition == null || resultDefinition == undefined || resultDefinition == '') { 50 | /// no results found 51 | definitionPanel.innerText = nothingFoundLabel; 52 | return; 53 | } else { 54 | onFinish(); 55 | } 56 | }); 57 | } else { 58 | onFinish(); 59 | } 60 | 61 | function onFinish(){ 62 | /// Set definition view 63 | definitionPanel.innerText = resultDefinition; 64 | definitionPanel.classList.add('selecton-live-translation'); 65 | definitionPanel.style.maxWidth = '450%'; 66 | 67 | /// If text contains line breaks, align by the left side 68 | if (resultDefinition.includes(` 69 | `)) definitionPanel.style.textAlign = 'start'; 70 | 71 | /// Create origin language label 72 | let originLabelWidth = configs.fontSize / 1.5; 73 | let originLabelPadding = 6; 74 | let langLabel; 75 | if (locale !== null && locale !== undefined && locale !== '') { 76 | langLabel = document.createElement('span'); 77 | langLabel.textContent = locale; 78 | langLabel.setAttribute('style', `opacity: 0.7; position: absolute; right: ${originLabelPadding}px; bottom: ${originLabelPadding}px; font-size: ${originLabelWidth}px;color: var(--selection-button-foreground) !important`) 79 | definitionPanel.appendChild(langLabel); 80 | } 81 | } 82 | 83 | }); 84 | 85 | 86 | function fetchFromWikipedia(locale, callback, onError) { 87 | try { 88 | /// exclude local language variations, such as ru-RU 89 | let langToFetch = locale; 90 | if (langToFetch.includes('-')) langToFetch = langToFetch.split('-')[0]; 91 | if (langToFetch.includes('_')) langToFetch = langToFetch.split('_')[0]; 92 | 93 | /// Fetch data from Wiktionary 94 | 95 | const wikiUrl = `https://${langToFetch}.wikipedia.org/w/api.php?action=query&exsectionformat=plain&prop=extracts&origin=*&exchars=${configs.dictionaryButtonResponseCharsAmount ?? 300}&exlimit=1&explaintext=0&formatversion=2&format=json&titles=${textToSearch}`; 96 | 97 | chrome.runtime.sendMessage({ type: 'background_fetch', url: wikiUrl }, (res) => { 98 | // let jsoned = res.json(); 99 | let jsoned = res; 100 | let extract = jsoned.query.pages[0].extract; 101 | 102 | if (extract) { 103 | resultDefinition = extract; 104 | } 105 | 106 | if (jsoned.query.pages[0].pageid) 107 | // wikiButton.onmousedown = function (e) { 108 | // let url = `https://${locale}.wikipedia.org/?curid=${jsoned.query.pages[0].pageid}`; 109 | // onTooltipButtonClick(e, url); 110 | // } 111 | wikiButton.href = `https://${locale}.wikipedia.org/?curid=${jsoned.query.pages[0].pageid}`; 112 | 113 | callback(res) 114 | }); 115 | } catch (e) { console.log(e); onError(e); } 116 | } 117 | } -------------------------------------------------------------------------------- /src/ui/buttons/hover-buttons/marker.js: -------------------------------------------------------------------------------- 1 | let possibleMarkerColors = [ 2 | { 3 | 'color': 'yellow', 4 | 'foreground': 'black' 5 | }, 6 | { 7 | 'color': 'lightblue', 8 | 'foreground': 'black' 9 | }, 10 | { 11 | 'color': 'lightgreen', 12 | 'foreground': 'black' 13 | }, 14 | { 15 | 'color': 'red', 16 | 'foreground': 'white' 17 | }, 18 | ]; 19 | 20 | function addMarkerButton() { 21 | /// Create button 22 | const markerButton = addBasicTooltipButton(markerLabel, markerIcon, function () { 23 | markTextSelection(undefined, undefined, selection.toString()); 24 | saveAllMarkers(); 25 | removeSelectionOnPage(); 26 | }); 27 | markerButton.classList.add('higher-z-index'); 28 | 29 | /// Create color chooser panel 30 | setTimeout(function () { 31 | let colorChooserPanel = createHoverPanelForButton(markerButton, undefined, undefined, false, true, false, false); 32 | colorChooserPanel.style.maxWidth = '500%'; 33 | colorChooserPanel.classList.add('default-padding-tooltip'); 34 | 35 | /// Generate buttons to panel 36 | for (let i = 0, l = possibleMarkerColors.length; i < l; i++) { 37 | let selectedColor = possibleMarkerColors[i]; 38 | 39 | const button = document.createElement('button'); 40 | button.className = 'selection-popup-button'; 41 | if (configs.showButtonBorders) 42 | button.classList.add('button-with-border'); 43 | 44 | const colorCircle = document.createElement('div'); 45 | colorCircle.setAttribute('class', 'selection-popup-color-preview-circle'); 46 | colorCircle.style.background = selectedColor.color; 47 | button.appendChild(colorCircle); 48 | 49 | button.addEventListener("mousedown", function (e) { 50 | e.stopPropagation(); 51 | markTextSelection(selectedColor.color, selectedColor.foreground, selection.toString()); 52 | saveAllMarkers(); 53 | removeSelectionOnPage(); 54 | }); 55 | 56 | colorChooserPanel.prepend(button); 57 | } 58 | 59 | setBorderRadiusForSideButtons(colorChooserPanel, false); 60 | 61 | /// Append panel 62 | markerButton.appendChild(colorChooserPanel); 63 | }, 5) 64 | } -------------------------------------------------------------------------------- /src/ui/buttons/hover-buttons/search-button.js: -------------------------------------------------------------------------------- 1 | 2 | function setHoverForSearchButton(searchButton) { 3 | /// Create search options panel 4 | let searchPanel = createHoverPanelForButton(searchButton, undefined, undefined, true); 5 | searchPanel.classList.add('no-padding-tooltip'); 6 | searchPanel.style.textAlign = 'start'; 7 | 8 | /// Generate buttons for panel 9 | let searchButtons = configs.customSearchButtons.filter((item, idx) => item['enabled']); 10 | 11 | const searchButtonsLength = searchButtons.length; 12 | if (searchButtonsLength == 0) return; 13 | 14 | const containerPrototype = document.createElement('a'); 15 | containerPrototype.style.display = verticalSecondaryTooltip ? 'block' : 'inline-block'; 16 | containerPrototype.style.textAlign = 'start'; 17 | containerPrototype.className = 'custom-search-image-button'; 18 | if (!verticalSecondaryTooltip) containerPrototype.style.padding = '0px'; 19 | const maxIconsInRow = configs.maxIconsInRow; 20 | 21 | for (var i = 0; i < searchButtonsLength; i++) { 22 | const item = searchButtons[i]; 23 | 24 | const url = item['url']; 25 | const optionEnabled = item['enabled']; 26 | const title = item['title']; 27 | const icon = item['icon']; 28 | 29 | if (optionEnabled && url !== '') { 30 | let imgButton = document.createElement('img'); 31 | imgButton.setAttribute('class', 'selecton-search-tooltip-icon'); 32 | 33 | imgButton.addEventListener('error', function () { 34 | if (configs.debugMode) { 35 | console.log('error loading favicon for: ' + url + ' because of security policies of website'); 36 | } 37 | }); 38 | 39 | imgButton.setAttribute('src', icon !== null && icon !== undefined && icon !== '' ? icon : 'https://www.google.com/s2/favicons?domain=' + url.split('/')[2]) 40 | 41 | /// Set title 42 | let titleText = title !== null && title !== undefined && title !== '' ? title : returnDomainFromUrl(url); 43 | const container = containerPrototype.cloneNode(true); 44 | 45 | /// Add label in vertical style 46 | if (verticalSecondaryTooltip) { 47 | container.appendChild(imgButton); 48 | 49 | const labelSpan = document.createElement('span'); 50 | labelSpan.textContent = titleText.charAt(0).toUpperCase() + titleText.slice(1); 51 | container.appendChild(labelSpan); 52 | } else { 53 | /// No label in horizontal style 54 | imgButton.style.margin = '3px 6px'; 55 | imgButton.title = titleText; 56 | container.appendChild(imgButton); 57 | } 58 | 59 | searchPanel.appendChild(container); 60 | 61 | /// Set click listeners 62 | container.addEventListener("mousedown", function (e) { 63 | e.stopPropagation(); 64 | // onSearchButtonClick(e, url); 65 | }); 66 | container.href = returnSearchButtonUrl(url); 67 | container.target = '_blank'; 68 | } 69 | } 70 | 71 | containerPrototype.remove(); 72 | 73 | /// Create grid style to horizontal panel, to limit amount of icons in row 74 | if (!verticalSecondaryTooltip && searchButtonsLength > maxIconsInRow) { 75 | searchPanel.style.display = 'grid'; 76 | searchPanel.style.gridTemplateColumns = `repeat(${maxIconsInRow}, 1fr)`; 77 | } 78 | 79 | /// Set border radius for first and last buttons 80 | // const borderRadiusForButton = configs.useCustomStyle ? configs.borderRadius : 3; 81 | // const firstSearchButtonBorderRadius = verticalSecondaryTooltip ? 82 | // `${borderRadiusForButton}px ${borderRadiusForButton}px 0px 0px` 83 | // : firstButtonBorderRadius; 84 | // const lastSearchButtonBorderRadius = verticalSecondaryTooltip ? 85 | // `0px 0px ${borderRadiusForButton}px ${borderRadiusForButton}px` 86 | // : lastButtonBorderRadius; 87 | 88 | // let buttons = searchPanel.children; 89 | // buttons[0].style.borderRadius = firstSearchButtonBorderRadius; 90 | // buttons[buttons.length - 1].style.borderRadius = lastSearchButtonBorderRadius; 91 | 92 | /// Append panel 93 | searchButton.appendChild(searchPanel); 94 | } 95 | 96 | function returnSearchButtonUrl(url){ 97 | let selectedText = selection.toString(); 98 | selectedText = encodeURI(selectedText); 99 | selectedText = selectedText.replaceAll('&', '%26').replaceAll('+', '%2B'); 100 | let urlToOpen = url.replaceAll('%s', selectedText); 101 | 102 | if (urlToOpen.includes('%w')) 103 | try { 104 | let currentDomain = window.location.href.split('/')[2]; 105 | urlToOpen = urlToOpen.replaceAll('%w', currentDomain); 106 | } catch (e) { 107 | if (configs.debugMode) console.log(e); 108 | } 109 | 110 | return urlToOpen; 111 | } 112 | 113 | function onSearchButtonClick(e, url) { 114 | let urlToOpen = returnSearchButtonUrl(url) 115 | 116 | try { 117 | let evt = e || window.event; 118 | 119 | if ("buttons" in evt) { 120 | if (evt.button == 0) { 121 | /// Left button click 122 | hideTooltip(); 123 | removeSelectionOnPage(); 124 | chrome.runtime.sendMessage({ type: 'selecton-open-new-tab', url: urlToOpen, focused: true }); 125 | } else if (evt.button == 1) { 126 | /// Middle button click 127 | evt.preventDefault(); 128 | if (configs.middleClickHidesTooltip) { 129 | hideTooltip(); 130 | removeSelectionOnPage(); 131 | } 132 | 133 | chrome.runtime.sendMessage({ type: 'selecton-open-new-tab', url: urlToOpen, focused: false }); 134 | } 135 | } 136 | 137 | } catch (e) { 138 | window.open(urlToOpen, '_blank'); 139 | } 140 | } -------------------------------------------------------------------------------- /src/ui/buttons/hover-buttons/translate-button.js: -------------------------------------------------------------------------------- 1 | function addTranslateButton(onFinish, selectionLength, wordsCount) { 2 | try { 3 | if (!chrome.i18n.detectLanguage) proccessButton(true); 4 | else 5 | chrome.i18n.detectLanguage(selectedText, function (result) { 6 | if (configs.debugMode) 7 | console.log('Checking if its needed to add Translate button...'); 8 | 9 | /// Show Translate button when language was not detected 10 | let shouldTranslate = false; 11 | 12 | if (configs.debugMode) 13 | console.log(`User language is: ${configs.languageToTranslate}`); 14 | 15 | let detectedLanguages = result; 16 | let languageOfSelectedText; 17 | 18 | if (detectedLanguages !== null && detectedLanguages !== undefined) { 19 | const langs = detectedLanguages.languages; 20 | 21 | if (langs.length > 0) { 22 | languageOfSelectedText = langs[0].language; 23 | if (configs.debugMode) console.log('Detected language: ' + languageOfSelectedText); 24 | 25 | /// Show detected language on info panel 26 | if (configs.showInfoPanel && detectedLanguages.isReliable && !configs.verticalLayoutTooltip) 27 | setTimeout(function () { 28 | if (infoPanel && infoPanel.isConnected) { 29 | infoPanel.innerText += ' · ' + languageOfSelectedText; 30 | // let languageNames = new Intl.DisplayNames([configs.languageToTranslate], { type: 'language' }); 31 | // infoPanel.innerText += ' · ' + languageNames.of(languageOfSelectedText); 32 | } 33 | }, 5) 34 | 35 | // if (configs.debugMode) 36 | // console.log(`Detection is reliable: ${detectedLanguages.isReliable}`); 37 | 38 | /// Don't show translate button if selected language is the same as desired 39 | if (languageOfSelectedText == configs.languageToTranslate && configs.hideTranslateButtonForUserLanguage) 40 | shouldTranslate = false; 41 | else shouldTranslate = true; 42 | } else { 43 | if (configs.debugMode) console.log('Selecton failed to detect language of selected text'); 44 | shouldTranslate = configs.showTranslateIfLanguageUnknown ?? false; 45 | } 46 | } else { 47 | if (configs.debugMode) console.log('Selecton failed to detect language of selected text'); 48 | shouldTranslate = configs.showTranslateIfLanguageUnknown ?? false; 49 | } 50 | 51 | if (configs.debugMode) 52 | console.log(`Should translate: ${shouldTranslate}`); 53 | 54 | proccessButton(shouldTranslate, languageOfSelectedText); 55 | 56 | }); 57 | } catch (e) { 58 | if (configs.debugMode) 59 | console.log(e); 60 | } 61 | 62 | function proccessButton(shouldTranslate, languageOfSelectedText) { 63 | if (shouldTranslate == true) { 64 | setRegularTranslateButton(languageOfSelectedText, selectionLength, wordsCount); 65 | } 66 | if (onFinish) onFinish(); 67 | } 68 | } 69 | 70 | 71 | function setRegularTranslateButton(languageOfSelectedText, selectionLength, wordsCount) { 72 | 73 | const translateUrl = languageOfSelectedText == configs.languageToTranslate && !configs.hideTranslateButtonForUserLanguage ? 74 | returnTranslateUrl(selectedText, 'en') : 75 | returnTranslateUrl(selectedText); 76 | const translateButton = addLinkTooltipButton(translateLabel, translateButtonIcon, translateUrl); 77 | 78 | translateButton.setAttribute('id', 'selecton-translate-button'); 79 | 80 | /// set live tranlsation listeners 81 | // if (configs.liveTranslation && configs.preferredTranslateService == 'google' && selectedText.length < 500) { 82 | if (configs.liveTranslation && selectionLength < 500) { 83 | setTimeout(function () { 84 | if (configs.translateSingleWordsImmediately && wordsCount == 1 && !/[:\/"'']/.test(selectedText)) { 85 | fetchTranslation(selectedText, 'auto', configs.languageToTranslate, undefined, translateButton, true) 86 | } else { 87 | setLiveTranslateOnHoverButton(selectedText, 'auto', configs.languageToTranslate, translateButton); 88 | } 89 | }, 5); 90 | } 91 | } 92 | 93 | function setLiveTranslateOnHoverButton(word, sourceLang, targetLang, translateButton) { 94 | let fetched = false; 95 | let liveTranslationPanel = createHoverPanelForButton(translateButton, `${chrome.i18n.getMessage("translating") ?? 'Translating'}...`, onShow); 96 | translateButton.appendChild(liveTranslationPanel); 97 | 98 | function onShow() { 99 | if (fetched == false) { 100 | /// Fetch definition from Google Translate 101 | fetched = true; 102 | fetchTranslation(word, sourceLang, targetLang, liveTranslationPanel, translateButton) 103 | } 104 | } 105 | } 106 | 107 | async function fetchTranslation(word, sourceLang, targetLang, liveTranslationPanel, translateButton, showResultInButton = false) { 108 | // let maxLengthForResult = 100; 109 | let noTranslationLabel = chrome.i18n.getMessage("noTranslationFound"); 110 | 111 | const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&dt=bd&dj=1&q=${encodeURIComponent(word)}`; 112 | // const xhr = new XMLHttpRequest(); 113 | // xhr.responseType = "json"; 114 | // xhr.open("GET", url); 115 | // xhr.send(); 116 | 117 | // let result = await new Promise((resolve, reject) => { 118 | // xhr.onload = () => { 119 | // resolve(xhr); 120 | // }; 121 | // xhr.onerror = () => { 122 | // resolve(xhr); 123 | // }; 124 | // }); 125 | 126 | chrome.runtime.sendMessage({ type: 'background_fetch', url: url }, (response) => { 127 | let result = response; 128 | 129 | if (configs.debugMode) { 130 | console.log('Response from Google Translate:'); 131 | console.log(result); 132 | } 133 | 134 | if (!result) { 135 | liveTranslationPanel.innerText = noTranslationLabel; 136 | return; 137 | } 138 | 139 | let resultOfLiveTranslation; 140 | let originLanguage; 141 | 142 | try { 143 | resultOfLiveTranslation = result.dict[0].terms[0]; 144 | } catch (e) { 145 | // resultOfLiveTranslation = result.response.sentences[0].trans; 146 | resultOfLiveTranslation = ''; 147 | result.sentences.forEach(function (sentenceObj) { 148 | resultOfLiveTranslation += sentenceObj.trans; 149 | }) 150 | } 151 | 152 | try { 153 | originLanguage = result.src; 154 | } catch (e) { } 155 | 156 | /// Set translation view 157 | if (resultOfLiveTranslation !== null && resultOfLiveTranslation !== undefined && resultOfLiveTranslation !== '' && resultOfLiveTranslation.replaceAll(' ', '') !== word.replaceAll(' ', '')) { 158 | // if (resultOfLiveTranslation.length > maxLengthForResult) 159 | // resultOfLiveTranslation = resultOfLiveTranslation.substring(0, maxLengthForResult - 3) + '...'; 160 | 161 | if (showResultInButton){ 162 | let span = translateButton.querySelector('span'); 163 | if (!span) span = translateButton 164 | span.innerText = resultOfLiveTranslation; 165 | span.classList.add('selecton-live-translation'); 166 | translateButton.title = 'Provided by Google Translate'; 167 | } else { 168 | liveTranslationPanel.innerText = resultOfLiveTranslation; 169 | liveTranslationPanel.classList.add('selecton-live-translation'); 170 | } 171 | 172 | // setTimeout(function () { 173 | // /// check if panel goes off-screen on top 174 | // checkHoverPanelToOverflowOnTop(liveTranslationPanel); 175 | // }, 3); 176 | 177 | /// Create origin language label 178 | let originLabelWidth = configs.fontSize / 1.5; 179 | let originLabelPadding = 3.5; 180 | let langLabel; 181 | if (originLanguage !== null && originLanguage !== undefined && originLanguage !== '') { 182 | langLabel = document.createElement('span'); 183 | langLabel.textContent = originLanguage; 184 | langLabel.setAttribute('style', `opacity: 0.7; position: relative; right: -${originLabelPadding}px; bottom: -2.5px; font-size: ${originLabelWidth}px;color: var(--selection-button-foreground) !important`) 185 | 186 | if (showResultInButton){ 187 | translateButton.appendChild(langLabel); 188 | } else { 189 | liveTranslationPanel.appendChild(langLabel); 190 | } 191 | } 192 | } else { 193 | /// no translation found 194 | liveTranslationPanel.innerHTML = noTranslationLabel; 195 | } 196 | }); 197 | 198 | } -------------------------------------------------------------------------------- /src/ui/tooltip.js: -------------------------------------------------------------------------------- 1 | function createTooltip(e, recreated = false) { 2 | 3 | if (isDraggingTooltip) return; 4 | if (dontShowTooltip == true) return; 5 | if (e !== undefined && e !== null && e.button !== 0) return; 6 | 7 | setTimeout(function () { 8 | lastMouseUpEvent = e; 9 | if (selection == null || selection == undefined) return; 10 | // hideTooltip(); 11 | tooltipOnBottom = false; /// reset the 'reverted' state of previous tooltip 12 | 13 | if (configs.snapSelectionToWord) { 14 | // if (isTextFieldFocused == true && configs.dontSnapTextfieldSelection == true) { 15 | if (isTextFieldFocused == true) { 16 | if (configs.debugMode) 17 | console.log('Word snapping rejected while textfield is focused'); 18 | } else if (configs.disableWordSnappingOnCtrlKey && e !== undefined && (e.ctrlKey == true || e.metaKey == true)) { 19 | if (configs.debugMode) 20 | console.log('Word snapping rejected due to pressed CTRL key'); 21 | } else { 22 | // selectedText = selection.toString(); 23 | 24 | selectedTextIsCode = false; 25 | if (configs.disableWordSnapForCode || configs.showInfoPanel) 26 | for (let i = 0, l = codeMarkers.length; i < l; i++) { 27 | if (selectedText.includes(codeMarkers[i])) { 28 | selectedTextIsCode = true; break; 29 | } 30 | } 31 | 32 | /// dont snap if selection is modified by drag handle, or if it looks like code 33 | if (isDraggingDragHandle == false && 34 | (selectedTextIsCode == false || !configs.disableWordSnapForCode)){ 35 | if (domainIsBlacklistedForSnapping == false && 36 | e.detail < 2 && 37 | (timerToRecreateOverlays == null || timerToRecreateOverlays == undefined) && 38 | e.target.id !== 'selecton-extend-selection-button' && (!e.target.parentNode || e.target.parentNode.id !== 'selecton-extend-selection-button') 39 | ) { 40 | snapSelectionByWords(selection); 41 | } 42 | } 43 | 44 | } 45 | } 46 | 47 | /// Special tooltip for text fields 48 | if (isTextFieldFocused) { 49 | if (configs.addActionButtonsForTextFields == false) return; 50 | 51 | /// Create text field tooltip 52 | setUpTooltip(); 53 | addBasicTooltipButtons('textfield'); 54 | 55 | if (tooltip.children.length < 2) { 56 | /// Don't add tooltip with no buttons 57 | tooltip.remove(); 58 | return; 59 | } 60 | 61 | document.body.appendChild(tooltip); 62 | 63 | /// Check resulting DY to be out of view 64 | let resultDy = e.clientY - tooltip.clientHeight - arrow.clientHeight - 9; 65 | let vertOutOfView = resultDy <= 0; 66 | if (vertOutOfView) { 67 | resultDy = e.clientY + arrow.clientHeight; 68 | arrow.classList.add('arrow-on-bottom'); 69 | tooltipOnBottom = true; 70 | } 71 | 72 | showTooltip(e.clientX, resultDy); 73 | return; 74 | } 75 | 76 | /// Hide previous tooltip if exists 77 | if (tooltip) hideTooltip(); 78 | 79 | /// Check text selection again 80 | /// Fix for recreating tooltip when clicked inside selected area (noticed only in Firefox) 81 | selection = window.getSelection(); 82 | selectedText = selection.toString().trim(); 83 | 84 | if (selectedText == '') { 85 | hideDragHandles(); 86 | return; 87 | } 88 | 89 | setUpTooltip(recreated); 90 | 91 | /// Add basic buttons (Copy, Search, etc) 92 | addBasicTooltipButtons(null); 93 | 94 | if (dontShowTooltip == false && selectedText !== null && selectedText !== '') { 95 | addContextualButtons(function () { 96 | /// Set border radius for first and last buttons 97 | setBorderRadiusForSideButtons(tooltip); 98 | 99 | /// Append tooltip to the DOM 100 | document.body.appendChild(tooltip); 101 | 102 | /// Calculate tooltip position and show tooltip 103 | calculateTooltipPosition(e); 104 | 105 | /// Create search tooltip for custom search options) 106 | if (configs.customSearchOptionsDisplay == 'hoverCustomSearchStyle') 107 | setTimeout(function () { 108 | if (configs.secondaryTooltipEnabled && configs.customSearchButtons) 109 | setHoverForSearchButton(searchButton); 110 | }, 5); 111 | 112 | /// Selection change listener 113 | setTimeout(function () { 114 | if (tooltipIsShown == false) return; 115 | document.addEventListener("selectionchange", selectionChangeListener); 116 | }, configs.animationDuration); 117 | }); 118 | 119 | } else hideTooltip(); 120 | 121 | }, 0); 122 | } 123 | 124 | function setUpTooltip(recreated = false) { 125 | 126 | /// Create tooltip and it's arrow 127 | tooltip = document.createElement('div'); 128 | tooltip.className = 'selection-tooltip selecton-entity'; 129 | if (configs.verticalLayoutTooltip) { 130 | tooltip.classList.add('vertical-layout-tooltip'); 131 | tooltip.classList.add('reversed-order'); 132 | } 133 | if (configs.buttonsStyle == 'onlyicon' || configs.buttonsStyle == 'iconlabel') tooltip.classList.add('tooltip-with-icons'); 134 | tooltip.style.opacity = 0.0; 135 | tooltip.style.position = 'fixed'; 136 | tooltip.style.pointerEvents = 'none'; 137 | tooltip.style.transition = `opacity ${configs.animationDuration}ms ease-out, transform ${configs.animationDuration}ms ease-out`; 138 | if (recreated) tooltip.style.transition = `opacity ${configs.animationDuration}ms ease-out`; 139 | tooltip.style.transform = returnTooltipRevealTransform(false); 140 | // tooltip.style.transformOrigin = '50% 100% 0'; 141 | tooltip.style.transformOrigin = configs.tooltipRevealEffect == 'scaleUpTooltipEffect' ? '50% 30% 0' : configs.tooltipRevealEffect == 'scaleUpFromBottomTooltipEffect' ? '50% 125% 0' : '50% 100% 0'; 142 | 143 | if (configs.useCustomStyle && configs.tooltipOpacity != 1.0 && configs.tooltipOpacity != 1 && configs.fullOpacityOnHover) { 144 | tooltip.onmouseover = function () { 145 | setTimeout(function () { 146 | if (dontShowTooltip == true) return; 147 | try { 148 | tooltip.style.opacity = 1.0; 149 | } catch (e) { } 150 | }, 1); 151 | } 152 | tooltip.onmouseout = function () { 153 | setTimeout(function () { 154 | if (dontShowTooltip == true) return; 155 | try { 156 | tooltip.style.opacity = configs.tooltipOpacity; 157 | } catch (e) { } 158 | }, 1); 159 | } 160 | if (configs.debugMode) { 161 | console.log('Selecton tooltip inactive opacity: ' + configs.tooltipOpacity.toString()); 162 | console.log('Set tooltip opacity listeners'); 163 | } 164 | } 165 | 166 | /// Add tooltip arrow 167 | arrow = document.createElement('div'); 168 | if (configs.showTooltipArrow) arrow.setAttribute('class', 'selection-tooltip-arrow'); 169 | tooltip.appendChild(arrow); 170 | 171 | /// Make the tooltip draggable by arrow 172 | if (configs.showTooltipArrow && configs.draggableTooltip) { 173 | makeTooltipElementDraggable(arrow); 174 | } 175 | 176 | /// Apply custom stylings 177 | if (configs.useCustomStyle) { 178 | if (configs.addTooltipShadow) { 179 | tooltip.style.boxShadow = `0 2px 7px rgba(0,0,0,${configs.shadowOpacity})`; 180 | arrow.style.boxShadow = `1px 1px 3px rgba(0,0,0,${configs.shadowOpacity / 1.5})`; 181 | } 182 | /// Set rounded corners for buttons 183 | if (configs.verticalLayoutTooltip) { 184 | firstButtonBorderRadius = `0px 0px ${configs.borderRadius / 1.5}px ${configs.borderRadius / 1.5}px`; 185 | lastButtonBorderRadius = `${configs.borderRadius / 1.5}px ${configs.borderRadius / 1.5}px 0px 0px`; 186 | } else { 187 | firstButtonBorderRadius = `${configs.borderRadius / 1.5}px 0px 0px ${configs.borderRadius / 1.5}px`; 188 | lastButtonBorderRadius = `0px ${configs.borderRadius / 1.5}px ${configs.borderRadius / 1.5}px 0px`; 189 | } 190 | 191 | onlyButtonBorderRadius = `${configs.borderRadius / 1.5}px`; 192 | } else { 193 | /// Set default corners for buttons 194 | firstButtonBorderRadius = '3px 0px 0px 3px'; 195 | lastButtonBorderRadius = '0px 3px 3px 0px'; 196 | onlyButtonBorderRadius = '3px'; 197 | } 198 | 199 | if (configs.debugMode) 200 | console.log('Selecton tooltip was created'); 201 | } 202 | 203 | function calculateTooltipPosition(e) { 204 | const selStartDimensions = getSelectionCoordinates(true); 205 | const selEndDimensions = getSelectionCoordinates(false); 206 | 207 | let canAddDragHandles = true; 208 | if (selStartDimensions.dontAddDragHandles) canAddDragHandles = false; 209 | let dyForFloatingTooltip = 15; 210 | let dyWhenOffscreen = window.innerHeight / 3; 211 | let tooltipHeight = tooltip.clientHeight; 212 | let dxToShowTooltip, dyToShowTooltip; 213 | 214 | if (configs.tooltipPosition == 'overCursor' && e.clientX < window.innerWidth - 30) { 215 | 216 | /// Show it on top of selection, dx aligned to cursor 217 | dyToShowTooltip = selStartDimensions.dy - tooltipHeight - (arrow.clientHeight / 1.5) - 2; 218 | let vertOutOfView = dyToShowTooltip <= 0; 219 | 220 | if (vertOutOfView || (selEndDimensions.dy - selStartDimensions.dy > 2.0 && selEndDimensions.backwards !== true)) { 221 | /// show tooltip under selection 222 | let possibleDyToShowTooltip = selEndDimensions.dy + (selEndDimensions.lineHeight ?? 0) + arrow.clientHeight; 223 | 224 | if (possibleDyToShowTooltip < window.innerHeight) { 225 | dyToShowTooltip = possibleDyToShowTooltip; 226 | setTooltipOnBottom(); 227 | 228 | if (configs.verticalLayoutTooltip) tooltip.classList.remove('reversed-order'); 229 | } 230 | } 231 | 232 | /// Check to be off-screen on top 233 | if (dyToShowTooltip < 0 && tooltipOnBottom == false) dyToShowTooltip = dyWhenOffscreen; 234 | 235 | /// Calculating DX 236 | dxToShowTooltip = e.clientX; 237 | 238 | } else { 239 | /// Calculating DY 240 | dyToShowTooltip = selStartDimensions.dy - tooltipHeight - arrow.clientHeight; 241 | 242 | /// If tooltip is going off-screen on top... 243 | let vertOutOfView = dyToShowTooltip <= 0; 244 | if (vertOutOfView) { 245 | /// check to display on bottom 246 | let resultingDyOnBottom = selEndDimensions.dy + (selEndDimensions.lineHeight ?? 0) + arrow.clientHeight; 247 | if (resultingDyOnBottom < window.innerHeight) { 248 | dyToShowTooltip = resultingDyOnBottom; 249 | setTooltipOnBottom(); 250 | if (configs.verticalLayoutTooltip) tooltip.classList.remove('reversed-order'); 251 | } else { 252 | /// if it will be off-screen as well, use off-screen dy 253 | dyToShowTooltip = dyWhenOffscreen; 254 | } 255 | } 256 | 257 | /// Add small padding 258 | if (configs.showTooltipArrow) dyToShowTooltip = dyToShowTooltip + 2; 259 | 260 | /// Calculating DX 261 | try { 262 | /// New approach - place tooltip in horizontal center between two selection handles 263 | const delta = selEndDimensions.dx > selStartDimensions.dx ? selEndDimensions.dx - selStartDimensions.dx : selStartDimensions.dx - selEndDimensions.dx; 264 | 265 | if (selEndDimensions.dx > selStartDimensions.dx) 266 | dxToShowTooltip = selStartDimensions.dx + (delta / 2); 267 | else 268 | dxToShowTooltip = selEndDimensions.dx + (delta / 2); 269 | } catch (e) { 270 | if (configs.debugMode) 271 | console.log(e); 272 | 273 | /// Fall back to old approach - place tooltip in horizontal center selection rect, 274 | /// which may be in fact bigger than visible selection 275 | const selDimensions = getSelectionRectDimensions(); 276 | dxToShowTooltip = selDimensions.dx + (selDimensions.width / 2); 277 | } 278 | } 279 | 280 | if (configs.floatingOffscreenTooltip) { 281 | /// Keep panel floating when off-screen 282 | floatingTooltipTop = false; floatingTooltipBottom = false; 283 | if (dyToShowTooltip < 0) { 284 | dyToShowTooltip = dyForFloatingTooltip; 285 | floatingTooltipTop = window.scrollY; 286 | } else if (dyToShowTooltip > window.innerHeight) { 287 | dyToShowTooltip = window.innerHeight - (tooltipHeight ?? 50) - dyForFloatingTooltip; 288 | floatingTooltipBottom = window.scrollY; 289 | } 290 | } 291 | 292 | showTooltip(dxToShowTooltip, dyToShowTooltip); 293 | 294 | if (configs.addDragHandles && canAddDragHandles) 295 | setDragHandles(selStartDimensions, selEndDimensions); 296 | } 297 | 298 | function showTooltip(dx, dy) { 299 | tooltip.style.pointerEvents = 'none'; 300 | tooltip.style.opacity = configs.useCustomStyle ? configs.tooltipOpacity : 1.0; 301 | tooltip.style.top = `${dy}px`; 302 | tooltip.style.left = `${dx}px`; 303 | 304 | /// Set reveal animation type 305 | tooltip.style.transform = returnTooltipRevealTransform(true); 306 | 307 | /// Check for colliding with side edges 308 | checkTooltipForCollidingWithSideEdges(); 309 | 310 | if (configs.debugMode) 311 | console.log('Selecton tooltip is shown'); 312 | tooltipIsShown = true; 313 | 314 | /// Make tooltip interactive only after transition ends 315 | let currentTooltip = tooltip; 316 | setTimeout(function () { 317 | if (tooltipIsShown == false || tooltip == null) return; 318 | currentTooltip.style.pointerEvents = 'all'; 319 | }, configs.animationDuration); 320 | } 321 | 322 | let oldTooltips; 323 | function hideTooltip(animated = true) { 324 | if (!tooltip) return; 325 | 326 | if (configs.debugMode) { 327 | console.log('--- Hiding Selecton tooltips ---'); 328 | console.log('Checking for existing tooltips...'); 329 | } 330 | 331 | /// Hide all tooltips 332 | if (!oldTooltips) oldTooltips = document.getElementsByClassName('selecton-entity'); 333 | 334 | if (oldTooltips && oldTooltips.length) { 335 | tooltipIsShown = false; 336 | 337 | if (configs.debugMode) 338 | console.log(`Found ${oldTooltips.length} tooltips to hide`); 339 | 340 | for (let i = 0, l = oldTooltips.length; i < l; i++) { 341 | const oldTooltip = oldTooltips[i]; 342 | if (!animated) 343 | oldTooltip.style.transition = ''; 344 | oldTooltip.style.opacity = 0.0; 345 | oldTooltip.style.pointerEvents = 'none'; 346 | 347 | setTimeout(function () { 348 | oldTooltip.remove(); 349 | }, animated ? configs.animationDuration : 0); 350 | } 351 | } else { 352 | if (configs.debugMode) 353 | console.log('No existing tooltips found'); 354 | } 355 | 356 | tooltip.style.pointerEvents = 'none'; 357 | tooltip = null; 358 | secondaryTooltip = null; 359 | timerToRecreateOverlays = null; 360 | isTextFieldFocused = false; 361 | 362 | document.removeEventListener("selectionchange", selectionChangeListener); 363 | window.removeEventListener('mousemove', mouseMoveToHideListener); 364 | } -------------------------------------------------------------------------------- /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 | background: "./src/functions/background.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/data/**", 25 | [ 26 | "./src/functions/**", 27 | "!./src/functions/background.js", 28 | ], 29 | "./src/ui/**/**", 30 | "./src/index.js", 31 | ] 32 | }), 33 | /// static files 34 | new CopyPlugin({ 35 | patterns: [ 36 | "src/manifest.json", 37 | "src/index.css", 38 | { from: "src/_locales", to: "_locales" }, 39 | { from: "src/assets", to: "assets" }, 40 | { from: "src/popup", to: "popup" }, 41 | { from: "src/options", to: "options" }, 42 | /// additional dependencies for toolbar popup and options page 43 | { from: "src/data/configs.js", to: "src/data/" }, 44 | { from: "src/data/currencies.js", to: "src/data/" }, 45 | ], 46 | }), 47 | ], 48 | mode: 'production', 49 | optimization: { 50 | minimize: true, 51 | minimizer: [ 52 | new TerserPlugin(), 53 | new CssMinimizerPlugin(), 54 | new JsonMinimizerPlugin(), 55 | ], 56 | }, 57 | }; --------------------------------------------------------------------------------