diff --git a/packages/browser-extension/docs/README.md b/packages/browser-extension/docs/README.md index eb6dbac..06eff4e 100644 --- a/packages/browser-extension/docs/README.md +++ b/packages/browser-extension/docs/README.md @@ -1,5 +1,9 @@ # Cookie Monster Dialog Browser Extension +This browser extension was designed to remove cookie consent dialogs that appear on websites without setting your preferences. Only in a few cases, it operates based on predefined rules specified in the file `data/fixes.txt` from where the extension will automatically decline or accept the cookie consent dialogs. + +Please note that the `data/fixes.txt` file should be regularly updated to reflect changes in websites' cookie consent practices and to ensure accurate handling of the dialogs by the extension. + ## Downloads - [Chrome Web Store](https://chrome.google.com/webstore/detail/djcbfpkdhdkaflcigibkbpboflaplabg) @@ -11,9 +15,9 @@ - All browsers based on Chromium 88+ (Blisk, Brave, Colibri, Epic Browser, Iron Browser, Vivaldi and many more) - Google Chrome 88+ - Microsoft Edge 88+ -- ~~Mozilla Firefox 54+~~ (development stalled until further notice, you can still download and use this extension in its **5.5.5** version) +- ~~Mozilla Firefox~~ (development stalled until further notice) -## Installation (only for developers) +## Installation (only for developers or Mozilla Firefox users) 1. Clone this repository and then run `yarn install` 2. Build this repository running the command `yarn workspace browser-extension run build` diff --git a/packages/browser-extension/src/_locales/de/messages.json b/packages/browser-extension/src/_locales/de/messages.json index 3c5bfda..0eb6fa4 100644 --- a/packages/browser-extension/src/_locales/de/messages.json +++ b/packages/browser-extension/src/_locales/de/messages.json @@ -5,6 +5,21 @@ "contextMenu_reportOption": { "message": "Webseite melden..." }, + "options_clearButton": { + "message": "Liste leeren" + }, + "options_exclusionListTitle": { + "message": "Ausschlussliste" + }, + "options_exportButton": { + "message": "Liste exportieren" + }, + "options_filterPlaceholder": { + "message": "Nach dem Eintippen ENTER drücken, um zu filtern" + }, + "options_importButton": { + "message": "Liste importieren" + }, "popup_contributeOption": { "message": "Tragen Sie zu diesem Projekt bei" }, diff --git a/packages/browser-extension/src/_locales/en/messages.json b/packages/browser-extension/src/_locales/en/messages.json index 5c6d67a..b9082e4 100644 --- a/packages/browser-extension/src/_locales/en/messages.json +++ b/packages/browser-extension/src/_locales/en/messages.json @@ -5,6 +5,21 @@ "contextMenu_reportOption": { "message": "Report site..." }, + "options_clearButton": { + "message": "Clear list" + }, + "options_exclusionListTitle": { + "message": "Exclusion list" + }, + "options_exportButton": { + "message": "Export list" + }, + "options_filterPlaceholder": { + "message": "Press ENTER to filter after typing" + }, + "options_importButton": { + "message": "Import list" + }, "popup_contributeOption": { "message": "Contribute to this project" }, diff --git a/packages/browser-extension/src/_locales/es/messages.json b/packages/browser-extension/src/_locales/es/messages.json index dbbc5a3..b06a079 100644 --- a/packages/browser-extension/src/_locales/es/messages.json +++ b/packages/browser-extension/src/_locales/es/messages.json @@ -5,6 +5,21 @@ "contextMenu_reportOption": { "message": "Reportar sitio..." }, + "options_clearButton": { + "message": "Borrar lista" + }, + "options_exclusionListTitle": { + "message": "Lista de exclusión" + }, + "options_exportButton": { + "message": "Exportar lista" + }, + "options_filterPlaceholder": { + "message": "Presiona ENTER para filtrar después de escribir" + }, + "options_importButton": { + "message": "Importar lista" + }, "popup_contributeOption": { "message": "Contribuye a este proyecto" }, diff --git a/packages/browser-extension/src/_locales/fr/messages.json b/packages/browser-extension/src/_locales/fr/messages.json index 085f0ec..d7fc993 100644 --- a/packages/browser-extension/src/_locales/fr/messages.json +++ b/packages/browser-extension/src/_locales/fr/messages.json @@ -5,6 +5,21 @@ "contextMenu_reportOption": { "message": "Signaler le site..." }, + "options_clearButton": { + "message": "Effacer la liste" + }, + "options_exclusionListTitle": { + "message": "Liste d'exclusion" + }, + "options_exportButton": { + "message": "Exporter la liste" + }, + "options_filterPlaceholder": { + "message": "Appuyez sur ENTRÉE pour filtrer après avoir tapé" + }, + "options_importButton": { + "message": "Importer la liste" + }, "popup_contributeOption": { "message": "Contribuez à ce projet" }, diff --git a/packages/browser-extension/src/_locales/it/messages.json b/packages/browser-extension/src/_locales/it/messages.json index 2f7a7de..c1534a1 100644 --- a/packages/browser-extension/src/_locales/it/messages.json +++ b/packages/browser-extension/src/_locales/it/messages.json @@ -5,6 +5,21 @@ "contextMenu_reportOption": { "message": "Segnala sito..." }, + "options_clearButton": { + "message": "Cancella elenco" + }, + "options_exclusionListTitle": { + "message": "Elenco di esclusione" + }, + "options_exportButton": { + "message": "Esporta elenco" + }, + "options_filterPlaceholder": { + "message": "Premi INVIO per filtrare dopo aver digitato" + }, + "options_importButton": { + "message": "Importa elenco" + }, "popup_contributeOption": { "message": "Contribuisci a questo progetto" }, diff --git a/packages/browser-extension/src/_locales/pt_BR/messages.json b/packages/browser-extension/src/_locales/pt_BR/messages.json index b2ea0e0..eb19af5 100644 --- a/packages/browser-extension/src/_locales/pt_BR/messages.json +++ b/packages/browser-extension/src/_locales/pt_BR/messages.json @@ -5,6 +5,21 @@ "contextMenu_reportOption": { "message": "Reportar site..." }, + "options_clearButton": { + "message": "Limpar lista" + }, + "options_exclusionListTitle": { + "message": "Lista de exclusão" + }, + "options_exportButton": { + "message": "Exportar lista" + }, + "options_filterPlaceholder": { + "message": "Pressione ENTER para filtrar após digitar" + }, + "options_importButton": { + "message": "Importar lista" + }, "popup_contributeOption": { "message": "Contribua para este projeto" }, diff --git a/packages/browser-extension/src/_locales/pt_PT/messages.json b/packages/browser-extension/src/_locales/pt_PT/messages.json index b2ea0e0..eb19af5 100644 --- a/packages/browser-extension/src/_locales/pt_PT/messages.json +++ b/packages/browser-extension/src/_locales/pt_PT/messages.json @@ -5,6 +5,21 @@ "contextMenu_reportOption": { "message": "Reportar site..." }, + "options_clearButton": { + "message": "Limpar lista" + }, + "options_exclusionListTitle": { + "message": "Lista de exclusão" + }, + "options_exportButton": { + "message": "Exportar lista" + }, + "options_filterPlaceholder": { + "message": "Pressione ENTER para filtrar após digitar" + }, + "options_importButton": { + "message": "Importar lista" + }, "popup_contributeOption": { "message": "Contribua para este projeto" }, diff --git a/packages/browser-extension/src/_locales/ro/messages.json b/packages/browser-extension/src/_locales/ro/messages.json index 8889b57..deeeafa 100644 --- a/packages/browser-extension/src/_locales/ro/messages.json +++ b/packages/browser-extension/src/_locales/ro/messages.json @@ -5,6 +5,21 @@ "contextMenu_reportOption": { "message": "Raportați site-ul..." }, + "options_clearButton": { + "message": "Ștergeți lista" + }, + "options_exclusionListTitle": { + "message": "Listă de excludere" + }, + "options_exportButton": { + "message": "Exportați lista" + }, + "options_filterPlaceholder": { + "message": "Apăsați ENTER pentru a filtra după ce ați tastat" + }, + "options_importButton": { + "message": "Importați lista" + }, "popup_contributeOption": { "message": "Contribuie la acest proiect" }, diff --git a/packages/browser-extension/src/_locales/ru/messages.json b/packages/browser-extension/src/_locales/ru/messages.json index 42dc71b..b12a04a 100644 --- a/packages/browser-extension/src/_locales/ru/messages.json +++ b/packages/browser-extension/src/_locales/ru/messages.json @@ -5,6 +5,21 @@ "contextMenu_reportOption": { "message": "Сообщить о сайте..." }, + "options_clearButton": { + "message": "Очистить список" + }, + "options_exclusionListTitle": { + "message": "Список исключений" + }, + "options_exportButton": { + "message": "Экспорт списка" + }, + "options_filterPlaceholder": { + "message": "Нажмите ENTER для фильтрации после ввода" + }, + "options_importButton": { + "message": "Импорт списка" + }, "popup_contributeOption": { "message": "Внести свой вклад в этот проект" }, diff --git a/packages/browser-extension/src/manifest.json b/packages/browser-extension/src/manifest.json index 06c8048..3fcdb87 100644 --- a/packages/browser-extension/src/manifest.json +++ b/packages/browser-extension/src/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Cookie Dialog Monster", - "version": "6.3.4", + "version": "6.4.0", "default_locale": "en", "description": "__MSG_appDesc__", "icons": { @@ -13,6 +13,7 @@ "default_icon": "assets/icons/disabled.png", "default_title": "Cookie Dialog Monster" }, + "options_page": "options.html", "author": "wanhose", "background": { "service_worker": "scripts/background.js" @@ -25,6 +26,7 @@ "*://*.sharepoint.com/*", "*://*.youtube.com/embed/*", "*://*.youtube-nocookie.com/embed/*", + "https://translate.google.com/*", "https://www.cookie-dialog-monster.com/*" ], "js": ["scripts/content.js", "scripts/dialog.js"], diff --git a/packages/browser-extension/src/options.html b/packages/browser-extension/src/options.html new file mode 100644 index 0000000..3c9f0ca --- /dev/null +++ b/packages/browser-extension/src/options.html @@ -0,0 +1,110 @@ + + + + Cookie Dialog Monster > Exclusion List + + + + + + +
+
+

