/* global messageBox */ 'use strict'; const installed = $('#installed'); const TARGET_LABEL = t('appliesDisplay', '').trim(); const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps']; const TARGET_LIMIT = 10; getStylesSafe() .then(showStyles) .then(initGlobalEvents); chrome.runtime.onMessage.addListener(msg => { switch (msg.method) { case 'styleUpdated': case 'styleAdded': handleUpdate(msg.style, msg); break; case 'styleDeleted': handleDelete(msg.id); break; } }); function initGlobalEvents() { $('#check-all-updates').onclick = checkUpdateAll; $('#apply-all-updates').onclick = applyUpdateAll; $('#search').oninput = searchStyles; $('#manage-options-button').onclick = () => chrome.runtime.openOptionsPage(); $('#manage-shortcuts-button').onclick = configureCommands.open; $('#editor-styles-button').onclick = () => openURL({ url: 'https://userstyles.org/styles/browse/chrome-extension', }); // focus search field on / key document.onkeypress = event => { if (event.keyCode == 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 document.addEventListener('visibilitychange', () => { if (document.visibilityState != 'visible') { rememberScrollPosition(); } }); setupLivePrefs([ 'manage.onlyEnabled', 'manage.onlyEdited', 'show-badge', 'popup.stylesFirst' ]); [ ['enabled-only', $('#manage.onlyEnabled')], ['edited-only', $('#manage.onlyEdited')], ] .forEach(([className, checkbox]) => { checkbox.onchange = () => installed.classList.toggle(className, checkbox.checked); checkbox.onchange(); }); } 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)); const shouldRenderAll = history.state && history.state.scrollY > innerHeight; const renderBin = document.createDocumentFragment(); tDocLoader.stop(); renderStyles(0); // TODO: remember how many styles fit one page to display just that portion first next time function renderStyles(index) { const t0 = performance.now(); while (index < sorted.length) { renderBin.appendChild(createStyleElement(sorted[index++])); if (!shouldRenderAll && performance.now() - t0 > 10) { break; } } if ($('#search').value) { // re-apply filtering on history Back searchStyles({immediately: true, container: renderBin}); } installed.appendChild(renderBin); if (index < sorted.length) { setTimeout(renderStyles, 0, index); } else if (shouldRenderAll && history.state && 'scrollY' in history.state) { setTimeout(() => scrollTo(0, history.state.scrollY)); } } } // silence the inapplicable warning for async code /* eslint no-use-before-define: [2, {"functions": false, "classes": false}] */ function createStyleElement({style, name}) { const entry = template.style.cloneNode(true); entry.classList.add(style.enabled ? 'enabled' : 'disabled'); entry.setAttribute('style-id', style.id); entry.styleId = style.id; entry.styleNameLowerCase = name || style.name.toLocaleLowerCase(); if (style.updateUrl) { entry.setAttribute('style-update-url', style.updateUrl); } if (style.md5Url) { entry.setAttribute('style-md5-url', style.md5Url); } if (style.originalMd5) { entry.setAttribute('style-original-md5', style.originalMd5); } const styleName = $('.style-name', entry); const styleNameEditLink = $('a', styleName); styleNameEditLink.appendChild(document.createTextNode(style.name)); styleNameEditLink.href = styleNameEditLink.getAttribute('href') + style.id; styleNameEditLink.onclick = EntryOnClick.edit; if (style.url) { const homepage = template.styleHomepage.cloneNode(true); homepage.href = style.url; styleName.appendChild(document.createTextNode(' ')); styleName.appendChild(homepage); } const targets = new Map(TARGET_TYPES.map(t => [t, new Set()])); const decorations = { urlPrefixesAfter: '*', regexpsBefore: '/', regexpsAfter: '/', }; for (const [name, target] of targets.entries()) { for (const section of style.sections) { for (const targetValue of section[name] || []) { target.add( (decorations[name + 'Before'] || '') + targetValue.trim() + (decorations[name + 'After'] || '')); } } } const appliesTo = $('.applies-to', entry); appliesTo.firstElementChild.textContent = TARGET_LABEL; const targetsList = Array.prototype.concat.apply([], [...targets.values()].map(set => [...set.values()])); if (!targetsList.length) { appliesTo.appendChild(template.appliesToEverything.cloneNode(true)); entry.classList.add('global'); } else { let index = 0; let container = appliesTo; for (const target of targetsList) { if (index > 0) { container.appendChild(template.appliesToSeparator.cloneNode(true)); } if (++index == TARGET_LIMIT) { container = appliesTo.appendChild(template.extraAppliesTo.cloneNode(true)); } const item = template.appliesToTarget.cloneNode(true); item.textContent = target; container.appendChild(item); } } const editLink = $('.style-edit-link', entry); editLink.href = editLink.getAttribute('href') + style.id; editLink.onclick = EntryOnClick.edit; $('.enable', entry).onclick = EntryOnClick.toggle; $('.disable', entry).onclick = EntryOnClick.toggle; $('.check-update', entry).onclick = EntryOnClick.check; $('.update', entry).onclick = EntryOnClick.update; $('.delete', entry).onclick = EntryOnClick.delete; return entry; } class EntryOnClick { static 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 (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; }); } } static toggle(event) { enableStyle(getClickedStyleId(event), this.matches('.enable')) .then(handleUpdate); } static check(event) { checkUpdate(getClickedStyleElement(event)); } static update(event) { const styleElement = getClickedStyleElement(event); // update everything but name saveStyle(Object.assign(styleElement.updatedCode, { id: styleElement.styleId, name: null, reason: 'update', })); } static delete(event) { const styleElement = getClickedStyleElement(event); const id = styleElement.styleId; const {name} = cachedStyles.byId.get(id) || {}; animateElement(styleElement, {className: 'highlight'}); messageBox({ title: t('deleteStyleConfirm'), contents: name, className: 'danger center', buttons: [t('confirmDelete'), t('confirmCancel')], }) .then(({button, enter, esc}) => { if (button == 0 || enter) { deleteStyle(id); } }); } } function handleUpdate(style, {reason} = {}) { const element = createStyleElement({style}); const oldElement = $(`[style-id="${style.id}"]`, installed); if (oldElement) { if (oldElement.styleNameLowerCase == element.styleNameLowerCase) { installed.replaceChild(element, oldElement); } else { oldElement.remove(); } if (reason == 'update') { element.classList.add('update-done'); $('.update-note', element).innerHTML = t('updateCompleted'); } } installed.insertBefore(element, findNextElement(style)); animateElement(element, {className: 'highlight'}); scrollElementIntoView(element); } function handleDelete(id) { const node = $(`[style-id="${id}"]`, installed); if (node) { node.remove(); } } function applyUpdateAll() { const btnApply = $('#apply-all-updates'); btnApply.disabled = true; setTimeout(() => { btnApply.style.display = 'none'; btnApply.disabled = false; }, 1000); $$('.can-update .update').forEach(button => { // align to the bottom of the visible area if wasn't visible button.scrollIntoView(false); button.click(); }); } function checkUpdateAll() { const btnCheck = $('#check-all-updates'); const btnApply = $('#apply-all-updates'); const noUpdates = $('#update-all-no-updates'); btnCheck.disabled = true; btnApply.classList.add('hidden'); noUpdates.classList.add('hidden'); Promise.all($$('[style-update-url]').map(checkUpdate)) .then(updatables => { btnCheck.disabled = false; if (updatables.includes(true)) { btnApply.classList.remove('hidden'); } else { noUpdates.classList.remove('hidden'); setTimeout(() => { noUpdates.classList.add('hidden'); }, 10e3); } }); // notify the automatic updater to reset the next automatic update accordingly chrome.runtime.sendMessage({ method: 'resetInterval' }); } function checkUpdate(element) { $('.update-note', element).innerHTML = t('checkingForUpdate'); element.classList.remove('checking-update', 'no-update', 'can-update'); element.classList.add('checking-update'); return new Updater(element).run(); } class Updater { constructor(element) { Object.assign(this, { element, id: element.getAttribute('style-id'), url: element.getAttribute('style-update-url'), md5Url: element.getAttribute('style-md5-url'), md5: element.getAttribute('style-original-md5'), }); } run() { return this.md5Url && this.md5 ? this.checkMd5() : this.checkFullCode(); } checkMd5() { return Updater.download(this.md5Url).then( md5 => (md5.length == 32 ? this.decideOnMd5(md5 != this.md5) : this.onFailure(-1)), this.onFailure); } decideOnMd5(md5changed) { if (md5changed) { return this.checkFullCode({forceUpdate: true}); } this.display(); } checkFullCode({forceUpdate = false} = {}) { return Updater.download(this.url).then( text => this.handleJson(forceUpdate, JSON.parse(text)), this.onFailure); } handleJson(forceUpdate, json) { return getStylesSafe({id: this.id}).then(([style]) => { const needsUpdate = forceUpdate || !styleSectionsEqual(style, json); this.display({json: needsUpdate && json}); return needsUpdate; }); } onFailure(status) { this.display({ message: status == 0 ? t('updateCheckFailServerUnreachable') : t('updateCheckFailBadResponseCode', [status]), }); } display({json, message} = {}) { // json on success // message on failure // none on update not needed this.element.classList.remove('checking-update'); if (json) { this.element.classList.add('can-update'); this.element.updatedCode = json; $('.update-note', this.element).innerHTML = ''; } else { this.element.classList.add('no-update'); $('.update-note', this.element).innerHTML = message || t('updateCheckSucceededNoUpdate'); } } static download(url) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.onloadend = () => (xhr.status == 200 ? resolve(xhr.responseText) : reject(xhr.status)); if (url.length > 2000) { const [mainUrl, query] = url.split('?'); xhr.open('POST', mainUrl, true); xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); xhr.send(query); } else { xhr.open('GET', url); xhr.send(); } }); } } function searchStyles({immediately, container}) { const query = $('#search').value.toLocaleLowerCase(); if (query == (searchStyles.lastQuery || '') && !immediately && !container) { return; } searchStyles.lastQuery = query; if (!immediately) { clearTimeout(searchStyles.timeout); searchStyles.timeout = setTimeout(searchStyles, 150, {immediately: true}); return; } for (const element of (container || installed).children) { const style = cachedStyles.byId.get(element.styleId) || {}; if (style) { const isMatching = !query || isMatchingText(style.name) || style.url && isMatchingText(style.url) || isMatchingStyle(style); element.style.display = isMatching ? '' : 'none'; } } function isMatchingStyle(style) { for (const section of style.sections) { for (const prop in section) { const value = section[prop]; switch (typeof value) { case 'string': if (isMatchingText(value)) { return true; } break; case 'object': for (const str of value) { if (isMatchingText(str)) { return true; } } break; } } } } function isMatchingText(text) { return text.toLocaleLowerCase().indexOf(query) >= 0; } } function rememberScrollPosition() { history.replaceState({scrollY}, document.title); } function findNextElement(style) { const nameLLC = style.name.toLocaleLowerCase(); const elements = installed.children; let a = 0; let b = elements.length - 1; if (b < 0) { return undefined; } if (elements[0].styleNameLowerCase > nameLLC) { return elements[0]; } if (elements[b].styleNameLowerCase <= nameLLC) { return undefined; } // bisect while (a < b - 1) { const c = (a + b) / 2 | 0; if (nameLLC < elements[c].styleNameLowerCase) { b = c; } else { a = c; } } if (elements[a].styleNameLowerCase > nameLLC) { return elements[a]; } while (a <= b && elements[a].name < nameLLC) { a++; } return elements[elements[a].styleNameLowerCase <= nameLLC ? a + 1 : a]; }