/** * @typedef {Object} ExtensionState * @property {ExtensionIssue} [issue] * @property {boolean} on */ /** * @typedef {Object} PopupState * @extends {ExtensionState} * @property {number} [tabId] */ if (typeof browser === 'undefined') { browser = chrome; } /** * @description Chrome Web Store link * @type {string} */ const chromeUrl = 'https://chrome.google.com/webstore/detail/djcbfpkdhdkaflcigibkbpboflaplabg'; /** * @description Shortcut to send messages to background script */ const dispatch = browser.runtime.sendMessage; /** * @description Edge Add-ons link * @type {string} */ const edgeUrl = 'https://microsoftedge.microsoft.com/addons/detail/hbogodfciblakeneadpcolhmfckmjcii'; /** * @description Firefox Add-ons link * @type {string} */ const firefoxUrl = 'https://addons.mozilla.org/firefox/addon/cookie-dialog-monster'; /** * @description Current hostname * @type {string} */ let hostname = '?'; /** * @description Is current browser an instance of Chromium? * @type {boolean} */ const isChromium = navigator.userAgent.indexOf('Chrome') !== -1; /** * @description Is current browser an instance of Edge? * @type {boolean} */ const isEdge = navigator.userAgent.indexOf('Edg') !== -1; /** * @description Is current browser an instance of Firefox? * @type {boolean} */ const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1; /** * @description Popup state * @type {PopupState} */ let state = { on: true }; /** * @description Close report form * @returns {void} */ function handleCancelClick() { const content = document.getElementsByClassName('content')[0]; const report = document.getElementsByClassName('report')[0]; if (content instanceof HTMLElement && report instanceof HTMLElement) { content.style.removeProperty('display'); report.style.display = 'none'; } } /** * @async * @description Setup stars handlers and result message links * @returns {Promise} */ async function handleContentLoaded() { const tab = await dispatch({ type: 'GET_TAB' }); const url = tab?.url ? new URL(tab.url) : undefined; hostname = url?.hostname.split('.').slice(-3).join('.').replace('www.', ''); state = { ...((await dispatch({ hostname, type: 'GET_STATE' })) ?? state), tabId: tab?.id }; const hostTextElement = document.getElementById('host'); hostTextElement.innerText = hostname ?? 'unknown'; const contributeButtonElement = document.getElementById('contribute-option'); contributeButtonElement?.addEventListener('click', handleLinkRedirect); const databaseRefreshButtonElement = document.getElementById('refresh-database-button'); databaseRefreshButtonElement?.addEventListener('click', handleDatabaseRefresh); const extensionVersionElement = document.getElementById('extension-version'); extensionVersionElement.innerText = browser.runtime.getManifest().version; const helpButtonElement = document.getElementById('help-button'); helpButtonElement?.addEventListener('click', handleLinkRedirect); 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(); await updateDatabaseVersion(); const { exclusions } = (await dispatch({ hostname, type: 'GET_DATA' })) ?? {}; const currentVersion = browser.runtime.getManifest().version; const latestVersion = await dispatch({ type: 'GET_LATEST_VERSION' }); const updateAvailable = currentVersion !== latestVersion; if (updateAvailable) { const updateBanner = document.getElementById('update-banner'); updateBanner.removeAttribute('aria-hidden'); const updateBannerUrl = document.getElementById('update-banner-url'); updateBannerUrl.href += `/tag/${latestVersion}`; } const loader = document.getElementById('loader'); loader.style.setProperty('display', 'none'); if (exclusions?.domains.some((x) => url.hostname.match(x.replaceAll(/\*/g, '[^ ]*')))) { const supportBanner = document.getElementById('support-banner'); supportBanner.removeAttribute('aria-hidden'); return; } const powerButtonElement = document.getElementById('power-option'); powerButtonElement?.addEventListener('click', handlePowerToggle); powerButtonElement?.removeAttribute('disabled'); if (state.on) powerButtonElement?.setAttribute('data-value', 'on'); else powerButtonElement?.setAttribute('data-value', 'off'); if (state.issue?.url) { const issueBanner = document.getElementById('issue-banner'); issueBanner.removeAttribute('aria-hidden'); const issueBannerText = document.getElementById('issue-banner-text'); if (state.issue.flags.includes('wontfix')) issueBannerText.innerText = browser.i18n.getMessage('popup_bannerIssueWontFix'); else issueBannerText.innerText = browser.i18n.getMessage('popup_bannerIssueOpen'); const issueBannerUrl = document.getElementById('issue-banner-url'); issueBannerUrl.setAttribute('href', state.issue.url); return; } const cancelButtonElement = document.getElementsByClassName('report-cancel-button')[0]; cancelButtonElement?.addEventListener('click', handleCancelClick); const reasonInputElement = document.getElementById('report-input-reason'); reasonInputElement?.addEventListener('input', handleInputChange); reasonInputElement?.addEventListener('keydown', handleInputKeyDown); const reportButtonElement = document.getElementById('report-option'); reportButtonElement?.addEventListener('click', handleReportClick); reportButtonElement?.removeAttribute('disabled'); const submitButtonElement = document.getElementsByClassName('report-submit-button')[0]; submitButtonElement?.addEventListener('click', handleSubmitButtonClick); const urlInputElement = document.getElementById('report-input-url'); urlInputElement?.addEventListener('input', handleInputChange); urlInputElement?.addEventListener('keydown', handleInputKeyDown); if (url) urlInputElement?.setAttribute('value', `${url.origin}${url.pathname}`); } /** * @async * @description Refresh the database * @param {MouseEvent} event */ async function handleDatabaseRefresh(event) { const target = event.currentTarget; if (target.getAttribute('aria-disabled') === 'true') { return; } const checkIcon = target.querySelector('#refresh-database-check'); const spinnerIcon = target.querySelector('#refresh-database-spinner'); target.setAttribute('data-refreshing', 'true'); target.setAttribute('aria-disabled', 'true'); await dispatch({ type: 'REFRESH_DATA' }); checkIcon.style.setProperty('display', 'block'); spinnerIcon.style.setProperty('display', 'none'); target.removeAttribute('data-animation'); target.removeAttribute('data-refreshing'); await updateDatabaseVersion(); window.setTimeout(() => { checkIcon.style.setProperty('display', 'none'); spinnerIcon.style.setProperty('display', 'block'); target.removeAttribute('aria-disabled'); target.setAttribute('data-animation', 'flip'); }, 5000); } /** * @description Input change handler * @param {InputEvent} event */ function handleInputChange(event) { event.currentTarget.removeAttribute('aria-invalid'); } /** * @description Input key down handler * @param {KeyboardEvent} event */ function handleInputKeyDown(event) { if (event.key === 'Enter') { event.preventDefault(); event.currentTarget.blur(); } } /** * @async * @description Open a new tab * @param {MouseEvent} event * @returns {Promise} */ async function handleLinkRedirect(event) { const { href } = event.currentTarget.dataset; if (href) { await browser.tabs.create({ url: href }); window.close(); } } /** * @async * @description Disable or enable extension on current page * @returns {void} */ async function handlePowerToggle() { const next = { ...state, on: !state.on }; await dispatch({ hostname, state: next, type: 'UPDATE_STORE' }); await browser.tabs.reload(state.tabId, { bypassCache: true }); window.close(); } /** * @description Show report form * @returns {void} */ function handleReportClick() { const content = document.getElementsByClassName('content')[0]; const report = document.getElementsByClassName('report')[0]; if (content instanceof HTMLElement && report instanceof HTMLElement) { content.style.display = 'none'; report.style.removeProperty('display'); } } /** * @async * @description Open options page * @returns {Promise} */ async function handleSettingsClick() { await browser.runtime.openOptionsPage(); } /** * @async * @description Report submit button click handler * @param {MouseEvent} event */ async function handleSubmitButtonClick(event) { event.preventDefault(); if (event.currentTarget.getAttribute('aria-disabled') === 'true') { return; } event.currentTarget.setAttribute('aria-disabled', 'true'); const reasonInput = document.getElementById('report-input-reason'); const reasonText = reasonInput?.value.trim(); const urlInput = document.getElementById('report-input-url'); const urlText = urlInput?.value.trim(); const errors = validateForm({ reason: reasonText, url: urlText }); if (errors) { if (errors.reason) { reasonInput?.setAttribute('aria-invalid', 'true'); reasonInput?.setAttribute('aria-errormessage', 'report-input-reason-error'); } if (errors.url) { urlInput?.setAttribute('aria-invalid', 'true'); urlInput?.setAttribute('aria-errormessage', 'report-input-url-error'); } event.currentTarget.setAttribute('aria-disabled', 'false'); return; } const issueButtons = document.getElementsByClassName('report-issue-button'); const formView = document.getElementsByClassName('report-form-view')[0]; const userAgent = window.navigator.userAgent; const response = await dispatch({ userAgent, reason: reasonText, url: urlText, type: 'REPORT' }); const hostname = new URL(urlText).hostname.split('.').slice(-3).join('.').replace('www.', ''); const issue = { expiresIn: Date.now() + 8 * 60 * 60 * 1000, flags: ['bug'], url: response.data }; if (response.success) { const successView = document.getElementsByClassName('report-submit-success-view')[0]; await dispatch({ hostname, state: { issue }, type: 'UPDATE_STORE' }); await dispatch({ hostname, type: 'ENABLE_ICON' }); formView?.setAttribute('hidden', 'true'); issueButtons[1]?.addEventListener('click', () => window.open(response.data, '_blank')); successView?.removeAttribute('hidden'); return; } if (response.data) { const errorView = document.getElementsByClassName('report-submit-error-view')[0]; if (response.errors?.some((error) => error.includes('wontfix'))) { issue.flags.push('wontfix'); } await dispatch({ hostname, state: { issue }, type: 'UPDATE_STORE' }); errorView?.removeAttribute('hidden'); formView?.setAttribute('hidden', 'true'); issueButtons[0]?.addEventListener('click', () => window.open(response.data, '_blank')); return; } window.close(); } /** * @description Apply 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 = browser.i18n.getMessage(i18n); } if (i18nPlaceholder) { node.setAttribute('placeholder', browser.i18n.getMessage(i18nPlaceholder)); } } } /** * @async * @description Update the database version element * @returns {Promise} */ async function updateDatabaseVersion() { const data = await dispatch({ hostname, type: 'GET_DATA' }); const databaseVersionElement = document.getElementById('database-version'); if (data.version) databaseVersionElement.innerText = data.version; else databaseVersionElement.style.setProperty('display', 'none'); } /** * @description Validate form * @param {{ reason: string | undefined | undefined, url: string | undefined }} params * @returns {{ reason: string | undefined, url: string | undefined } | undefined} */ function validateForm(params) { const { reason, url } = params; let errors = undefined; if (!reason || reason.length < 10 || reason.length > 1000) { errors = { ...(errors ?? {}), reason: browser.i18n.getMessage('report_reasonInputError'), }; } try { if (/\s/.test(url)) throw new Error(); new URL(url); } catch { errors = { ...(errors ?? {}), url: browser.i18n.getMessage('report_urlInputError'), }; } return errors; } /** * @description Listen to document ready * @listens document#DOMContentLoaded */ document.addEventListener('DOMContentLoaded', handleContentLoaded);