+ Cookie Dialog Monster > +

+
+
+
+
+ + + +
+ + + +
+ + + diff --git a/packages/browser-extension/src/popup.html b/packages/browser-extension/src/popup.html index 3005f9b..a314d83 100644 --- a/packages/browser-extension/src/popup.html +++ b/packages/browser-extension/src/popup.html @@ -1,7 +1,6 @@ - @@ -10,6 +9,24 @@

Cookie Dialog Monster

+
@@ -17,10 +34,10 @@ aria-hidden="true" fill="none" height="32" - stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" + stroke="currentColor" viewBox="0 0 24 24" width="32" > @@ -34,10 +51,10 @@ aria-hidden="true" fill="none" height="32" - stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" + stroke="currentColor" viewBox="0 0 24 24" width="32" > @@ -57,10 +74,10 @@ aria-hidden="true" fill="none" height="32" - stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" + stroke="currentColor" viewBox="0 0 24 24" width="32" > @@ -75,10 +92,10 @@ aria-hidden="true" fill="none" height="32" - stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" + stroke="currentColor" viewBox="0 0 24 24" width="32" > diff --git a/packages/browser-extension/src/scripts/background.js b/packages/browser-extension/src/scripts/background.js index 595d54c..54ce3a2 100644 --- a/packages/browser-extension/src/scripts/background.js +++ b/packages/browser-extension/src/scripts/background.js @@ -5,13 +5,6 @@ const apiUrl = 'https://api.cookie-dialog-monster.com/rest/v2'; -/** - * @description Initial state - * @type {{ enabled: boolean }} - */ - -const initial = { enabled: true }; - /** * @description Context menu identifier * @type {string} @@ -90,31 +83,65 @@ chrome.runtime.onMessage.addListener((message, sender, callback) => { switch (message.type) { case 'DISABLE_ICON': - if (isPage && tabId) chrome.action.setIcon({ path: '/assets/icons/disabled.png', tabId }); + if (isPage && tabId) { + chrome.action.setIcon({ path: '/assets/icons/disabled.png', tabId }); + } break; case 'ENABLE_ICON': - if (isPage && tabId) chrome.action.setIcon({ path: '/assets/icons/enabled.png', tabId }); + if (isPage && tabId) { + chrome.action.setIcon({ path: '/assets/icons/enabled.png', tabId }); + } break; case 'ENABLE_POPUP': - if (isPage && tabId) chrome.action.setPopup({ popup: '/popup.html', tabId }); + if (isPage && tabId) { + chrome.action.setPopup({ popup: '/popup.html', tabId }); + } break; case 'GET_DATA': - storage.get('data', ({ data }) => (data ? callback(data) : refreshData(callback))); + storage.get('data', ({ data }) => { + if (data) { + callback(data); + } else { + refreshData(callback); + } + }); return true; - case 'GET_STATE': - if (hostname) storage.get(hostname, (state) => callback(state[hostname] ?? initial)); + case 'GET_EXCLUSION_LIST': + storage.get(null, (exclusions) => { + const exclusionList = Object.entries(exclusions || {}).flatMap((x) => + x[0] !== 'data' && !x[1]?.enabled ? [x[0]] : [] + ); + callback(exclusionList); + }); + return true; + case 'GET_HOSTNAME_STATE': + if (hostname) { + storage.get(hostname, (state) => { + callback(state[hostname] ?? { enabled: true }); + }); + } return true; case 'GET_TAB': - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => callback(tabs[0])); + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + callback(tabs[0]); + }); return true; case 'INSERT_DIALOG_CSS': - if (isPage && tabId) script.insertCSS({ files: ['styles/dialog.css'], target: { tabId } }); + if (isPage && tabId) { + script.insertCSS({ files: ['styles/dialog.css'], target: { tabId } }); + } break; case 'REPORT': - if (tabId) report(message, sender.tab); + if (tabId) { + report(message, sender.tab); + } break; - case 'UPDATE_STATE': - if (hostname) storage.set({ [hostname]: message.state }); + case 'SET_HOSTNAME_STATE': + if (hostname && message.state.enabled === false) { + storage.set({ [hostname]: message.state }); + } else if (hostname) { + storage.remove(hostname); + } break; default: break; diff --git a/packages/browser-extension/src/scripts/content.js b/packages/browser-extension/src/scripts/content.js index 477871d..d988c53 100644 --- a/packages/browser-extension/src/scripts/content.js +++ b/packages/browser-extension/src/scripts/content.js @@ -42,6 +42,7 @@ let state = { enabled: true }; * @description Cleans DOM * @param {Element[]} elements * @param {boolean?} skipMatch + * @returns {void} */ function clean(elements, skipMatch) { @@ -59,6 +60,7 @@ function clean(elements, skipMatch) { /** * @description Forces a DOM clean in the specific element * @param {HTMLElement} element + * @returns {void} */ function forceClean(element) { @@ -73,6 +75,7 @@ function forceClean(element) { /** * @description Forces element to have these styles * @param {HTMLElement} element + * @returns {void} */ function forceElementStyles(element) { @@ -112,7 +115,7 @@ function match(element, skipMatch) { return false; } - if (data?.tags.includes(element.tagName?.toUpperCase?.())) { + if (!data?.tags?.length || data.tags.includes(element.tagName?.toUpperCase?.())) { return false; } @@ -136,6 +139,7 @@ function match(element, skipMatch) { /** * @description Fixes scroll issues + * @returns {void} */ function fix() { @@ -198,6 +202,29 @@ function readingTime() { return time; } +/** + * @async + * @description Sets up everything + * @param {boolean} skipReadyStateHack + */ + +async function runSetup(skipReadyStateHack) { + state = (await dispatch({ hostname, type: 'GET_HOSTNAME_STATE' })) ?? state; + dispatch({ type: 'ENABLE_POPUP' }); + + if (state.enabled) { + data = await dispatch({ hostname, type: 'GET_DATA' }); + + // 2023-06-13: hack to force clean when data request takes too long and there are no changes later + if (document.readyState === 'complete' && !skipReadyStateHack) { + window.dispatchEvent(new Event('run')); + } + + dispatch({ type: 'ENABLE_ICON' }); + observer.observe(document.body ?? document.documentElement, options); + } +} + /** * @description Mutation Observer instance * @type {MutationObserver} @@ -212,22 +239,40 @@ const observer = new MutationObserver((mutations) => { } }); +/** + * @async + * @description Runs setup if the page wasn't focused yet + * @listens window#focus + * @returns {void} + */ + +window.addEventListener('focus', async () => { + if (!data) { + await runSetup(true); + forceClean(document.body); + } +}); + /** * @description Fixes still existing elements when page fully load * @listens window#load + * @returns {void} */ window.addEventListener('load', () => { - window.dispatchEvent(new Event('run')); + if (document.hasFocus()) { + window.dispatchEvent(new Event('run')); + } }); /** * @description Fixes bfcache issues * @listens window#pageshow + * @returns {void} */ window.addEventListener('pageshow', (event) => { - if (event.persisted) { + if (document.hasFocus() && event.persisted) { window.dispatchEvent(new Event('run')); } }); @@ -235,6 +280,7 @@ window.addEventListener('pageshow', (event) => { /** * @description Forces a clean when this event is fired * @listens window#run + * @returns {void} */ window.addEventListener('run', () => { @@ -249,23 +295,9 @@ window.addEventListener('run', () => { }); /** - * @async - * @description Sets up everything + * @description As this extension do really expensive work, it only runs if the user is on the page */ -(async () => { - state = (await dispatch({ hostname, type: 'GET_STATE' })) ?? state; - dispatch({ type: 'ENABLE_POPUP' }); - - if (state.enabled) { - data = await dispatch({ hostname, type: 'GET_DATA' }); - - // 2023-06-13: hack to force clean when data request takes too long and there are no changes later - if (document.readyState === 'complete') { - window.dispatchEvent(new Event('run')); - } - - dispatch({ type: 'ENABLE_ICON' }); - observer.observe(document.body ?? document.documentElement, options); - } -})(); +if (document.hasFocus()) { + runSetup(); +} diff --git a/packages/browser-extension/src/scripts/options.js b/packages/browser-extension/src/scripts/options.js new file mode 100644 index 0000000..2a9864f --- /dev/null +++ b/packages/browser-extension/src/scripts/options.js @@ -0,0 +1,244 @@ +/** + * @description Shortcut to send messages to background script + */ + +const dispatch = chrome.runtime.sendMessage; + +/** + * @description Exclusion list, URLs where the user prefers to disable the extension + * @type {string[]} + */ + +let exclusionList = []; + +/** + * @description Renders exclusion items into exclusion list + * @returns {void} + */ + +function createList() { + const emptyItemElement = document.getElementById('exclusion-list-item-empty'); + const exclusionListElement = document.getElementById('exclusion-list'); + const exclusionListItemTemplateElement = document.getElementById('exclusion-list-item-template'); + + Array.from(exclusionListElement.querySelectorAll('[data-value]')).forEach((exclusionItem) => { + exclusionItem.remove(); + }); + + if (exclusionList.length) { + for (const exclusionValue of exclusionList) { + const ariaLabelOrTitle = `Delete ${exclusionValue}`; + const itemElement = exclusionListItemTemplateElement.cloneNode(true); + const deleteButtonElement = itemElement.getElementsByTagName('button')[0]; + + deleteButtonElement.addEventListener('click', handleDeleteClick); + deleteButtonElement.setAttribute('aria-label', ariaLabelOrTitle); + deleteButtonElement.setAttribute('title', ariaLabelOrTitle); + itemElement.removeAttribute('id'); + itemElement.getElementsByTagName('span')[0].innerText = exclusionValue; + itemElement.setAttribute('data-value', exclusionValue); + itemElement.style.removeProperty('display'); + exclusionListElement.appendChild(itemElement); + } + } else { + emptyItemElement.innerText = "You don't have any exclusions yet"; + emptyItemElement.style.removeProperty('display'); + } +} + +/** + * @async + * @description Clear all items from the exclusion list + * @returns {Promise} + */ + +async function handleClearClick() { + const filterInputElement = document.getElementById('filter-input'); + + for (const exclusionValue of exclusionList) { + const state = { enabled: true }; + await dispatch({ hostname: exclusionValue, state, type: 'SET_HOSTNAME_STATE' }); + } + + exclusionList = []; + createList(); + updateList(filterInputElement.value.trim()); +} + +/** + * @async + * @description Setup handlers and items + */ + +async function handleContentLoaded() { + exclusionList = await dispatch({ type: 'GET_EXCLUSION_LIST' }); + createList(); + + const clearButtonElement = document.getElementById('clear-button'); + clearButtonElement.addEventListener('click', handleClearClick); + + const exportButtonElement = document.getElementById('export-button'); + exportButtonElement.addEventListener('click', handleExportClick); + + const fileInputElement = document.getElementById('file-input'); + fileInputElement.addEventListener('change', handleFileChange); + + const filterInputElement = document.getElementById('filter-input'); + filterInputElement.addEventListener('keydown', handleFilterKeyDown); + + const importButtonElement = document.getElementById('import-button'); + importButtonElement.addEventListener('click', handleImportClick); + + translate(); +} + +/** + * @async + * @description Deletes the clicked element from the exclusion list + * @param {MouseEvent} event + * @returns {Promise} + */ + +async function handleDeleteClick(event) { + const filterInputElement = document.getElementById('filter-input'); + const { value } = event.currentTarget.parentElement.dataset; + const state = { enabled: true }; + + await dispatch({ hostname: value, state, type: 'SET_HOSTNAME_STATE' }); + exclusionList = exclusionList.filter((exclusionValue) => exclusionValue !== value); + itemElement.remove(); + updateList(filterInputElement.value.trim()); +} + +/** + * @description Exports a file with the current exclusion list + * @returns {void} + */ + +function handleExportClick() { + const anchor = document.createElement('a'); + const text = exclusionList.join('\n'); + const blob = new Blob([text], { type: 'octet/stream' }); + const url = window.URL.createObjectURL(blob); + + anchor.href = url; + anchor.download = `${new Date().valueOf()}.cdm`; + anchor.click(); + window.URL.revokeObjectURL(url); +} + +/** + * @description Processes a file and sends the updates + * @param {InputEvent} event + * @returns {void} + */ + +function handleFileChange(event) { + const file = event.currentTarget.files[0]; + const filterInputElement = document.getElementById('filter-input'); + const reader = new FileReader(); + + reader.addEventListener('load', async (event) => { + const newExclusionList = event.currentTarget.result.split('\n').filter((x) => x.trim()); + + for (const exclusionValue of newExclusionList) { + const state = { enabled: false }; + await dispatch({ hostname: exclusionValue, state, type: 'SET_HOSTNAME_STATE' }); + } + + if (newExclusionList.length) { + exclusionList = [...new Set([...exclusionList, ...newExclusionList])].sort(); + createList(); + updateList(filterInputElement.value.trim()); + } + }); + + event.currentTarget.value = ''; + reader.readAsText(file); +} + +/** + * @description Applies filter to the exclusion list when the user presses ENTER key + * @param {KeyboardEvent} event + * @returns {void} + */ + +function handleFilterKeyDown(event) { + if (event.key === 'Enter') { + const filterValue = event.currentTarget.value.trim(); + updateList(filterValue); + } +} + +/** + * @description Shallow clicks an hidden input to open the file explorer + * @returns {void} + */ + +function handleImportClick() { + const fileInputElement = document.getElementById('file-input'); + fileInputElement.click(); +} + +/** + * @description Applies translations to tags with i18n data attribute + * @returns {void} + */ + +function translate() { + const nodes = document.querySelectorAll('[data-i18n], [data-i18n-placeholder]'); + + for (let i = nodes.length; i--; ) { + const node = nodes[i]; + const { i18n, i18nPlaceholder } = node.dataset; + + if (i18n) { + node.innerHTML = chrome.i18n.getMessage(i18n); + } + + if (i18nPlaceholder) { + node.setAttribute('placeholder', chrome.i18n.getMessage(i18nPlaceholder)); + } + } +} + +/** + * @description Updates exclusion items in DOM + * @param {string | undefined} filterValue + * @returns {void} + */ + +function updateList(filterValue) { + const emptyItemElement = document.getElementById('exclusion-list-item-empty'); + const exclusionListElement = document.getElementById('exclusion-list'); + const exclusionListElements = exclusionListElement.querySelectorAll(`[data-value]`); + + if (exclusionListElements.length) { + let isEmpty = true; + emptyItemElement.style.setProperty('display', 'none'); + + for (const exclusionItemElement of Array.from(exclusionListElements)) { + if (exclusionItemElement.matches(`[data-value*="${filterValue}"]`) || !filterValue) { + exclusionItemElement.style.removeProperty('display'); + isEmpty = false; + } else { + exclusionItemElement.style.setProperty('display', 'none'); + } + } + + if (isEmpty) { + emptyItemElement.innerText = 'No exclusions found'; + emptyItemElement.style.removeProperty('display'); + } + } else { + emptyItemElement.innerText = "You don't have any exclusions yet"; + emptyItemElement.style.removeProperty('display'); + } +} + +/** + * @description Listen to document ready + * @listens document#DOMContentLoaded + */ + +document.addEventListener('DOMContentLoaded', handleContentLoaded); diff --git a/packages/browser-extension/src/scripts/popup.js b/packages/browser-extension/src/scripts/popup.js index ef1f342..783f090 100644 --- a/packages/browser-extension/src/scripts/popup.js +++ b/packages/browser-extension/src/scripts/popup.js @@ -64,80 +64,104 @@ let state = { enabled: true }; /** * @async * @description Setup stars handlers and result message links + * @returns {Promise} */ -const handleContentLoaded = async () => { +async function handleContentLoaded() { const tab = await dispatch({ type: 'GET_TAB' }); hostname = tab?.url ? new URL(tab.url).hostname.split('.').slice(-3).join('.').replace('www.', '') : undefined; - state = (await dispatch({ hostname, type: 'GET_STATE' })) ?? state; + state = (await dispatch({ hostname, type: 'GET_HOSTNAME_STATE' })) ?? state; - const host = document.getElementById('host'); - host.innerText = hostname ?? 'unknown'; + const hostTextElement = document.getElementById('host'); + hostTextElement.innerText = hostname ?? 'unknown'; - const contribute = document.getElementById('contribute-option'); - contribute?.addEventListener('click', handleLinkRedirect); + const contributeButtonElement = document.getElementById('contribute-option'); + contributeButtonElement?.addEventListener('click', handleLinkRedirect); - const help = document.getElementById('help-option'); - help?.addEventListener('click', handleLinkRedirect); + const helpButtonElement = document.getElementById('help-option'); + helpButtonElement?.addEventListener('click', handleLinkRedirect); - const power = document.getElementById('power-option'); - power?.addEventListener('click', handlePowerToggle); - if (state.enabled) power?.setAttribute('data-value', 'on'); - else power?.setAttribute('data-value', 'off'); + const powerButtonElement = document.getElementById('power-option'); + powerButtonElement?.addEventListener('click', handlePowerToggle); + if (state.enabled) powerButtonElement?.setAttribute('data-value', 'on'); + else powerButtonElement?.setAttribute('data-value', 'off'); - const rate = document.getElementById('rate-option'); - rate?.addEventListener('click', handleLinkRedirect); - if (isEdge) rate?.setAttribute('data-href', edgeUrl); - else if (isChromium) rate?.setAttribute('data-href', chromeUrl); - else if (isFirefox) rate?.setAttribute('data-href', firefoxUrl); + const rateButtonElement = document.getElementById('rate-option'); + rateButtonElement?.addEventListener('click', handleLinkRedirect); + if (isEdge) rateButtonElement?.setAttribute('data-href', edgeUrl); + else if (isChromium) rateButtonElement?.setAttribute('data-href', chromeUrl); + else if (isFirefox) rateButtonElement?.setAttribute('data-href', firefoxUrl); + + const settingsButtonElement = document.getElementById('settings-button'); + settingsButtonElement.addEventListener('click', handleSettingsClick); translate(); -}; +} /** * @async * @description Opens a new tab * @param {MouseEvent} event + * @returns {Promise} */ -const handleLinkRedirect = async (event) => { +async function handleLinkRedirect(event) { const { href } = event.currentTarget.dataset; if (href) { await chrome.tabs.create({ url: href }); } -}; +} /** + * @async * @description Disables or enables extension on current page * @param {MouseEvent} event + * @returns {Promise} */ -const handlePowerToggle = async (event) => { +async function handlePowerToggle(event) { state = { enabled: !state.enabled }; - dispatch({ hostname, state, type: 'UPDATE_STATE' }); + dispatch({ hostname, state, type: 'SET_HOSTNAME_STATE' }); if (state.enabled) event.currentTarget.setAttribute('data-value', 'on'); else event.currentTarget.setAttribute('data-value', 'off'); await chrome.tabs.reload({ bypassCache: true }); -}; + window.close(); +} + +/** + * @description Opens options page + * @returns {void} + */ + +function handleSettingsClick() { + chrome.runtime.openOptionsPage(); +} /** * @description Applies translations to tags with i18n data attribute + * @returns {void} */ -const translate = () => { - const nodes = document.querySelectorAll('[data-i18n]'); +function translate() { + const nodes = document.querySelectorAll('[data-i18n], [data-i18n-placeholder]'); for (let i = nodes.length; i--; ) { const node = nodes[i]; - const { i18n } = node.dataset; + const { i18n, i18nPlaceholder } = node.dataset; - node.innerHTML = chrome.i18n.getMessage(i18n); + if (i18n) { + node.innerHTML = chrome.i18n.getMessage(i18n); + } + + if (i18nPlaceholder) { + node.setAttribute('placeholder', chrome.i18n.getMessage(i18nPlaceholder)); + } } -}; +} /** * @description Listen to document ready diff --git a/packages/browser-extension/src/styles/options.css b/packages/browser-extension/src/styles/options.css new file mode 100644 index 0000000..1b97cff --- /dev/null +++ b/packages/browser-extension/src/styles/options.css @@ -0,0 +1,137 @@ +:root { + --color-error: #cc0000; + --color-primary: #3dd9eb; + --color-secondary: #34495e; + --color-success: #5cb85c; + --color-tertiary: #6b7280; + --color-transparent: transparent; + --color-warning: #ffdf00; + --color-white: #ffffff; +} + +body { + box-sizing: border-box; + color: var(--color-tertiary); + display: flex; + flex-direction: column; + font-family: Inter, Arial, Helvetica, sans-serif; + min-height: 100vh; +} + +body * { + box-sizing: border-box; + font-family: inherit; +} + +button { + align-items: center; + background-color: var(--color-white); + border: none; + border-radius: 4px; + color: var(--color-secondary); + display: inline-flex; + gap: 4px; + outline: none; + transition: 0.4s; +} + +button[data-variant='large'] { + direction: rtl; + padding: 8px; +} + +button:focus, +button:hover { + background-color: var(--color-secondary); + color: var(--color-white); +} + +footer { + background-color: var(--color-secondary); + font-size: 12px; + height: 4px; + margin-top: auto; + text-align: center; +} + +header { + background-color: var(--color-secondary); + color: var(--color-white); + font-size: 16px !important; + height: 48px; +} + +header > div { + align-items: center; + display: flex; + height: 100%; + justify-content: space-between; + margin: auto 0px; +} + +main input { + -webkit-appearance: none; + appearance: none; + background-color: var(--color-white); + border: none; + border-bottom: 1px solid var(--color-tertiary); + border-radius: 0px; + color: var(--color-secondary); + font-size: 16px; + height: 42px; + outline: none; + padding: 0px 8px; + width: 100%; +} + +main input::placeholder { + color: var(--color-tertiary); + opacity: 1; +} + +main input:focus, +main input:hover { + border-bottom: 1px solid var(--color-primary); +} + +header > div, +main { + margin: 0px auto; + max-width: 768px; + padding: 16px; + width: 100%; +} + +.button-group { + display: flex; + justify-content: flex-end; + gap: 4px; + margin-bottom: 4px; +} + +#exclusion-list { + font-size: 14px; + list-style: none; + padding: 0px; +} + +#exclusion-list > li { + align-items: center; + border-radius: 4px; + display: flex; + justify-content: space-between; + padding: 8px; + transition: 0.4s; +} + +#exclusion-list > li:focus-within, +#exclusion-list > li:hover { + background-color: var(--color-secondary); + color: var(--color-white); +} + +#exclusion-list > li > button { + background-color: var(--color-white); + color: var(--color-error); + padding: 4px; +} diff --git a/packages/browser-extension/src/styles/popup.css b/packages/browser-extension/src/styles/popup.css index 8a0da66..1e8be98 100644 --- a/packages/browser-extension/src/styles/popup.css +++ b/packages/browser-extension/src/styles/popup.css @@ -21,6 +21,25 @@ body * { box-sizing: border-box; } +button { + align-items: center; + background-color: var(--color-secondary); + border: none; + border-radius: 4px; + color: var(--color-white); + display: inline-flex; + gap: 4px; + outline: none; + padding: 2px; + transition: 0.4s; +} + +button:focus, +button:hover { + background-color: var(--color-white); + color: var(--color-secondary); +} + footer { background-color: var(--color-secondary); font-size: 12px;