diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2d7fd34..e3a2a57 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,71 +1,71 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ master ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] - schedule: - - cron: '38 20 * * 0' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '38 20 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.gitignore b/.gitignore index 21cbec3..1e52bdd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.vscode -**/*.Identifier -.idea +.vscode +**/*.Identifier +.idea .**/*.DS_Store \ No newline at end of file diff --git a/LICENSE b/LICENSE index 983aab6..f8bebd8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2019-2022 Mansur - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2019-2022 Mansur + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 9b327f9..81aa198 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,28 @@ -# PricyMorph 🔄🌍 - -#### đŸ”Ĩ A powerful Chrome Extension that lets you convert currencies effortlessly on any website! Simply select an element in the DOM, and it does the rest. 🌍💱 - -![Demo](assets/img/demo.gif) - -## đŸ“ĸ Notice -### 🐞 Found a bug?
Please open an issue! Your help is greatly appreciated! 🙏🔧
- -### [Download](https://github.com/AydievMansur/price_parse_chrome/archive/master.zip) ⚡ī¸ **WIP** - -##### 🚀 Features: - -- Easy & friendly UI -- A multi site selector support -- Any chrome based browser support -- No metric collection -- Open source -- Regular updates -- Lifetime free - - -🙌 You can also: - -- Create a pull request to help develop this plugin -- All contributors will be mentioned in this repository - -[Download](https://github.com/AydievMansur/price_parse_chrome/archive/master.zip) +# PricyMorph 🔄🌍 + +#### đŸ”Ĩ A powerful Chrome Extension that lets you convert currencies effortlessly on any website! Simply select an element in the DOM, and it does the rest. 🌍💱 + +![Demo](assets/img/demo.gif) + +## đŸ“ĸ Notice +### 🐞 Found a bug?
Please open an issue! Your help is greatly appreciated! 🙏🔧
+ +### [Download](https://github.com/AydievMansur/price_parse_chrome/archive/master.zip) ⚡ī¸ **WIP** + +##### 🚀 Features: + +- Easy & friendly UI +- A multi site selector support +- Any chrome based browser support +- No metric collection +- Open source +- Regular updates +- Lifetime free + + +🙌 You can also: + +- Create a pull request to help develop this plugin +- All contributors will be mentioned in this repository + +[Download](https://github.com/AydievMansur/price_parse_chrome/archive/master.zip) diff --git a/assets/img/icon.png b/assets/img/icon.png new file mode 100644 index 0000000..f6ff5a5 Binary files /dev/null and b/assets/img/icon.png differ diff --git a/assets/js/app.js b/assets/js/app.js index 70ef5cb..e3cad1f 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -7,15 +7,34 @@ class DOMCurrencyConverter { this.convertedElements = new Map(); this.selectionMode = false; this.highlightOverlay = null; + this.currentDropdownType = null; + this.observer = null; + this.updateTimeout = null; this.handleMouseMove = this.handleMouseMove.bind(this); this.handleClick = this.handleClick.bind(this); this.handleCurrencyChange = this.handleCurrencyChange.bind(this); this.handleSwitchCurrencies = this.handleSwitchCurrencies.bind(this); + this.toggleSelectionMode = this.toggleSelectionMode.bind(this); + + this.debouncedHandleMouseMove = this.debounce(this.handleMouseMove, 16); + this.debouncedUpdatePrices = this.debounce(this.updateAllPrices.bind(this), 100); this.initialize(); } + debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } + async initialize() { try { await this.loadPreferences(); @@ -28,7 +47,7 @@ class DOMCurrencyConverter { this.initializePopup(); } - console.log('DOMCurrencyConverter initialized with:', { + console.debug('DOMCurrencyConverter initialized with:', { fromCurrency: this.fromCurrency, toCurrency: this.toCurrency }); @@ -48,13 +67,17 @@ class DOMCurrencyConverter { this.fromBox?.addEventListener('click', () => this.showDropdown('from')); this.toBox?.addEventListener('click', () => this.showDropdown('to')); this.switchButton?.addEventListener('click', this.handleSwitchCurrencies); - this.selectModeButton?.addEventListener('click', () => this.toggleSelectionMode()); + this.selectModeButton?.addEventListener('click', () => { + this.selectionMode = !this.selectionMode; + this.toggleSelectionMode(this.selectionMode); + }); document.addEventListener('click', (e) => { if (this.dropdown && !this.dropdown.contains(e.target) && !this.fromBox.contains(e.target) && !this.toBox.contains(e.target)) { this.dropdown.style.display = 'none'; + this.currentDropdownType = null; } }); @@ -102,6 +125,11 @@ class DOMCurrencyConverter { if (message.action === 'toggleSelection') { this.toggleSelectionMode(message.selectionMode); sendResponse({ success: true }); + } else if (message.action === 'currencyUpdated') { + this.fromCurrency = message.fromCurrency; + this.toCurrency = message.toCurrency; + this.debouncedUpdatePrices(); + sendResponse({ success: true }); } return true; }); @@ -131,13 +159,13 @@ class DOMCurrencyConverter { } if (enabled) { - document.addEventListener('mousemove', this.handleMouseMove); + document.addEventListener('mousemove', this.debouncedHandleMouseMove); document.addEventListener('click', this.handleClick); } else { if (this.highlightOverlay) { this.highlightOverlay.style.display = 'none'; } - document.removeEventListener('mousemove', this.handleMouseMove); + document.removeEventListener('mousemove', this.debouncedHandleMouseMove); document.removeEventListener('click', this.handleClick); } @@ -187,8 +215,11 @@ class DOMCurrencyConverter { if (this.highlightOverlay) { this.highlightOverlay.style.display = 'none'; } - + + this.selectionMode = false; this.toggleSelectionMode(false); + + this.updateSelectionButton(); } } @@ -205,26 +236,44 @@ class DOMCurrencyConverter { } setupMutationObserver() { - if (!document.body) return; + if (this.observer) { + this.observer.disconnect(); + } - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { + this.observer = new MutationObserver(mutations => { + let shouldUpdate = false; + for (const mutation of mutations) { if (mutation.type === 'childList' || mutation.type === 'characterData') { - this.convertedElements.forEach((originalData, element) => { - if (document.contains(element)) { - this.updateConvertedPrice(element, originalData); - } else { - this.convertedElements.delete(element); - } + const affectedElement = [...this.convertedElements.keys()].some(element => { + return mutation.target.contains(element) || element.contains(mutation.target); }); + if (affectedElement) { + shouldUpdate = true; + break; + } } - }); + } + if (shouldUpdate) { + this.debouncedUpdatePrices(); + } }); - observer.observe(document.body, { - childList: true, - subtree: true, - characterData: true + if (document.body) { + this.observer.observe(document.body, { + childList: true, + subtree: true, + characterData: true + }); + } + } + + updateAllPrices() { + this.convertedElements.forEach((originalData, element) => { + if (document.contains(element)) { + this.updateConvertedPrice(element, originalData); + } else { + this.convertedElements.delete(element); + } }); } @@ -275,21 +324,34 @@ class DOMCurrencyConverter { showDropdown(type) { if (!this.dropdown || !this.rates) return; + if (this.currentDropdownType === type) { + this.dropdown.style.display = 'none'; + this.currentDropdownType = null; + return; + } + const box = type === 'from' ? this.fromBox : this.toBox; const rect = box.getBoundingClientRect(); this.dropdown.style.top = `${rect.bottom}px`; this.dropdown.style.left = `${rect.left}px`; this.dropdown.style.display = 'block'; + this.currentDropdownType = type; - this.dropdown.innerHTML = Object.keys(this.rates).map(currency => ` -
- ${currency} -
- `).join(''); + const newDropdown = this.dropdown.cloneNode(false); + this.dropdown.parentNode.replaceChild(newDropdown, this.dropdown); + this.dropdown = newDropdown; + + this.dropdown.innerHTML = Object.keys(this.rates) + .sort() + .map(currency => ` +
+ ${currency} +
+ `).join(''); this.dropdown.querySelectorAll('.currency-option').forEach(option => { - option.addEventListener('click', (e) => this.handleCurrencyChange(e)); + option.addEventListener('click', this.handleCurrencyChange); }); } @@ -304,6 +366,7 @@ class DOMCurrencyConverter { } this.updatePopupUI(); + this.currentDropdownType = null; // Reset dropdown state if (this.dropdown) { this.dropdown.style.display = 'none'; } @@ -338,6 +401,26 @@ class DOMCurrencyConverter { this.rateInfo.textContent = `1 ${this.fromCurrency} = ${rate.toFixed(2)} ${this.toCurrency}`; } + + cleanup() { + if (this.observer) { + this.observer.disconnect(); + this.observer = null; + } + + document.removeEventListener('mousemove', this.debouncedHandleMouseMove); + document.removeEventListener('click', this.handleClick); + + if (this.highlightOverlay && this.highlightOverlay.parentNode) { + this.highlightOverlay.parentNode.removeChild(this.highlightOverlay); + } + + this.convertedElements.clear(); + + if (this.updateTimeout) { + clearTimeout(this.updateTimeout); + } + } } if (document.body) { diff --git a/manifest.json b/manifest.json index 04d7371..037fdbd 100644 --- a/manifest.json +++ b/manifest.json @@ -1,29 +1,29 @@ -{ - "manifest_version": 3, - "name": "PricyMorph", - "description": "Automatically convert currencies on any webpage", - "version": "1.0.0", - "icons": { - "128": "assets/img/demo.gif" - }, - "action": { - "default_popup": "popup.html" - }, - "content_scripts": [ - { - "matches": [""], - "js": ["assets/js/app.js"], - "run_at": "document_end" - } - ], - "permissions": [ - "storage", - "activeTab", - "scripting", - "tabs" - ], - "host_permissions": [ - "https://api.exchangerate-api.com/*", - "" - ] -} +{ + "manifest_version": 3, + "name": "PricyMorph", + "description": "Automatically convert currencies on any webpage", + "version": "1.0.0", + "icons": { + "128": "assets/img/icon.png" + }, + "action": { + "default_popup": "popup.html" + }, + "content_scripts": [ + { + "matches": [""], + "js": ["assets/js/app.js"], + "run_at": "document_end" + } + ], + "permissions": [ + "storage", + "activeTab", + "scripting", + "tabs" + ], + "host_permissions": [ + "https://api.exchangerate-api.com/*", + "" + ] +} diff --git a/popup.html b/popup.html index 045a890..685d94c 100644 --- a/popup.html +++ b/popup.html @@ -1,309 +1,309 @@ - - - - - - - - - -
-
-
USD
-
1.00
-
- - - -
-
RUB
-
60.00
-
- -
1 USD = 60.00 RUB
- - -
-
- - - + + + + + + + + + +
+
+
USD
+
1.00
+
+ + + +
+
RUB
+
60.00
+
+ +
1 USD = 60.00 RUB
+ + +
+
+ + + \ No newline at end of file diff --git a/popup.js b/popup.js index da177bd..b540954 100644 --- a/popup.js +++ b/popup.js @@ -38,7 +38,6 @@ class CurrencyConverter { this.rateInfo = document.querySelector('.rate-info'); this.selectModeButton = document.getElementById('selectModeButton'); - // Add app info section const container = document.querySelector('.currency-container'); const manifestData = await this.getManifestData(); const appInfoDiv = document.createElement('div'); @@ -117,11 +116,11 @@ class CurrencyConverter { } setupEventListeners() { - // Show dropdown only when clicking currency code this.fromCode.addEventListener('click', (e) => { e.stopPropagation(); this.showDropdown('from'); }); + this.toCode.addEventListener('click', (e) => { e.stopPropagation(); this.showDropdown('to'); @@ -130,7 +129,6 @@ class CurrencyConverter { this.switchButton.addEventListener('click', () => this.switchCurrencies()); this.selectModeButton.addEventListener('click', () => this.toggleSelectionMode()); - // Add input event listeners for currency amounts this.fromAmount.addEventListener('click', (e) => { e.stopPropagation(); }); @@ -145,14 +143,15 @@ class CurrencyConverter { this.handleAmountInput(e, 'to'); }); - // Prevent non-numeric input this.fromAmount.addEventListener('keypress', this.validateNumericInput); this.toAmount.addEventListener('keypress', this.validateNumericInput); document.addEventListener('click', (e) => { - if (!this.dropdown.contains(e.target) && - !this.fromCode.contains(e.target) && - !this.toCode.contains(e.target)) { + const clickedFromCode = this.fromCode.contains(e.target); + const clickedToCode = this.toCode.contains(e.target); + const clickedDropdown = this.dropdown.contains(e.target); + + if (!clickedDropdown && !clickedFromCode && !clickedToCode) { this.dropdown.style.display = 'none'; } }); @@ -232,8 +231,13 @@ class CurrencyConverter { showDropdown(type) { const currencies = Object.keys(this.rates || this.currencyNames); - - // Create search input and currency list + const isVisible = this.dropdown.style.display === 'flex' || this.dropdown.style.display === 'block'; + const isSameType = this.dropdown.dataset.type === type; + + if (isVisible && isSameType) { + this.dropdown.style.display = 'none'; + return; + } this.dropdown.innerHTML = ` `; - // Position the dropdown + this.dropdown.dataset.type = type; + const activeBox = type === 'from' ? this.fromBox : this.toBox; const boxRect = activeBox.getBoundingClientRect(); const containerRect = this.dropdown.parentElement.getBoundingClientRect(); - // Always position below the clicked element this.dropdown.style.display = 'block'; this.dropdown.style.top = `${boxRect.bottom - containerRect.top + 5}px`; @@ -296,13 +300,26 @@ class CurrencyConverter { this.toCurrency = code; this.toCode.textContent = code; } - this.updateConversion(); - this.dropdown.style.display = 'none'; chrome.storage.local.set({ fromCurrency: this.fromCurrency, toCurrency: this.toCurrency }); + + this.updateConversion(); + if (this.selectionMode) { + chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => { + if (tab) { + chrome.tabs.sendMessage(tab.id, { + action: 'currencyUpdated', + fromCurrency: this.fromCurrency, + toCurrency: this.toCurrency + }); + } + }); + } + + this.dropdown.style.display = 'none'; }); }); }