From 591e132008165f0959b8f4e68d929cd2e69dee82 Mon Sep 17 00:00:00 2001 From: wanhose Date: Mon, 25 Sep 2023 18:38:52 +0200 Subject: [PATCH 1/8] feat(browser-extension): add options page to manage exclusion list --- .../src/_locales/de/messages.json | 15 ++ .../src/_locales/en/messages.json | 15 ++ .../src/_locales/es/messages.json | 15 ++ .../src/_locales/fr/messages.json | 15 ++ .../src/_locales/it/messages.json | 15 ++ .../src/_locales/pt_BR/messages.json | 15 ++ .../src/_locales/pt_PT/messages.json | 15 ++ .../src/_locales/ro/messages.json | 15 ++ .../src/_locales/ru/messages.json | 15 ++ packages/browser-extension/src/options.html | 110 ++++++++ packages/browser-extension/src/popup.html | 27 +- .../src/scripts/background.js | 63 +++-- .../browser-extension/src/scripts/content.js | 2 +- .../browser-extension/src/scripts/options.js | 248 ++++++++++++++++++ .../browser-extension/src/scripts/popup.js | 65 +++-- .../browser-extension/src/styles/options.css | 137 ++++++++++ .../browser-extension/src/styles/popup.css | 19 ++ 17 files changed, 763 insertions(+), 43 deletions(-) create mode 100644 packages/browser-extension/src/options.html create mode 100644 packages/browser-extension/src/scripts/options.js create mode 100644 packages/browser-extension/src/styles/options.css 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/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..1b7587b 100644 --- a/packages/browser-extension/src/scripts/content.js +++ b/packages/browser-extension/src/scripts/content.js @@ -254,7 +254,7 @@ window.addEventListener('run', () => { */ (async () => { - state = (await dispatch({ hostname, type: 'GET_STATE' })) ?? state; + state = (await dispatch({ hostname, type: 'GET_HOSTNAME_STATE' })) ?? state; dispatch({ type: 'ENABLE_POPUP' }); if (state.enabled) { diff --git a/packages/browser-extension/src/scripts/options.js b/packages/browser-extension/src/scripts/options.js new file mode 100644 index 0000000..42be9a6 --- /dev/null +++ b/packages/browser-extension/src/scripts/options.js @@ -0,0 +1,248 @@ +/** + * @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} + */ + +const 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} + */ + +const handleClearClick = async () => { + 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 + */ + +const handleContentLoaded = async () => { + 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} + */ + +const handleDeleteClick = async (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} + */ + +const 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} + */ + +const 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} + */ + +const 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} + */ + +const handleImportClick = () => { + const fileInputElement = document.getElementById('file-input'); + fileInputElement.click(); +}; + +/** + * @description Applies translations to tags with i18n data attribute + * @returns {void} + */ + +const translate = () => { + const nodes = document.querySelectorAll('[data-i18n]'); + + for (let i = nodes.length; i--; ) { + const node = nodes[i]; + const { i18n, i18nAriaLabel, i18nPlaceholder } = node.dataset; + + if (i18n) { + node.innerHTML = chrome.i18n.getMessage(i18n); + } + + if (i18nAriaLabel) { + node.setAttribute('aria-label', i18nAriaLabel); + } + + if (i18nPlaceholder) { + node.setAttribute('placeholder', i18nPlaceholder); + } + } +}; + +/** + * @description Updates exclusion items in DOM + * @param {string | undefined} filterValue + * @returns {void} + */ + +const 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..79369e7 100644 --- a/packages/browser-extension/src/scripts/popup.js +++ b/packages/browser-extension/src/scripts/popup.js @@ -64,6 +64,7 @@ let state = { enabled: true }; /** * @async * @description Setup stars handlers and result message links + * @returns {Promise} */ const handleContentLoaded = async () => { @@ -72,27 +73,30 @@ const handleContentLoaded = async () => { 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(); }; @@ -101,6 +105,7 @@ const handleContentLoaded = async () => { * @async * @description Opens a new tab * @param {MouseEvent} event + * @returns {Promise} */ const handleLinkRedirect = async (event) => { @@ -112,20 +117,32 @@ const handleLinkRedirect = async (event) => { }; /** + * @async * @description Disables or enables extension on current page * @param {MouseEvent} event + * @returns {Promise} */ const handlePowerToggle = async (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 }); }; +/** + * @description Opens options page + * @returns {void} + */ + +const handleSettingsClick = () => { + chrome.runtime.openOptionsPage(); +}; + /** * @description Applies translations to tags with i18n data attribute + * @returns {void} */ const translate = () => { @@ -133,9 +150,19 @@ const translate = () => { for (let i = nodes.length; i--; ) { const node = nodes[i]; - const { i18n } = node.dataset; + const { i18n, i18nAriaLabel, i18nPlaceholder } = node.dataset; - node.innerHTML = chrome.i18n.getMessage(i18n); + if (i18n) { + node.innerHTML = chrome.i18n.getMessage(i18n); + } + + if (i18nAriaLabel) { + node.setAttribute('aria-label', i18nAriaLabel); + } + + if (i18nPlaceholder) { + node.setAttribute('placeholder', i18nPlaceholder); + } } }; 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; From 418ff15e9bd57bfe7f4b81df6b15651eb7890f03 Mon Sep 17 00:00:00 2001 From: wanhose Date: Mon, 25 Sep 2023 18:39:11 +0200 Subject: [PATCH 2/8] chore(browser-extension): add options page and upgrade version --- packages/browser-extension/src/manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/browser-extension/src/manifest.json b/packages/browser-extension/src/manifest.json index 06c8048..3f6efe1 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" From cb8f1732261687b2c7e8dc87eacbe197086fd7b1 Mon Sep 17 00:00:00 2001 From: wanhose Date: Mon, 25 Sep 2023 18:47:46 +0200 Subject: [PATCH 3/8] fix(browser-extension): issue #287 --- packages/browser-extension/src/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/browser-extension/src/manifest.json b/packages/browser-extension/src/manifest.json index 3f6efe1..3fcdb87 100644 --- a/packages/browser-extension/src/manifest.json +++ b/packages/browser-extension/src/manifest.json @@ -26,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"], From 072f684e7449a8a561b805c010d756752150cc0f Mon Sep 17 00:00:00 2001 From: wanhose Date: Mon, 25 Sep 2023 18:47:56 +0200 Subject: [PATCH 4/8] feat(browser-extension): minor improvement if tags are not loaded --- packages/browser-extension/src/scripts/content.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-extension/src/scripts/content.js b/packages/browser-extension/src/scripts/content.js index 1b7587b..e5e29e6 100644 --- a/packages/browser-extension/src/scripts/content.js +++ b/packages/browser-extension/src/scripts/content.js @@ -112,7 +112,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; } From 09f2ce9f22f030286812debef797a135e64e973c Mon Sep 17 00:00:00 2001 From: wanhose Date: Mon, 25 Sep 2023 19:30:18 +0200 Subject: [PATCH 5/8] docs(browser-extension): add extra info about how this extension works --- packages/browser-extension/docs/README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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` From 9d4d2f4f6ff061e476aac9b704178c0b23f6230b Mon Sep 17 00:00:00 2001 From: wanhose Date: Mon, 25 Sep 2023 19:42:57 +0200 Subject: [PATCH 6/8] feat(browser-extension): improve performance based on run only when page is focused --- .../browser-extension/src/scripts/content.js | 133 +++++++++++------- .../browser-extension/src/scripts/popup.js | 1 + 2 files changed, 82 insertions(+), 52 deletions(-) diff --git a/packages/browser-extension/src/scripts/content.js b/packages/browser-extension/src/scripts/content.js index e5e29e6..8647652 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) { @@ -136,6 +139,7 @@ function match(element, skipMatch) { /** * @description Fixes scroll issues + * @returns {void} */ function fix() { @@ -198,62 +202,12 @@ function readingTime() { return time; } -/** - * @description Mutation Observer instance - * @type {MutationObserver} - */ - -const observer = new MutationObserver((mutations) => { - const elements = mutations.map((mutation) => Array.from(mutation.addedNodes)).flat(); - - if (data?.elements.length && !preview) { - fix(); - clean(elements); - } -}); - -/** - * @description Fixes still existing elements when page fully load - * @listens window#load - */ - -window.addEventListener('load', () => { - window.dispatchEvent(new Event('run')); -}); - -/** - * @description Fixes bfcache issues - * @listens window#pageshow - */ - -window.addEventListener('pageshow', (event) => { - if (event.persisted) { - window.dispatchEvent(new Event('run')); - } -}); - -/** - * @description Forces a clean when this event is fired - * @listens window#run - */ - -window.addEventListener('run', () => { - if (data?.elements.length && state.enabled && !preview) { - if (readingTime() < 4) { - forceClean(document.body); - } else { - // 2023-06-13: look into the first level of the document body, there are dialogs there very often - clean([...document.body.children]); - } - } -}); - /** * @async * @description Sets up everything */ -(async () => { +async function runSetup() { state = (await dispatch({ hostname, type: 'GET_HOSTNAME_STATE' })) ?? state; dispatch({ type: 'ENABLE_POPUP' }); @@ -268,4 +222,79 @@ window.addEventListener('run', () => { dispatch({ type: 'ENABLE_ICON' }); observer.observe(document.body ?? document.documentElement, options); } -})(); +} + +/** + * @description Mutation Observer instance + * @type {MutationObserver} + */ + +const observer = new MutationObserver((mutations) => { + const elements = mutations.map((mutation) => Array.from(mutation.addedNodes)).flat(); + + if (data?.elements.length && !preview) { + fix(); + clean(elements); + } +}); + +/** + * @description Runs setup if the page wasn't focused yet + * @listens window#focus + * @returns {void} + */ + +window.addEventListener('focus', () => { + if (!data) { + runSetup(); + } +}); + +/** + * @description Fixes still existing elements when page fully load + * @listens window#load + * @returns {void} + */ + +window.addEventListener('load', () => { + if (document.hasFocus()) { + window.dispatchEvent(new Event('run')); + } +}); + +/** + * @description Fixes bfcache issues + * @listens window#pageshow + * @returns {void} + */ + +window.addEventListener('pageshow', (event) => { + if (document.hasFocus() && event.persisted) { + window.dispatchEvent(new Event('run')); + } +}); + +/** + * @description Forces a clean when this event is fired + * @listens window#run + * @returns {void} + */ + +window.addEventListener('run', () => { + if (data?.elements.length && state.enabled && !preview) { + if (readingTime() < 4) { + forceClean(document.body); + } else { + // 2023-06-13: look into the first level of the document body, there are dialogs there very often + clean([...document.body.children]); + } + } +}); + +/** + * @description As this extension do really expensive work, it only runs if the user is on the page + */ + +if (document.hasFocus()) { + runSetup(); +} diff --git a/packages/browser-extension/src/scripts/popup.js b/packages/browser-extension/src/scripts/popup.js index 79369e7..6dc08ca 100644 --- a/packages/browser-extension/src/scripts/popup.js +++ b/packages/browser-extension/src/scripts/popup.js @@ -129,6 +129,7 @@ const handlePowerToggle = async (event) => { if (state.enabled) event.currentTarget.setAttribute('data-value', 'on'); else event.currentTarget.setAttribute('data-value', 'off'); await chrome.tabs.reload({ bypassCache: true }); + window.close(); }; /** From 8d79e35d38e88779fa0a93d3e33886e20270fece Mon Sep 17 00:00:00 2001 From: wanhose Date: Mon, 25 Sep 2023 20:15:47 +0200 Subject: [PATCH 7/8] fix: bug about i18n placeholders --- .../browser-extension/src/scripts/options.js | 50 +++++++++---------- .../browser-extension/src/scripts/popup.js | 30 +++++------ 2 files changed, 36 insertions(+), 44 deletions(-) diff --git a/packages/browser-extension/src/scripts/options.js b/packages/browser-extension/src/scripts/options.js index 42be9a6..2a9864f 100644 --- a/packages/browser-extension/src/scripts/options.js +++ b/packages/browser-extension/src/scripts/options.js @@ -16,7 +16,7 @@ let exclusionList = []; * @returns {void} */ -const createList = () => { +function createList() { const emptyItemElement = document.getElementById('exclusion-list-item-empty'); const exclusionListElement = document.getElementById('exclusion-list'); const exclusionListItemTemplateElement = document.getElementById('exclusion-list-item-template'); @@ -44,7 +44,7 @@ const createList = () => { emptyItemElement.innerText = "You don't have any exclusions yet"; emptyItemElement.style.removeProperty('display'); } -}; +} /** * @async @@ -52,7 +52,7 @@ const createList = () => { * @returns {Promise} */ -const handleClearClick = async () => { +async function handleClearClick() { const filterInputElement = document.getElementById('filter-input'); for (const exclusionValue of exclusionList) { @@ -63,14 +63,14 @@ const handleClearClick = async () => { exclusionList = []; createList(); updateList(filterInputElement.value.trim()); -}; +} /** * @async * @description Setup handlers and items */ -const handleContentLoaded = async () => { +async function handleContentLoaded() { exclusionList = await dispatch({ type: 'GET_EXCLUSION_LIST' }); createList(); @@ -90,7 +90,7 @@ const handleContentLoaded = async () => { importButtonElement.addEventListener('click', handleImportClick); translate(); -}; +} /** * @async @@ -99,7 +99,7 @@ const handleContentLoaded = async () => { * @returns {Promise} */ -const handleDeleteClick = async (event) => { +async function handleDeleteClick(event) { const filterInputElement = document.getElementById('filter-input'); const { value } = event.currentTarget.parentElement.dataset; const state = { enabled: true }; @@ -108,14 +108,14 @@ const handleDeleteClick = async (event) => { exclusionList = exclusionList.filter((exclusionValue) => exclusionValue !== value); itemElement.remove(); updateList(filterInputElement.value.trim()); -}; +} /** * @description Exports a file with the current exclusion list * @returns {void} */ -const handleExportClick = () => { +function handleExportClick() { const anchor = document.createElement('a'); const text = exclusionList.join('\n'); const blob = new Blob([text], { type: 'octet/stream' }); @@ -125,7 +125,7 @@ const handleExportClick = () => { anchor.download = `${new Date().valueOf()}.cdm`; anchor.click(); window.URL.revokeObjectURL(url); -}; +} /** * @description Processes a file and sends the updates @@ -133,7 +133,7 @@ const handleExportClick = () => { * @returns {void} */ -const handleFileChange = (event) => { +function handleFileChange(event) { const file = event.currentTarget.files[0]; const filterInputElement = document.getElementById('filter-input'); const reader = new FileReader(); @@ -155,7 +155,7 @@ const handleFileChange = (event) => { event.currentTarget.value = ''; reader.readAsText(file); -}; +} /** * @description Applies filter to the exclusion list when the user presses ENTER key @@ -163,48 +163,44 @@ const handleFileChange = (event) => { * @returns {void} */ -const handleFilterKeyDown = (event) => { +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} */ -const handleImportClick = () => { +function handleImportClick() { const fileInputElement = document.getElementById('file-input'); fileInputElement.click(); -}; +} /** * @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, i18nAriaLabel, i18nPlaceholder } = node.dataset; + const { i18n, i18nPlaceholder } = node.dataset; if (i18n) { node.innerHTML = chrome.i18n.getMessage(i18n); } - if (i18nAriaLabel) { - node.setAttribute('aria-label', i18nAriaLabel); - } - if (i18nPlaceholder) { - node.setAttribute('placeholder', i18nPlaceholder); + node.setAttribute('placeholder', chrome.i18n.getMessage(i18nPlaceholder)); } } -}; +} /** * @description Updates exclusion items in DOM @@ -212,7 +208,7 @@ const translate = () => { * @returns {void} */ -const updateList = (filterValue) => { +function updateList(filterValue) { const emptyItemElement = document.getElementById('exclusion-list-item-empty'); const exclusionListElement = document.getElementById('exclusion-list'); const exclusionListElements = exclusionListElement.querySelectorAll(`[data-value]`); @@ -238,7 +234,7 @@ const updateList = (filterValue) => { emptyItemElement.innerText = "You don't have any exclusions yet"; emptyItemElement.style.removeProperty('display'); } -}; +} /** * @description Listen to document ready diff --git a/packages/browser-extension/src/scripts/popup.js b/packages/browser-extension/src/scripts/popup.js index 6dc08ca..783f090 100644 --- a/packages/browser-extension/src/scripts/popup.js +++ b/packages/browser-extension/src/scripts/popup.js @@ -67,7 +67,7 @@ let state = { enabled: true }; * @returns {Promise} */ -const handleContentLoaded = async () => { +async function handleContentLoaded() { const tab = await dispatch({ type: 'GET_TAB' }); hostname = tab?.url @@ -99,7 +99,7 @@ const handleContentLoaded = async () => { settingsButtonElement.addEventListener('click', handleSettingsClick); translate(); -}; +} /** * @async @@ -108,13 +108,13 @@ const handleContentLoaded = async () => { * @returns {Promise} */ -const handleLinkRedirect = async (event) => { +async function handleLinkRedirect(event) { const { href } = event.currentTarget.dataset; if (href) { await chrome.tabs.create({ url: href }); } -}; +} /** * @async @@ -123,49 +123,45 @@ const handleLinkRedirect = async (event) => { * @returns {Promise} */ -const handlePowerToggle = async (event) => { +async function handlePowerToggle(event) { state = { enabled: !state.enabled }; 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} */ -const handleSettingsClick = () => { +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, i18nAriaLabel, i18nPlaceholder } = node.dataset; + const { i18n, i18nPlaceholder } = node.dataset; if (i18n) { node.innerHTML = chrome.i18n.getMessage(i18n); } - if (i18nAriaLabel) { - node.setAttribute('aria-label', i18nAriaLabel); - } - if (i18nPlaceholder) { - node.setAttribute('placeholder', i18nPlaceholder); + node.setAttribute('placeholder', chrome.i18n.getMessage(i18nPlaceholder)); } } -}; +} /** * @description Listen to document ready From c8d1f927b31b2eb1c35a9196f14986eedbb32e63 Mon Sep 17 00:00:00 2001 From: wanhose Date: Fri, 29 Sep 2023 17:12:08 +0200 Subject: [PATCH 8/8] feat(browser-extension): skip ready state check in some cases --- packages/browser-extension/src/scripts/content.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/browser-extension/src/scripts/content.js b/packages/browser-extension/src/scripts/content.js index 8647652..d988c53 100644 --- a/packages/browser-extension/src/scripts/content.js +++ b/packages/browser-extension/src/scripts/content.js @@ -205,9 +205,10 @@ function readingTime() { /** * @async * @description Sets up everything + * @param {boolean} skipReadyStateHack */ -async function runSetup() { +async function runSetup(skipReadyStateHack) { state = (await dispatch({ hostname, type: 'GET_HOSTNAME_STATE' })) ?? state; dispatch({ type: 'ENABLE_POPUP' }); @@ -215,7 +216,7 @@ async function runSetup() { 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') { + if (document.readyState === 'complete' && !skipReadyStateHack) { window.dispatchEvent(new Event('run')); } @@ -239,14 +240,16 @@ const observer = new MutationObserver((mutations) => { }); /** + * @async * @description Runs setup if the page wasn't focused yet * @listens window#focus * @returns {void} */ -window.addEventListener('focus', () => { +window.addEventListener('focus', async () => { if (!data) { - runSetup(); + await runSetup(true); + forceClean(document.body); } });