| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601 |
- /* global messageBox, getStyleWithNoCode, retranslateCSS */
- /* global filtersSelector, filterAndAppend */
- /* global checkUpdate, handleUpdateInstalled */
- /* global objectDiff */
- /* global configDialog */
- 'use strict';
- let installed;
- const ENTRY_ID_PREFIX_RAW = 'style-';
- const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
- const newUI = {
- enabled: prefs.get('manage.newUI'),
- favicons: prefs.get('manage.newUI.favicons'),
- faviconsGray: prefs.get('manage.newUI.faviconsGray'),
- targets: prefs.get('manage.newUI.targets'),
- renderClass() {
- document.documentElement.classList.toggle('newUI', newUI.enabled);
- },
- };
- newUI.renderClass();
- usePrefsDuringPageLoad();
- const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps'];
- const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
- const OWN_ICON = chrome.runtime.getManifest().icons['16'];
- const handleEvent = {};
- Promise.all([
- getStylesSafe(),
- onDOMready().then(initGlobalEvents),
- ]).then(([styles]) => {
- showStyles(styles);
- });
- dieOnNullBackground();
- chrome.runtime.onMessage.addListener(onRuntimeMessage);
- function onRuntimeMessage(msg) {
- switch (msg.method) {
- case 'styleUpdated':
- case 'styleAdded':
- handleUpdate(msg.style, msg);
- break;
- case 'styleDeleted':
- handleDelete(msg.id);
- break;
- }
- }
- function initGlobalEvents() {
- installed = $('#installed');
- installed.onclick = handleEvent.entryClicked;
- $('#manage-options-button').onclick = () => chrome.runtime.openOptionsPage();
- $('#manage-shortcuts-button').onclick = () => openURL({url: URLS.configureCommands});
- $$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external));
- // focus search field on / key
- document.onkeypress = event => {
- if ((event.keyCode || event.which) === 47
- && !event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey
- && !event.target.matches('[type="text"], [type="search"]')) {
- event.preventDefault();
- $('#search').focus();
- }
- };
- // remember scroll position on normal history navigation
- window.onbeforeunload = rememberScrollPosition;
- $$('[data-toggle-on-click]').forEach(el => {
- // dataset on SVG doesn't work in Chrome 49-??, works in 57+
- const target = $(el.getAttribute('data-toggle-on-click'));
- el.onclick = () => target.classList.toggle('hidden');
- });
- // triggered automatically by setupLivePrefs() below
- enforceInputRange($('#manage.newUI.targets'));
- // N.B. triggers existing onchange listeners
- setupLivePrefs();
- $$('[id^="manage.newUI"]')
- .forEach(el => (el.oninput = (el.onchange = switchUI)));
- switchUI({styleOnly: true});
- // translate CSS manually
- document.head.appendChild($element({tag: 'style', textContent: `
- .disabled h2::after {
- content: "${t('genericDisabledLabel')}";
- }
- #update-all-no-updates[data-skipped-edited="true"]:after {
- content: " ${t('updateAllCheckSucceededSomeEdited')}";
- }
- `}));
- }
- function showStyles(styles = []) {
- const sorted = styles
- .map(style => ({name: style.name.toLocaleLowerCase(), style}))
- .sort((a, b) => (a.name < b.name ? -1 : a.name === b.name ? 0 : 1));
- let index = 0;
- const scrollY = (history.state || {}).scrollY;
- const shouldRenderAll = scrollY > window.innerHeight || sessionStorage.justEditedStyleId;
- const renderBin = document.createDocumentFragment();
- if (scrollY) {
- renderStyles();
- } else {
- requestAnimationFrame(renderStyles);
- }
- function renderStyles() {
- const t0 = performance.now();
- let rendered = 0;
- while (
- index < sorted.length &&
- // eslint-disable-next-line no-unmodified-loop-condition
- (shouldRenderAll || ++rendered < 10 || performance.now() - t0 < 10)
- ) {
- renderBin.appendChild(createStyleElement(sorted[index++]));
- }
- filterAndAppend({container: renderBin});
- if (index < sorted.length) {
- requestAnimationFrame(renderStyles);
- return;
- }
- if ('scrollY' in (history.state || {})) {
- setTimeout(window.scrollTo, 0, 0, history.state.scrollY);
- }
- if (newUI.enabled && newUI.favicons) {
- debounce(handleEvent.loadFavicons, 16);
- }
- if (sessionStorage.justEditedStyleId) {
- const entry = $(ENTRY_ID_PREFIX + sessionStorage.justEditedStyleId);
- delete sessionStorage.justEditedStyleId;
- if (entry) {
- animateElement(entry);
- scrollElementIntoView(entry);
- }
- }
- }
- }
- function createStyleElement({style, name}) {
- // query the sub-elements just once, then reuse the references
- if ((createStyleElement.parts || {}).newUI !== newUI.enabled) {
- const entry = template[`style${newUI.enabled ? 'Compact' : ''}`];
- createStyleElement.parts = {
- newUI: newUI.enabled,
- entry,
- entryClassBase: entry.className,
- checker: $('.checker', entry) || {},
- nameLink: $('.style-name-link', entry),
- editLink: $('.style-edit-link', entry) || {},
- editHrefBase: 'edit.html?id=',
- homepage: $('.homepage', entry),
- homepageIcon: template[`homepageIcon${newUI.enabled ? 'Small' : 'Big'}`],
- appliesTo: $('.applies-to', entry),
- targets: $('.targets', entry),
- expander: $('.expander', entry),
- decorations: {
- urlPrefixesAfter: '*',
- regexpsBefore: '/',
- regexpsAfter: '/',
- },
- };
- }
- const parts = createStyleElement.parts;
- parts.checker.checked = style.enabled;
- parts.nameLink.textContent = style.name;
- parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
- parts.homepage.href = parts.homepage.title = style.url || '';
- const entry = parts.entry.cloneNode(true);
- entry.id = ENTRY_ID_PREFIX_RAW + style.id;
- entry.styleId = style.id;
- entry.styleNameLowerCase = name || style.name.toLocaleLowerCase();
- entry.styleMeta = getStyleWithNoCode(style);
- entry.className = parts.entryClassBase + ' ' +
- (style.enabled ? 'enabled' : 'disabled') +
- (style.updateUrl ? ' updatable' : '') +
- (style.usercssData ? ' usercss' : '');
- if (style.url) {
- $('.homepage', entry).appendChild(parts.homepageIcon.cloneNode(true));
- }
- if (style.updateUrl && newUI.enabled) {
- $('.actions', entry).appendChild(template.updaterIcons.cloneNode(true));
- }
- if (shouldShowConfig() && newUI.enabled) {
- $('.actions', entry).appendChild(template.configureIcon.cloneNode(true));
- }
- // name being supplied signifies we're invoked by showStyles()
- // which debounces its main loop thus loading the postponed favicons
- createStyleTargetsElement({entry, style, postponeFavicons: name});
- return entry;
- function shouldShowConfig() {
- return style.usercssData && Object.keys(style.usercssData.vars).length > 0;
- }
- }
- function createStyleTargetsElement({entry, style, postponeFavicons}) {
- const parts = createStyleElement.parts;
- const targets = parts.targets.cloneNode(true);
- let container = targets;
- let numTargets = 0;
- let numIcons = 0;
- const displayed = new Set();
- for (const type of TARGET_TYPES) {
- for (const section of style.sections) {
- for (const targetValue of section[type] || []) {
- if (displayed.has(targetValue)) {
- continue;
- }
- displayed.add(targetValue);
- const element = template.appliesToTarget.cloneNode(true);
- if (!newUI.enabled) {
- if (numTargets === 10) {
- container = container.appendChild(template.extraAppliesTo.cloneNode(true));
- } else if (numTargets > 1) {
- container.appendChild(template.appliesToSeparator.cloneNode(true));
- }
- } else if (newUI.favicons) {
- let favicon = '';
- if (type === 'domains') {
- favicon = GET_FAVICON_URL + targetValue;
- } else if (targetValue.startsWith('chrome-extension:')) {
- favicon = OWN_ICON;
- } else if (type !== 'regexps') {
- favicon = targetValue.includes('://') && targetValue.match(/^.*?:\/\/([^/]+)/);
- favicon = favicon ? GET_FAVICON_URL + favicon[1] : '';
- }
- if (favicon) {
- element.appendChild(document.createElement('img')).dataset.src = favicon;
- numIcons++;
- }
- }
- element.appendChild(
- document.createTextNode(
- (parts.decorations[type + 'Before'] || '') +
- targetValue +
- (parts.decorations[type + 'After'] || '')));
- container.appendChild(element);
- numTargets++;
- }
- }
- }
- if (newUI.enabled) {
- if (numTargets > newUI.targets) {
- $('.applies-to', entry).classList.add('has-more');
- }
- if (numIcons && !postponeFavicons) {
- debounce(handleEvent.loadFavicons);
- }
- }
- const entryTargets = $('.targets', entry);
- if (numTargets) {
- entryTargets.parentElement.replaceChild(targets, entryTargets);
- } else {
- entryTargets.appendChild(template.appliesToEverything.cloneNode(true));
- }
- entry.classList.toggle('global', !numTargets);
- }
- Object.assign(handleEvent, {
- ENTRY_ROUTES: {
- '.checker, .enable, .disable': 'toggle',
- '.style-name-link': 'edit',
- '.homepage': 'external',
- '.check-update': 'check',
- '.update': 'update',
- '.delete': 'delete',
- '.applies-to .expander': 'expandTargets',
- '.configure-usercss': 'config'
- },
- config(event, {styleMeta: style}) {
- configDialog(style).then(vars => {
- if (!vars) {
- return;
- }
- const keys = Object.keys(vars).filter(k => vars[k].dirty);
- if (!keys.length) {
- return;
- }
- style.reason = 'config';
- for (const key of keys) {
- style.usercssData.vars[key].value = vars[key].value;
- }
- onBackgroundReady()
- .then(() => BG.usercssHelper.save(style));
- });
- },
- entryClicked(event) {
- const target = event.target;
- const entry = target.closest('.entry');
- for (const selector in handleEvent.ENTRY_ROUTES) {
- for (let el = target; el && el !== entry; el = el.parentElement) {
- if (el.matches(selector)) {
- const handler = handleEvent.ENTRY_ROUTES[selector];
- return handleEvent[handler].call(el, event, entry);
- }
- }
- }
- },
- edit(event) {
- if (event.altKey) {
- return;
- }
- event.preventDefault();
- event.stopPropagation();
- const left = event.button === 0;
- const middle = event.button === 1;
- const shift = event.shiftKey;
- const ctrl = event.ctrlKey;
- const openWindow = left && shift && !ctrl;
- const openBackgroundTab = (middle && !shift) || (left && ctrl && !shift);
- const openForegroundTab = (middle && shift) || (left && ctrl && shift);
- const url = event.target.closest('[href]').href;
- if (openWindow || openBackgroundTab || openForegroundTab) {
- if (chrome.windows && openWindow) {
- chrome.windows.create(Object.assign(prefs.get('windowPosition'), {url}));
- } else {
- openURL({url, active: openForegroundTab});
- }
- } else {
- rememberScrollPosition();
- getActiveTab().then(tab => {
- sessionStorageHash('manageStylesHistory').set(tab.id, url);
- location.href = url;
- });
- }
- },
- toggle(event, entry) {
- saveStyleSafe({
- id: entry.styleId,
- enabled: this.matches('.enable') || this.checked,
- });
- },
- check(event, entry) {
- checkUpdate(entry);
- },
- update(event, entry) {
- const request = Object.assign(entry.updatedCode, {
- id: entry.styleId,
- reason: 'update',
- });
- if (entry.updatedCode.usercssData) {
- onBackgroundReady()
- .then(() => BG.usercssHelper.save(request));
- } else {
- // update everything but name
- request.name = null;
- saveStyleSafe(request);
- }
- },
- delete(event, entry) {
- const id = entry.styleId;
- const {name} = BG.cachedStyles.byId.get(id) || {};
- animateElement(entry);
- messageBox({
- title: t('deleteStyleConfirm'),
- contents: name,
- className: 'danger center',
- buttons: [t('confirmDelete'), t('confirmCancel')],
- })
- .then(({button, enter}) => {
- if (button === 0 || enter) {
- deleteStyleSafe({id});
- }
- });
- },
- external(event) {
- openURL({url: event.target.closest('a').href});
- event.preventDefault();
- },
- expandTargets() {
- this.closest('.applies-to').classList.toggle('expanded');
- },
- loadFavicons(container = document.body) {
- for (const img of $$('img', container)) {
- if (img.dataset.src) {
- img.src = img.dataset.src;
- delete img.dataset.src;
- }
- }
- },
- });
- function handleUpdate(style, {reason, method} = {}) {
- let entry;
- let oldEntry = $(ENTRY_ID_PREFIX + style.id);
- if (oldEntry && method === 'styleUpdated') {
- handleToggledOrCodeOnly();
- }
- entry = entry || createStyleElement({style});
- if (oldEntry) {
- if (oldEntry.styleNameLowerCase === entry.styleNameLowerCase) {
- installed.replaceChild(entry, oldEntry);
- } else {
- oldEntry.remove();
- }
- }
- if (reason === 'update' && entry.matches('.updatable')) {
- handleUpdateInstalled(entry);
- }
- filterAndAppend({entry});
- if (!entry.matches('.hidden') && reason !== 'import') {
- animateElement(entry);
- scrollElementIntoView(entry);
- }
- function handleToggledOrCodeOnly() {
- const newStyleMeta = getStyleWithNoCode(style);
- const diff = objectDiff(oldEntry.styleMeta, newStyleMeta);
- if (diff.length === 0) {
- // only code was modified
- entry = oldEntry;
- oldEntry = null;
- }
- if (diff.length === 1 && diff[0].key === 'enabled') {
- oldEntry.classList.toggle('enabled', style.enabled);
- oldEntry.classList.toggle('disabled', !style.enabled);
- $$('.checker', oldEntry).forEach(el => (el.checked = style.enabled));
- oldEntry.styleMeta = newStyleMeta;
- entry = oldEntry;
- oldEntry = null;
- }
- }
- }
- function handleDelete(id) {
- const node = $(ENTRY_ID_PREFIX + id);
- if (node) {
- node.remove();
- if (node.matches('.can-update')) {
- const btnApply = $('#apply-all-updates');
- btnApply.dataset.value = Number(btnApply.dataset.value) - 1;
- }
- }
- }
- function switchUI({styleOnly} = {}) {
- const current = {};
- const changed = {};
- let someChanged = false;
- // ensure the global option is processed first
- for (const el of [$('#manage.newUI'), ...$$('[id^="manage.newUI."]')]) {
- const id = el.id.replace(/^manage\.newUI\.?/, '') || 'enabled';
- const value = el.type === 'checkbox' ? el.checked : Number(el.value);
- const valueChanged = value !== newUI[id] && (id === 'enabled' || current.enabled);
- current[id] = value;
- changed[id] = valueChanged;
- someChanged |= valueChanged;
- }
- if (!styleOnly && !someChanged) {
- return;
- }
- Object.assign(newUI, current);
- newUI.renderClass();
- installed.classList.toggle('has-favicons', newUI.favicons);
- $('#style-overrides').textContent = `
- .newUI .targets {
- max-height: ${newUI.targets * 18}px;
- }
- ` + (newUI.faviconsGray ? `
- .newUI .target img {
- -webkit-filter: grayscale(1);
- filter: grayscale(1);
- opacity: .25;
- }
- ` : `
- .newUI .target img {
- -webkit-filter: none;
- filter: none;
- opacity: 1;
- }
- `);
- if (styleOnly) {
- return;
- }
- const missingFavicons = newUI.enabled && newUI.favicons && !$('.applies-to img');
- if (changed.enabled || (missingFavicons && !createStyleElement.parts)) {
- installed.textContent = '';
- getStylesSafe().then(showStyles);
- return;
- }
- if (changed.targets) {
- for (const targets of $$('.entry .targets')) {
- const hasMore = targets.children.length > newUI.targets;
- targets.parentElement.classList.toggle('has-more', hasMore);
- }
- return;
- }
- if (missingFavicons) {
- getStylesSafe().then(styles => {
- for (const style of styles) {
- const entry = $(ENTRY_ID_PREFIX + style.id);
- if (entry) {
- createStyleTargetsElement({entry, style, postponeFavicons: true});
- }
- }
- debounce(handleEvent.loadFavicons);
- });
- return;
- }
- }
- function rememberScrollPosition() {
- history.replaceState({scrollY: window.scrollY}, document.title);
- }
- function usePrefsDuringPageLoad() {
- const observer = new MutationObserver(mutations => {
- const adjustedNodes = [];
- for (const mutation of mutations) {
- for (const node of mutation.addedNodes) {
- // [naively] assuming each element of addedNodes is a childless element
- const prefValue = node.id ? prefs.readOnlyValues[node.id] : undefined;
- if (prefValue !== undefined) {
- if (node.type === 'checkbox') {
- node.checked = prefValue;
- } else {
- node.value = prefValue;
- }
- if (node.adjustWidth) {
- adjustedNodes.push(node);
- }
- }
- }
- }
- if (adjustedNodes.length) {
- observer.disconnect();
- for (const node of adjustedNodes) {
- node.adjustWidth();
- }
- startObserver();
- }
- });
- function startObserver() {
- observer.observe(document, {subtree: true, childList: true});
- }
- startObserver();
- onDOMready().then(() => observer.disconnect());
- }
- // TODO: remove when these bugs are fixed in FF
- function dieOnNullBackground() {
- if (!FIREFOX || BG) {
- return;
- }
- sendMessage({method: 'healthCheck'}, health => {
- if (health && !chrome.extension.getBackgroundPage()) {
- onDOMready().then(() => {
- sendMessage({method: 'getStyles'}, showStyles);
- messageBox({
- title: 'Stylus',
- className: 'danger center',
- contents: t('dysfunctionalBackgroundConnection'),
- onshow: () => {
- $('#message-box-close-icon').remove();
- window.removeEventListener('keydown', messageBox.listeners.key, true);
- }
- });
- document.documentElement.style.pointerEvents = 'none';
- });
- }
- });
- }
|