| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435 |
- /* global $ $$ $create setupLivePrefs */// dom.js
- /* global ABOUT_BLANK getStyleDataMerged preinit */// preinit.js
- /* global API msg */// msg.js
- /* global Events */
- /* global prefs */
- /* global t */// localization.js
- /* global
- CHROME
- CHROME_POPUP_BORDER_BUG
- FIREFOX
- URLS
- capitalize
- getActiveTab
- isEmptyObj
- */// toolbox.js
- 'use strict';
- let tabURL;
- let isBlocked;
- /** @type Element */
- const installed = $('#installed');
- const ENTRY_ID_PREFIX_RAW = 'style-';
- const $entry = styleOrId => $(`#${ENTRY_ID_PREFIX_RAW}${styleOrId.id || styleOrId}`);
- preinit.then(({frames, styles, url}) => {
- tabURL = url;
- initPopup(frames);
- if (styles[0]) {
- showStyles(styles);
- } else {
- // unsupported URL;
- $('#popup-manage-button').removeAttribute('title');
- }
- });
- msg.onExtension(onRuntimeMessage);
- prefs.subscribe('popup.stylesFirst', (key, stylesFirst) => {
- const actions = $('body > .actions');
- const before = stylesFirst ? actions : actions.nextSibling;
- document.body.insertBefore(installed, before);
- });
- if (CHROME_POPUP_BORDER_BUG) {
- prefs.subscribe('popup.borders', toggleSideBorders, {runNow: true});
- }
- if (CHROME >= 66 && CHROME <= 69) { // Chrome 66-69 adds a gap, https://crbug.com/821143
- document.head.appendChild($create('style', 'html { overflow: overlay }'));
- }
- function onRuntimeMessage(msg) {
- if (!tabURL) return;
- let ready = Promise.resolve();
- switch (msg.method) {
- case 'styleAdded':
- case 'styleUpdated':
- if (msg.reason === 'editPreview' || msg.reason === 'editPreviewEnd') return;
- ready = handleUpdate(msg);
- break;
- case 'styleDeleted':
- handleDelete(msg.style.id);
- break;
- }
- ready.then(() => dispatchEvent(new CustomEvent(msg.method, {detail: msg})));
- }
- function setPopupWidth(_key, width) {
- document.body.style.width =
- Math.max(200, Math.min(800, width)) + 'px';
- }
- function toggleSideBorders(_key, state) {
- // runs before <body> is parsed
- const style = document.documentElement.style;
- if (state) {
- style.cssText +=
- 'border-left: 2px solid white !important;' +
- 'border-right: 2px solid white !important;';
- } else if (style.cssText) {
- style.borderLeft = style.borderRight = '';
- }
- }
- /** @param {chrome.webNavigation.GetAllFrameResultDetails[]} frames */
- async function initPopup(frames) {
- prefs.subscribe('popupWidth', setPopupWidth, {runNow: true});
- // action buttons
- $('#disableAll').onchange = function () {
- installed.classList.toggle('disabled', this.checked);
- };
- setupLivePrefs();
- Object.assign($('#find-styles-link'), {
- href: URLS.usoArchive + 'browse/styles',
- async onclick(e) {
- e.preventDefault();
- await require(['/popup/search']);
- Events.searchOnClick(this, e);
- },
- });
- Object.assign($('#popup-manage-button'), {
- onclick: Events.openManager,
- oncontextmenu: Events.openManager,
- });
- $('#popup-options-button').onclick = () => {
- API.openManage({options: true});
- window.close();
- };
- $('#popup-wiki-button').onclick = Events.openURLandHide;
- $('#confirm').onclick = function (e) {
- const {id} = this.dataset;
- switch (e.target.dataset.cmd) {
- case 'ok':
- Events.hideModal(this, {animate: true});
- API.styles.delete(Number(id));
- break;
- case 'cancel':
- Events.showModal($('.menu', $entry(id)), '.menu-close');
- break;
- }
- };
- if (!prefs.get('popup.stylesFirst')) {
- document.body.insertBefore(
- $('body > .actions'),
- installed);
- }
- for (const el of $$('link[media=print]')) {
- el.removeAttribute('media');
- }
- if (!tabURL) {
- blockPopup();
- return;
- }
- frames.forEach(createWriterElement);
- Object.assign($('#write-for-frames'), {
- onclick: e => e.currentTarget.classList.toggle('expanded'),
- hidden: frames.length < 2 || !$('.match .match:not(.dupe)'),
- });
- const isStore = tabURL.startsWith(URLS.browserWebStore);
- if (isStore && !FIREFOX) {
- blockPopup();
- return;
- }
- for (let retryCountdown = 10; retryCountdown-- > 0;) {
- const tab = await getActiveTab();
- if (await msg.sendTab(tab.id, {method: 'ping'}, {frameId: 0}).catch(() => {})) {
- return;
- }
- if (tab.status === 'complete' && (!FIREFOX || tab.url !== ABOUT_BLANK)) {
- break;
- }
- // FF and some Chrome forks (e.g. CentBrowser) implement tab-on-demand
- // so we'll wait a bit to handle popup being invoked right after switching
- await new Promise(resolve => setTimeout(resolve, 100));
- }
- initUnreachable(isStore);
- }
- function initUnreachable(isStore) {
- const info = t.template.unreachableInfo;
- if (!FIREFOX) {
- // Chrome "Allow access to file URLs" in chrome://extensions message
- info.appendChild($create('p', t('unreachableFileHint')));
- } else {
- $('label', info).textContent = t('unreachableAMO');
- const note = [
- isStore && t(FIREFOX >= 59 ? 'unreachableAMOHint' : 'unreachableMozSiteHintOldFF'),
- FIREFOX >= 60 && t('unreachableMozSiteHint'),
- ].filter(Boolean).join('\n');
- const renderToken = s => s[0] === '<'
- ? $create('a.copy', {
- textContent: s.slice(1, -1),
- onclick: Events.copyContent,
- tabIndex: 0,
- title: t('copy'),
- })
- : s;
- const renderLine = line => $create('p', line.split(/(<.*?>)/).map(renderToken));
- const noteNode = $create('fragment', note.split('\n').map(renderLine));
- info.appendChild(noteNode);
- }
- // Inaccessible locally hosted file type, e.g. JSON, PDF, etc.
- if (tabURL.length - tabURL.lastIndexOf('.') <= 5) {
- info.appendChild($create('p', t('InaccessibleFileHint')));
- }
- document.body.classList.add('unreachable');
- document.body.insertBefore(info, document.body.firstChild);
- }
- /** @param {chrome.webNavigation.GetAllFrameResultDetails} frame */
- function createWriterElement(frame) {
- const {url, frameId, parentFrameId, isDupe} = frame;
- const targets = $create('span');
- // For this URL
- const urlLink = t.template.writeStyle.cloneNode(true);
- const isAboutBlank = url === ABOUT_BLANK;
- Object.assign(urlLink, {
- href: 'edit.html?url-prefix=' + encodeURIComponent(url),
- title: `url-prefix("${url}")`,
- tabIndex: isAboutBlank ? -1 : 0,
- textContent: prefs.get('popup.breadcrumbs.usePath')
- ? new URL(url).pathname.slice(1)
- : frameId
- ? isAboutBlank ? url : 'URL'
- : t('writeStyleForURL').replace(/ /g, '\u00a0'), // this URL
- onclick: e => Events.openEditor(e, {'url-prefix': url}),
- });
- if (prefs.get('popup.breadcrumbs')) {
- urlLink.onmouseenter =
- urlLink.onfocus = () => urlLink.parentNode.classList.add('url()');
- urlLink.onmouseleave =
- urlLink.onblur = () => urlLink.parentNode.classList.remove('url()');
- }
- targets.appendChild(urlLink);
- // For domain
- const domains = getDomains(url);
- for (const domain of domains) {
- const numParts = domain.length - domain.replace(/\./g, '').length + 1;
- // Don't include TLD
- if (domains.length > 1 && numParts === 1) {
- continue;
- }
- const domainLink = t.template.writeStyle.cloneNode(true);
- Object.assign(domainLink, {
- href: 'edit.html?domain=' + encodeURIComponent(domain),
- textContent: numParts > 2 ? domain.split('.')[0] : domain,
- title: `domain("${domain}")`,
- onclick: e => Events.openEditor(e, {domain}),
- });
- domainLink.setAttribute('subdomain', numParts > 1 ? 'true' : '');
- targets.appendChild(domainLink);
- }
- if (prefs.get('popup.breadcrumbs')) {
- targets.classList.add('breadcrumbs');
- targets.appendChild(urlLink); // making it the last element
- }
- const root = $('#write-style');
- const parent = $(`[data-frame-id="${parentFrameId}"]`, root) || root;
- const child = $create({
- tag: 'span',
- className: `match${isDupe ? ' dupe' : ''}${isAboutBlank ? ' about-blank' : ''}`,
- dataset: {frameId},
- appendChild: targets,
- });
- parent.appendChild(child);
- parent.dataset.children = (Number(parent.dataset.children) || 0) + 1;
- }
- function getDomains(url) {
- let d = url.split(/[/:]+/, 2)[1];
- if (!d || url.startsWith('file:')) {
- return [];
- }
- const domains = [d];
- while (d.includes('.')) {
- d = d.substring(d.indexOf('.') + 1);
- domains.push(d);
- }
- return domains;
- }
- function sortStyles(entries) {
- const enabledFirst = prefs.get('popup.enabledFirst');
- return entries.sort(({styleMeta: a}, {styleMeta: b}) =>
- Boolean(a.frameUrl) - Boolean(b.frameUrl) ||
- enabledFirst && Boolean(b.enabled) - Boolean(a.enabled) ||
- (a.customName || a.name).localeCompare(b.customName || b.name));
- }
- function showStyles(frameResults) {
- const entries = new Map();
- frameResults.forEach(({styles = [], url}, index) => {
- if (isBlocked && !index) return;
- styles.forEach(style => {
- const {id} = style;
- if (!entries.has(id)) {
- style.frameUrl = index === 0 ? '' : url;
- entries.set(id, createStyleElement(style));
- }
- });
- });
- if (entries.size) {
- resortEntries([...entries.values()]);
- } else {
- installed.appendChild(t.template.noStyles);
- }
- require(['/popup/hotkeys']);
- }
- function resortEntries(entries) {
- // `entries` is specified only at startup, after that we respect the prefs
- if (entries || prefs.get('popup.autoResort')) {
- installed.append(...sortStyles(entries || $$('.entry', installed)));
- }
- }
- function createStyleElement(style) {
- let entry = $entry(style);
- if (!entry) {
- entry = t.template.style.cloneNode(true);
- Object.assign(entry, {
- id: ENTRY_ID_PREFIX_RAW + style.id,
- styleId: style.id,
- styleIsUsercss: Boolean(style.usercssData),
- onmousedown: Events.maybeEdit,
- styleMeta: style,
- });
- Object.assign($('input', entry), {
- onclick: Events.toggleState,
- });
- Object.assign($('.style-edit-link', entry), {
- onclick: e => Events.openEditor(e, {id: style.id}),
- });
- const styleName = $('.style-name', entry);
- Object.assign(styleName, {
- htmlFor: ENTRY_ID_PREFIX_RAW + style.id,
- onclick: Events.name,
- });
- styleName.appendChild(document.createTextNode(' '));
- const config = $('.configure', entry);
- config.onclick = Events.configure;
- if (!style.usercssData) {
- if (style.updateUrl && style.updateUrl.includes('?') && style.url) {
- config.href = style.url;
- config.target = '_blank';
- config.title = t('configureStyleOnHomepage');
- config._sendMessage = {method: 'openSettings'};
- $('use', config).attributes['xlink:href'].nodeValue = '#svg-icon-config-uso';
- } else {
- config.classList.add('hidden');
- }
- } else if (isEmptyObj(style.usercssData.vars)) {
- config.classList.add('hidden');
- }
- $('.delete', entry).onclick = Events.delete;
- const indicator = t.template.regexpProblemIndicator.cloneNode(true);
- indicator.appendChild(document.createTextNode('!'));
- indicator.onclick = Events.indicator;
- $('.main-controls', entry).appendChild(indicator);
- $('.menu-button', entry).onclick = Events.toggleMenu;
- $('.menu-close', entry).onclick = Events.toggleMenu;
- $('.exclude-by-domain-checkbox', entry).onchange = e => Events.toggleExclude(e, 'domain');
- $('.exclude-by-url-checkbox', entry).onchange = e => Events.toggleExclude(e, 'url');
- }
- style = Object.assign(entry.styleMeta, style);
- entry.classList.toggle('disabled', !style.enabled);
- entry.classList.toggle('enabled', style.enabled);
- $('input', entry).checked = style.enabled;
- const styleName = $('.style-name', entry);
- styleName.lastChild.textContent = style.customName || style.name;
- setTimeout(() => {
- styleName.title =
- entry.styleMeta.sloppy ? t('styleNotAppliedRegexpProblemTooltip') :
- entry.styleMeta.excludedScheme ? t(`styleNotAppliedScheme${capitalize(entry.styleMeta.preferScheme)}`) :
- styleName.scrollWidth > styleName.clientWidth + 1 ? styleName.textContent :
- '';
- });
- entry.classList.toggle('force-applied', style.included);
- entry.classList.toggle('not-applied', style.excluded || style.sloppy || style.excludedScheme);
- entry.classList.toggle('regexp-partial', style.sloppy);
- $('.exclude-by-domain-checkbox', entry).checked = Events.isStyleExcluded(style, 'domain');
- $('.exclude-by-url-checkbox', entry).checked = Events.isStyleExcluded(style, 'url');
- $('.exclude-by-domain', entry).title = Events.getExcludeRule('domain');
- $('.exclude-by-url', entry).title = Events.getExcludeRule('url');
- const {frameUrl} = style;
- if (frameUrl) {
- const sel = 'span.frame-url';
- const frameEl = $(sel, entry) || styleName.insertBefore($create(sel), styleName.lastChild);
- frameEl.title = frameUrl;
- frameEl.onmousedown = Events.maybeEdit;
- }
- entry.classList.toggle('frame', Boolean(frameUrl));
- return entry;
- }
- async function handleUpdate({style, reason}) {
- if (reason !== 'toggle' || !$entry(style)) {
- style = await getStyleDataMerged(tabURL, style.id);
- if (!style) return;
- }
- const el = createStyleElement(style);
- if (!el.parentNode) {
- installed.appendChild(el);
- blockPopup(false);
- }
- resortEntries();
- }
- function handleDelete(id) {
- const el = $entry(id);
- if (el) {
- el.remove();
- if (!$('.entry')) installed.appendChild(t.template.noStyles);
- }
- }
- function blockPopup(val = true) {
- isBlocked = val;
- document.body.classList.toggle('blocked', isBlocked);
- if (isBlocked) {
- document.body.prepend(t.template.unavailableInfo);
- } else {
- t.template.unavailableInfo.remove();
- t.template.noStyles.remove();
- }
- }
|