426 lines
13 KiB
JavaScript

/**
* @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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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);