/** * @description Shortcut to send messages to background script */ const dispatch = chrome.runtime.sendMessage; /** * @description Domain RegExp */ const domainRx = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/g; /** * @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 Add a new item to the exclusion list * @returns {Promise} */ async function handleAddClick() { const exclusionValue = window.prompt(chrome.i18n.getMessage('options_addPrompt')); if (exclusionValue?.trim() && (domainRx.test(exclusionValue) || exclusionValue === 'localhost')) { const filterInputElement = document.getElementById('filter-input'); const state = { enabled: false }; await dispatch({ hostname: exclusionValue, state, type: 'SET_HOSTNAME_STATE' }); exclusionList = [...new Set([...exclusionList, exclusionValue])].sort(); createList(); updateList(filterInputElement.value.trim()); } } /** * @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 addButtonElement = document.getElementById('add-button'); addButtonElement.addEventListener('click', handleAddClick); 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 itemElement = document.querySelector(`[data-value="${value}"]`); 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 now = new Date(); const day = now.getDate().toString().padStart(2, '0'); const month = now.getMonth().toString().padStart(2, '0'); const year = now.getUTCFullYear(); const text = exclusionList.join('\n'); const defaultTitle = `${year}${month}${day}`; const customTitle = window.prompt('Enter a file name', defaultTitle); const blob = new Blob([text], { type: 'octet/stream' }); const url = window.URL.createObjectURL(blob); anchor.href = url; anchor.download = `${customTitle || defaultTitle}.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);