diff --git a/.eslintignore b/.eslintignore index 325be71d..a710e413 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,2 @@ -beautify/ -codemirror/ -csslint/ +vendor/ +vendor-overwrites/ diff --git a/.eslintrc b/.eslintrc index a0e8a12e..2e335468 100644 --- a/.eslintrc +++ b/.eslintrc @@ -82,7 +82,7 @@ rules: dot-location: [2, property] dot-notation: [0] eol-last: [2] - eqeqeq: [0] + eqeqeq: [1, always] func-call-spacing: [2, never] func-name-matching: [0] func-names: [0] @@ -132,7 +132,7 @@ rules: no-duplicate-imports: [2] no-else-return: [0] no-empty-character-class: [2] - no-empty-function: [0] + no-empty-function: [1] no-empty-pattern: [2] no-empty: [2, {allowEmptyCatch: true}] no-eq-null: [2] @@ -166,7 +166,7 @@ rules: no-mixed-operators: [0] no-mixed-requires: [2, true] no-mixed-spaces-and-tabs: [2] - no-multi-spaces: [0] + no-multi-spaces: [2, {ignoreEOLComments: true}] no-multi-str: [2] no-multiple-empty-lines: [2, {max: 2, maxEOF: 0, maxBOF: 0}] no-native-reassign: [2] @@ -249,7 +249,7 @@ rules: sort-imports: [0] sort-keys: [0] space-before-blocks: [2, always] - space-before-function-paren: [2, never] + space-before-function-paren: [2, {anonymous: always, asyncArrow: always, named: never}] space-in-parens: [2, never] space-infix-ops: [2] space-unary-ops: [2] diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..0fb81935 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# Contributing to Stylus + +1. [Getting Involved](#getting-involved) +2. [How to Report Issues](#how-to-report-issues) +3. [Adding Tranlations](#adding-translations) +4. [Core Style Guide](#core-style-guide) +5. [Getting Started](#getting-started) + +## Getting Involved + +There are a number of ways to get involved with the development of Stylus. Even if you've never contributed to an Open Source project before, we're always looking for help by identifying issues and suggesting improvements. + +## How to Report issues + +When an issue is opened, a template is provided. Please answer these questions as thoroughly as possible. If we were psychic, we'd be hanging out in casinos playing poker until they kicked us out. We can't read your mind! Please provide step-by-step direction on how to reproduce the issue as well as the browser, operating system and versions of each. + +When adding a feature request, please include + +## Adding Translations + +You can help us translate the extension on [Transifex](https://www.transifex.com/github-7/Stylus). When `messages.json` file is ready to be merged, please open a new bug report in [stylus/issues](https://github.com/openstyles/stylus/issues). + +## Core Style Guide + +* Use the provided `.editorconfig` file with your code editor. Don't know what that is? Then check out http://editorconfig.org/. + +## Getting Started + +* First open an issue to discuss your changes. +* Then download, fork or clone this repository. + +* Make any changes within a branch of this repository (not the `master` branch). +* Submit a pull request and include a reference to the initial issue with the discussion. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..a44bd728 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,28 @@ + + +* **Browser**: +* **Operating System**: +* **Screenshot**: + +* **HTML of the section where the issue occurs**: + + + +````html + +```` diff --git a/README.md b/README.md index f5e0f4ff..cc1c379b 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,15 @@ See the [help docs](http://userstyles.org/help/stylish_chrome) or [ask in userst ## Contributing -The source is hosted on [GitHub](https://github.com/schomery/stylus) and pull requests are welcome. +The source is hosted on [GitHub](https://github.com/openstyles/stylus) and pull requests are welcome. -You can help us translate the extension on [Transifex](https://www.transifex.com/github-7/Stylus). When `messages.json` file is ready to be merged, please open a new bug report in [stylus/issues](https://github.com/schomery/stylus/issues). +You can help us translate the extension on [Transifex](https://www.transifex.com/github-7/Stylus). When `messages.json` file is ready to be merged, please open a new bug report in [stylus/issues](https://github.com/openstyles/stylus/issues). + +See our [contributing](./.github/CONTRIBUTING.md) page for more details. ## License -For copyright status of the "codemirror" directory, see codemirror/LICENSE. Everything else is: +For copyright status of the "codemirror" directory, see [codemirror/LICENSE](https://github.com/openstyles/stylus/blob/master/src/vendor/codemirror/LICENSE). Everything else is: Copyright (C) 2005-2014 Jason Barnabe diff --git a/background.js b/background/background.js similarity index 93% rename from background.js rename to background/background.js index a2ae0627..13d12e2f 100644 --- a/background.js +++ b/background/background.js @@ -26,14 +26,14 @@ chrome.webNavigation.onHistoryStateUpdated.addListener(data => chrome.webNavigation.onReferenceFragmentUpdated.addListener(data => webNavigationListener('styleReplaceAll', data)); -chrome.tabs.onAttached.addListener((tabId, data) => { +chrome.tabs.onAttached.addListener(tabId => { // When an edit page gets attached or detached, remember its state // so we can do the same to the next one to open. chrome.tabs.get(tabId, tab => { if (tab.url.startsWith(URLS.ownOrigin + 'edit.html')) { chrome.windows.get(tab.windowId, {populate: true}, win => { // If there's only one tab in this window, it's been dragged to new window - prefs.set('openEditInWindow', win.tabs.length == 1); + prefs.set('openEditInWindow', win.tabs.length === 1); }); } }); @@ -59,13 +59,13 @@ updateIcon({id: undefined}, {}); const manifest = chrome.runtime.getManifest(); // Open FAQs page once after installation to guide new users. // Do not display it in development mode. - if (reason == 'install' && manifest.update_url) { + if (reason === 'install' && manifest.update_url) { setTimeout(openURL, 100, { url: 'http://add0n.com/stylus.html' }); } // reset L10N cache on update - if (reason == 'update') { + if (reason === 'update') { localStorage.L10N = JSON.stringify({ browserUIlanguage: chrome.i18n.getUILanguage(), }); @@ -98,7 +98,7 @@ updateIcon({id: undefined}, {}); // browser commands browserCommands = { openManage() { - openURL({url: '/manage.html'}); + openURL({url: 'manage.html'}); }, styleDisableAll(info) { prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll')); @@ -138,7 +138,7 @@ contextMenus = Object.assign({ const item = Object.assign({id}, contextMenus[id]); const prefValue = prefs.readOnlyValues[id]; item.title = chrome.i18n.getMessage(item.title); - if (!item.type && typeof prefValue == 'boolean') { + if (!item.type && typeof prefValue === 'boolean') { item.type = 'checkbox'; item.checked = prefValue; } @@ -151,7 +151,7 @@ contextMenus = Object.assign({ }; createContextMenus(); prefs.subscribe((id, checked) => { - if (id == 'editor.contextDelete') { + if (id === 'editor.contextDelete') { if (checked) { createContextMenus([id]); } else { @@ -160,7 +160,7 @@ contextMenus = Object.assign({ } else { chrome.contextMenus.update(id, {checked}, ignoreChromeError); } - }, Object.keys(contextMenus).filter(key => typeof prefs.readOnlyValues[key] == 'boolean')); + }, Object.keys(contextMenus).filter(key => typeof prefs.readOnlyValues[key] === 'boolean')); } // ************************************************************************* @@ -176,7 +176,7 @@ contextMenus = Object.assign({ .replace(/\*/g, '.*?'), flags); for (const cs of contentScripts) { cs.matches = cs.matches.map(m => ( - m == ALL_URLS ? m : wildcardAsRegExp(m) + m === ALL_URLS ? m : wildcardAsRegExp(m) )); } @@ -191,8 +191,8 @@ contextMenus = Object.assign({ const pingCS = (cs, {id, url}) => { cs.matches.some(match => { - if ((match == ALL_URLS || url.match(match)) - && (!url.startsWith('chrome') || url == NTP)) { + if ((match === ALL_URLS || url.match(match)) + && (!url.startsWith('chrome') || url === NTP)) { chrome.tabs.sendMessage(id, PING, pong => { if (!pong) { injectCS(cs, id); @@ -229,7 +229,7 @@ function webNavigationListener(method, {url, tabId, frameId}) { }); } // main page frame id is 0 - if (frameId == 0) { + if (frameId === 0) { updateIcon({id: tabId, url}, styles); } }); @@ -258,7 +258,7 @@ function updateIcon(tab, styles) { } } const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll'); - const postfix = disableAll ? 'x' : numStyles == 0 ? 'w' : ''; + const postfix = disableAll ? 'x' : numStyles === 0 ? 'w' : ''; const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal'); const text = prefs.get('show-badge') && numStyles ? String(numStyles) : ''; const iconset = ['', 'light/'][prefs.get('iconset')] || ''; diff --git a/storage.js b/background/storage.js similarity index 91% rename from storage.js rename to background/storage.js index 0791d6b3..7531f0fb 100644 --- a/storage.js +++ b/background/storage.js @@ -63,7 +63,7 @@ function dbExec(method, data) { reject(event); }, onupgradeneeded(event) { - if (event.oldVersion == 0) { + if (event.oldVersion === 0) { event.target.result.createObjectStore('styles', { keyPath: 'id', autoIncrement: true, @@ -111,15 +111,17 @@ function filterStyles({ asHash = null, strictRegexp = true, // used by the popup to detect bad regexps } = {}) { - enabled = enabled === null || typeof enabled == 'boolean' ? enabled : - typeof enabled == 'string' ? enabled == 'true' : null; + enabled = enabled === null || typeof enabled === 'boolean' ? enabled : + typeof enabled === 'string' ? enabled === 'true' : null; id = id === null ? null : Number(id); - if (enabled === null - && url === null - && id === null - && matchUrl === null - && asHash != true) { + if ( + enabled === null && + url === null && + id === null && + matchUrl === null && + asHash !== true + ) { return cachedStyles.list; } const blankHash = asHash && { @@ -189,10 +191,11 @@ function filterStylesInternal({ const needSections = asHash || matchUrl !== null; - for (let i = 0, style; (style = styles[i]); i++) { - if ((enabled === null || style.enabled == enabled) - && (url === null || style.url == url) - && (id === null || style.id == id)) { + let style; + for (let i = 0; (style = styles[i]); i++) { + if ((enabled === null || style.enabled === enabled) + && (url === null || style.url === url) + && (id === null || style.id === id)) { const sections = needSections && getApplicableSections({style, matchUrl, strictRegexp, stopOnFirst: !asHash}); if (asHash) { @@ -230,17 +233,18 @@ function saveStyle(style) { if (!style.name) { delete style.name; } - let existed, codeIsUpdated; - if (reason == 'update' || reason == 'update-digest') { + let existed; + let codeIsUpdated; + if (reason === 'update' || reason === 'update-digest') { return calcStyleDigest(style).then(digest => { style.originalDigest = digest; return decide(); }); } - if (reason == 'import') { + if (reason === 'import') { style.originalDigest = style.originalDigest || style.styleDigest; // TODO: remove in the future delete style.styleDigest; // TODO: remove in the future - if (typeof style.originalDigest != 'string' || style.originalDigest.length != 40) { + if (typeof style.originalDigest !== 'string' || style.originalDigest.length !== 40) { delete style.originalDigest; } } @@ -253,7 +257,7 @@ function saveStyle(style) { return dbExec('get', id).then((event, store) => { const oldStyle = event.target.result; existed = Boolean(oldStyle); - if (reason == 'update-digest' && oldStyle.originalDigest == style.originalDigest) { + if (reason === 'update-digest' && oldStyle.originalDigest === style.originalDigest) { return style; } codeIsUpdated = !existed || 'sections' in style && !styleSectionsEqual(style, oldStyle); @@ -287,7 +291,7 @@ function saveStyle(style) { } function done(event) { - if (reason == 'update-digest') { + if (reason === 'update-digest') { return style; } style.id = style.id || event.target.result; @@ -365,14 +369,14 @@ function getApplicableSections({style, matchUrl, strictRegexp = true, stopOnFirs function arraySomeMatches(array, matchUrl, strictRegexp) { for (const regexp of array) { for (let pass = 1; pass <= (strictRegexp ? 1 : 2); pass++) { - const cacheKey = pass == 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp; + const cacheKey = pass === 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp; let rx = cachedStyles.regexps.get(cacheKey); - if (rx == false) { + if (rx === false) { // invalid regexp break; } if (!rx) { - const anchored = pass == 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$'; + const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$'; rx = tryRegExp(anchored); cachedStyles.regexps.set(cacheKey, rx || false); if (!rx) { @@ -413,7 +417,7 @@ function styleSectionsEqual({sections: a}, {sections: b}) { if (!a || !b) { return undefined; } - if (a.length != b.length) { + if (a.length !== b.length) { return false; } const checkedInB = []; @@ -430,16 +434,16 @@ function styleSectionsEqual({sections: a}, {sections: b}) { return false; } } - return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a == b); + return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b); } function equalOrEmpty(a, b, telltale, comparator) { - const typeA = a && typeof a[telltale] == 'function'; - const typeB = b && typeof b[telltale] == 'function'; + const typeA = a && typeof a[telltale] === 'function'; + const typeB = b && typeof b[telltale] === 'function'; return ( (a === null || a === undefined || (typeA && !a.length)) && (b === null || b === undefined || (typeB && !b.length)) - ) || typeA && typeB && a.length == b.length && comparator(a, b); + ) || typeA && typeB && a.length === b.length && comparator(a, b); } function arrayMirrors(array1, array2) { @@ -523,12 +527,12 @@ function cleanupCachedFilters({force = false} = {}) { function getDomains(url) { - if (url.indexOf('file:') == 0) { + if (url.indexOf('file:') === 0) { return []; } let d = /.*?:\/*([^/:]+)/.exec(url)[1]; const domains = [d]; - while (d.indexOf('.') != -1) { + while (d.indexOf('.') !== -1) { d = d.substring(d.indexOf('.') + 1); domains.push(d); } diff --git a/update.js b/background/update.js similarity index 95% rename from update.js rename to background/update.js index a536098c..04f368f5 100644 --- a/update.js +++ b/background/update.js @@ -68,17 +68,17 @@ var updater = { }); function maybeFetchMd5(digest) { - if (!ignoreDigest && style.originalDigest && style.originalDigest != digest) { + if (!ignoreDigest && style.originalDigest && style.originalDigest !== digest) { return Promise.reject(updater.EDITED); } return download(style.md5Url); } function maybeFetchCode(md5) { - if (!md5 || md5.length != 32) { + if (!md5 || md5.length !== 32) { return Promise.reject(updater.ERROR_MD5); } - if (md5 == style.originalMd5 && style.originalDigest && !ignoreDigest) { + if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) { return Promise.reject(updater.SAME_MD5); } return download(style.updateUrl); @@ -109,8 +109,8 @@ var updater = { return json && json.sections && json.sections.length - && typeof json.sections.every == 'function' - && typeof json.sections[0].code == 'string'; + && typeof json.sections.every === 'function' + && typeof json.sections[0].code === 'string'; } }, diff --git a/apply.js b/content/apply.js similarity index 96% rename from apply.js rename to content/apply.js index 6e310f05..0cb626e0 100644 --- a/apply.js +++ b/content/apply.js @@ -28,7 +28,7 @@ function requestStyles(options, callback = applyStyles) { // dynamic about: and javascript: iframes don't have an URL yet // so we'll try the parent frame which is guaranteed to have a real URL try { - if (window != parent) { + if (window !== parent) { matchUrl = parent.location.href; } } catch (e) {} @@ -49,7 +49,7 @@ function requestStyles(options, callback = applyStyles) { function applyOnMessage(request, sender, sendResponse) { - if (request.styles == 'DIY') { + if (request.styles === 'DIY') { // Do-It-Yourself tells our built-in pages to fetch the styles directly // which is faster because IPC messaging JSON-ifies everything internally requestStyles({}, styles => { @@ -114,7 +114,7 @@ function doDisableAll(disable = disableAll) { disableAll = disable; Array.prototype.forEach.call(document.styleSheets, stylesheet => { if (stylesheet.ownerNode.matches(`STYLE.stylus[id^="${ID_PREFIX}"]`) - && stylesheet.disabled != disable) { + && stylesheet.disabled !== disable) { stylesheet.disabled = disable; } }); @@ -122,14 +122,14 @@ function doDisableAll(disable = disableAll) { function doExposeIframes(state = exposeIframes) { - if (state === exposeIframes || window == parent) { + if (state === exposeIframes || window === parent) { return; } exposeIframes = state; const attr = document.documentElement.getAttribute('stylus-iframe'); - if (state && attr != '') { + if (state && attr !== '') { document.documentElement.setAttribute('stylus-iframe', ''); - } else if (!state && attr == '') { + } else if (!state && attr === '') { document.documentElement.removeAttribute('stylus-iframe'); } } @@ -193,7 +193,7 @@ function applyStyles(styles) { } if (document.head && document.head.firstChild - && document.head.firstChild.id == 'xml-viewer-style') { + && document.head.firstChild.id === 'xml-viewer-style') { // when site response is application/xml Chrome displays our style elements // under document.documentElement as plain text so we need to move them into HEAD // which is already autogenerated at this moment @@ -293,7 +293,7 @@ function initDocRewriteObserver() { for (let m = mutations.length; --m >= 0;) { const added = mutations[m].addedNodes; for (let n = added.length; --n >= 0;) { - if (added[n].localName == 'html') { + if (added[n].localName === 'html') { reinjectStyles(); return; } @@ -303,7 +303,7 @@ function initDocRewriteObserver() { docRewriteObserver.observe(document, {childList: true}); // detect dynamic iframes rewritten after creation by the embedder i.e. externally setTimeout(() => { - if (document.documentElement != ROOT) { + if (document.documentElement !== ROOT) { reinjectStyles(); } }); diff --git a/install.js b/content/install.js similarity index 90% rename from install.js rename to content/install.js index 7ce385e3..0445af7f 100644 --- a/install.js +++ b/content/install.js @@ -1,360 +1,360 @@ -'use strict'; - -const CHROMIUM = /Chromium/.test(navigator.userAgent); // non-Windows Chromium -const FIREFOX = /Firefox/.test(navigator.userAgent); -const VIVALDI = /Vivaldi/.test(navigator.userAgent); -const OPERA = /OPR/.test(navigator.userAgent); - -document.addEventListener('stylishUpdate', onUpdateClicked); -document.addEventListener('stylishUpdateChrome', onUpdateClicked); -document.addEventListener('stylishUpdateOpera', onUpdateClicked); - -document.addEventListener('stylishInstall', onInstallClicked); -document.addEventListener('stylishInstallChrome', onInstallClicked); -document.addEventListener('stylishInstallOpera', onInstallClicked); - -chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { - // orphaned content script check - if (msg.method == 'ping') { - sendResponse(true); - } -}); - -// TODO: remove the following statement when USO is fixed -document.documentElement.appendChild(document.createElement('script')).text = '(' + - function() { - let settings; - document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) { - document.removeEventListener('stylusFixBuggyUSOsettings', _); - settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search); - }); - const originalResponseJson = Response.prototype.json; - Response.prototype.json = function(...args) { - return originalResponseJson.call(this, ...args).then(json => { - Response.prototype.json = originalResponseJson; - if (!settings || typeof ((json || {}).style_settings || {}).every != 'function') { - return json; - } - const images = new Map(); - for (const jsonSetting of json.style_settings) { - let value = settings.get('ik-' + jsonSetting.install_key); - if (!value - || !jsonSetting.style_setting_options - || !jsonSetting.style_setting_options[0]) { - continue; - } - if (value.startsWith('ik-')) { - value = value.replace(/^ik-/, ''); - const defaultItem = jsonSetting.style_setting_options.find(item => item.default); - if (!defaultItem || defaultItem.install_key != value) { - if (defaultItem) { - defaultItem.default = false; - } - jsonSetting.style_setting_options.some(item => { - if (item.install_key == value) { - item.default = true; - return true; - } - }); - } - } else if (jsonSetting.setting_type == 'image') { - jsonSetting.style_setting_options.some(item => { - if (item.default) { - item.default = false; - return true; - } - }); - images.set(jsonSetting.install_key, value); - } else { - const item = jsonSetting.style_setting_options[0]; - if (item.value !== value && item.install_key == 'placeholder') { - item.value = value; - } - } - } - if (images.size) { - new MutationObserver((_, observer) => { - if (!document.getElementById('style-settings')) { - return; - } - observer.disconnect(); - for (const [name, url] of images.entries()) { - const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`); - const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url')); - if (elUrl) { - elUrl.value = url; - } - } - }).observe(document, {childList: true, subtree: true}); - } - return json; - }); - }; - } + ')()'; - -// TODO: remove the following statement when USO pagination is fixed -if (location.search.includes('category=')) { - document.addEventListener('DOMContentLoaded', function _() { - document.removeEventListener('DOMContentLoaded', _); - new MutationObserver((_, observer) => { - if (!document.getElementById('pagination')) { - return; - } - observer.disconnect(); - const category = '&' + location.search.match(/category=[^&]+/)[0]; - const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])'); - for (let i = 0; i < links.length; i++) { - links[i].href += category; - } - }).observe(document, {childList: true, subtree: true}); - }); -} - -new MutationObserver((mutations, observer) => { - if (document.body) { - observer.disconnect(); - // TODO: remove the following statement when USO pagination title is fixed - document.title = document.title.replace(/^\d+&category=/, ''); - chrome.runtime.sendMessage({ - method: 'getStyles', - url: getMeta('stylish-id-url') || location.href - }, checkUpdatability); - } -}).observe(document.documentElement, {childList: true}); - -/* since we are using "stylish-code-chrome" meta key on all browsers and - US.o does not provide "advanced settings" on this url if browser is not Chrome, - we need to fix this URL using "stylish-update-url" meta key -*/ -function getStyleURL() { - const url = getMeta('stylish-code-chrome'); - // TODO: remove when USO is fixed - const directUrl = getMeta('stylish-update-url'); - if (directUrl.includes('?') && !url.includes('?')) { - /* get custom settings from the update url */ - return Object.assign(new URL(url), { - search: (new URL(directUrl)).search - }).href; - } - return url; -} - -function checkUpdatability([installedStyle]) { - // TODO: remove the following statement when USO is fixed - document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', { - detail: installedStyle && installedStyle.updateUrl, - })); - if (!installedStyle) { - sendEvent('styleCanBeInstalledChrome'); - return; - } - const md5Url = getMeta('stylish-md5-url'); - if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) { - getResource(md5Url).then(md5 => { - reportUpdatable(md5 != installedStyle.originalMd5); - }); - } else { - getResource(getStyleURL()).then(code => { - reportUpdatable(code === null || - !styleSectionsEqual(JSON.parse(code), installedStyle)); - }); - } - - function reportUpdatable(isUpdatable) { - sendEvent( - isUpdatable - ? 'styleCanBeUpdatedChrome' - : 'styleAlreadyInstalledChrome', - { - updateUrl: installedStyle.updateUrl - } - ); - } -} - - -function sendEvent(type, detail = null) { - if (FIREFOX) { - type = type.replace('Chrome', ''); - } else if (OPERA || VIVALDI) { - type = type.replace('Chrome', 'Opera'); - } - detail = {detail}; - if (typeof cloneInto != 'undefined') { - // Firefox requires explicit cloning, however USO can't process our messages anyway - // because USO tries to use a global "event" variable deprecated in Firefox - detail = cloneInto(detail, document); // eslint-disable-line no-undef - } - onDOMready().then(() => { - document.dispatchEvent(new CustomEvent(type, detail)); - }); -} - - -function onInstallClicked() { - if (!orphanCheck || !orphanCheck()) { - return; - } - getResource(getMeta('stylish-description')) - .then(name => saveStyleCode('styleInstall', name)) - .then(() => getResource(getMeta('stylish-install-ping-url-chrome'))); -} - - -function onUpdateClicked() { - if (!orphanCheck || !orphanCheck()) { - return; - } - chrome.runtime.sendMessage({ - method: 'getStyles', - url: getMeta('stylish-id-url') || location.href, - }, ([style]) => { - saveStyleCode('styleUpdate', style.name, {id: style.id}); - }); -} - - -function saveStyleCode(message, name, addProps) { - return new Promise(resolve => { - if (!confirm(chrome.i18n.getMessage(message, [name]))) { - return; - } - enableUpdateButton(false); - getResource(getStyleURL()).then(code => { - chrome.runtime.sendMessage( - Object.assign(JSON.parse(code), addProps, { - method: 'saveStyle', - reason: 'update', - }), - style => { - if (message == 'styleUpdate' && style.updateUrl.includes('?')) { - enableUpdateButton(true); - } else { - sendEvent('styleInstalledChrome'); - } - } - ); - resolve(); - }); - }); - - function enableUpdateButton(state) { - const button = document.getElementById('update_style_button'); - if (button) { - button.style.cssText = state ? '' : - 'pointer-events: none !important; opacity: .25 !important;'; - } - } -} - - -function getMeta(name) { - const e = document.querySelector(`link[rel="${name}"]`); - return e ? e.getAttribute('href') : null; -} - - -function getResource(url) { - return new Promise(resolve => { - if (url.startsWith('#')) { - resolve(document.getElementById(url.slice(1)).textContent); - } else { - chrome.runtime.sendMessage({method: 'download', url}, resolve); - } - }); -} - - -function styleSectionsEqual({sections: a}, {sections: b}) { - if (!a || !b) { - return undefined; - } - if (a.length != b.length) { - return false; - } - const checkedInB = []; - return a.every(sectionA => b.some(sectionB => { - if (!checkedInB.includes(sectionB) && propertiesEqual(sectionA, sectionB)) { - checkedInB.push(sectionB); - return true; - } - })); - - function propertiesEqual(secA, secB) { - for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) { - if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) { - return false; - } - } - return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a == b); - } - - function equalOrEmpty(a, b, telltale, comparator) { - const typeA = a && typeof a[telltale] == 'function'; - const typeB = b && typeof b[telltale] == 'function'; - return ( - (a === null || a === undefined || (typeA && !a.length)) && - (b === null || b === undefined || (typeB && !b.length)) - ) || typeA && typeB && a.length == b.length && comparator(a, b); - } - - function arrayMirrors(array1, array2) { - for (const el of array1) { - if (array2.indexOf(el) < 0) { - return false; - } - } - for (const el of array2) { - if (array1.indexOf(el) < 0) { - return false; - } - } - return true; - } -} - - -function onDOMready() { - if (document.readyState != 'loading') { - return Promise.resolve(); - } - return new Promise(resolve => { - document.addEventListener('DOMContentLoaded', function _() { - document.removeEventListener('DOMContentLoaded', _); - resolve(); - }); - }); -} - - -function orphanCheck() { - const port = chrome.runtime.connect(); - if (port) { - port.disconnect(); - return true; - } - // we're orphaned due to an extension update - // we can detach event listeners - document.removeEventListener('stylishUpdate', onUpdateClicked); - document.removeEventListener('stylishUpdateChrome', onUpdateClicked); - document.removeEventListener('stylishUpdateOpera', onUpdateClicked); - - document.removeEventListener('stylishInstall', onInstallClicked); - document.removeEventListener('stylishInstallChrome', onInstallClicked); - document.removeEventListener('stylishInstallOpera', onInstallClicked); - - // we can't detach chrome.runtime.onMessage because it's no longer connected internally - // we can destroy global functions in this context to free up memory - [ - 'checkUpdatability', - 'getMeta', - 'getResource', - 'onDOMready', - 'onInstallClicked', - 'onUpdateClicked', - 'orphanCheck', - 'saveStyleCode', - 'sendEvent', - 'styleSectionsEqual', - ].forEach(fn => (window[fn] = null)); -} +'use strict'; + +const CHROMIUM = /Chromium/.test(navigator.userAgent); // non-Windows Chromium +const FIREFOX = /Firefox/.test(navigator.userAgent); +const VIVALDI = /Vivaldi/.test(navigator.userAgent); +const OPERA = /OPR/.test(navigator.userAgent); + +document.addEventListener('stylishUpdate', onUpdateClicked); +document.addEventListener('stylishUpdateChrome', onUpdateClicked); +document.addEventListener('stylishUpdateOpera', onUpdateClicked); + +document.addEventListener('stylishInstall', onInstallClicked); +document.addEventListener('stylishInstallChrome', onInstallClicked); +document.addEventListener('stylishInstallOpera', onInstallClicked); + +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + // orphaned content script check + if (msg.method === 'ping') { + sendResponse(true); + } +}); + +// TODO: remove the following statement when USO is fixed +document.documentElement.appendChild(document.createElement('script')).text = '(' + + function () { + let settings; + document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) { + document.removeEventListener('stylusFixBuggyUSOsettings', _); + settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search); + }); + const originalResponseJson = Response.prototype.json; + Response.prototype.json = function (...args) { + return originalResponseJson.call(this, ...args).then(json => { + Response.prototype.json = originalResponseJson; + if (!settings || typeof ((json || {}).style_settings || {}).every !== 'function') { + return json; + } + const images = new Map(); + for (const jsonSetting of json.style_settings) { + let value = settings.get('ik-' + jsonSetting.install_key); + if (!value + || !jsonSetting.style_setting_options + || !jsonSetting.style_setting_options[0]) { + continue; + } + if (value.startsWith('ik-')) { + value = value.replace(/^ik-/, ''); + const defaultItem = jsonSetting.style_setting_options.find(item => item.default); + if (!defaultItem || defaultItem.install_key !== value) { + if (defaultItem) { + defaultItem.default = false; + } + jsonSetting.style_setting_options.some(item => { + if (item.install_key === value) { + item.default = true; + return true; + } + }); + } + } else if (jsonSetting.setting_type === 'image') { + jsonSetting.style_setting_options.some(item => { + if (item.default) { + item.default = false; + return true; + } + }); + images.set(jsonSetting.install_key, value); + } else { + const item = jsonSetting.style_setting_options[0]; + if (item.value !== value && item.install_key === 'placeholder') { + item.value = value; + } + } + } + if (images.size) { + new MutationObserver((_, observer) => { + if (!document.getElementById('style-settings')) { + return; + } + observer.disconnect(); + for (const [name, url] of images.entries()) { + const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`); + const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url')); + if (elUrl) { + elUrl.value = url; + } + } + }).observe(document, {childList: true, subtree: true}); + } + return json; + }); + }; + } + ')()'; + +// TODO: remove the following statement when USO pagination is fixed +if (location.search.includes('category=')) { + document.addEventListener('DOMContentLoaded', function _() { + document.removeEventListener('DOMContentLoaded', _); + new MutationObserver((_, observer) => { + if (!document.getElementById('pagination')) { + return; + } + observer.disconnect(); + const category = '&' + location.search.match(/category=[^&]+/)[0]; + const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])'); + for (let i = 0; i < links.length; i++) { + links[i].href += category; + } + }).observe(document, {childList: true, subtree: true}); + }); +} + +new MutationObserver((mutations, observer) => { + if (document.body) { + observer.disconnect(); + // TODO: remove the following statement when USO pagination title is fixed + document.title = document.title.replace(/^\d+&category=/, ''); + chrome.runtime.sendMessage({ + method: 'getStyles', + url: getMeta('stylish-id-url') || location.href + }, checkUpdatability); + } +}).observe(document.documentElement, {childList: true}); + +/* since we are using "stylish-code-chrome" meta key on all browsers and + US.o does not provide "advanced settings" on this url if browser is not Chrome, + we need to fix this URL using "stylish-update-url" meta key +*/ +function getStyleURL() { + const url = getMeta('stylish-code-chrome'); + // TODO: remove when USO is fixed + const directUrl = getMeta('stylish-update-url'); + if (directUrl.includes('?') && !url.includes('?')) { + /* get custom settings from the update url */ + return Object.assign(new URL(url), { + search: (new URL(directUrl)).search + }).href; + } + return url; +} + +function checkUpdatability([installedStyle]) { + // TODO: remove the following statement when USO is fixed + document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', { + detail: installedStyle && installedStyle.updateUrl, + })); + if (!installedStyle) { + sendEvent('styleCanBeInstalledChrome'); + return; + } + const md5Url = getMeta('stylish-md5-url'); + if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) { + getResource(md5Url).then(md5 => { + reportUpdatable(md5 !== installedStyle.originalMd5); + }); + } else { + getResource(getStyleURL()).then(code => { + reportUpdatable(code === null || + !styleSectionsEqual(JSON.parse(code), installedStyle)); + }); + } + + function reportUpdatable(isUpdatable) { + sendEvent( + isUpdatable + ? 'styleCanBeUpdatedChrome' + : 'styleAlreadyInstalledChrome', + { + updateUrl: installedStyle.updateUrl + } + ); + } +} + + +function sendEvent(type, detail = null) { + if (FIREFOX) { + type = type.replace('Chrome', ''); + } else if (OPERA || VIVALDI) { + type = type.replace('Chrome', 'Opera'); + } + detail = {detail}; + if (typeof cloneInto !== 'undefined') { + // Firefox requires explicit cloning, however USO can't process our messages anyway + // because USO tries to use a global "event" variable deprecated in Firefox + detail = cloneInto(detail, document); // eslint-disable-line no-undef + } + onDOMready().then(() => { + document.dispatchEvent(new CustomEvent(type, detail)); + }); +} + + +function onInstallClicked() { + if (!orphanCheck || !orphanCheck()) { + return; + } + getResource(getMeta('stylish-description')) + .then(name => saveStyleCode('styleInstall', name)) + .then(() => getResource(getMeta('stylish-install-ping-url-chrome'))); +} + + +function onUpdateClicked() { + if (!orphanCheck || !orphanCheck()) { + return; + } + chrome.runtime.sendMessage({ + method: 'getStyles', + url: getMeta('stylish-id-url') || location.href, + }, ([style]) => { + saveStyleCode('styleUpdate', style.name, {id: style.id}); + }); +} + + +function saveStyleCode(message, name, addProps) { + return new Promise(resolve => { + if (!confirm(chrome.i18n.getMessage(message, [name]))) { + return; + } + enableUpdateButton(false); + getResource(getStyleURL()).then(code => { + chrome.runtime.sendMessage( + Object.assign(JSON.parse(code), addProps, { + method: 'saveStyle', + reason: 'update', + }), + style => { + if (message === 'styleUpdate' && style.updateUrl.includes('?')) { + enableUpdateButton(true); + } else { + sendEvent('styleInstalledChrome'); + } + } + ); + resolve(); + }); + }); + + function enableUpdateButton(state) { + const button = document.getElementById('update_style_button'); + if (button) { + button.style.cssText = state ? '' : + 'pointer-events: none !important; opacity: .25 !important;'; + } + } +} + + +function getMeta(name) { + const e = document.querySelector(`link[rel="${name}"]`); + return e ? e.getAttribute('href') : null; +} + + +function getResource(url) { + return new Promise(resolve => { + if (url.startsWith('#')) { + resolve(document.getElementById(url.slice(1)).textContent); + } else { + chrome.runtime.sendMessage({method: 'download', url}, resolve); + } + }); +} + + +function styleSectionsEqual({sections: a}, {sections: b}) { + if (!a || !b) { + return undefined; + } + if (a.length !== b.length) { + return false; + } + const checkedInB = []; + return a.every(sectionA => b.some(sectionB => { + if (!checkedInB.includes(sectionB) && propertiesEqual(sectionA, sectionB)) { + checkedInB.push(sectionB); + return true; + } + })); + + function propertiesEqual(secA, secB) { + for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) { + if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) { + return false; + } + } + return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b); + } + + function equalOrEmpty(a, b, telltale, comparator) { + const typeA = a && typeof a[telltale] === 'function'; + const typeB = b && typeof b[telltale] === 'function'; + return ( + (a === null || a === undefined || (typeA && !a.length)) && + (b === null || b === undefined || (typeB && !b.length)) + ) || typeA && typeB && a.length === b.length && comparator(a, b); + } + + function arrayMirrors(array1, array2) { + for (const el of array1) { + if (array2.indexOf(el) < 0) { + return false; + } + } + for (const el of array2) { + if (array1.indexOf(el) < 0) { + return false; + } + } + return true; + } +} + + +function onDOMready() { + if (document.readyState !== 'loading') { + return Promise.resolve(); + } + return new Promise(resolve => { + document.addEventListener('DOMContentLoaded', function _() { + document.removeEventListener('DOMContentLoaded', _); + resolve(); + }); + }); +} + + +function orphanCheck() { + const port = chrome.runtime.connect(); + if (port) { + port.disconnect(); + return true; + } + // we're orphaned due to an extension update + // we can detach event listeners + document.removeEventListener('stylishUpdate', onUpdateClicked); + document.removeEventListener('stylishUpdateChrome', onUpdateClicked); + document.removeEventListener('stylishUpdateOpera', onUpdateClicked); + + document.removeEventListener('stylishInstall', onInstallClicked); + document.removeEventListener('stylishInstallChrome', onInstallClicked); + document.removeEventListener('stylishInstallOpera', onInstallClicked); + + // we can't detach chrome.runtime.onMessage because it's no longer connected internally + // we can destroy global functions in this context to free up memory + [ + 'checkUpdatability', + 'getMeta', + 'getResource', + 'onDOMready', + 'onInstallClicked', + 'onUpdateClicked', + 'orphanCheck', + 'saveStyleCode', + 'sendEvent', + 'styleSectionsEqual', + ].forEach(fn => (window[fn] = null)); +} diff --git a/edit.html b/edit.html index 2ec63cc2..5df54d54 100644 --- a/edit.html +++ b/edit.html @@ -1,792 +1,213 @@ - - + + - - - - - - + + + + + + + - - - + + + - - - - - - - - - - + + + + + + + + + + - - - - - + + + + + - + - - - - + + + + - - - + + + - - - + + + + - - - - - - - - - - - - - - - - - - -
-

-
-
-
-
-
- - - - - - - - - - - - - - + diff --git a/edit.js b/edit.js deleted file mode 100644 index 48ee1bad..00000000 --- a/edit.js +++ /dev/null @@ -1,2037 +0,0 @@ -/* eslint no-tabs: 0, no-var: 0, indent-legacy: [2, tab, {VariableDeclarator: 0, SwitchCase: 1}], quotes: 0 */ -/* global CodeMirror */ -"use strict"; - -var styleId = null; -var dirty = {}; // only the actually dirty items here -var editors = []; // array of all CodeMirror instances -var saveSizeOnClose; -var useHistoryBack; // use browser history back when "back to manage" is clicked - -// direct & reverse mapping of @-moz-document keywords and internal property names -var propertyToCss = {urls: "url", urlPrefixes: "url-prefix", domains: "domain", regexps: "regexp"}; -var CssToProperty = {"url": "urls", "url-prefix": "urlPrefixes", "domain": "domains", "regexp": "regexps"}; - -// if background page hasn't been loaded yet, increase the chances it has before DOMContentLoaded -onBackgroundReady(); - -// make querySelectorAll enumeration code readable -["forEach", "some", "indexOf", "map"].forEach(function(method) { - NodeList.prototype[method]= Array.prototype[method]; -}); - -// Chrome pre-34 -Element.prototype.matches = Element.prototype.matches || Element.prototype.webkitMatchesSelector; - -// Chrome pre-41 polyfill -Element.prototype.closest = Element.prototype.closest || function(selector) { - for (var e = this; e && !e.matches(selector); e = e.parentElement) {} - return e; -}; - -Array.prototype.rotate = function(amount) { // negative amount == rotate left - var r = this.slice(-amount, this.length); - Array.prototype.push.apply(r, this.slice(0, this.length - r.length)); - return r; -}; - -Object.defineProperty(Array.prototype, "last", {get: function() { return this[this.length - 1]; }}); - -// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs() -new MutationObserver((mutations, observer) => { - const themeElement = document.getElementById("cm-theme"); - if (themeElement) { - themeElement.href = prefs.get("editor.theme") == "default" ? "" - : "codemirror/theme/" + prefs.get("editor.theme") + ".css"; - observer.disconnect(); - } -}).observe(document, {subtree: true, childList: true}); - -getCodeMirrorThemes(); - -// reroute handling to nearest editor when keypress resolves to one of these commands -var hotkeyRerouter = { - commands: { - save: true, jumpToLine: true, nextEditor: true, prevEditor: true, - find: true, findNext: true, findPrev: true, replace: true, replaceAll: true, - toggleStyle: true, - }, - setState: function(enable) { - setTimeout(function() { - document[(enable ? "add" : "remove") + "EventListener"]("keydown", hotkeyRerouter.eventHandler); - }, 0); - }, - eventHandler: function(event) { - var keyName = CodeMirror.keyName(event); - if ("handled" == CodeMirror.lookupKey(keyName, CodeMirror.getOption("keyMap"), handleCommand) - || "handled" == CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, handleCommand)) { - event.preventDefault(); - event.stopPropagation(); - } - function handleCommand(command) { - if (hotkeyRerouter.commands[command] === true) { - CodeMirror.commands[command](getEditorInSight(event.target)); - return true; - } - } - } -}; - -function onChange(event) { - var node = event.target; - if ("savedValue" in node) { - var currentValue = "checkbox" === node.type ? node.checked : node.value; - setCleanItem(node, node.savedValue === currentValue); - } else { - // the manually added section's applies-to is dirty only when the value is non-empty - setCleanItem(node, node.localName != "input" || !node.value.trim()); - delete node.savedValue; // only valid when actually saved - } - updateTitle(); -} - -// Set .dirty on stylesheet contributors that have changed -function setDirtyClass(node, isDirty) { - node.classList.toggle("dirty", isDirty); -} - -function setCleanItem(node, isClean) { - if (!node.id) { - node.id = Date.now().toString(32).substr(-6); - } - - if (isClean) { - delete dirty[node.id]; - // code sections have .CodeMirror property - if (node.CodeMirror) { - node.savedValue = node.CodeMirror.changeGeneration(); - } else { - node.savedValue = "checkbox" === node.type ? node.checked : node.value; - } - } else { - dirty[node.id] = true; - } - - setDirtyClass(node, !isClean); -} - -function isCleanGlobal() { - var clean = Object.keys(dirty).length == 0; - setDirtyClass(document.body, !clean); - var saveBtn = document.getElementById("save-button") - if (clean){ - //saveBtn.removeAttribute('disabled'); - }else{ - //saveBtn.setAttribute('disabled', true); - } - return clean; -} - -function setCleanGlobal() { - document.querySelectorAll("#header, #sections > div").forEach(setCleanSection); - dirty = {}; // forget the dirty applies-to ids from a deleted section after the style was saved -} - -function setCleanSection(section) { - section.querySelectorAll(".style-contributor").forEach(function(node) { setCleanItem(node, true) }); - - // #header section has no codemirror - var cm = section.CodeMirror; - if (cm) { - section.savedValue = cm.changeGeneration(); - indicateCodeChange(cm); - } -} - -function initCodeMirror() { - var CM = CodeMirror; - var isWindowsOS = navigator.appVersion.indexOf("Windows") > 0; - - // CodeMirror miserably fails on keyMap="" so let's ensure it's not - if (!prefs.get('editor.keyMap')) { - prefs.reset('editor.keyMap'); - } - - // default option values - Object.assign(CM.defaults, { - mode: 'css', - lineNumbers: true, - lineWrapping: true, - foldGutter: true, - gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"], - matchBrackets: true, - highlightSelectionMatches: {showToken: /[#.\-\w]/, annotateScrollbar: true}, - hintOptions: {}, - lint: {getAnnotations: CodeMirror.lint.css, delay: prefs.get("editor.lintDelay")}, - lintReportDelay: prefs.get("editor.lintReportDelay"), - styleActiveLine: true, - theme: "default", - keyMap: prefs.get("editor.keyMap"), - extraKeys: { // independent of current keyMap - "Alt-Enter": "toggleStyle", - "Alt-PageDown": "nextEditor", - "Alt-PageUp": "prevEditor" - } - }, prefs.get("editor.options")); - - // additional commands - CM.commands.jumpToLine = jumpToLine; - CM.commands.nextEditor = function(cm) { nextPrevEditor(cm, 1) }; - CM.commands.prevEditor = function(cm) { nextPrevEditor(cm, -1) }; - CM.commands.save = save; - CM.commands.blockComment = function(cm) { - cm.blockComment(cm.getCursor("from"), cm.getCursor("to"), {fullLines: false}); - }; - CM.commands.toggleStyle = toggleStyle; - - // "basic" keymap only has basic keys by design, so we skip it - - var extraKeysCommands = {}; - Object.keys(CM.defaults.extraKeys).forEach(function(key) { - extraKeysCommands[CM.defaults.extraKeys[key]] = true; - }); - if (!extraKeysCommands.jumpToLine) { - CM.keyMap.sublime["Ctrl-G"] = "jumpToLine"; - CM.keyMap.emacsy["Ctrl-G"] = "jumpToLine"; - CM.keyMap.pcDefault["Ctrl-J"] = "jumpToLine"; - CM.keyMap.macDefault["Cmd-J"] = "jumpToLine"; - } - if (!extraKeysCommands.autocomplete) { - CM.keyMap.pcDefault["Ctrl-Space"] = "autocomplete"; // will be used by "sublime" on PC via fallthrough - CM.keyMap.macDefault["Alt-Space"] = "autocomplete"; // OSX uses Ctrl-Space and Cmd-Space for something else - CM.keyMap.emacsy["Alt-/"] = "autocomplete"; // copied from "emacs" keymap - // "vim" and "emacs" define their own autocomplete hotkeys - } - if (!extraKeysCommands.blockComment) { - CM.keyMap.sublime["Shift-Ctrl-/"] = "blockComment"; - } - - if (isWindowsOS) { - // "pcDefault" keymap on Windows should have F3/Shift-F3 - if (!extraKeysCommands.findNext) { - CM.keyMap.pcDefault["F3"] = "findNext"; - } - if (!extraKeysCommands.findPrev) { - CM.keyMap.pcDefault["Shift-F3"] = "findPrev"; - } - - // try to remap non-interceptable Ctrl-(Shift-)N/T/W hotkeys - ["N", "T", "W"].forEach(function(char) { - [{from: "Ctrl-", to: ["Alt-", "Ctrl-Alt-"]}, - {from: "Shift-Ctrl-", to: ["Ctrl-Alt-", "Shift-Ctrl-Alt-"]} // Note: modifier order in CM is S-C-A - ].forEach(function(remap) { - var oldKey = remap.from + char; - Object.keys(CM.keyMap).forEach(function(keyMapName) { - var keyMap = CM.keyMap[keyMapName]; - var command = keyMap[oldKey]; - if (!command) { - return; - } - remap.to.some(function(newMod) { - var newKey = newMod + char; - if (!(newKey in keyMap)) { - delete keyMap[oldKey]; - keyMap[newKey] = command; - return true; - } - }); - }); - }); - }); - } - - // user option values - CM.getOption = function (o) { - return CodeMirror.defaults[o]; - }; - CM.setOption = function (o, v) { - CodeMirror.defaults[o] = v; - editors.forEach(function(editor) { - editor.setOption(o, v); - }); - }; - - CM.prototype.getSection = function() { - return this.display.wrapper.parentNode; - }; - - // initialize global editor controls - function optionsHtmlFromArray(options) { - return options.map(function(opt) { return ""; }).join(""); - } - var themeControl = document.getElementById("editor.theme"); - const themeList = localStorage.codeMirrorThemes; - if (themeList) { - themeControl.innerHTML = optionsHtmlFromArray(themeList.split(/\s+/)); - } else { - // Chrome is starting up and shows our edit.html, but the background page isn't loaded yet - const theme = prefs.get("editor.theme"); - themeControl.innerHTML = optionsHtmlFromArray([theme == "default" ? t("defaultTheme") : theme]); - getCodeMirrorThemes().then(() => { - const themes = (localStorage.codeMirrorThemes || '').split(/\s+/); - themeControl.innerHTML = optionsHtmlFromArray(themes); - themeControl.selectedIndex = Math.max(0, themes.indexOf(theme)); - }); - } - document.getElementById("editor.keyMap").innerHTML = optionsHtmlFromArray(Object.keys(CM.keyMap).sort()); - document.getElementById("options").addEventListener("change", acmeEventListener, false); - setupLivePrefs(); - - hotkeyRerouter.setState(true); -} - -function acmeEventListener(event) { - var el = event.target; - var option = el.id.replace(/^editor\./, ''); - //console.log("acmeEventListener heard %s on %s", event.type, el.id); - if (!option) { - console.error("acmeEventListener: no 'cm_option' %O", el); - return; - } - var value = el.type == "checkbox" ? el.checked : el.value; - switch (option) { - case "tabSize": - CodeMirror.setOption("indentUnit", Number(value)); - break; - case "theme": - var themeLink = document.getElementById("cm-theme"); - // use non-localized "default" internally - if (!value || value == "default" || value == t("defaultTheme")) { - value = "default"; - if (prefs.get(el.id) != value) { - prefs.set(el.id, value); - } - themeLink.href = ""; - el.selectedIndex = 0; - break; - } - var url = chrome.runtime.getURL("codemirror/theme/" + value + ".css"); - if (themeLink.href == url) { // preloaded in initCodeMirror() - break; - } - // avoid flicker: wait for the second stylesheet to load, then apply the theme - document.head.insertAdjacentHTML("beforeend", - ''); - (function() { - setTimeout(function() { - CodeMirror.setOption(option, value); - themeLink.remove(); - document.getElementById("cm-theme2").id = "cm-theme"; - }, 100); - })(); - return; - case 'autocompleteOnTyping': - editors.forEach(cm => { - const onOff = el.checked ? 'on' : 'off'; - cm[onOff]('change', autocompleteOnTyping); - cm[onOff]('pick', autocompletePicked); - }); - return; - case "matchHighlight": - switch (value) { - case 'token': - case 'selection': - document.body.dataset[option] = value; - value = {showToken: value == 'token' && /[#.\-\w]/, annotateScrollbar: true}; - break; - default: - value = null; - } - } - CodeMirror.setOption(option, value); -} - -// replace given textarea with the CodeMirror editor -function setupCodeMirror(textarea, index) { - const cm = CodeMirror.fromTextArea(textarea, {lint: null}); - const wrapper = cm.display.wrapper; - - cm.on('change', indicateCodeChange); - if (prefs.get('editor.autocompleteOnTyping')) { - cm.on('change', autocompleteOnTyping); - cm.on('pick', autocompletePicked); - } - cm.on('blur', () => { - editors.lastActive = cm; - hotkeyRerouter.setState(true); - setTimeout(() => { - wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement)); - }); - }); - cm.on('focus', () => { - hotkeyRerouter.setState(false); - wrapper.classList.add('CodeMirror-active'); - }); - cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event)); - - let lastClickTime = 0; - const resizeGrip = wrapper.appendChild(template.resizeGrip.cloneNode(true)); - resizeGrip.onmousedown = event => { - if (event.button != 0) { - return; - } - event.preventDefault(); - if (Date.now() - lastClickTime < 500) { - lastClickTime = 0; - toggleSectionHeight(cm); - return; - } - lastClickTime = Date.now(); - const minHeight = cm.defaultTextHeight() - + cm.display.lineDiv.offsetParent.offsetTop /* .CodeMirror-lines padding */ - + wrapper.offsetHeight - wrapper.clientHeight /* borders */; - wrapper.style.pointerEvents = 'none'; - document.body.style.cursor = 's-resize'; - function resize(e) { - const cmPageY = wrapper.getBoundingClientRect().top + window.scrollY; - const height = Math.max(minHeight, e.pageY - cmPageY); - if (height != wrapper.clientHeight) { - cm.setSize(null, height); - } - } - document.addEventListener('mousemove', resize); - document.addEventListener('mouseup', function resizeStop() { - document.removeEventListener('mouseup', resizeStop); - document.removeEventListener('mousemove', resize); - wrapper.style.pointerEvents = ''; - document.body.style.cursor = ''; - }); - }; - - editors.splice(index || editors.length, 0, cm); - return cm; -} - -function indicateCodeChange(cm) { - var section = cm.getSection(); - setCleanItem(section, cm.isClean(section.savedValue)); - updateTitle(); - updateLintReport(cm); -} - -function getSectionForChild(e) { - return e.closest("#sections > div"); -} - -function getSections() { - return document.querySelectorAll("#sections > div"); -} - -// remind Chrome to repaint a previously invisible editor box by toggling any element's transform -// this bug is present in some versions of Chrome (v37-40 or something) -document.addEventListener("scroll", function(event) { - var style = document.getElementById("name").style; - style.webkitTransform = style.webkitTransform ? "" : "scale(1)"; -}); - -// Shift-Ctrl-Wheel scrolls entire page even when mouse is over a code editor -document.addEventListener("wheel", function(event) { - if (event.shiftKey && event.ctrlKey && !event.altKey && !event.metaKey) { - // Chrome scrolls horizontally when Shift is pressed but on some PCs this might be different - window.scrollBy(0, event.deltaX || event.deltaY); - event.preventDefault(); - } -}); - -queryTabs({currentWindow: true}).then(tabs => { - var windowId = tabs[0].windowId; - if (prefs.get("openEditInWindow")) { - if (sessionStorage.saveSizeOnClose - && 'left' in prefs.get('windowPosition', {}) - && !isWindowMaximized()) { - // window was reopened via Ctrl-Shift-T etc. - chrome.windows.update(windowId, prefs.get('windowPosition')); - } - if (tabs.length == 1 && window.history.length == 1) { - chrome.windows.getAll(function(windows) { - if (windows.length > 1) { - sessionStorageHash("saveSizeOnClose").set(windowId, true); - saveSizeOnClose = true; - } - }); - } else { - saveSizeOnClose = sessionStorageHash("saveSizeOnClose").value[windowId]; - } - } - chrome.tabs.onRemoved.addListener(function(tabId, info) { - sessionStorageHash("manageStylesHistory").unset(tabId); - if (info.windowId == windowId && info.isWindowClosing) { - sessionStorageHash("saveSizeOnClose").unset(windowId); - } - }); -}); - -getActiveTab().then(tab => { - useHistoryBack = sessionStorageHash("manageStylesHistory").value[tab.id] == location.href; -}); - -function goBackToManage(event) { - if (useHistoryBack) { - event.stopPropagation(); - event.preventDefault(); - history.back(); - } else if (styleId) { - sessionStorage.justEditedStyleId = styleId; - } -} - -function isWindowMaximized() { - return window.screenLeft == 0 - && window.screenTop == 0 - && window.outerWidth == screen.availWidth - && window.outerHeight == screen.availHeight; -} - -window.onbeforeunload = function() { - if (saveSizeOnClose && !isWindowMaximized()) { - prefs.set("windowPosition", { - left: screenLeft, - top: screenTop, - width: outerWidth, - height: outerHeight - }); - } - document.activeElement.blur(); - if (isCleanGlobal()) { - return; - } - updateLintReport(null, 0); - return confirm(t('styleChangesNotSaved')); -}; - -function addAppliesTo(list, name, value) { - var showingEverything = list.querySelector(".applies-to-everything") != null; - // blow away "Everything" if it's there - if (showingEverything) { - list.removeChild(list.firstChild); - } - var e; - if (name && value) { - e = template.appliesTo.cloneNode(true); - e.querySelector("[name=applies-type]").value = name; - e.querySelector("[name=applies-value]").value = value; - e.querySelector(".remove-applies-to").addEventListener("click", removeAppliesTo, false); - } else if (showingEverything || list.hasChildNodes()) { - e = template.appliesTo.cloneNode(true); - if (list.hasChildNodes()) { - e.querySelector("[name=applies-type]").value = list.querySelector("li:last-child [name='applies-type']").value; - } - e.querySelector(".remove-applies-to").addEventListener("click", removeAppliesTo, false); - } else { - e = template.appliesToEverything.cloneNode(true); - } - e.querySelector(".add-applies-to").addEventListener("click", function() {addAppliesTo(this.parentNode.parentNode)}, false); - list.appendChild(e); -} - -function addSection(event, section) { - var div = template.section.cloneNode(true); - div.querySelector(".applies-to-help").addEventListener("click", showAppliesToHelp, false); - div.querySelector(".remove-section").addEventListener("click", removeSection, false); - div.querySelector(".add-section").addEventListener("click", addSection, false); - div.querySelector(".beautify-section").addEventListener("click", beautify); - - var codeElement = div.querySelector(".code"); - var appliesTo = div.querySelector(".applies-to-list"); - var appliesToAdded = false; - - if (section) { - codeElement.value = section.code; - for (var i in propertyToCss) { - if (section[i]) { - section[i].forEach(function(url) { - addAppliesTo(appliesTo, propertyToCss[i], url); - appliesToAdded = true; - }); - } - } - } - if (!appliesToAdded) { - addAppliesTo(appliesTo); - } - - appliesTo.addEventListener("change", onChange); - appliesTo.addEventListener("input", onChange); - - toggleTestRegExpVisibility(); - appliesTo.addEventListener('change', toggleTestRegExpVisibility); - div.querySelector('.test-regexp').onclick = showRegExpTester; - function toggleTestRegExpVisibility() { - const show = [...appliesTo.children].some(item => - !item.matches('.applies-to-everything') && - item.querySelector('.applies-type').value == 'regexp' && - item.querySelector('.applies-value').value.trim()); - div.classList.toggle('has-regexp', show); - appliesTo.oninput = appliesTo.oninput || show && (event => { - if (event.target.matches('.applies-value') - && event.target.parentElement.querySelector('.applies-type').value == 'regexp') { - showRegExpTester(null, div); - } - }); - } - - var sections = document.getElementById("sections"); - if (event) { - var clickedSection = getSectionForChild(event.target); - sections.insertBefore(div, clickedSection.nextElementSibling); - var newIndex = getSections().indexOf(clickedSection) + 1; - var cm = setupCodeMirror(codeElement, newIndex); - makeSectionVisible(cm); - cm.focus() - renderLintReport(); - } else { - sections.appendChild(div); - var cm = setupCodeMirror(codeElement); - } - - div.CodeMirror = cm; - setCleanSection(div); - return div; -} - -function removeAppliesTo(event) { - var appliesTo = event.target.parentNode, - appliesToList = appliesTo.parentNode; - removeAreaAndSetDirty(appliesTo); - if (!appliesToList.hasChildNodes()) { - addAppliesTo(appliesToList); - } -} - -function removeSection(event) { - var section = getSectionForChild(event.target); - var cm = section.CodeMirror; - removeAreaAndSetDirty(section); - editors.splice(editors.indexOf(cm), 1); - renderLintReport(); -} - -function removeAreaAndSetDirty(area) { - var contributors = area.querySelectorAll('.style-contributor'); - if(!contributors.length){ - setCleanItem(area, false); - } - contributors.some(function(node) { - if (node.savedValue) { - // it's a saved section, so make it dirty and stop the enumeration - setCleanItem(area, false); - return true; - } else { - // it's an empty section, so undirty the applies-to items, - // otherwise orphaned ids would keep the style dirty - setCleanItem(node, true); - } - }); - updateTitle(); - area.parentNode.removeChild(area); -} - -function makeSectionVisible(cm) { - var section = cm.getSection(); - var bounds = section.getBoundingClientRect(); - if ((bounds.bottom > window.innerHeight && bounds.top > 0) || (bounds.top < 0 && bounds.bottom < window.innerHeight)) { - if (bounds.top < 0) { - window.scrollBy(0, bounds.top - 1); - } else { - window.scrollBy(0, bounds.bottom - window.innerHeight + 1); - } - } -} - -function setupGlobalSearch() { - var originalCommand = { - find: CodeMirror.commands.find, - findNext: CodeMirror.commands.findNext, - findPrev: CodeMirror.commands.findPrev, - replace: CodeMirror.commands.replace - }; - var originalOpenDialog = CodeMirror.prototype.openDialog; - var originalOpenConfirm = CodeMirror.prototype.openConfirm; - - var curState; // cm.state.search for last used 'find' - - function shouldIgnoreCase(query) { // treat all-lowercase non-regexp queries as case-insensitive - return typeof query == "string" && query == query.toLowerCase(); - } - - function updateState(cm, newState) { - if (!newState) { - if (cm.state.search) { - return cm.state.search; - } - if (!curState) { - return null; - } - newState = curState; - } - cm.state.search = { - query: newState.query, - overlay: newState.overlay, - annotate: cm.showMatchesOnScrollbar(newState.query, shouldIgnoreCase(newState.query)) - } - cm.addOverlay(newState.overlay); - return cm.state.search; - } - - // temporarily overrides the original openDialog with the provided template's innerHTML - function customizeOpenDialog(cm, template, callback) { - cm.openDialog = function(tmpl, cb, opt) { - // invoke 'callback' and bind 'this' to the original callback - originalOpenDialog.call(cm, template.innerHTML, callback.bind(cb), opt); - }; - setTimeout(function() { cm.openDialog = originalOpenDialog; }, 0); - refocusMinidialog(cm); - } - - function focusClosestCM(activeCM) { - editors.lastActive = activeCM; - var cm = getEditorInSight(); - if (cm != activeCM) { - cm.focus(); - } - return cm; - - } - - function find(activeCM) { - activeCM = focusClosestCM(activeCM); - customizeOpenDialog(activeCM, template.find, function(query) { - this(query); - curState = activeCM.state.search; - if (editors.length == 1 || !curState.query) { - return; - } - editors.forEach(function(cm) { - if (cm != activeCM) { - cm.execCommand("clearSearch"); - updateState(cm, curState); - } - }); - if (CodeMirror.cmpPos(curState.posFrom, curState.posTo) == 0) { - findNext(activeCM); - } - }); - originalCommand.find(activeCM); - } - - function findNext(activeCM, reverse) { - var state = updateState(activeCM); - if (!state || !state.query) { - find(activeCM); - return; - } - var pos = activeCM.getCursor(reverse ? "from" : "to"); - activeCM.setSelection(activeCM.getCursor()); // clear the selection, don't move the cursor - - var rxQuery = typeof state.query == "object" - ? state.query : stringAsRegExp(state.query, shouldIgnoreCase(state.query) ? "i" : ""); - - if (document.activeElement && document.activeElement.name == "applies-value" - && searchAppliesTo(activeCM)) { - return; - } - for (var i=0, cm=activeCM; i < editors.length; i++) { - state = updateState(cm); - if (!cm.hasFocus()) { - pos = reverse ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(0, 0); - } - var searchCursor = cm.getSearchCursor(state.query, pos, shouldIgnoreCase(state.query)); - if (searchCursor.find(reverse)) { - if (editors.length > 1) { - makeSectionVisible(cm); - cm.focus(); - } - // speedup the original findNext - state.posFrom = reverse ? searchCursor.to() : searchCursor.from(); - state.posTo = CodeMirror.Pos(state.posFrom.line, state.posFrom.ch); - originalCommand[reverse ? "findPrev" : "findNext"](cm); - return; - } else if (!reverse && searchAppliesTo(cm)) { - return; - } - cm = editors[(editors.indexOf(cm) + (reverse ? -1 + editors.length : 1)) % editors.length]; - if (reverse && searchAppliesTo(cm)) { - return; - } - } - // nothing found so far, so call the original search with wrap-around - originalCommand[reverse ? "findPrev" : "findNext"](activeCM); - - function searchAppliesTo(cm) { - var inputs = [].slice.call(cm.getSection().querySelectorAll(".applies-value")); - if (reverse) { - inputs = inputs.reverse(); - } - inputs.splice(0, inputs.indexOf(document.activeElement) + 1); - return inputs.some(function(input) { - var match = rxQuery.exec(input.value); - if (match) { - input.focus(); - var end = match.index + match[0].length; - // scroll selected part into view in long inputs, - // works only outside of current event handlers chain, hence timeout=0 - setTimeout(function() { - input.setSelectionRange(end, end); - input.setSelectionRange(match.index, end) - }, 0); - return true; - } - }); - } - } - - function findPrev(cm) { - findNext(cm, true); - } - - function replace(activeCM, all) { - var queue, query, replacement; - activeCM = focusClosestCM(activeCM); - customizeOpenDialog(activeCM, template[all ? "replaceAll" : "replace"], function(txt) { - query = txt; - customizeOpenDialog(activeCM, template.replaceWith, function(txt) { - replacement = txt; - queue = editors.rotate(-editors.indexOf(activeCM)); - all ? editors.forEach(doReplace) : doReplace(); - }); - this(query); - }); - originalCommand.replace(activeCM, all); - - function doReplace() { - var cm = queue.shift(); - if (!cm) { - if (!all) { - editors.lastActive.focus(); - } - return; - } - // hide the first two dialogs (replace, replaceWith) - cm.openDialog = function(tmpl, callback, opt) { - cm.openDialog = function(tmpl, callback, opt) { - cm.openDialog = originalOpenDialog; - if (all) { - callback(replacement); - } else { - doConfirm(cm); - callback(replacement); - if (!cm.getWrapperElement().querySelector(".CodeMirror-dialog")) { - // no dialog == nothing found in the current CM, move to the next - doReplace(); - } - } - }; - callback(query); - }; - originalCommand.replace(cm, all); - } - function doConfirm(cm) { - var wrapAround = false; - var origPos = cm.getCursor(); - cm.openConfirm = function overrideConfirm(tmpl, callbacks, opt) { - var ovrCallbacks = callbacks.map(function(callback) { - return function() { - makeSectionVisible(cm); - cm.openConfirm = overrideConfirm; - setTimeout(function() { cm.openConfirm = originalOpenConfirm; }, 0); - - var pos = cm.getCursor(); - callback(); - var cmp = CodeMirror.cmpPos(cm.getCursor(), pos); - wrapAround |= cmp <= 0; - - var dlg = cm.getWrapperElement().querySelector(".CodeMirror-dialog"); - if (!dlg || cmp == 0 || wrapAround && CodeMirror.cmpPos(cm.getCursor(), origPos) >= 0) { - if (dlg) { - dlg.remove(); - } - doReplace(); - } - } - }); - originalOpenConfirm.call(cm, template.replaceConfirm.innerHTML, ovrCallbacks, opt); - }; - } - } - - function replaceAll(cm) { - replace(cm, true); - } - - CodeMirror.commands.find = find; - CodeMirror.commands.findNext = findNext; - CodeMirror.commands.findPrev = findPrev; - CodeMirror.commands.replace = replace; - CodeMirror.commands.replaceAll = replaceAll; -} - -function jumpToLine(cm) { - var cur = cm.getCursor(); - refocusMinidialog(cm); - cm.openDialog(template.jumpToLine.innerHTML, function(str) { - var m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/); - if (m) { - cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch); - } - }, {value: cur.line+1}); -} - -function toggleStyle() { - $('#enabled').checked = !$('#enabled').checked; - save(); -} - -function toggleSectionHeight(cm) { - if (cm.state.toggleHeightSaved) { - // restore previous size - cm.setSize(null, cm.state.toggleHeightSaved); - cm.state.toggleHeightSaved = 0; - } else { - // maximize - const wrapper = cm.display.wrapper; - const allBounds = $('#sections').getBoundingClientRect(); - const pageExtrasHeight = allBounds.top + window.scrollY + - parseFloat(getComputedStyle($('#sections')).paddingBottom); - const sectionExtrasHeight = cm.getSection().clientHeight - wrapper.offsetHeight; - cm.state.toggleHeightSaved = wrapper.clientHeight; - cm.setSize(null, window.innerHeight - sectionExtrasHeight - pageExtrasHeight); - const bounds = cm.getSection().getBoundingClientRect(); - if (bounds.top < 0 || bounds.bottom > window.innerHeight) { - window.scrollBy(0, bounds.top); - } - } -} - -function autocompleteOnTyping(cm, info, debounced) { - if (cm.state.completionActive - || info.origin && !info.origin.includes('input') - || !info.text.last) { - return; - } - if (cm.state.autocompletePicked) { - cm.state.autocompletePicked = false; - return; - } - if (!debounced) { - debounce(autocompleteOnTyping, 100, cm, info, true); - return; - } - if (info.text.last.match(/[-\w!]+$/)) { - cm.state.autocompletePicked = false; - cm.options.hintOptions.completeSingle = false; - cm.execCommand('autocomplete'); - setTimeout(() => { - cm.options.hintOptions.completeSingle = true; - }); - } -} - -function autocompletePicked(cm) { - cm.state.autocompletePicked = true; -} - -function refocusMinidialog(cm) { - var section = cm.getSection(); - if (!section.querySelector(".CodeMirror-dialog")) { - return; - } - // close the currently opened minidialog - cm.focus(); - // make sure to focus the input in newly opened minidialog - setTimeout(function() { - section.querySelector(".CodeMirror-dialog").focus(); - }, 0); -} - -function nextPrevEditor(cm, direction) { - cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length]; - makeSectionVisible(cm); - cm.focus(); -} - -function getEditorInSight(nearbyElement) { - // priority: 1. associated CM for applies-to element 2. last active if visible 3. first visible - var cm; - if (nearbyElement && nearbyElement.className.indexOf("applies-") >= 0) { - cm = getSectionForChild(nearbyElement).CodeMirror; - } else { - cm = editors.lastActive; - } - if (!cm || offscreenDistance(cm) > 0) { - var sorted = editors - .map(function(cm, index) { return {cm: cm, distance: offscreenDistance(cm), index: index} }) - .sort(function(a, b) { return a.distance - b.distance || a.index - b.index }); - cm = sorted[0].cm; - if (sorted[0].distance > 0) { - makeSectionVisible(cm) - } - } - return cm; - - function offscreenDistance(cm) { - var LINES_VISIBLE = 2; // closest editor should have at least # lines visible - var bounds = cm.getSection().getBoundingClientRect(); - if (bounds.top < 0) { - return -bounds.top; - } else if (bounds.top < window.innerHeight - cm.defaultTextHeight() * LINES_VISIBLE) { - return 0; - } else { - return bounds.top - bounds.height; - } - } -} - -function updateLintReport(cm, delay) { - if (delay == 0) { - // immediately show pending csslint messages in onbeforeunload and save - update(cm); - return; - } - if (delay > 0) { - setTimeout(cm => { cm.performLint(); update(cm) }, delay, cm); - return; - } - var state = cm.state.lint; - if (!state) { - return; - } - // user is editing right now: postpone updating the report for the new issues (default: 500ms lint + 4500ms) - // or update it as soon as possible (default: 500ms lint + 100ms) in case an existing issue was just fixed - clearTimeout(state.reportTimeout); - state.reportTimeout = setTimeout(update, state.options.delay + 100, cm); - state.postponeNewIssues = delay == undefined || delay == null; - - function update(cm) { - var scope = cm ? [cm] : editors; - var changed = false; - var fixedOldIssues = false; - scope.forEach(function(cm) { - var state = cm.state.lint || {}; - var oldMarkers = state.markedLast || {}; - var newMarkers = {}; - var html = !state.marked || state.marked.length == 0 ? "" : "" + - state.marked.map(function(mark) { - var info = mark.__annotation; - var isActiveLine = info.from.line == cm.getCursor().line; - var pos = isActiveLine ? "cursor" : (info.from.line + "," + info.from.ch); - var message = escapeHtml(info.message.replace(/ at line \d.+$/, "")); - if (message.length > 100) { - message = message.substr(0, 100) + "..."; - } - if (isActiveLine || oldMarkers[pos] == message) { - delete oldMarkers[pos]; - } - newMarkers[pos] = message; - return "" + - "" + - info.severity + "" + - "" + (info.from.line+1) + "" + - ":" + - "" + (info.from.ch+1) + "" + - "" + message + ""; - }).join("") + ""; - state.markedLast = newMarkers; - fixedOldIssues |= state.reportDisplayed && Object.keys(oldMarkers).length > 0; - if (state.html != html) { - state.html = html; - changed = true; - } - }); - if (changed) { - clearTimeout(state ? state.renderTimeout : undefined); - if (!state || !state.postponeNewIssues || fixedOldIssues) { - renderLintReport(true); - } else { - state.renderTimeout = setTimeout(function() { - renderLintReport(true); - }, CodeMirror.defaults.lintReportDelay); - } - } - } - function escapeHtml(html) { - var chars = {"&": "&", "<": "<", ">": ">", '"': '"', "'": ''', "/": '/'}; - return html.replace(/[&<>"'\/]/g, function(char) { return chars[char] }); - } -} - -function renderLintReport(someBlockChanged) { - var container = document.getElementById("lint"); - var content = container.children[1]; - var label = t("sectionCode"); - var newContent = content.cloneNode(false); - var issueCount = 0; - editors.forEach(function(cm, index) { - if (cm.state.lint && cm.state.lint.html) { - var newBlock = newContent.appendChild(document.createElement("table")); - var html = "" + label + " " + (index+1) + "" + cm.state.lint.html; - newBlock.innerHTML = html; - newBlock.cm = cm; - issueCount += newBlock.rows.length; - - var block = content.children[newContent.children.length - 1]; - var blockChanged = !block || cm != block.cm || html != block.innerHTML; - someBlockChanged |= blockChanged; - cm.state.lint.reportDisplayed = blockChanged; - } - }); - if (someBlockChanged || newContent.children.length != content.children.length) { - document.getElementById('issue-count').textContent = issueCount; - container.replaceChild(newContent, content); - container.style.display = newContent.children.length ? "block" : "none"; - resizeLintReport(null, newContent); - } -} - -function resizeLintReport(event, content) { - content = content || document.getElementById("lint").children[1]; - if (content.children.length) { - var bounds = content.getBoundingClientRect(); - var newMaxHeight = bounds.bottom <= innerHeight ? '' : (innerHeight - bounds.top) + "px"; - if (newMaxHeight != content.style.maxHeight) { - content.style.maxHeight = newMaxHeight; - } - } -} - -function gotoLintIssue(event) { - var issue = event.target.closest("tr"); - if (!issue) { - return; - } - var block = issue.closest("table"); - makeSectionVisible(block.cm); - block.cm.focus(); - block.cm.setSelection({ - line: parseInt(issue.querySelector("td[role='line']").textContent) - 1, - ch: parseInt(issue.querySelector("td[role='col']").textContent) - 1 - }); -} - -function toggleLintReport() { - document.getElementById("lint").classList.toggle("collapsed"); -} - -function beautify(event) { - if (exports.css_beautify) { // thanks to csslint's definition of 'exports' - doBeautify(); - } else { - var script = document.head.appendChild(document.createElement("script")); - script.src = "beautify/beautify-css-mod.js"; - script.onload = doBeautify; - } - function doBeautify() { - var tabs = prefs.get("editor.indentWithTabs"); - var options = prefs.get("editor.beautify"); - options.indent_size = tabs ? 1 : prefs.get("editor.tabSize"); - options.indent_char = tabs ? "\t" : " "; - - var section = getSectionForChild(event.target); - var scope = section ? [section.CodeMirror] : editors; - - showHelp(t("styleBeautify"), "
" + - optionHtml(".selector1,", "selector_separator_newline") + - optionHtml(".selector2,", "newline_before_open_brace") + - optionHtml("{", "newline_after_open_brace") + - optionHtml("border: none;", "newline_between_properties", true) + - optionHtml("display: block;", "newline_before_close_brace", true) + - optionHtml("}", "newline_between_rules") + - `' + - "
" + - "
"); - - var undoButton = document.querySelector("#help-popup button[role='undo']"); - undoButton.textContent = t(scope.length == 1 ? "undo" : "undoGlobal"); - undoButton.addEventListener("click", function() { - var undoable = false; - scope.forEach(function(cm) { - if (cm.beautifyChange && cm.beautifyChange[cm.changeGeneration()]) { - delete cm.beautifyChange[cm.changeGeneration()]; - cm.undo(); - cm.scrollIntoView(cm.getCursor()); - undoable |= cm.beautifyChange[cm.changeGeneration()]; - } - }); - undoButton.disabled = !undoable; - }); - - scope.forEach(function(cm) { - setTimeout(function() { - const pos = options.translate_positions = - [].concat.apply([], cm.doc.sel.ranges.map(r => - [Object.assign({}, r.anchor), Object.assign({}, r.head)])); - var text = cm.getValue(); - var newText = exports.css_beautify(text, options); - if (newText != text) { - if (!cm.beautifyChange || !cm.beautifyChange[cm.changeGeneration()]) { - // clear the list if last change wasn't a css-beautify - cm.beautifyChange = {}; - } - cm.setValue(newText); - const selections = []; - for (let i = 0; i < pos.length; i += 2) { - selections.push({anchor: pos[i], head: pos[i + 1]}); - } - cm.setSelections(selections); - cm.beautifyChange[cm.changeGeneration()] = true; - undoButton.disabled = false; - } - }, 0); - }); - - document.querySelector('.beautify-options').onchange = ({target}) => { - const value = target.type == 'checkbox' ? target.checked : target.selectedIndex > 0; - prefs.set('editor.beautify', Object.assign(options, {[target.dataset.option]: value})); - if (target.parentNode.hasAttribute('newline')) { - target.parentNode.setAttribute('newline', value.toString()); - } - doBeautify(); - }; - - function optionHtml(label, optionName, indent) { - var value = options[optionName]; - return "
" + - "" + label + "" + - "
"; - } - } -} - -document.addEventListener("DOMContentLoaded", init); - -function init() { - initCodeMirror(); - var params = getParams(); - if (!params.id) { // match should be 2 - one for the whole thing, one for the parentheses - // This is an add - tE("heading", "addStyleTitle"); - var section = {code: ""} - for (var i in CssToProperty) { - if (params[i]) { - section[CssToProperty[i]] = [params[i]]; - } - } - window.onload = () => { - window.onload = null; - addSection(null, section); - editors[0].setOption('lint', CodeMirror.defaults.lint); - // default to enabled - document.getElementById("enabled").checked = true - initHooks(); - }; - return; - } - // This is an edit - tE("heading", "editStyleHeading", null, false); - getStylesSafe({id: params.id}).then(styles => { - let style = styles[0]; - if (!style) { - style = {id: null, sections: []}; - history.replaceState({}, document.title, location.pathname); - } - styleId = style.id; - setStyleMeta(style); - window.onload = () => { - window.onload = null; - initWithStyle({style}); - }; - if (document.readyState != 'loading') { - window.onload(); - } - }); -} - -function setStyleMeta(style) { - document.getElementById("name").value = style.name; - document.getElementById("enabled").checked = style.enabled; - document.getElementById("url").href = style.url; -} - -function initWithStyle({style, codeIsUpdated}) { - setStyleMeta(style); - - if (codeIsUpdated === false) { - setCleanGlobal(); - updateTitle(); - return; - } - - // if this was done in response to an update, we need to clear existing sections - getSections().forEach(function(div) { div.remove(); }); - var queue = style.sections.length ? style.sections.slice() : [{code: ""}]; - var queueStart = new Date().getTime(); - // after 100ms the sections will be added asynchronously - while (new Date().getTime() - queueStart <= 100 && queue.length) { - add(); - } - (function processQueue() { - if (queue.length) { - add(); - setTimeout(processQueue, 0); - } - })(); - initHooks(); - - function add() { - var sectionDiv = addSection(null, queue.shift()); - maximizeCodeHeight(sectionDiv, !queue.length); - const cm = sectionDiv.CodeMirror; - setTimeout(() => { - cm.setOption('lint', CodeMirror.defaults.lint); - updateLintReport(cm, 0); - }, prefs.get("editor.lintDelay")); - } -} - -function initHooks() { - document.querySelectorAll("#header .style-contributor").forEach(function(node) { - node.addEventListener("change", onChange); - node.addEventListener("input", onChange); - }); - document.getElementById("toggle-style-help").addEventListener("click", showToggleStyleHelp); - document.getElementById("to-mozilla").addEventListener("click", showMozillaFormat, false); - document.getElementById("to-mozilla-help").addEventListener("click", showToMozillaHelp, false); - document.getElementById("from-mozilla").addEventListener("click", fromMozillaFormat); - document.getElementById("beautify").addEventListener("click", beautify); - document.getElementById("save-button").addEventListener("click", save, false); - document.getElementById("sections-help").addEventListener("click", showSectionHelp, false); - document.getElementById("keyMap-help").addEventListener("click", showKeyMapHelp, false); - document.getElementById("cancel-button").addEventListener("click", goBackToManage); - document.getElementById("lint-help").addEventListener("click", showLintHelp); - document.getElementById("lint").addEventListener("click", gotoLintIssue); - window.addEventListener("resize", resizeLintReport); - - // touch devices don't have onHover events so the element we'll be toggled via clicking (touching) - if ("ontouchstart" in document.body) { - document.querySelector("#lint h2").addEventListener("click", toggleLintReport); - } - - document.querySelectorAll( - 'input:not([type]), input[type="text"], input[type="search"], input[type="number"]') - .forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete)); - - setupGlobalSearch(); - setCleanGlobal(); - updateTitle(); -} - - -function toggleContextMenuDelete(event) { - if (event.button == 2 && prefs.get('editor.contextDelete')) { - chrome.contextMenus.update('editor.contextDelete', { - enabled: Boolean( - this.selectionStart != this.selectionEnd || - this.somethingSelected && this.somethingSelected() - ), - }, ignoreChromeError); - } -} - - -function maximizeCodeHeight(sectionDiv, isLast) { - var cm = sectionDiv.CodeMirror; - var stats = maximizeCodeHeight.stats = maximizeCodeHeight.stats || {totalHeight: 0, deltas: []}; - if (!stats.cmActualHeight) { - stats.cmActualHeight = getComputedHeight(cm.display.wrapper); - } - if (!stats.sectionMarginTop) { - stats.sectionMarginTop = parseFloat(getComputedStyle(sectionDiv).marginTop); - } - var sectionTop = sectionDiv.getBoundingClientRect().top - stats.sectionMarginTop; - if (!stats.firstSectionTop) { - stats.firstSectionTop = sectionTop; - } - var extrasHeight = getComputedHeight(sectionDiv) - stats.cmActualHeight; - var cmMaxHeight = window.innerHeight - extrasHeight - sectionTop - stats.sectionMarginTop; - var cmDesiredHeight = cm.display.sizer.clientHeight + 2*cm.defaultTextHeight(); - var cmGrantableHeight = Math.max(stats.cmActualHeight, Math.min(cmMaxHeight, cmDesiredHeight)); - stats.deltas.push(cmGrantableHeight - stats.cmActualHeight); - stats.totalHeight += cmGrantableHeight + extrasHeight; - if (!isLast) { - return; - } - stats.totalHeight += stats.firstSectionTop; - if (stats.totalHeight <= window.innerHeight) { - editors.forEach(function(cm, index) { - cm.setSize(null, stats.deltas[index] + stats.cmActualHeight); - }); - return; - } - // scale heights to fill the gap between last section and bottom edge of the window - var sections = document.getElementById("sections"); - var available = window.innerHeight - sections.getBoundingClientRect().bottom - - parseFloat(getComputedStyle(sections).marginBottom); - if (available <= 0) { - return; - } - var totalDelta = stats.deltas.reduce(function(sum, d) { return sum + d; }, 0); - var q = available / totalDelta; - var baseHeight = stats.cmActualHeight - stats.sectionMarginTop; - stats.deltas.forEach(function(delta, index) { - editors[index].setSize(null, baseHeight + Math.floor(q * delta)); - }); -} - -function updateTitle() { - var DIRTY_TITLE = "* $"; - - var name = document.getElementById("name").savedValue; - var clean = isCleanGlobal(); - var title = styleId === null ? t("addStyleTitle") : t('editStyleTitle', [name]); - document.title = clean ? title : DIRTY_TITLE.replace("$", title); -} - -function validate() { - var name = document.getElementById("name").value; - if (name == "") { - return t("styleMissingName"); - } - // validate the regexps - if (document.querySelectorAll(".applies-to-list").some(function(list) { - return list.childNodes.some(function(li) { - if (li.className == template.appliesToEverything.className) { - return false; - } - var valueElement = li.querySelector("[name=applies-value]"); - var type = li.querySelector("[name=applies-type]").value; - var value = valueElement.value; - if (type && value) { - if (type == "regexp") { - try { - new RegExp(value); - } catch (ex) { - valueElement.focus(); - return true; - } - } - } - return false; - }); - })) { - return t("styleBadRegexp"); - } - return null; -} - -function save() { - updateLintReport(null, 0); - - // save the contents of the CodeMirror editors back into the textareas - for (var i=0; i < editors.length; i++) { - editors[i].save(); - } - - var error = validate(); - if (error) { - alert(error); - return; - } - var name = document.getElementById("name").value; - var enabled = document.getElementById("enabled").checked; - saveStyleSafe({ - id: styleId, - name: name, - enabled: enabled, - reason: 'editSave', - sections: getSectionsHashes() - }) - .then(saveComplete); -} - -function getSectionsHashes() { - var sections = []; - getSections().forEach(function(div) { - var meta = getMeta(div); - var code = div.CodeMirror.getValue(); - if (/^\s*$/.test(code) && Object.keys(meta).length == 0) { - return; - } - meta.code = code; - sections.push(meta); - }); - return sections; -} - -function getMeta(e) { - var meta = {urls: [], urlPrefixes: [], domains: [], regexps: []}; - e.querySelector(".applies-to-list").childNodes.forEach(function(li) { - if (li.className == template.appliesToEverything.className) { - return; - } - var type = li.querySelector("[name=applies-type]").value; - var value = li.querySelector("[name=applies-value]").value; - if (type && value) { - var property = CssToProperty[type]; - meta[property].push(value); - } - }); - return meta; -} - -function saveComplete(style) { - styleId = style.id; - setCleanGlobal(); - - // Go from new style URL to edit style URL - if (location.href.indexOf("id=") == -1) { - history.replaceState({}, document.title, "edit.html?id=" + style.id); - tE("heading", "editStyleHeading", null, false); - } - updateTitle(); -} - -function showMozillaFormat() { - var popup = showCodeMirrorPopup(t("styleToMozillaFormatTitle"), "", {readOnly: true}); - popup.codebox.setValue(toMozillaFormat()); - popup.codebox.execCommand("selectAll"); -} - -function toMozillaFormat() { - return getSectionsHashes().map(function(section) { - var cssMds = []; - for (var i in propertyToCss) { - if (section[i]) { - cssMds = cssMds.concat(section[i].map(function (v){ - return propertyToCss[i] + "(\"" + v.replace(/\\/g, "\\\\") + "\")"; - })); - } - } - return cssMds.length ? "@-moz-document " + cssMds.join(", ") + " {\n" + section.code + "\n}" : section.code; - }).join("\n\n"); -} - -function fromMozillaFormat() { - var popup = showCodeMirrorPopup(t("styleFromMozillaFormatPrompt"), tHTML("
\ - \ - \ -
").innerHTML); - - var contents = popup.querySelector(".contents"); - contents.insertBefore(popup.codebox.display.wrapper, contents.firstElementChild); - popup.codebox.focus(); - - popup.querySelector("[name='import-append']").addEventListener("click", doImport); - popup.querySelector("[name='import-replace']").addEventListener("click", doImport); - - popup.codebox.on("change", function() { - clearTimeout(popup.mozillaTimeout); - popup.mozillaTimeout = setTimeout(function() { - popup.classList.toggle("ready", trimNewLines(popup.codebox.getValue())); - }, 100); - }); - - function doImport() { - var replaceOldStyle = this.name == "import-replace"; - popup.querySelector(".dismiss").onclick(); - var mozStyle = trimNewLines(popup.codebox.getValue()); - var parser = new parserlib.css.Parser(), lines = mozStyle.split("\n"); - var sectionStack = [{code: "", start: {line: 1, col: 1}}]; - var errors = "", oldSectionCount = editors.length; - var firstAddedCM; - - parser.addListener("startdocument", function(e) { - var outerText = getRange(sectionStack.last.start, (--e.col, e)); - var gapComment = outerText.match(/(\/\*[\s\S]*?\*\/)[\s\n]*$/); - var section = {code: "", start: backtrackTo(this, parserlib.css.Tokens.LBRACE, "end")}; - // move last comment before @-moz-document inside the section - if (gapComment && !gapComment[1].match(/\/\*\s*AGENT_SHEET\s*\*\//)) { - section.code = gapComment[1] + "\n"; - outerText = trimNewLines(outerText.substring(0, gapComment.index)); - } - if (outerText.trim()) { - sectionStack.last.code = outerText; - doAddSection(sectionStack.last); - sectionStack.last.code = ""; - } - e.functions.forEach(function(f) { - var m = f.match(/^(url|url-prefix|domain|regexp)\((['"]?)(.+?)\2?\)$/); - var aType = CssToProperty[m[1]]; - var aValue = aType != "regexps" ? m[3] : m[3].replace(/\\\\/g, "\\"); - (section[aType] = section[aType] || []).push(aValue); - }); - sectionStack.push(section); - }); - - parser.addListener("enddocument", function(e) { - var end = backtrackTo(this, parserlib.css.Tokens.RBRACE, "start"); - var section = sectionStack.pop(); - section.code += getRange(section.start, end); - sectionStack.last.start = (++end.col, end); - doAddSection(section); - }); - - parser.addListener("endstylesheet", function() { - // add nonclosed outer sections (either broken or the last global one) - var endOfText = {line: lines.length, col: lines.last.length + 1}; - sectionStack.last.code += getRange(sectionStack.last.start, endOfText); - sectionStack.forEach(doAddSection); - - delete maximizeCodeHeight.stats; - editors.forEach(function(cm) { - maximizeCodeHeight(cm.getSection(), cm == editors.last); - }); - - makeSectionVisible(firstAddedCM); - firstAddedCM.focus(); - - if (errors) { - showHelp(t("issues"), errors); - } - }); - - parser.addListener("error", function(e) { - errors += e.line + ":" + e.col + " " + e.message.replace(/ at line \d.+$/, "") + "
"; - }); - - parser.parse(mozStyle); - - function getRange( start, end) { - const L1 = start.line - 1, C1 = start.col - 1; - const L2 = end.line - 1, C2 = end.col - 1; - if (L1 == L2) { - return lines[L1].substr(C1, C2 - C1 + 1); - } else { - const middle = lines.slice(L1 + 1, L2).join('\n'); - return lines[L1].substr(C1) + '\n' + middle + - (L2 >= lines.length ? '' : ((middle ? '\n' : '') + lines[L2].substring(0, C2))); - } - } - function doAddSection(section) { - section.code = section.code.trim(); - // don't add empty sections - if (!section.code - && !section.urls - && !section.urlPrefixes - && !section.domains - && !section.regexps) { - return; - } - if (!firstAddedCM) { - if (!initFirstSection(section)) { - return; - } - } - setCleanItem(addSection(null, section), false); - firstAddedCM = firstAddedCM || editors.last; - } - // do onetime housekeeping as the imported text is confirmed to be a valid style - function initFirstSection(section) { - // skip adding the first global section when there's no code/comments - if (!section.code.replace("@namespace url(http://www.w3.org/1999/xhtml);", "") /* ignore boilerplate NS */ - .replace(/[\s\n]/g, "")) { /* ignore all whitespace including new lines */ - return false; - } - if (replaceOldStyle) { - editors.slice(0).reverse().forEach(function(cm) { - removeSection({target: cm.getSection().firstElementChild}); - }); - } else if (!editors.last.getValue()) { - // nuke the last blank section - if (editors.last.getSection().querySelector(".applies-to-everything")) { - removeSection({target: editors.last.getSection()}); - } - } - return true; - } - } - function backtrackTo(parser, tokenType, startEnd) { - var tokens = parser._tokenStream._lt; - for (var i = parser._tokenStream._ltIndex - 1; i >= 0; --i) { - if (tokens[i].type == tokenType) { - return {line: tokens[i][startEnd+"Line"], col: tokens[i][startEnd+"Col"]}; - } - } - } - function trimNewLines(s) { - return s.replace(/^[\s\n]+/, "").replace(/[\s\n]+$/, ""); - } -} - -function showSectionHelp() { - showHelp(t("styleSectionsTitle"), t("sectionHelp")); -} - -function showAppliesToHelp() { - showHelp(t("appliesLabel"), t("appliesHelp")); -} - -function showToMozillaHelp() { - showHelp(t("styleMozillaFormatHeading"), t("styleToMozillaFormatHelp")); -} - -function showToggleStyleHelp() { - showHelp(t("helpAlt"), t("styleEnabledToggleHint")); -} - -function showKeyMapHelp() { - var keyMap = mergeKeyMaps({}, prefs.get("editor.keyMap"), CodeMirror.defaults.extraKeys); - var keyMapSorted = Object.keys(keyMap) - .map(function(key) { return {key: key, cmd: keyMap[key]} }) - .concat([{key: "Shift-Ctrl-Wheel", cmd: "scrollWindow"}]) - .sort(function(a, b) { return a.cmd < b.cmd || (a.cmd == b.cmd && a.key < b.key) ? -1 : 1 }); - showHelp(t("cm_keyMap") + ": " + prefs.get("editor.keyMap"), - '' + - '' + - '' + - "" + keyMapSorted.map(function(value) { - return ""; - }).join("") + - "" + - "
" + value.key + "" + value.cmd + "
"); - - var table = document.querySelector("#help-popup table"); - table.addEventListener("input", filterTable); - - var inputs = table.querySelectorAll("input"); - inputs[0].addEventListener("keydown", hotkeyHandler); - inputs[1].focus(); - - function hotkeyHandler(event) { - var keyName = CodeMirror.keyName(event); - if (keyName == "Esc" || keyName == "Tab" || keyName == "Shift-Tab") { - return; - } - event.preventDefault(); - event.stopPropagation(); - // normalize order of modifiers, - // for modifier-only keys ("Ctrl-Shift") a dummy main key has to be temporarily added - var keyMap = {}; - keyMap[keyName.replace(/(Shift|Ctrl|Alt|Cmd)$/, "$&-dummy")] = ""; - var normalizedKey = Object.keys(CodeMirror.normalizeKeyMap(keyMap))[0]; - this.value = normalizedKey.replace("-dummy", ""); - filterTable(event); - } - function filterTable(event) { - var input = event.target; - var query = stringAsRegExp(input.value, "gi"); - var col = input.parentNode.cellIndex; - inputs[1 - col].value = ""; - table.tBodies[0].childNodes.forEach(function(row) { - var cell = row.children[col]; - cell.innerHTML = cell.textContent.replace(query, "$&"); - row.style.display = query.test(cell.textContent) ? "" : "none"; - // clear highlight from the other column - cell = row.children[1 - col]; - cell.innerHTML = cell.textContent; - }); - } - function mergeKeyMaps(merged, ...more) { - more.forEach(keyMap => { - if (typeof keyMap == "string") { - keyMap = CodeMirror.keyMap[keyMap]; - } - Object.keys(keyMap).forEach(function(key) { - var cmd = keyMap[key]; - // filter out '...', 'attach', etc. (hotkeys start with an uppercase letter) - if (!merged[key] && !key.match(/^[a-z]/) && cmd != "...") { - if (typeof cmd == "function") { - // for 'emacs' keymap: provide at least something meaningful (hotkeys and the function body) - // for 'vim*' keymaps: almost nothing as it doesn't rely on CM keymap mechanism - cmd = cmd.toString().replace(/^function.*?\{[\s\r\n]*([\s\S]+?)[\s\r\n]*\}$/, "$1"); - merged[key] = cmd.length <= 200 ? cmd : cmd.substr(0, 200) + "..."; - } else { - merged[key] = cmd; - } - } - }); - if (keyMap.fallthrough) { - merged = mergeKeyMaps(merged, keyMap.fallthrough); - } - }); - return merged; - } -} - -function showLintHelp() { - showHelp(t("issues"), t("issuesHelp") + "" - ); -} - -function showRegExpTester(event, section = getSectionForChild(this)) { - const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain='; - const OWN_ICON = chrome.runtime.getManifest().icons['16']; - const cachedRegexps = showRegExpTester.cachedRegexps = - showRegExpTester.cachedRegexps || new Map(); - const regexps = [...section.querySelector('.applies-to-list').children] - .map(item => - !item.matches('.applies-to-everything') && - item.querySelector('.applies-type').value == 'regexp' && - item.querySelector('.applies-value').value.trim()) - .filter(item => item) - .map(text => { - const rxData = Object.assign({text}, cachedRegexps.get(text)); - if (!rxData.urls) { - cachedRegexps.set(text, Object.assign(rxData, { - rx: tryRegExp(text), - urls: new Map(), - })); - } - return rxData; - }); - chrome.tabs.onUpdated.addListener(function _(tabId, info) { - if (document.querySelector('.regexp-report')) { - if (info.url) { - showRegExpTester(event, section); - } - } else { - chrome.tabs.onUpdated.removeListener(_); - } - }); - queryTabs().then(tabs => { - const supported = tabs.map(tab => tab.url) - .filter(url => URLS.supported.test(url)); - const unique = [...new Set(supported).values()]; - for (const rxData of regexps) { - const {rx, urls} = rxData; - if (rx) { - const urlsNow = new Map(); - for (const url of unique) { - const match = urls.get(url) || (url.match(rx) || [])[0]; - if (match) { - urlsNow.set(url, match); - } - } - rxData.urls = urlsNow; - } - } - const moreInfoLink = template.regexpTestPartial.outerHTML; - const stats = { - full: {data: [], label: t('styleRegexpTestFull')}, - partial: {data: [], label: t('styleRegexpTestPartial') + moreInfoLink}, - none: {data: [], label: t('styleRegexpTestNone')}, - invalid: {data: [], label: t('styleRegexpTestInvalid')}, - }; - for (const {text, rx, urls} of regexps) { - if (!rx) { - stats.invalid.data.push({text}); - continue; - } - if (!urls.size) { - stats.none.data.push({text}); - continue; - } - const full = []; - const partial = []; - for (const [url, match] of urls.entries()) { - const faviconUrl = url.startsWith(URLS.ownOrigin) - ? OWN_ICON - : GET_FAVICON_URL + new URL(url).hostname; - const icon = ``; - if (match.length == url.length) { - full.push(`
${icon + url}
`); - } else { - partial.push(`
${icon}${match}` + - url.substr(match.length) + '
'); - } - } - if (full.length) { - stats.full.data.push({text, urls: full}); - } - if (partial.length) { - stats.partial.data.push({text, urls: partial}); - } - } - showHelp(t('styleRegexpTestTitle'), - '
' + - Object.keys(stats).map(type => (!stats[type].data.length ? '' : - `
- ${stats[type].label}` + - stats[type].data.map(({text, urls}) => (!urls ? text : - `
${text}${urls.join('')}
` - )).join('
') + - '
' - )).join('') + - '
'); - document.querySelector('.regexp-report').onclick = event => { - const target = event.target.closest('a, .regexp-report div'); - if (target) { - openURL({url: target.href || target.textContent}); - event.preventDefault(); - } - }; - }); -} - -function showHelp(title, text) { - var div = document.getElementById("help-popup"); - div.classList.remove("big"); - div.querySelector(".contents").innerHTML = text; - div.querySelector(".title").innerHTML = title; - - if (getComputedStyle(div).display == "none") { - document.addEventListener("keydown", closeHelp); - div.querySelector(".dismiss").onclick = closeHelp; // avoid chaining on multiple showHelp() calls - } - - div.style.display = "block"; - return div; - - function closeHelp(e) { - if (!e - || e.type == "click" - || ((e.keyCode || e.which) == 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)) { - div.style.display = ""; - document.querySelector(".contents").innerHTML = ""; - document.removeEventListener("keydown", closeHelp); - } - } -} - -function showCodeMirrorPopup(title, html, options) { - var popup = showHelp(title, html); - popup.classList.add("big"); - - popup.codebox = CodeMirror(popup.querySelector(".contents"), Object.assign({ - mode: "css", - lineNumbers: true, - lineWrapping: true, - foldGutter: true, - gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"], - matchBrackets: true, - lint: {getAnnotations: CodeMirror.lint.css, delay: 0}, - styleActiveLine: true, - theme: prefs.get("editor.theme"), - keyMap: prefs.get("editor.keyMap") - }, options)); - popup.codebox.focus(); - popup.codebox.on("focus", function() { hotkeyRerouter.setState(false) }); - popup.codebox.on("blur", function() { hotkeyRerouter.setState(true) }); - return popup; -} - -function getParams() { - var params = {}; - var urlParts = location.href.split("?", 2); - if (urlParts.length == 1) { - return params; - } - urlParts[1].split("&").forEach(function(keyValue) { - var splitKeyValue = keyValue.split("=", 2); - params[decodeURIComponent(splitKeyValue[0])] = decodeURIComponent(splitKeyValue[1]); - }); - return params; -} - -chrome.runtime.onMessage.addListener(onRuntimeMessage); - -function onRuntimeMessage(request) { - switch (request.method) { - case "styleUpdated": - if (styleId && styleId == request.style.id && request.reason != 'editSave') { - if ((request.style.sections[0] || {}).code === null) { - // the code-less style came from notifyAllTabs - onBackgroundReady().then(() => { - request.style = BG.cachedStyles.byId.get(request.style.id); - initWithStyle(request); - }); - } else { - initWithStyle(request); - } - } - break; - case "styleDeleted": - if (styleId && styleId == request.id) { - window.onbeforeunload = function() {}; - window.close(); - break; - } - break; - case "prefChanged": - if ('editor.smartIndent' in request.prefs) { - CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']); - } - break; - case 'editDeleteText': - document.execCommand('delete'); - break; - } -} - -function getComputedHeight(el) { - var compStyle = getComputedStyle(el); - return el.getBoundingClientRect().height + - parseFloat(compStyle.marginTop) + parseFloat(compStyle.marginBottom); -} - - -function getCodeMirrorThemes() { - if (!chrome.runtime.getPackageDirectoryEntry) { - const themes = [ - chrome.i18n.getMessage('defaultTheme'), - '3024-day', - '3024-night', - 'abcdef', - 'ambiance', - 'ambiance-mobile', - 'base16-dark', - 'base16-light', - 'bespin', - 'blackboard', - 'cobalt', - 'colorforth', - 'dracula', - 'duotone-dark', - 'duotone-light', - 'eclipse', - 'elegant', - 'erlang-dark', - 'hopscotch', - 'icecoder', - 'isotope', - 'lesser-dark', - 'liquibyte', - 'material', - 'mbo', - 'mdn-like', - 'midnight', - 'monokai', - 'neat', - 'neo', - 'night', - 'panda-syntax', - 'paraiso-dark', - 'paraiso-light', - 'pastel-on-dark', - 'railscasts', - 'rubyblue', - 'seti', - 'solarized', - 'the-matrix', - 'tomorrow-night-bright', - 'tomorrow-night-eighties', - 'ttcn', - 'twilight', - 'vibrant-ink', - 'xq-dark', - 'xq-light', - 'yeti', - 'zenburn', - ]; - localStorage.codeMirrorThemes = themes.join(' '); - return Promise.resolve(themes); - } - return new Promise(resolve => { - chrome.runtime.getPackageDirectoryEntry(rootDir => { - rootDir.getDirectory('codemirror/theme', {create: false}, themeDir => { - themeDir.createReader().readEntries(entries => { - const themes = [ - chrome.i18n.getMessage('defaultTheme') - ].concat( - entries.filter(entry => entry.isFile) - .sort((a, b) => (a.name < b.name ? -1 : 1)) - .map(entry => entry.name.replace(/\.css$/, '')) - ); - localStorage.codeMirrorThemes = themes.join(' '); - resolve(themes); - }); - }); - }); - }); -} diff --git a/edit/edit.css b/edit/edit.css new file mode 100644 index 00000000..f41e93cb --- /dev/null +++ b/edit/edit.css @@ -0,0 +1,576 @@ +body { + margin: 0; + font: 12px arial,sans-serif; +} +/************ header ************/ +#header { + width: 280px; + height: 100vh; + overflow: auto; + position: fixed; + top: 0; + padding: 15px; + border-right: 1px dashed #AAA; + -webkit-box-shadow: 0 0 3rem -1.2rem black; + box-sizing: border-box; +} +#header h1 { + margin-top: 0; +} +#sections { + padding-left: 280px; +} +#sections h2 { + margin-top: 1rem; + margin-left: 1.7rem; +} +.aligned { + display: table-row; +} +.aligned > *:not(svg) { + display: table-cell; + margin-top: 0.1rem; + min-height: 1.4rem; +} +input[type="checkbox"] { + margin-left: 0.1rem; +} +/* basic info */ +#basic-info { + margin-bottom: 1rem; +} +#name { + width: 100%; +} +#basic-info-name { + display: flex; + align-items: center; +} +#url { + margin-left: 0.25rem; +} +#url:not([href^="http"]) { + display: none; +} +#save-button { + opacity: .5; + pointer-events: none; +} +.dirty #save-button { + opacity: 1; + pointer-events: all; +} +.svg-icon { + cursor: pointer; + vertical-align: middle; + transition: fill .5s; + width: 16px; + height: 16px; +} +.svg-icon:not(.dismiss) { + margin-left: 0.2rem; +} +h2 .svg-icon, label .svg-icon { + margin-top: -1px; +} +.svg-icon.info { + width: 14px; + height: 16px; +} +.svg-icon:hover, +.svg-icon.info { + fill: #666; +} +.svg-icon, +.svg-icon.info:hover { + fill: #000; +} +#enabled { + margin-left: 0; + vertical-align: middle; +} +#enabled-label { + vertical-align: middle; +} +/* actions */ +#actions > * { + margin-right: 0.5rem; + margin-bottom: 0.5rem; +} +/* options */ +#options [type="number"] { + max-width: 2.5rem; + text-align: right; +} +#options .option > * { + padding-right: 0.25rem; +} +/************ content ***********/ +#sections > div { + margin: 0.7rem; + padding: 1rem; +} +#sections > div:not(:first-of-type) { + border-top: 2px solid black; +} +#sections > div:only-of-type .remove-section { + display: none; +} +#sections > div > button:not(:first-of-type) { + margin-left: 0.2rem; +} +.dirty > label::before { + content: "*"; + font-weight: bold; +} +#sections { + counter-reset: codebox; +} +#sections > div > label::after { + counter-increment: codebox; + content: counter(codebox); + margin-left: 0.25rem; +} +/* code */ +.CodeMirror-hint:hover { + color: white; + background: #08f; +} +.code { + height: 10rem; + width: 40rem; +} +.CodeMirror { + border: solid #CCC 1px; +} +.CodeMirror-scroll { + height: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 6px; /* resize-grip height */ +} +.CodeMirror-lint-mark-warning { + background: none; +} +.CodeMirror-vscrollbar { + margin-bottom: 7px; /* make space for resize-grip */ +} +.CodeMirror-hscrollbar { + bottom: 7px; /* make space for resize-grip */ +} +.CodeMirror-scrollbar-filler { + bottom: 7px; /* make space for resize-grip */ +} +.CodeMirror-dialog { + -webkit-animation: highlight 3s ease-out; +} +.CodeMirror-focused { + outline: -webkit-focus-ring-color auto 5px; + outline-offset: -2px; +} +.CodeMirror-search-field { + width: 10em; +} +.CodeMirror-jump-field { + width: 5em; +} +.CodeMirror-search-hint { + color: #888; +} +body[data-match-highlight="token"] .cm-matchhighlight-approved .cm-matchhighlight, +body[data-match-highlight="token"] .CodeMirror-selection-highlight-scrollbar { + animation: fadein-match-highlighter 1s cubic-bezier(.97,.01,.42,.98); + animation-fill-mode: both; +} +body[data-match-highlight="selection"] .cm-matchhighlight-approved .cm-matchhighlight, +body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar { + background-color: rgba(1, 151, 193, 0.1); +} +@-webkit-keyframes highlight { + from { + background-color: #ff9; + } + to { + background-color: none; + } +} +@keyframes fadein { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes fadein-match-highlighter { + from { background-color: transparent; } + to { background-color: rgba(1, 151, 193, 0.1); } +} +.resize-grip { + position: absolute; + display: block; + height: 6px; + content: ""; + left: 0; + right: 0; + bottom: 0; + z-index: 9; + cursor: n-resize; + background-color: inherit; + border-top-width: 1px; + border-top-style: solid; + border-top-color: inherit; +} +.resize-grip:after { + content: ""; + bottom: 2px; + left: 0; + right: 0; + margin: 0 8px; + display: block; + position: absolute; + border-top-width: 2px; + border-top-style: dotted; + border-top-color: inherit; +} +/* applies-to */ +.applies-to { + display: flex; +} +.applies-to label { + flex: auto; + margin-top: 0.2rem; +} +.applies-to ul { + flex: auto; + flex-grow: 99; + margin: 0; + padding: 0; +} +.applies-to li { + display: flex; + list-style-type: none; + align-items: center; + margin-bottom: 0.35rem; +} +.applies-to li > *:not(button) { + flex: auto; + min-height: 1.4rem; + margin-left: 0.35rem; +} +.applies-to li .add-applies-to { + visibility: hidden; + text-align: left; +} +.applies-to li:last-child .add-applies-to { + visibility: visible +} +.applies-to li .add-applies-to:first-child { + margin-left: 1rem; +} +.applies-to li .applies-value { + flex-grow: 99; + padding-left: 0.2rem; +} +.applies-to img { + vertical-align: bottom; +} +.test-regexp { + display: none; +} +.has-regexp .test-regexp { + display: inline-block; +} +.regexp-report summary, .regexp-report div { + cursor: pointer; + outline: none; +} +.regexp-report mark { + background-color: rgba(255, 255, 0, .5); +} +.regexp-report details { + margin-left: 1rem; +} +.regexp-report details:not(:last-child) { + margin-bottom: 1rem; +} +.regexp-report summary { + font-weight: bold; + margin-left: -1rem; + margin-bottom: .5rem; + outline: none; + cursor: default; +} +.regexp-report details[data-type="full"] { + color: darkgreen; +} +.regexp-report details[data-type="partial"] { + color: darkgray; +} +.regexp-report details[data-type="invalid"] { + color: maroon; +} +.regexp-report details details { + margin-left: 2rem; + margin-top: .5rem; +} +.regexp-report .svg-icon { + position: absolute; + margin-top: -1px; +} +.regexp-report details div:hover { + text-decoration: underline; + text-decoration-skip: ink; +} +.regexp-report details div img { + width: 16px; + max-height: 16px; + position: absolute; + margin-left: -20px; + margin-top: -1px; + animation: fadein 1s cubic-bezier(.03, .67, .08, .94); + animation-fill-mode: both; +} +/************ help popup ************/ +#help-popup { + top: 3rem; + right: 3rem; + max-width: 50vw; + position: fixed; + display: none; + background-color: white; + box-shadow: 3px 3px 30px rgba(0, 0, 0, 0.5); + padding: 0.5rem; + z-index: 99; +} +#help-popup.big { + max-width: 100%; + left: 3rem; +} +#help-popup.big .CodeMirror { + min-height: 2rem; + height: 70vh; +} +#help-popup .title { + font-weight: bold; + background-color: rgba(0,0,0,0.05); + margin: -0.5rem -0.5rem 0.5rem; + padding: .5rem 32px .5rem .5rem; +} +#help-popup .contents { + max-height: calc(100vh - 8rem); + overflow-y: auto; +} +#help-popup .dismiss { + position: absolute; + right: 4px; + top: .5em; +} + +.keymap-list { + font-size: 85%; + line-height: 1.0; + border-spacing: 0; + word-break: break-all; +} +.keymap-list input { + width: 100%; +} +.keymap-list tr:nth-child(odd) { + background-color: rgba(0, 0, 0, 0.07); +} +.keymap-list td:first-child { + white-space: nowrap; + font-family: monospace; + padding-right: 0.5rem; +} + +#help-popup button[name^="import"] { + line-height: 1.5rem; + padding: 0 0.5rem; + margin: 0.5rem 0 0 0.5rem; + pointer-events: none; + opacity: 0.5; + float: right; +} +#help-popup.ready button[name^="import"] { + pointer-events: all; + opacity: 1.0; +} + +/************ lint ************/ +#lint { + display: none; +} +#lint > div { + overflow-y: auto; +} +#lint table { + font-size: 100%; + border-spacing: 0; + margin-bottom: 1rem; + line-height: 1.0; +} +#lint table:last-child { + margin-bottom: 0; +} +#lint caption { + text-align: left; + font-weight: bold; +} +#lint tbody { + font-size: 85%; + cursor: pointer; +} +#lint tr:hover { + background-color: rgba(0, 0, 0, 0.1); +} +#lint td[role="severity"] { + font-size: 0; + width: 16px; + padding-right: 0.25rem; +} +#lint td[role="line"], #lint td[role="sep"] { + text-align: right; + padding-right: 0; +} +#lint td[role="col"] { + text-align: left; + padding-right: 0.25rem; +} +#lint td[role="message"] { + text-align: left; +} + +/************ CSS beautifier ************/ +.beautify-options { + white-space: nowrap; + font-family: monospace; +} +.beautify-options div { + float: left; +} +.beautify-options div[newline="true"] + div { + clear: left; +} +.beautify-options div[newline="true"] + div span[indent] { + padding-left: 2rem; +} +.beautify-options:after { + clear: both; + display: block; + content: " "; + height: 1rem; +} +.beautify-options span { + font-weight: bold; +} +.beautify-options select { + border: none; + background-color: rgba(0, 0, 0, 0.05); +} + +/************ reponsive layouts ************/ +@media(max-width:737px) { + #header { + width: auto; + height: auto; + position: inherit; + border-right: none; + border-bottom: 1px dashed #AAA; + } + #header section:not(:last-child) { + margin-bottom: 0.4rem; + } + #header input[type="checkbox"] { + vertical-align: middle; + } + h2 { + display: none; + } + #basic-info { + display: flex; + align-items: baseline; + } + #basic-info > * { + flex: auto; + } + #basic-info > *:first-child { + flex-grow: 99; + display: flex; + } + #basic-info > *:not(:last-child) { + margin-right: 0.8rem; + } + #basic-info #name { + width: auto; + flex-grow: 99; + } + #actions { + margin-top: 1rem; + } + #actions > * { + display: inline-block; + } + #options { + -webkit-column-count: 2; + } + #options .aligned > *:not(svg) { + margin: 1px 0 0 0; /* workaround the flowing-padding column bug in webkit */ + padding-right: 0.4rem; + vertical-align: baseline; + min-height: 1.4rem; + } + .option { + -webkit-column-break-inside: avoid; + } + .option label { + line-height: 1.25rem; + margin: 0; + } + #options [type="number"] { + text-align: left; /* workaround the column flow bug in webkit */ + padding-left: 0.2rem; + } + #options #tabSize-label { + position: relative; + top: 0.2rem; + } + #lint h2 { + display: block; + cursor: default; + margin-bottom: 0; + } + #lint > div { + max-height: 0; + } + #lint.collapsed > div { + display: none; + } + #lint:hover > div { + margin-top: 1em; + max-height: 30vh; + } + #sections { + padding-left: 0; + } + #sections > div { + padding: 0; + } + #sections > *:not(h2) { + padding-left: 0.4rem; + } + .applies-type { + width: 30%; + } +} +@media(max-width:500px) { + #options { + -webkit-column-count: 1; + } + #options #tabSize-label { + position: static; + } +} diff --git a/edit/edit.js b/edit/edit.js new file mode 100644 index 00000000..d7edd6cc --- /dev/null +++ b/edit/edit.js @@ -0,0 +1,2072 @@ +/* eslint brace-style: 0, operator-linebreak: 0 */ +/* global CodeMirror exports parserlib CSSLint */ +'use strict'; + +let styleId = null; +let dirty = {}; // only the actually dirty items here +const editors = []; // array of all CodeMirror instances +let saveSizeOnClose; +let useHistoryBack; // use browser history back when 'back to manage' is clicked + +// direct & reverse mapping of @-moz-document keywords and internal property names +const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'}; +const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps'}; + +// if background page hasn't been loaded yet, increase the chances it has before DOMContentLoaded +onBackgroundReady(); + +// make querySelectorAll enumeration code readable +['forEach', 'some', 'indexOf', 'map'].forEach(method => { + NodeList.prototype[method] = Array.prototype[method]; +}); + +// Chrome pre-34 +Element.prototype.matches = Element.prototype.matches || Element.prototype.webkitMatchesSelector; + +// Chrome pre-41 polyfill +Element.prototype.closest = Element.prototype.closest || function (selector) { + let e; + // eslint-disable-next-line no-empty + for (e = this; e && !e.matches(selector); e = e.parentElement) {} + return e; +}; + +// eslint-disable-next-line no-extend-native +Array.prototype.rotate = function (amount) { // negative amount == rotate left + const r = this.slice(-amount, this.length); + Array.prototype.push.apply(r, this.slice(0, this.length - r.length)); + return r; +}; + +// eslint-disable-next-line no-extend-native +Object.defineProperty(Array.prototype, 'last', {get: function () { return this[this.length - 1]; }}); + +// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs() +new MutationObserver((mutations, observer) => { + const themeElement = document.getElementById('cm-theme'); + if (themeElement) { + themeElement.href = prefs.get('editor.theme') === 'default' ? '' + : 'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css'; + observer.disconnect(); + } +}).observe(document, {subtree: true, childList: true}); + +getCodeMirrorThemes(); + +// reroute handling to nearest editor when keypress resolves to one of these commands +const hotkeyRerouter = { + commands: { + save: true, jumpToLine: true, nextEditor: true, prevEditor: true, + find: true, findNext: true, findPrev: true, replace: true, replaceAll: true, + toggleStyle: true, + }, + setState: enable => { + setTimeout(() => { + document[(enable ? 'add' : 'remove') + 'EventListener']('keydown', hotkeyRerouter.eventHandler); + }, 0); + }, + eventHandler: event => { + const keyName = CodeMirror.keyName(event); + if ( + CodeMirror.lookupKey(keyName, CodeMirror.getOption('keyMap'), handleCommand) === 'handled' || + CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, handleCommand) === 'handled' + ) { + event.preventDefault(); + event.stopPropagation(); + } + function handleCommand(command) { + if (hotkeyRerouter.commands[command] === true) { + CodeMirror.commands[command](getEditorInSight(event.target)); + return true; + } + } + } +}; + +function onChange(event) { + const node = event.target; + if ('savedValue' in node) { + const currentValue = node.type === 'checkbox' ? node.checked : node.value; + setCleanItem(node, node.savedValue === currentValue); + } else { + // the manually added section's applies-to is dirty only when the value is non-empty + setCleanItem(node, node.localName !== 'input' || !node.value.trim()); + delete node.savedValue; // only valid when actually saved + } + updateTitle(); +} + +// Set .dirty on stylesheet contributors that have changed +function setDirtyClass(node, isDirty) { + node.classList.toggle('dirty', isDirty); +} + +function setCleanItem(node, isClean) { + if (!node.id) { + node.id = Date.now().toString(32).substr(-6); + } + + if (isClean) { + delete dirty[node.id]; + // code sections have .CodeMirror property + if (node.CodeMirror) { + node.savedValue = node.CodeMirror.changeGeneration(); + } else { + node.savedValue = node.type === 'checkbox' ? node.checked : node.value; + } + } else { + dirty[node.id] = true; + } + + setDirtyClass(node, !isClean); +} + +function isCleanGlobal() { + const clean = Object.keys(dirty).length === 0; + setDirtyClass(document.body, !clean); + // let saveBtn = document.getElementById('save-button') + // if (clean){ + // //saveBtn.removeAttribute('disabled'); + // }else{ + // //saveBtn.setAttribute('disabled', true); + // } + return clean; +} + +function setCleanGlobal() { + document.querySelectorAll('#header, #sections > div').forEach(setCleanSection); + dirty = {}; // forget the dirty applies-to ids from a deleted section after the style was saved +} + +function setCleanSection(section) { + section.querySelectorAll('.style-contributor').forEach(node => { setCleanItem(node, true); }); + + // #header section has no codemirror + const cm = section.CodeMirror; + if (cm) { + section.savedValue = cm.changeGeneration(); + indicateCodeChange(cm); + } +} + +function initCodeMirror() { + const CM = CodeMirror; + const isWindowsOS = navigator.appVersion.indexOf('Windows') > 0; + + // CodeMirror miserably fails on keyMap='' so let's ensure it's not + if (!prefs.get('editor.keyMap')) { + prefs.reset('editor.keyMap'); + } + + // default option values + Object.assign(CM.defaults, { + mode: 'css', + lineNumbers: true, + lineWrapping: true, + foldGutter: true, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'], + matchBrackets: true, + highlightSelectionMatches: {showToken: /[#.\-\w]/, annotateScrollbar: true}, + hintOptions: {}, + lint: {getAnnotations: CodeMirror.lint.css, delay: prefs.get('editor.lintDelay')}, + lintReportDelay: prefs.get('editor.lintReportDelay'), + styleActiveLine: true, + theme: 'default', + keyMap: prefs.get('editor.keyMap'), + extraKeys: { // independent of current keyMap + 'Alt-Enter': 'toggleStyle', + 'Alt-PageDown': 'nextEditor', + 'Alt-PageUp': 'prevEditor' + } + }, prefs.get('editor.options')); + + // additional commands + CM.commands.jumpToLine = jumpToLine; + CM.commands.nextEditor = cm => { nextPrevEditor(cm, 1); }; + CM.commands.prevEditor = cm => { nextPrevEditor(cm, -1); }; + CM.commands.save = save; + CM.commands.blockComment = cm => { + cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false}); + }; + CM.commands.toggleStyle = toggleStyle; + + // 'basic' keymap only has basic keys by design, so we skip it + + const extraKeysCommands = {}; + Object.keys(CM.defaults.extraKeys).forEach(key => { + extraKeysCommands[CM.defaults.extraKeys[key]] = true; + }); + if (!extraKeysCommands.jumpToLine) { + CM.keyMap.sublime['Ctrl-G'] = 'jumpToLine'; + CM.keyMap.emacsy['Ctrl-G'] = 'jumpToLine'; + CM.keyMap.pcDefault['Ctrl-J'] = 'jumpToLine'; + CM.keyMap.macDefault['Cmd-J'] = 'jumpToLine'; + } + if (!extraKeysCommands.autocomplete) { + CM.keyMap.pcDefault['Ctrl-Space'] = 'autocomplete'; // will be used by 'sublime' on PC via fallthrough + CM.keyMap.macDefault['Alt-Space'] = 'autocomplete'; // OSX uses Ctrl-Space and Cmd-Space for something else + CM.keyMap.emacsy['Alt-/'] = 'autocomplete'; // copied from 'emacs' keymap + // 'vim' and 'emacs' define their own autocomplete hotkeys + } + if (!extraKeysCommands.blockComment) { + CM.keyMap.sublime['Shift-Ctrl-/'] = 'blockComment'; + } + + if (isWindowsOS) { + // 'pcDefault' keymap on Windows should have F3/Shift-F3 + if (!extraKeysCommands.findNext) { + CM.keyMap.pcDefault['F3'] = 'findNext'; + } + if (!extraKeysCommands.findPrev) { + CM.keyMap.pcDefault['Shift-F3'] = 'findPrev'; + } + + // try to remap non-interceptable Ctrl-(Shift-)N/T/W hotkeys + ['N', 'T', 'W'].forEach(char => { + [{from: 'Ctrl-', to: ['Alt-', 'Ctrl-Alt-']}, + {from: 'Shift-Ctrl-', to: ['Ctrl-Alt-', 'Shift-Ctrl-Alt-']} // Note: modifier order in CM is S-C-A + ].forEach(remap => { + const oldKey = remap.from + char; + Object.keys(CM.keyMap).forEach(keyMapName => { + const keyMap = CM.keyMap[keyMapName]; + const command = keyMap[oldKey]; + if (!command) { + return; + } + remap.to.some(newMod => { + const newKey = newMod + char; + if (!(newKey in keyMap)) { + delete keyMap[oldKey]; + keyMap[newKey] = command; + return true; + } + }); + }); + }); + }); + } + + // user option values + CM.getOption = o => CodeMirror.defaults[o]; + CM.setOption = (o, v) => { + CodeMirror.defaults[o] = v; + editors.forEach(editor => { + editor.setOption(o, v); + }); + }; + + CM.prototype.getSection = function () { + return this.display.wrapper.parentNode; + }; + + // initialize global editor controls + function optionsHtmlFromArray(options) { + return options.map(opt => '').join(''); + } + const themeControl = document.getElementById('editor.theme'); + const themeList = localStorage.codeMirrorThemes; + if (themeList) { + themeControl.innerHTML = optionsHtmlFromArray(themeList.split(/\s+/)); + } else { + // Chrome is starting up and shows our edit.html, but the background page isn't loaded yet + const theme = prefs.get('editor.theme'); + themeControl.innerHTML = optionsHtmlFromArray([theme === 'default' ? t('defaultTheme') : theme]); + getCodeMirrorThemes().then(() => { + const themes = (localStorage.codeMirrorThemes || '').split(/\s+/); + themeControl.innerHTML = optionsHtmlFromArray(themes); + themeControl.selectedIndex = Math.max(0, themes.indexOf(theme)); + }); + } + document.getElementById('editor.keyMap').innerHTML = optionsHtmlFromArray(Object.keys(CM.keyMap).sort()); + document.getElementById('options').addEventListener('change', acmeEventListener, false); + setupLivePrefs(); + + hotkeyRerouter.setState(true); +} + +function acmeEventListener(event) { + const el = event.target; + const option = el.id.replace(/^editor\./, ''); + //console.log('acmeEventListener heard %s on %s', event.type, el.id); + if (!option) { + console.error('acmeEventListener: no "cm_option" %O', el); + return; + } + let value = el.type === 'checkbox' ? el.checked : el.value; + switch (option) { + case 'tabSize': + CodeMirror.setOption('indentUnit', Number(value)); + break; + case 'theme': { + const themeLink = document.getElementById('cm-theme'); + // use non-localized 'default' internally + if (!value || value === 'default' || value === t('defaultTheme')) { + value = 'default'; + if (prefs.get(el.id) !== value) { + prefs.set(el.id, value); + } + themeLink.href = ''; + el.selectedIndex = 0; + break; + } + const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css'); + if (themeLink.href === url) { // preloaded in initCodeMirror() + break; + } + // avoid flicker: wait for the second stylesheet to load, then apply the theme + document.head.insertAdjacentHTML('beforeend', + ''); + (() => { + setTimeout(() => { + CodeMirror.setOption(option, value); + themeLink.remove(); + document.getElementById('cm-theme2').id = 'cm-theme'; + }, 100); + })(); + return; + } + case 'autocompleteOnTyping': + editors.forEach(cm => { + const onOff = el.checked ? 'on' : 'off'; + cm[onOff]('change', autocompleteOnTyping); + cm[onOff]('pick', autocompletePicked); + }); + return; + case 'matchHighlight': + switch (value) { + case 'token': + case 'selection': + document.body.dataset[option] = value; + value = {showToken: value === 'token' && /[#.\-\w]/, annotateScrollbar: true}; + break; + default: + value = null; + } + } + CodeMirror.setOption(option, value); +} + +// replace given textarea with the CodeMirror editor +function setupCodeMirror(textarea, index) { + const cm = CodeMirror.fromTextArea(textarea, {lint: null}); + const wrapper = cm.display.wrapper; + + cm.on('change', indicateCodeChange); + if (prefs.get('editor.autocompleteOnTyping')) { + cm.on('change', autocompleteOnTyping); + cm.on('pick', autocompletePicked); + } + cm.on('blur', () => { + editors.lastActive = cm; + hotkeyRerouter.setState(true); + setTimeout(() => { + wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement)); + }); + }); + cm.on('focus', () => { + hotkeyRerouter.setState(false); + wrapper.classList.add('CodeMirror-active'); + }); + cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event)); + + let lastClickTime = 0; + const resizeGrip = wrapper.appendChild(template.resizeGrip.cloneNode(true)); + resizeGrip.onmousedown = event => { + if (event.button !== 0) { + return; + } + event.preventDefault(); + if (Date.now() - lastClickTime < 500) { + lastClickTime = 0; + toggleSectionHeight(cm); + return; + } + lastClickTime = Date.now(); + const minHeight = cm.defaultTextHeight() + + cm.display.lineDiv.offsetParent.offsetTop + /* .CodeMirror-lines padding */ + wrapper.offsetHeight - wrapper.clientHeight; /* borders */ + wrapper.style.pointerEvents = 'none'; + document.body.style.cursor = 's-resize'; + function resize(e) { + const cmPageY = wrapper.getBoundingClientRect().top + window.scrollY; + const height = Math.max(minHeight, e.pageY - cmPageY); + if (height !== wrapper.clientHeight) { + cm.setSize(null, height); + } + } + document.addEventListener('mousemove', resize); + document.addEventListener('mouseup', function resizeStop() { + document.removeEventListener('mouseup', resizeStop); + document.removeEventListener('mousemove', resize); + wrapper.style.pointerEvents = ''; + document.body.style.cursor = ''; + }); + }; + + editors.splice(index || editors.length, 0, cm); + return cm; +} + +function indicateCodeChange(cm) { + const section = cm.getSection(); + setCleanItem(section, cm.isClean(section.savedValue)); + updateTitle(); + updateLintReport(cm); +} + +function getSectionForChild(e) { + return e.closest('#sections > div'); +} + +function getSections() { + return document.querySelectorAll('#sections > div'); +} + +// remind Chrome to repaint a previously invisible editor box by toggling any element's transform +// this bug is present in some versions of Chrome (v37-40 or something) +document.addEventListener('scroll', () => { + const style = document.getElementById('name').style; + style.webkitTransform = style.webkitTransform ? '' : 'scale(1)'; +}); + +// Shift-Ctrl-Wheel scrolls entire page even when mouse is over a code editor +document.addEventListener('wheel', event => { + if (event.shiftKey && event.ctrlKey && !event.altKey && !event.metaKey) { + // Chrome scrolls horizontally when Shift is pressed but on some PCs this might be different + window.scrollBy(0, event.deltaX || event.deltaY); + event.preventDefault(); + } +}); + +queryTabs({currentWindow: true}).then(tabs => { + const windowId = tabs[0].windowId; + if (prefs.get('openEditInWindow')) { + if ( + sessionStorage.saveSizeOnClose && + 'left' in prefs.get('windowPosition', {}) && + !isWindowMaximized() + ) { + // window was reopened via Ctrl-Shift-T etc. + chrome.windows.update(windowId, prefs.get('windowPosition')); + } + if (tabs.length === 1 && window.history.length === 1) { + chrome.windows.getAll(windows => { + if (windows.length > 1) { + sessionStorageHash('saveSizeOnClose').set(windowId, true); + saveSizeOnClose = true; + } + }); + } else { + saveSizeOnClose = sessionStorageHash('saveSizeOnClose').value[windowId]; + } + } + chrome.tabs.onRemoved.addListener((tabId, info) => { + sessionStorageHash('manageStylesHistory').unset(tabId); + if (info.windowId === windowId && info.isWindowClosing) { + sessionStorageHash('saveSizeOnClose').unset(windowId); + } + }); +}); + +getActiveTab().then(tab => { + useHistoryBack = sessionStorageHash('manageStylesHistory').value[tab.id] === location.href; +}); + +function goBackToManage(event) { + if (useHistoryBack) { + event.stopPropagation(); + event.preventDefault(); + history.back(); + } else if (styleId) { + sessionStorage.justEditedStyleId = styleId; + } +} + +function isWindowMaximized() { + return window.screenLeft === 0 && + window.screenTop === 0 && + window.outerWidth === screen.availWidth && + window.outerHeight === screen.availHeight; +} + +window.onbeforeunload = () => { + if (saveSizeOnClose && !isWindowMaximized()) { + prefs.set('windowPosition', { + left: screenLeft, + top: screenTop, + width: outerWidth, + height: outerHeight + }); + } + document.activeElement.blur(); + if (isCleanGlobal()) { + return; + } + updateLintReport(null, 0); + return confirm(t('styleChangesNotSaved')); +}; + +function addAppliesTo(list, name, value) { + const showingEverything = list.querySelector('.applies-to-everything') !== null; + // blow away 'Everything' if it's there + if (showingEverything) { + list.removeChild(list.firstChild); + } + let e; + if (name && value) { + e = template.appliesTo.cloneNode(true); + e.querySelector('[name=applies-type]').value = name; + e.querySelector('[name=applies-value]').value = value; + e.querySelector('.remove-applies-to').addEventListener('click', removeAppliesTo, false); + } else if (showingEverything || list.hasChildNodes()) { + e = template.appliesTo.cloneNode(true); + if (list.hasChildNodes()) { + e.querySelector('[name=applies-type]').value = list.querySelector('li:last-child [name="applies-type"]').value; + } + e.querySelector('.remove-applies-to').addEventListener('click', removeAppliesTo, false); + } else { + e = template.appliesToEverything.cloneNode(true); + } + e.querySelector('.add-applies-to').addEventListener('click', function () { + addAppliesTo(this.parentNode.parentNode); + }, false); + list.appendChild(e); +} + +function addSection(event, section) { + const div = template.section.cloneNode(true); + div.querySelector('.applies-to-help').addEventListener('click', showAppliesToHelp, false); + div.querySelector('.remove-section').addEventListener('click', removeSection, false); + div.querySelector('.add-section').addEventListener('click', addSection, false); + div.querySelector('.beautify-section').addEventListener('click', beautify); + + const codeElement = div.querySelector('.code'); + const appliesTo = div.querySelector('.applies-to-list'); + let appliesToAdded = false; + + if (section) { + codeElement.value = section.code; + for (const i in propertyToCss) { + if (section[i]) { + section[i].forEach(url => { + addAppliesTo(appliesTo, propertyToCss[i], url); + appliesToAdded = true; + }); + } + } + } + if (!appliesToAdded) { + addAppliesTo(appliesTo); + } + + appliesTo.addEventListener('change', onChange); + appliesTo.addEventListener('input', onChange); + + toggleTestRegExpVisibility(); + appliesTo.addEventListener('change', toggleTestRegExpVisibility); + div.querySelector('.test-regexp').onclick = showRegExpTester; + function toggleTestRegExpVisibility() { + const show = [...appliesTo.children].some(item => + !item.matches('.applies-to-everything') && + item.querySelector('.applies-type').value === 'regexp' && + item.querySelector('.applies-value').value.trim()); + div.classList.toggle('has-regexp', show); + appliesTo.oninput = appliesTo.oninput || show && (event => { + if ( + event.target.matches('.applies-value') && + event.target.parentElement.querySelector('.applies-type').value === 'regexp' + ) { + showRegExpTester(null, div); + } + }); + } + + const sections = document.getElementById('sections'); + let cm; + if (event) { + const clickedSection = getSectionForChild(event.target); + sections.insertBefore(div, clickedSection.nextElementSibling); + const newIndex = getSections().indexOf(clickedSection) + 1; + cm = setupCodeMirror(codeElement, newIndex); + makeSectionVisible(cm); + cm.focus(); + renderLintReport(); + } else { + sections.appendChild(div); + cm = setupCodeMirror(codeElement); + } + + div.CodeMirror = cm; + setCleanSection(div); + return div; +} + +function removeAppliesTo(event) { + const appliesTo = event.target.parentNode; + const appliesToList = appliesTo.parentNode; + removeAreaAndSetDirty(appliesTo); + if (!appliesToList.hasChildNodes()) { + addAppliesTo(appliesToList); + } +} + +function removeSection(event) { + const section = getSectionForChild(event.target); + const cm = section.CodeMirror; + removeAreaAndSetDirty(section); + editors.splice(editors.indexOf(cm), 1); + renderLintReport(); +} + +function removeAreaAndSetDirty(area) { + const contributors = area.querySelectorAll('.style-contributor'); + if (!contributors.length) { + setCleanItem(area, false); + } + contributors.some(node => { + if (node.savedValue) { + // it's a saved section, so make it dirty and stop the enumeration + setCleanItem(area, false); + return true; + } else { + // it's an empty section, so undirty the applies-to items, + // otherwise orphaned ids would keep the style dirty + setCleanItem(node, true); + } + }); + updateTitle(); + area.parentNode.removeChild(area); +} + +function makeSectionVisible(cm) { + const section = cm.getSection(); + const bounds = section.getBoundingClientRect(); + if ( + (bounds.bottom > window.innerHeight && bounds.top > 0) || + (bounds.top < 0 && bounds.bottom < window.innerHeight) + ) { + if (bounds.top < 0) { + window.scrollBy(0, bounds.top - 1); + } else { + window.scrollBy(0, bounds.bottom - window.innerHeight + 1); + } + } +} + +function setupGlobalSearch() { + const originalCommand = { + find: CodeMirror.commands.find, + findNext: CodeMirror.commands.findNext, + findPrev: CodeMirror.commands.findPrev, + replace: CodeMirror.commands.replace + }; + const originalOpenDialog = CodeMirror.prototype.openDialog; + const originalOpenConfirm = CodeMirror.prototype.openConfirm; + + let curState; // cm.state.search for last used 'find' + + function shouldIgnoreCase(query) { // treat all-lowercase non-regexp queries as case-insensitive + return typeof query === 'string' && query === query.toLowerCase(); + } + + function updateState(cm, newState) { + if (!newState) { + if (cm.state.search) { + return cm.state.search; + } + if (!curState) { + return null; + } + newState = curState; + } + cm.state.search = { + query: newState.query, + overlay: newState.overlay, + annotate: cm.showMatchesOnScrollbar(newState.query, shouldIgnoreCase(newState.query)) + }; + cm.addOverlay(newState.overlay); + return cm.state.search; + } + + // temporarily overrides the original openDialog with the provided template's innerHTML + function customizeOpenDialog(cm, template, callback) { + cm.openDialog = (tmpl, cb, opt) => { + // invoke 'callback' and bind 'this' to the original callback + originalOpenDialog.call(cm, template.innerHTML, callback.bind(cb), opt); + }; + setTimeout(() => { cm.openDialog = originalOpenDialog; }, 0); + refocusMinidialog(cm); + } + + function focusClosestCM(activeCM) { + editors.lastActive = activeCM; + const cm = getEditorInSight(); + if (cm !== activeCM) { + cm.focus(); + } + return cm; + } + + function find(activeCM) { + activeCM = focusClosestCM(activeCM); + customizeOpenDialog(activeCM, template.find, function (query) { + this(query); + curState = activeCM.state.search; + if (editors.length === 1 || !curState.query) { + return; + } + editors.forEach(cm => { + if (cm !== activeCM) { + cm.execCommand('clearSearch'); + updateState(cm, curState); + } + }); + if (CodeMirror.cmpPos(curState.posFrom, curState.posTo) === 0) { + findNext(activeCM); + } + }); + originalCommand.find(activeCM); + } + + function findNext(activeCM, reverse) { + let state = updateState(activeCM); + if (!state || !state.query) { + find(activeCM); + return; + } + let pos = activeCM.getCursor(reverse ? 'from' : 'to'); + activeCM.setSelection(activeCM.getCursor()); // clear the selection, don't move the cursor + + const rxQuery = typeof state.query === 'object' + ? state.query : stringAsRegExp(state.query, shouldIgnoreCase(state.query) ? 'i' : ''); + + if ( + document.activeElement && + document.activeElement.name === 'applies-value' && + searchAppliesTo(activeCM) + ) { + return; + } + let cm = activeCM; + for (let i = 0; i < editors.length; i++) { + state = updateState(cm); + if (!cm.hasFocus()) { + pos = reverse ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(0, 0); + } + const searchCursor = cm.getSearchCursor(state.query, pos, shouldIgnoreCase(state.query)); + if (searchCursor.find(reverse)) { + if (editors.length > 1) { + makeSectionVisible(cm); + cm.focus(); + } + // speedup the original findNext + state.posFrom = reverse ? searchCursor.to() : searchCursor.from(); + state.posTo = CodeMirror.Pos(state.posFrom.line, state.posFrom.ch); + originalCommand[reverse ? 'findPrev' : 'findNext'](cm); + return; + } else if (!reverse && searchAppliesTo(cm)) { + return; + } + cm = editors[(editors.indexOf(cm) + (reverse ? -1 + editors.length : 1)) % editors.length]; + if (reverse && searchAppliesTo(cm)) { + return; + } + } + // nothing found so far, so call the original search with wrap-around + originalCommand[reverse ? 'findPrev' : 'findNext'](activeCM); + + function searchAppliesTo(cm) { + let inputs = [].slice.call(cm.getSection().querySelectorAll('.applies-value')); + if (reverse) { + inputs = inputs.reverse(); + } + inputs.splice(0, inputs.indexOf(document.activeElement) + 1); + return inputs.some(input => { + const match = rxQuery.exec(input.value); + if (match) { + input.focus(); + const end = match.index + match[0].length; + // scroll selected part into view in long inputs, + // works only outside of current event handlers chain, hence timeout=0 + setTimeout(() => { + input.setSelectionRange(end, end); + input.setSelectionRange(match.index, end); + }, 0); + return true; + } + }); + } + } + + function findPrev(cm) { + findNext(cm, true); + } + + function replace(activeCM, all) { + let queue; + let query; + let replacement; + activeCM = focusClosestCM(activeCM); + customizeOpenDialog(activeCM, template[all ? 'replaceAll' : 'replace'], txt => { + query = txt; + customizeOpenDialog(activeCM, template.replaceWith, txt => { + replacement = txt; + queue = editors.rotate(-editors.indexOf(activeCM)); + if (all) { + editors.forEach(doReplace); + } else { + doReplace(); + } + }); + this(query); + }); + originalCommand.replace(activeCM, all); + + function doReplace() { + const cm = queue.shift(); + if (!cm) { + if (!all) { + editors.lastActive.focus(); + } + return; + } + // hide the first two dialogs (replace, replaceWith) + cm.openDialog = (tmpl, callback) => { + cm.openDialog = (tmpl, callback) => { + cm.openDialog = originalOpenDialog; + if (all) { + callback(replacement); + } else { + doConfirm(cm); + callback(replacement); + if (!cm.getWrapperElement().querySelector('.CodeMirror-dialog')) { + // no dialog == nothing found in the current CM, move to the next + doReplace(); + } + } + }; + callback(query); + }; + originalCommand.replace(cm, all); + } + function doConfirm(cm) { + let wrapAround = false; + const origPos = cm.getCursor(); + cm.openConfirm = function overrideConfirm(tmpl, callbacks, opt) { + const ovrCallbacks = callbacks.map(callback => () => { + makeSectionVisible(cm); + cm.openConfirm = overrideConfirm; + setTimeout(() => { cm.openConfirm = originalOpenConfirm; }, 0); + + const pos = cm.getCursor(); + callback(); + const cmp = CodeMirror.cmpPos(cm.getCursor(), pos); + wrapAround |= cmp <= 0; + + const dlg = cm.getWrapperElement().querySelector('.CodeMirror-dialog'); + if (!dlg || cmp === 0 || wrapAround && CodeMirror.cmpPos(cm.getCursor(), origPos) >= 0) { + if (dlg) { + dlg.remove(); + } + doReplace(); + } + }); + originalOpenConfirm.call(cm, template.replaceConfirm.innerHTML, ovrCallbacks, opt); + }; + } + } + + function replaceAll(cm) { + replace(cm, true); + } + + CodeMirror.commands.find = find; + CodeMirror.commands.findNext = findNext; + CodeMirror.commands.findPrev = findPrev; + CodeMirror.commands.replace = replace; + CodeMirror.commands.replaceAll = replaceAll; +} + +function jumpToLine(cm) { + const cur = cm.getCursor(); + refocusMinidialog(cm); + cm.openDialog(template.jumpToLine.innerHTML, str => { + const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/); + if (m) { + cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch); + } + }, {value: cur.line + 1}); +} + +function toggleStyle() { + $('#enabled').checked = !$('#enabled').checked; + save(); +} + +function toggleSectionHeight(cm) { + if (cm.state.toggleHeightSaved) { + // restore previous size + cm.setSize(null, cm.state.toggleHeightSaved); + cm.state.toggleHeightSaved = 0; + } else { + // maximize + const wrapper = cm.display.wrapper; + const allBounds = $('#sections').getBoundingClientRect(); + const pageExtrasHeight = allBounds.top + window.scrollY + + parseFloat(getComputedStyle($('#sections')).paddingBottom); + const sectionExtrasHeight = cm.getSection().clientHeight - wrapper.offsetHeight; + cm.state.toggleHeightSaved = wrapper.clientHeight; + cm.setSize(null, window.innerHeight - sectionExtrasHeight - pageExtrasHeight); + const bounds = cm.getSection().getBoundingClientRect(); + if (bounds.top < 0 || bounds.bottom > window.innerHeight) { + window.scrollBy(0, bounds.top); + } + } +} + +function autocompleteOnTyping(cm, info, debounced) { + if ( + cm.state.completionActive || + info.origin && !info.origin.includes('input') || + !info.text.last + ) { + return; + } + if (cm.state.autocompletePicked) { + cm.state.autocompletePicked = false; + return; + } + if (!debounced) { + debounce(autocompleteOnTyping, 100, cm, info, true); + return; + } + if (info.text.last.match(/[-\w!]+$/)) { + cm.state.autocompletePicked = false; + cm.options.hintOptions.completeSingle = false; + cm.execCommand('autocomplete'); + setTimeout(() => { + cm.options.hintOptions.completeSingle = true; + }); + } +} + +function autocompletePicked(cm) { + cm.state.autocompletePicked = true; +} + +function refocusMinidialog(cm) { + const section = cm.getSection(); + if (!section.querySelector('.CodeMirror-dialog')) { + return; + } + // close the currently opened minidialog + cm.focus(); + // make sure to focus the input in newly opened minidialog + setTimeout(() => { + section.querySelector('.CodeMirror-dialog').focus(); + }, 0); +} + +function nextPrevEditor(cm, direction) { + cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length]; + makeSectionVisible(cm); + cm.focus(); +} + +function getEditorInSight(nearbyElement) { + // priority: 1. associated CM for applies-to element 2. last active if visible 3. first visible + let cm; + if (nearbyElement && nearbyElement.className.indexOf('applies-') >= 0) { + cm = getSectionForChild(nearbyElement).CodeMirror; + } else { + cm = editors.lastActive; + } + if (!cm || offscreenDistance(cm) > 0) { + const sorted = editors + .map((cm, index) => ({cm: cm, distance: offscreenDistance(cm), index: index})) + .sort((a, b) => a.distance - b.distance || a.index - b.index); + cm = sorted[0].cm; + if (sorted[0].distance > 0) { + makeSectionVisible(cm); + } + } + return cm; + + function offscreenDistance(cm) { + const LINES_VISIBLE = 2; // closest editor should have at least # lines visible + const bounds = cm.getSection().getBoundingClientRect(); + if (bounds.top < 0) { + return -bounds.top; + } else if (bounds.top < window.innerHeight - cm.defaultTextHeight() * LINES_VISIBLE) { + return 0; + } else { + return bounds.top - bounds.height; + } + } +} + +function updateLintReport(cm, delay) { + if (delay === 0) { + // immediately show pending csslint messages in onbeforeunload and save + update(cm); + return; + } + if (delay > 0) { + setTimeout(cm => { cm.performLint(); update(cm); }, delay, cm); + return; + } + // eslint-disable-next-line no-var + var state = cm.state.lint; + if (!state) { + return; + } + // user is editing right now: postpone updating the report for the new issues (default: 500ms lint + 4500ms) + // or update it as soon as possible (default: 500ms lint + 100ms) in case an existing issue was just fixed + clearTimeout(state.reportTimeout); + state.reportTimeout = setTimeout(update, state.options.delay + 100, cm); + state.postponeNewIssues = delay === undefined || delay === null; + + function update(cm) { + const scope = cm ? [cm] : editors; + let changed = false; + let fixedOldIssues = false; + scope.forEach(cm => { + const scopedState = cm.state.lint || {}; + const oldMarkers = scopedState.markedLast || {}; + const newMarkers = {}; + const html = !scopedState.marked || scopedState.marked.length === 0 ? '' : '' + + scopedState.marked.map(mark => { + const info = mark.__annotation; + const isActiveLine = info.from.line === cm.getCursor().line; + const pos = isActiveLine ? 'cursor' : (info.from.line + ',' + info.from.ch); + let message = escapeHtml(info.message.replace(/ at line \d.+$/, '')); + if (message.length > 100) { + message = message.substr(0, 100) + '...'; + } + if (isActiveLine || oldMarkers[pos] === message) { + delete oldMarkers[pos]; + } + newMarkers[pos] = message; + return '' + + '' + + info.severity + '' + + '' + (info.from.line + 1) + '' + + ':' + + '' + (info.from.ch + 1) + '' + + '' + message + ''; + }).join('') + ''; + scopedState.markedLast = newMarkers; + fixedOldIssues |= scopedState.reportDisplayed && Object.keys(oldMarkers).length > 0; + if (scopedState.html !== html) { + scopedState.html = html; + changed = true; + } + }); + if (changed) { + clearTimeout(state ? state.renderTimeout : undefined); + if (!state || !state.postponeNewIssues || fixedOldIssues) { + renderLintReport(true); + } else { + state.renderTimeout = setTimeout(() => { + renderLintReport(true); + }, CodeMirror.defaults.lintReportDelay); + } + } + } + function escapeHtml(html) { + const chars = {'&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/'}; + return html.replace(/[&<>"'/]/g, char => chars[char]); + } +} + +function renderLintReport(someBlockChanged) { + const container = document.getElementById('lint'); + const content = container.children[1]; + const label = t('sectionCode'); + const newContent = content.cloneNode(false); + let issueCount = 0; + editors.forEach((cm, index) => { + if (cm.state.lint && cm.state.lint.html) { + const newBlock = newContent.appendChild(document.createElement('table')); + const html = '' + label + ' ' + (index + 1) + '' + cm.state.lint.html; + newBlock.innerHTML = html; + newBlock.cm = cm; + issueCount += newBlock.rows.length; + + const block = content.children[newContent.children.length - 1]; + const blockChanged = !block || cm !== block.cm || html !== block.innerHTML; + someBlockChanged |= blockChanged; + cm.state.lint.reportDisplayed = blockChanged; + } + }); + if (someBlockChanged || newContent.children.length !== content.children.length) { + document.getElementById('issue-count').textContent = issueCount; + container.replaceChild(newContent, content); + container.style.display = newContent.children.length ? 'block' : 'none'; + resizeLintReport(null, newContent); + } +} + +function resizeLintReport(event, content) { + content = content || document.getElementById('lint').children[1]; + if (content.children.length) { + const bounds = content.getBoundingClientRect(); + const newMaxHeight = bounds.bottom <= innerHeight ? '' : (innerHeight - bounds.top) + 'px'; + if (newMaxHeight !== content.style.maxHeight) { + content.style.maxHeight = newMaxHeight; + } + } +} + +function gotoLintIssue(event) { + const issue = event.target.closest('tr'); + if (!issue) { + return; + } + const block = issue.closest('table'); + makeSectionVisible(block.cm); + block.cm.focus(); + block.cm.setSelection({ + line: parseInt(issue.querySelector('td[role="line"]').textContent) - 1, + ch: parseInt(issue.querySelector('td[role="col"]').textContent) - 1 + }); +} + +function toggleLintReport() { + document.getElementById('lint').classList.toggle('collapsed'); +} + +function beautify(event) { + if (exports.css_beautify) { // thanks to csslint's definition of 'exports' + doBeautify(); + } else { + const script = document.head.appendChild(document.createElement('script')); + script.src = 'vendor-overwrites/beautify/beautify-css-mod.js'; + script.onload = doBeautify; + } + function doBeautify() { + const tabs = prefs.get('editor.indentWithTabs'); + const options = prefs.get('editor.beautify'); + options.indent_size = tabs ? 1 : prefs.get('editor.tabSize'); + options.indent_char = tabs ? '\t' : ' '; + + const section = getSectionForChild(event.target); + const scope = section ? [section.CodeMirror] : editors; + + showHelp(t('styleBeautify'), '
' + + optionHtml('.selector1,', 'selector_separator_newline') + + optionHtml('.selector2,', 'newline_before_open_brace') + + optionHtml('{', 'newline_after_open_brace') + + optionHtml('border: none;', 'newline_between_properties', true) + + optionHtml('display: block;', 'newline_before_close_brace', true) + + optionHtml('}', 'newline_between_rules') + + `' + + '
' + + '
'); + + const undoButton = document.querySelector('#help-popup button[role="undo"]'); + undoButton.textContent = t(scope.length === 1 ? 'undo' : 'undoGlobal'); + undoButton.addEventListener('click', () => { + let undoable = false; + scope.forEach(cm => { + if (cm.beautifyChange && cm.beautifyChange[cm.changeGeneration()]) { + delete cm.beautifyChange[cm.changeGeneration()]; + cm.undo(); + cm.scrollIntoView(cm.getCursor()); + undoable |= cm.beautifyChange[cm.changeGeneration()]; + } + }); + undoButton.disabled = !undoable; + }); + + scope.forEach(cm => { + setTimeout(() => { + const pos = options.translate_positions = + [].concat.apply([], cm.doc.sel.ranges.map(r => + [Object.assign({}, r.anchor), Object.assign({}, r.head)])); + const text = cm.getValue(); + const newText = exports.css_beautify(text, options); + if (newText !== text) { + if (!cm.beautifyChange || !cm.beautifyChange[cm.changeGeneration()]) { + // clear the list if last change wasn't a css-beautify + cm.beautifyChange = {}; + } + cm.setValue(newText); + const selections = []; + for (let i = 0; i < pos.length; i += 2) { + selections.push({anchor: pos[i], head: pos[i + 1]}); + } + cm.setSelections(selections); + cm.beautifyChange[cm.changeGeneration()] = true; + undoButton.disabled = false; + } + }, 0); + }); + + document.querySelector('.beautify-options').onchange = ({target}) => { + const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0; + prefs.set('editor.beautify', Object.assign(options, {[target.dataset.option]: value})); + if (target.parentNode.hasAttribute('newline')) { + target.parentNode.setAttribute('newline', value.toString()); + } + doBeautify(); + }; + + function optionHtml(label, optionName, indent) { + const value = options[optionName]; + return '
' + + '' + label + '' + + '
'; + } + } +} + +document.addEventListener('DOMContentLoaded', init); + +function init() { + initCodeMirror(); + const params = getParams(); + if (!params.id) { // match should be 2 - one for the whole thing, one for the parentheses + // This is an add + tE('heading', 'addStyleTitle'); + const section = {code: ''}; + for (const i in CssToProperty) { + if (params[i]) { + section[CssToProperty[i]] = [params[i]]; + } + } + window.onload = () => { + window.onload = null; + addSection(null, section); + editors[0].setOption('lint', CodeMirror.defaults.lint); + // default to enabled + document.getElementById('enabled').checked = true; + initHooks(); + }; + return; + } + // This is an edit + tE('heading', 'editStyleHeading', null, false); + getStylesSafe({id: params.id}).then(styles => { + let style = styles[0]; + if (!style) { + style = {id: null, sections: []}; + history.replaceState({}, document.title, location.pathname); + } + styleId = style.id; + setStyleMeta(style); + window.onload = () => { + window.onload = null; + initWithStyle({style}); + }; + if (document.readyState !== 'loading') { + window.onload(); + } + }); +} + +function setStyleMeta(style) { + document.getElementById('name').value = style.name; + document.getElementById('enabled').checked = style.enabled; + document.getElementById('url').href = style.url; +} + +function initWithStyle({style, codeIsUpdated}) { + setStyleMeta(style); + + if (codeIsUpdated === false) { + setCleanGlobal(); + updateTitle(); + return; + } + + // if this was done in response to an update, we need to clear existing sections + getSections().forEach(div => { div.remove(); }); + const queue = style.sections.length ? style.sections.slice() : [{code: ''}]; + const queueStart = new Date().getTime(); + // after 100ms the sections will be added asynchronously + while (new Date().getTime() - queueStart <= 100 && queue.length) { + add(); + } + (function processQueue() { + if (queue.length) { + add(); + setTimeout(processQueue, 0); + } + })(); + initHooks(); + + function add() { + const sectionDiv = addSection(null, queue.shift()); + maximizeCodeHeight(sectionDiv, !queue.length); + const cm = sectionDiv.CodeMirror; + setTimeout(() => { + cm.setOption('lint', CodeMirror.defaults.lint); + updateLintReport(cm, 0); + }, prefs.get('editor.lintDelay')); + } +} + +function initHooks() { + document.querySelectorAll('#header .style-contributor').forEach(node => { + node.addEventListener('change', onChange); + node.addEventListener('input', onChange); + }); + document.getElementById('toggle-style-help').addEventListener('click', showToggleStyleHelp); + document.getElementById('to-mozilla').addEventListener('click', showMozillaFormat, false); + document.getElementById('to-mozilla-help').addEventListener('click', showToMozillaHelp, false); + document.getElementById('from-mozilla').addEventListener('click', fromMozillaFormat); + document.getElementById('beautify').addEventListener('click', beautify); + document.getElementById('save-button').addEventListener('click', save, false); + document.getElementById('sections-help').addEventListener('click', showSectionHelp, false); + document.getElementById('keyMap-help').addEventListener('click', showKeyMapHelp, false); + document.getElementById('cancel-button').addEventListener('click', goBackToManage); + document.getElementById('lint-help').addEventListener('click', showLintHelp); + document.getElementById('lint').addEventListener('click', gotoLintIssue); + window.addEventListener('resize', resizeLintReport); + + // touch devices don't have onHover events so the element we'll be toggled via clicking (touching) + if ('ontouchstart' in document.body) { + document.querySelector('#lint h2').addEventListener('click', toggleLintReport); + } + + document.querySelectorAll( + 'input:not([type]), input[type="text"], input[type="search"], input[type="number"]') + .forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete)); + + setupGlobalSearch(); + setCleanGlobal(); + updateTitle(); +} + + +function toggleContextMenuDelete(event) { + if (event.button === 2 && prefs.get('editor.contextDelete')) { + chrome.contextMenus.update('editor.contextDelete', { + enabled: Boolean( + this.selectionStart !== this.selectionEnd || + this.somethingSelected && this.somethingSelected() + ), + }, ignoreChromeError); + } +} + + +function maximizeCodeHeight(sectionDiv, isLast) { + const cm = sectionDiv.CodeMirror; + const stats = maximizeCodeHeight.stats = maximizeCodeHeight.stats || {totalHeight: 0, deltas: []}; + if (!stats.cmActualHeight) { + stats.cmActualHeight = getComputedHeight(cm.display.wrapper); + } + if (!stats.sectionMarginTop) { + stats.sectionMarginTop = parseFloat(getComputedStyle(sectionDiv).marginTop); + } + const sectionTop = sectionDiv.getBoundingClientRect().top - stats.sectionMarginTop; + if (!stats.firstSectionTop) { + stats.firstSectionTop = sectionTop; + } + const extrasHeight = getComputedHeight(sectionDiv) - stats.cmActualHeight; + const cmMaxHeight = window.innerHeight - extrasHeight - sectionTop - stats.sectionMarginTop; + const cmDesiredHeight = cm.display.sizer.clientHeight + 2 * cm.defaultTextHeight(); + const cmGrantableHeight = Math.max(stats.cmActualHeight, Math.min(cmMaxHeight, cmDesiredHeight)); + stats.deltas.push(cmGrantableHeight - stats.cmActualHeight); + stats.totalHeight += cmGrantableHeight + extrasHeight; + if (!isLast) { + return; + } + stats.totalHeight += stats.firstSectionTop; + if (stats.totalHeight <= window.innerHeight) { + editors.forEach((cm, index) => { + cm.setSize(null, stats.deltas[index] + stats.cmActualHeight); + }); + return; + } + // scale heights to fill the gap between last section and bottom edge of the window + const sections = document.getElementById('sections'); + const available = window.innerHeight - sections.getBoundingClientRect().bottom - + parseFloat(getComputedStyle(sections).marginBottom); + if (available <= 0) { + return; + } + const totalDelta = stats.deltas.reduce((sum, d) => sum + d, 0); + const q = available / totalDelta; + const baseHeight = stats.cmActualHeight - stats.sectionMarginTop; + stats.deltas.forEach((delta, index) => { + editors[index].setSize(null, baseHeight + Math.floor(q * delta)); + }); +} + +function updateTitle() { + const DIRTY_TITLE = '* $'; + + const name = document.getElementById('name').savedValue; + const clean = isCleanGlobal(); + const title = styleId === null ? t('addStyleTitle') : t('editStyleTitle', [name]); + document.title = clean ? title : DIRTY_TITLE.replace('$', title); +} + +function validate() { + const name = document.getElementById('name').value; + if (name === '') { + return t('styleMissingName'); + } + // validate the regexps + if (document.querySelectorAll('.applies-to-list').some(list => { + list.childNodes.some(li => { + if (li.className === template.appliesToEverything.className) { + return false; + } + const valueElement = li.querySelector('[name=applies-value]'); + const type = li.querySelector('[name=applies-type]').value; + const value = valueElement.value; + if (type && value) { + if (type === 'regexp') { + try { + new RegExp(value); + } catch (ex) { + valueElement.focus(); + return true; + } + } + } + return false; + }); + })) { + return t('styleBadRegexp'); + } + return null; +} + +function save() { + updateLintReport(null, 0); + + // save the contents of the CodeMirror editors back into the textareas + for (let i = 0; i < editors.length; i++) { + editors[i].save(); + } + + const error = validate(); + if (error) { + alert(error); + return; + } + const name = document.getElementById('name').value; + const enabled = document.getElementById('enabled').checked; + saveStyleSafe({ + id: styleId, + name: name, + enabled: enabled, + reason: 'editSave', + sections: getSectionsHashes() + }) + .then(saveComplete); +} + +function getSectionsHashes() { + const sections = []; + getSections().forEach(div => { + const meta = getMeta(div); + const code = div.CodeMirror.getValue(); + if (/^\s*$/.test(code) && Object.keys(meta).length === 0) { + return; + } + meta.code = code; + sections.push(meta); + }); + return sections; +} + +function getMeta(e) { + const meta = {urls: [], urlPrefixes: [], domains: [], regexps: []}; + e.querySelector('.applies-to-list').childNodes.forEach(li => { + if (li.className === template.appliesToEverything.className) { + return; + } + const type = li.querySelector('[name=applies-type]').value; + const value = li.querySelector('[name=applies-value]').value; + if (type && value) { + const property = CssToProperty[type]; + meta[property].push(value); + } + }); + return meta; +} + +function saveComplete(style) { + styleId = style.id; + setCleanGlobal(); + + // Go from new style URL to edit style URL + if (location.href.indexOf('id=') === -1) { + history.replaceState({}, document.title, 'edit.html?id=' + style.id); + tE('heading', 'editStyleHeading', null, false); + } + updateTitle(); +} + +function showMozillaFormat() { + const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true}); + popup.codebox.setValue(toMozillaFormat()); + popup.codebox.execCommand('selectAll'); +} + +function toMozillaFormat() { + return getSectionsHashes().map(section => { + let cssMds = []; + for (const i in propertyToCss) { + if (section[i]) { + cssMds = cssMds.concat(section[i].map(v => + propertyToCss[i] + '("' + v.replace(/\\/g, '\\\\') + '")' + )); + } + } + return cssMds.length ? '@-moz-document ' + cssMds.join(', ') + ' {\n' + section.code + '\n}' : section.code; + }).join('\n\n'); +} + +function fromMozillaFormat() { + const popup = showCodeMirrorPopup(t('styleFromMozillaFormatPrompt'), tHTML(`
+ + +
` + ).innerHTML); + + const contents = popup.querySelector('.contents'); + contents.insertBefore(popup.codebox.display.wrapper, contents.firstElementChild); + popup.codebox.focus(); + + popup.querySelector('[name="import-append"]').addEventListener('click', doImport); + popup.querySelector('[name="import-replace"]').addEventListener('click', doImport); + + popup.codebox.on('change', () => { + clearTimeout(popup.mozillaTimeout); + popup.mozillaTimeout = setTimeout(() => { + popup.classList.toggle('ready', trimNewLines(popup.codebox.getValue())); + }, 100); + }); + + function doImport() { + const replaceOldStyle = this.name === 'import-replace'; + popup.querySelector('.dismiss').onclick(); + const mozStyle = trimNewLines(popup.codebox.getValue()); + const parser = new parserlib.css.Parser(); + const lines = mozStyle.split('\n'); + const sectionStack = [{code: '', start: {line: 1, col: 1}}]; + let errors = ''; + // let oldSectionCount = editors.length; + let firstAddedCM; + + parser.addListener('startdocument', function (e) { + let outerText = getRange(sectionStack.last.start, (--e.col, e)); + const gapComment = outerText.match(/(\/\*[\s\S]*?\*\/)[\s\n]*$/); + const section = {code: '', start: backtrackTo(this, parserlib.css.Tokens.LBRACE, 'end')}; + // move last comment before @-moz-document inside the section + if (gapComment && !gapComment[1].match(/\/\*\s*AGENT_SHEET\s*\*\//)) { + section.code = gapComment[1] + '\n'; + outerText = trimNewLines(outerText.substring(0, gapComment.index)); + } + if (outerText.trim()) { + sectionStack.last.code = outerText; + doAddSection(sectionStack.last); + sectionStack.last.code = ''; + } + e.functions.forEach(f => { + const m = f.match(/^(url|url-prefix|domain|regexp)\((['"]?)(.+?)\2?\)$/); + const aType = CssToProperty[m[1]]; + const aValue = aType !== 'regexps' ? m[3] : m[3].replace(/\\\\/g, '\\'); + (section[aType] = section[aType] || []).push(aValue); + }); + sectionStack.push(section); + }); + + parser.addListener('enddocument', function () { + const end = backtrackTo(this, parserlib.css.Tokens.RBRACE, 'start'); + const section = sectionStack.pop(); + section.code += getRange(section.start, end); + sectionStack.last.start = (++end.col, end); + doAddSection(section); + }); + + parser.addListener('endstylesheet', () => { + // add nonclosed outer sections (either broken or the last global one) + const endOfText = {line: lines.length, col: lines.last.length + 1}; + sectionStack.last.code += getRange(sectionStack.last.start, endOfText); + sectionStack.forEach(doAddSection); + + delete maximizeCodeHeight.stats; + editors.forEach(cm => { + maximizeCodeHeight(cm.getSection(), cm === editors.last); + }); + + makeSectionVisible(firstAddedCM); + firstAddedCM.focus(); + + if (errors) { + showHelp(t('issues'), errors); + } + }); + + parser.addListener('error', e => { + errors += e.line + ':' + e.col + ' ' + e.message.replace(/ at line \d.+$/, '') + '
'; + }); + + parser.parse(mozStyle); + + function getRange(start, end) { + const L1 = start.line - 1; + const C1 = start.col - 1; + const L2 = end.line - 1; + const C2 = end.col - 1; + if (L1 === L2) { + return lines[L1].substr(C1, C2 - C1 + 1); + } else { + const middle = lines.slice(L1 + 1, L2).join('\n'); + return lines[L1].substr(C1) + '\n' + middle + + (L2 >= lines.length ? '' : ((middle ? '\n' : '') + lines[L2].substring(0, C2))); + } + } + function doAddSection(section) { + section.code = section.code.trim(); + // don't add empty sections + if ( + !section.code && + !section.urls && + !section.urlPrefixes && + !section.domains && + !section.regexps + ) { + return; + } + if (!firstAddedCM) { + if (!initFirstSection(section)) { + return; + } + } + setCleanItem(addSection(null, section), false); + firstAddedCM = firstAddedCM || editors.last; + } + // do onetime housekeeping as the imported text is confirmed to be a valid style + function initFirstSection(section) { + // skip adding the first global section when there's no code/comments + if (!section.code.replace('@namespace url(http://www.w3.org/1999/xhtml);', '') /* ignore boilerplate NS */ + .replace(/[\s\n]/g, '')) { /* ignore all whitespace including new lines */ + return false; + } + if (replaceOldStyle) { + editors.slice(0).reverse().forEach(cm => { + removeSection({target: cm.getSection().firstElementChild}); + }); + } else if (!editors.last.getValue()) { + // nuke the last blank section + if (editors.last.getSection().querySelector('.applies-to-everything')) { + removeSection({target: editors.last.getSection()}); + } + } + return true; + } + } + function backtrackTo(parser, tokenType, startEnd) { + const tokens = parser._tokenStream._lt; + for (let i = parser._tokenStream._ltIndex - 1; i >= 0; --i) { + if (tokens[i].type === tokenType) { + return {line: tokens[i][startEnd + 'Line'], col: tokens[i][startEnd + 'Col']}; + } + } + } + function trimNewLines(s) { + return s.replace(/^[\s\n]+/, '').replace(/[\s\n]+$/, ''); + } +} + +function showSectionHelp() { + showHelp(t('styleSectionsTitle'), t('sectionHelp')); +} + +function showAppliesToHelp() { + showHelp(t('appliesLabel'), t('appliesHelp')); +} + +function showToMozillaHelp() { + showHelp(t('styleMozillaFormatHeading'), t('styleToMozillaFormatHelp')); +} + +function showToggleStyleHelp() { + showHelp(t('helpAlt'), t('styleEnabledToggleHint')); +} + +function showKeyMapHelp() { + const keyMap = mergeKeyMaps({}, prefs.get('editor.keyMap'), CodeMirror.defaults.extraKeys); + const keyMapSorted = Object.keys(keyMap) + .map(key => ({key: key, cmd: keyMap[key]})) + .concat([{key: 'Shift-Ctrl-Wheel', cmd: 'scrollWindow'}]) + .sort((a, b) => (a.cmd < b.cmd || (a.cmd === b.cmd && a.key < b.key) ? -1 : 1)); + showHelp(t('cm_keyMap') + ': ' + prefs.get('editor.keyMap'), + '' + + '' + + '' + + '' + keyMapSorted.map(value => + '' + ).join('') + + '' + + '
' + value.key + '' + value.cmd + '
'); + + const table = document.querySelector('#help-popup table'); + table.addEventListener('input', filterTable); + + const inputs = table.querySelectorAll('input'); + inputs[0].addEventListener('keydown', hotkeyHandler); + inputs[1].focus(); + + function hotkeyHandler(event) { + const keyName = CodeMirror.keyName(event); + if (keyName === 'Esc' || keyName === 'Tab' || keyName === 'Shift-Tab') { + return; + } + event.preventDefault(); + event.stopPropagation(); + // normalize order of modifiers, + // for modifier-only keys ('Ctrl-Shift') a dummy main key has to be temporarily added + const keyMap = {}; + keyMap[keyName.replace(/(Shift|Ctrl|Alt|Cmd)$/, '$&-dummy')] = ''; + const normalizedKey = Object.keys(CodeMirror.normalizeKeyMap(keyMap))[0]; + this.value = normalizedKey.replace('-dummy', ''); + filterTable(event); + } + + function filterTable(event) { + const input = event.target; + const query = stringAsRegExp(input.value, 'gi'); + const col = input.parentNode.cellIndex; + inputs[1 - col].value = ''; + table.tBodies[0].childNodes.forEach(row => { + let cell = row.children[col]; + cell.innerHTML = cell.textContent.replace(query, '$&'); + row.style.display = query.test(cell.textContent) ? '' : 'none'; + // clear highlight from the other column + cell = row.children[1 - col]; + cell.innerHTML = cell.textContent; + }); + } + function mergeKeyMaps(merged, ...more) { + more.forEach(keyMap => { + if (typeof keyMap === 'string') { + keyMap = CodeMirror.keyMap[keyMap]; + } + Object.keys(keyMap).forEach(key => { + let cmd = keyMap[key]; + // filter out '...', 'attach', etc. (hotkeys start with an uppercase letter) + if (!merged[key] && !key.match(/^[a-z]/) && cmd !== '...') { + if (typeof cmd === 'function') { + // for 'emacs' keymap: provide at least something meaningful (hotkeys and the function body) + // for 'vim*' keymaps: almost nothing as it doesn't rely on CM keymap mechanism + cmd = cmd.toString().replace(/^function.*?\{[\s\r\n]*([\s\S]+?)[\s\r\n]*\}$/, '$1'); + merged[key] = cmd.length <= 200 ? cmd : cmd.substr(0, 200) + '...'; + } else { + merged[key] = cmd; + } + } + }); + if (keyMap.fallthrough) { + merged = mergeKeyMaps(merged, keyMap.fallthrough); + } + }); + return merged; + } +} + +function showLintHelp() { + showHelp(t('issues'), t('issuesHelp') + '' + ); +} + +function showRegExpTester(event, section = getSectionForChild(this)) { + const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain='; + const OWN_ICON = chrome.runtime.getManifest().icons['16']; + const cachedRegexps = showRegExpTester.cachedRegexps = + showRegExpTester.cachedRegexps || new Map(); + const regexps = [...section.querySelector('.applies-to-list').children] + .map(item => + !item.matches('.applies-to-everything') && + item.querySelector('.applies-type').value === 'regexp' && + item.querySelector('.applies-value').value.trim()) + .filter(item => item) + .map(text => { + const rxData = Object.assign({text}, cachedRegexps.get(text)); + if (!rxData.urls) { + cachedRegexps.set(text, Object.assign(rxData, { + rx: tryRegExp(text), + urls: new Map(), + })); + } + return rxData; + }); + chrome.tabs.onUpdated.addListener(function _(tabId, info) { + if (document.querySelector('.regexp-report')) { + if (info.url) { + showRegExpTester(event, section); + } + } else { + chrome.tabs.onUpdated.removeListener(_); + } + }); + queryTabs().then(tabs => { + const supported = tabs.map(tab => tab.url) + .filter(url => URLS.supported.test(url)); + const unique = [...new Set(supported).values()]; + for (const rxData of regexps) { + const {rx, urls} = rxData; + if (rx) { + const urlsNow = new Map(); + for (const url of unique) { + const match = urls.get(url) || (url.match(rx) || [])[0]; + if (match) { + urlsNow.set(url, match); + } + } + rxData.urls = urlsNow; + } + } + const moreInfoLink = template.regexpTestPartial.outerHTML; + const stats = { + full: {data: [], label: t('styleRegexpTestFull')}, + partial: {data: [], label: t('styleRegexpTestPartial') + moreInfoLink}, + none: {data: [], label: t('styleRegexpTestNone')}, + invalid: {data: [], label: t('styleRegexpTestInvalid')}, + }; + for (const {text, rx, urls} of regexps) { + if (!rx) { + stats.invalid.data.push({text}); + continue; + } + if (!urls.size) { + stats.none.data.push({text}); + continue; + } + const full = []; + const partial = []; + for (const [url, match] of urls.entries()) { + const faviconUrl = url.startsWith(URLS.ownOrigin) + ? OWN_ICON + : GET_FAVICON_URL + new URL(url).hostname; + const icon = ``; + if (match.length === url.length) { + full.push(`
${icon + url}
`); + } else { + partial.push(`
${icon}${match}` + + url.substr(match.length) + '
'); + } + } + if (full.length) { + stats.full.data.push({text, urls: full}); + } + if (partial.length) { + stats.partial.data.push({text, urls: partial}); + } + } + showHelp(t('styleRegexpTestTitle'), + '
' + + Object.keys(stats).map(type => (!stats[type].data.length ? '' : + `
+ ${stats[type].label}` + + stats[type].data.map(({text, urls}) => (!urls ? text : + `
${text}${urls.join('')}
` + )).join('
') + + '
' + )).join('') + + '
'); + document.querySelector('.regexp-report').onclick = event => { + const target = event.target.closest('a, .regexp-report div'); + if (target) { + openURL({url: target.href || target.textContent}); + event.preventDefault(); + } + }; + }); +} + +function showHelp(title, text) { + const div = document.getElementById('help-popup'); + div.classList.remove('big'); + div.querySelector('.contents').innerHTML = text; + div.querySelector('.title').innerHTML = title; + + if (getComputedStyle(div).display === 'none') { + document.addEventListener('keydown', closeHelp); + div.querySelector('.dismiss').onclick = closeHelp; // avoid chaining on multiple showHelp() calls + } + + div.style.display = 'block'; + return div; + + function closeHelp(e) { + if ( + !e || + e.type === 'click' || + ((e.keyCode || e.which) === 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey) + ) { + div.style.display = ''; + document.querySelector('.contents').innerHTML = ''; + document.removeEventListener('keydown', closeHelp); + } + } +} + +function showCodeMirrorPopup(title, html, options) { + const popup = showHelp(title, html); + popup.classList.add('big'); + + popup.codebox = CodeMirror(popup.querySelector('.contents'), Object.assign({ + mode: 'css', + lineNumbers: true, + lineWrapping: true, + foldGutter: true, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'], + matchBrackets: true, + lint: {getAnnotations: CodeMirror.lint.css, delay: 0}, + styleActiveLine: true, + theme: prefs.get('editor.theme'), + keyMap: prefs.get('editor.keyMap') + }, options)); + popup.codebox.focus(); + popup.codebox.on('focus', () => { hotkeyRerouter.setState(false); }); + popup.codebox.on('blur', () => { hotkeyRerouter.setState(true); }); + return popup; +} + +function getParams() { + const params = {}; + const urlParts = location.href.split('?', 2); + if (urlParts.length === 1) { + return params; + } + urlParts[1].split('&').forEach(keyValue => { + const splitKeyValue = keyValue.split('=', 2); + params[decodeURIComponent(splitKeyValue[0])] = decodeURIComponent(splitKeyValue[1]); + }); + return params; +} + +chrome.runtime.onMessage.addListener(onRuntimeMessage); + +function onRuntimeMessage(request) { + switch (request.method) { + case 'styleUpdated': + if (styleId && styleId === request.style.id && request.reason !== 'editSave') { + if ((request.style.sections[0] || {}).code === null) { + // the code-less style came from notifyAllTabs + onBackgroundReady().then(() => { + request.style = BG.cachedStyles.byId.get(request.style.id); + initWithStyle(request); + }); + } else { + initWithStyle(request); + } + } + break; + case 'styleDeleted': + if (styleId && styleId === request.id) { + window.onbeforeunload = () => {}; + window.close(); + break; + } + break; + case 'prefChanged': + if ('editor.smartIndent' in request.prefs) { + CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']); + } + break; + case 'editDeleteText': + document.execCommand('delete'); + break; + } +} + +function getComputedHeight(el) { + const compStyle = getComputedStyle(el); + return el.getBoundingClientRect().height + + parseFloat(compStyle.marginTop) + parseFloat(compStyle.marginBottom); +} + + +function getCodeMirrorThemes() { + if (!chrome.runtime.getPackageDirectoryEntry) { + const themes = [ + chrome.i18n.getMessage('defaultTheme'), + '3024-day', + '3024-night', + 'abcdef', + 'ambiance', + 'ambiance-mobile', + 'base16-dark', + 'base16-light', + 'bespin', + 'blackboard', + 'cobalt', + 'colorforth', + 'dracula', + 'duotone-dark', + 'duotone-light', + 'eclipse', + 'elegant', + 'erlang-dark', + 'hopscotch', + 'icecoder', + 'isotope', + 'lesser-dark', + 'liquibyte', + 'material', + 'mbo', + 'mdn-like', + 'midnight', + 'monokai', + 'neat', + 'neo', + 'night', + 'panda-syntax', + 'paraiso-dark', + 'paraiso-light', + 'pastel-on-dark', + 'railscasts', + 'rubyblue', + 'seti', + 'solarized', + 'the-matrix', + 'tomorrow-night-bright', + 'tomorrow-night-eighties', + 'ttcn', + 'twilight', + 'vibrant-ink', + 'xq-dark', + 'xq-light', + 'yeti', + 'zenburn', + ]; + localStorage.codeMirrorThemes = themes.join(' '); + return Promise.resolve(themes); + } + return new Promise(resolve => { + chrome.runtime.getPackageDirectoryEntry(rootDir => { + rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => { + themeDir.createReader().readEntries(entries => { + const themes = [ + chrome.i18n.getMessage('defaultTheme') + ].concat( + entries.filter(entry => entry.isFile) + .sort((a, b) => (a.name < b.name ? -1 : 1)) + .map(entry => entry.name.replace(/\.css$/, '')) + ); + localStorage.codeMirrorThemes = themes.join(' '); + resolve(themes); + }); + }); + }); + }); +} diff --git a/dom.js b/js/dom.js similarity index 92% rename from dom.js rename to js/dom.js index 2dca2090..6a71a10a 100644 --- a/dom.js +++ b/js/dom.js @@ -12,6 +12,7 @@ for (const type of [NodeList, NamedNodeMap, HTMLCollection, HTMLAllCollection]) } // add favicon in Firefox +// eslint-disable-next-line no-unused-expressions navigator.userAgent.includes('Firefox') && setTimeout(() => { const iconset = ['', 'light/'][prefs.get('iconset')] || ''; for (const size of [38, 32, 19, 16]) { @@ -26,7 +27,7 @@ navigator.userAgent.includes('Firefox') && setTimeout(() => { function onDOMready() { - if (document.readyState != 'loading') { + if (document.readyState !== 'loading') { return Promise.resolve(); } return new Promise(resolve => { @@ -78,9 +79,9 @@ function enforceInputRange(element) { const max = Number(element.max); const doNotify = () => element.dispatchEvent(new Event('change', {bubbles: true})); const onChange = ({type}) => { - if (type == 'input' && element.checkValidity()) { + if (type === 'input' && element.checkValidity()) { doNotify(); - } else if (type == 'change' && !element.checkValidity()) { + } else if (type === 'change' && !element.checkValidity()) { element.value = Math.max(min, Math.min(max, Number(element.value))); doNotify(); } @@ -112,7 +113,7 @@ function $element(opt) { ? opt.tag.split('#') : [null, opt.tag]; const element = ns - ? document.createElementNS(ns == 'SVG' || ns == 'svg' ? 'http://www.w3.org/2000/svg' : ns, tag) + ? document.createElementNS(ns === 'SVG' || ns === 'svg' ? 'http://www.w3.org/2000/svg' : ns, tag) : document.createElement(tag || 'div'); (opt.appendChild instanceof Array ? opt.appendChild : [opt.appendChild]) .forEach(child => child && element.appendChild(child)); diff --git a/localization.js b/js/localization.js similarity index 93% rename from localization.js rename to js/localization.js index 0c5f578c..1db7577b 100644 --- a/localization.js +++ b/js/localization.js @@ -7,7 +7,7 @@ tDocLoader(); function t(key, params) { const cache = !params && t.cache[key]; const s = cache || chrome.i18n.getMessage(key, params); - if (s == '') { + if (s === '') { throw `Missing string "${key}"`; } if (!params && !cache) { @@ -20,7 +20,7 @@ function t(key, params) { function tE(id, key, attr, esc) { if (attr) { document.getElementById(id).setAttribute(attr, t(key)); - } else if (typeof esc == 'undefined' || esc) { + } else if (typeof esc === 'undefined' || esc) { document.getElementById(id).appendChild(document.createTextNode(t(key))); } else { document.getElementById(id).innerHTML = t(key); @@ -43,10 +43,10 @@ function tNodeList(nodes) { for (let n = nodes.length; --n >= 0;) { const node = nodes[n]; // skip non-ELEMENT_NODE - if (node.nodeType != 1) { + if (node.nodeType !== 1) { continue; } - if (node.localName == 'template') { + if (node.localName === 'template') { const elements = node.content.querySelectorAll('*'); tNodeList(elements); template[node.dataset.id] = elements[0]; @@ -94,7 +94,7 @@ function tDocLoader() { // reset L10N cache on UI language change const UIlang = chrome.i18n.getUILanguage(); - if (t.cache.browserUIlanguage != UIlang) { + if (t.cache.browserUIlanguage !== UIlang) { t.cache = {browserUIlanguage: UIlang}; localStorage.L10N = JSON.stringify(t.cache); } @@ -114,7 +114,7 @@ function tDocLoader() { const onLoad = () => { tDocLoader.stop(); process(observer.takeRecords()); - if (cacheLength != Object.keys(t.cache).length) { + if (cacheLength !== Object.keys(t.cache).length) { localStorage.L10N = JSON.stringify(t.cache); } }; diff --git a/messaging.js b/js/messaging.js similarity index 90% rename from messaging.js rename to js/messaging.js index 42f8b593..386f7409 100644 --- a/messaging.js +++ b/js/messaging.js @@ -1,371 +1,371 @@ -/* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */ -'use strict'; - -// keep message channel open for sendResponse in chrome.runtime.onMessage listener -const KEEP_CHANNEL_OPEN = true; - -const FIREFOX = /Firefox/.test(navigator.userAgent); -const OPERA = /OPR/.test(navigator.userAgent); - -const URLS = { - ownOrigin: chrome.runtime.getURL(''), - - optionsUI: [ - chrome.runtime.getURL('options/index.html'), - 'chrome://extensions/?options=' + chrome.runtime.id, - ], - - configureCommands: - OPERA ? 'opera://settings/configureCommands' - : 'chrome://extensions/configureCommands', - - // CWS cannot be scripted in chromium, see ChromeExtensionsClient::IsScriptableURL - // https://cs.chromium.org/chromium/src/chrome/common/extensions/chrome_extensions_client.cc - chromeWebStore: FIREFOX ? 'https://addons.mozilla.org/' : ( - OPERA ? 'https://addons.opera.com/' : 'https://chrome.google.com/webstore/' - ), - - supported: new RegExp( - '^(file|ftps?|http)://|' + - `^https://${FIREFOX ? '(?!addons\\.mozilla\\.org)' : ( - OPERA ? '(?!addons\\.opera\\.com)' : '(?!chrome\\.google\\.com/webstore)' - )}|` + - '^' + chrome.runtime.getURL('')), -}; - -let BG = chrome.extension.getBackgroundPage(); - -if (!BG || BG != window) { - document.documentElement.classList.toggle('firefox', FIREFOX); - document.documentElement.classList.toggle('opera', OPERA); - // TODO: remove once our manifest's minimum_chrome_version is 50+ - // Chrome 49 doesn't report own extension pages in webNavigation apparently - if (navigator.userAgent.includes('Chrome/49.')) { - getActiveTab().then(BG.updateIcon); - } -} - -function notifyAllTabs(msg) { - const originalMessage = msg; - if (msg.method == 'styleUpdated' || msg.method == 'styleAdded') { - // apply/popup/manage use only meta for these two methods, - // editor may need the full code but can fetch it directly, - // so we send just the meta to avoid spamming lots of tabs with huge styles - msg = Object.assign({}, msg, { - style: getStyleWithNoCode(msg.style) - }); - } - const affectsAll = !msg.affects || msg.affects.all; - const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager); - const affectsTabs = affectsAll || affectsOwnOriginOnly; - const affectsIcon = affectsAll || msg.affects.icon; - const affectsPopup = affectsAll || msg.affects.popup; - const affectsSelf = affectsPopup || msg.prefs; - if (affectsTabs || affectsIcon) { - const notifyTab = tab => { - // own pages will be notified via runtime.sendMessage later - if ((affectsTabs || URLS.optionsUI.includes(tab.url)) - && !(affectsSelf && tab.url.startsWith(URLS.ownOrigin)) - // skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF - && (!FIREFOX || tab.width)) { - chrome.tabs.sendMessage(tab.id, msg); - } - if (affectsIcon && BG) { - BG.updateIcon(tab); - } - }; - // list all tabs including chrome-extension:// which can be ours - Promise.all([ - queryTabs(affectsOwnOriginOnly ? {url: URLS.ownOrigin + '*'} : {}), - getActiveTab(), - ]).then(([tabs, activeTab]) => { - const activeTabId = activeTab && activeTab.id; - for (const tab of tabs) { - invokeOrPostpone(tab.id === activeTabId, notifyTab, tab); - } - }); - } - // notify self: the message no longer is sent to the origin in new Chrome - if (typeof onRuntimeMessage != 'undefined') { - onRuntimeMessage(originalMessage); - } - // notify apply.js on own pages - if (typeof applyOnMessage != 'undefined') { - applyOnMessage(originalMessage); - } - // notify background page and all open popups - if (affectsSelf) { - chrome.runtime.sendMessage(msg); - } -} - - -function queryTabs(options = {}) { - return new Promise(resolve => - chrome.tabs.query(options, tabs => - resolve(tabs))); -} - - -function getTab(id) { - return new Promise(resolve => - chrome.tabs.get(id, tab => - !chrome.runtime.lastError && resolve(tab))); -} - - -function getOwnTab() { - return new Promise(resolve => - chrome.tabs.getCurrent(tab => resolve(tab))); -} - - -function getActiveTab() { - return queryTabs({currentWindow: true, active: true}) - .then(tabs => tabs[0]); -} - - -function getActiveTabRealURL() { - return getActiveTab() - .then(getTabRealURL); -} - - -function getTabRealURL(tab) { - return new Promise(resolve => { - if (tab.url != 'chrome://newtab/') { - resolve(tab.url); - } else { - chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, frame => { - resolve(frame && frame.url || ''); - }); - } - }); -} - - -// opens a tab or activates the already opened one, -// reuses the New Tab page if it's focused now -function openURL({url, currentWindow = true}) { - if (!url.includes('://')) { - url = chrome.runtime.getURL(url); - } - return new Promise(resolve => { - // [some] chromium forks don't handle their fake branded protocols - url = url.replace(/^(opera|vivaldi)/, 'chrome'); - // FF doesn't handle moz-extension:// URLs (bug) - // API doesn't handle the hash-fragment part - const urlQuery = url.startsWith('moz-extension') ? undefined : url.replace(/#.*/, ''); - queryTabs({url: urlQuery, currentWindow}).then(tabs => { - for (const tab of tabs) { - if (tab.url == url) { - activateTab(tab).then(resolve); - return; - } - } - getActiveTab().then(tab => { - if (tab && tab.url == 'chrome://newtab/' - // prevent redirecting incognito NTP to a chrome URL as it crashes Chrome - && (!url.startsWith('chrome') || !tab.incognito)) { - chrome.tabs.update({url}, resolve); - } else { - chrome.tabs.create(tab && !FIREFOX ? {url, openerTabId: tab.id} : {url}, resolve); - } - }); - }); - }); -} - - -function activateTab(tab) { - return Promise.all([ - new Promise(resolve => { - chrome.tabs.update(tab.id, {active: true}, resolve); - }), - new Promise(resolve => { - chrome.windows.update(tab.windowId, {focused: true}, resolve); - }), - ]); -} - - -function stringAsRegExp(s, flags) { - return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=*!|]/g, '\\$&'), flags); -} - - -function ignoreChromeError() { - chrome.runtime.lastError; // eslint-disable-line no-unused-expressions -} - - -function getStyleWithNoCode(style) { - const stripped = Object.assign({}, style, {sections: []}); - for (const section of style.sections) { - stripped.sections.push(Object.assign({}, section, {code: null})); - } - return stripped; -} - - -// js engine can't optimize the entire function if it contains try-catch -// so we should keep it isolated from normal code in a minimal wrapper -// Update: might get fixed in V8 TurboFan in the future -function tryCatch(func, ...args) { - try { - return func(...args); - } catch (e) {} -} - - -function tryRegExp(regexp) { - try { - return new RegExp(regexp); - } catch (e) {} -} - - -function tryJSONparse(jsonString) { - try { - return JSON.parse(jsonString); - } catch (e) {} -} - - -const debounce = Object.assign((fn, delay, ...args) => { - clearTimeout(debounce.timers.get(fn)); - debounce.timers.set(fn, setTimeout(debounce.run, delay, fn, ...args)); -}, { - timers: new Map(), - run(fn, ...args) { - debounce.timers.delete(fn); - fn(...args); - }, - unregister(fn) { - clearTimeout(debounce.timers.get(fn)); - debounce.timers.delete(fn); - }, -}); - - -function deepCopy(obj) { - return obj !== null && obj !== undefined && typeof obj == 'object' - ? deepMerge(typeof obj.slice == 'function' ? [] : {}, obj) - : obj; -} - - -function deepMerge(target, ...args) { - const isArray = typeof target.slice == 'function'; - for (const obj of args) { - if (isArray && obj !== null && obj !== undefined) { - for (const element of obj) { - target.push(deepCopy(element)); - } - continue; - } - for (const k in obj) { - const value = obj[k]; - if (k in target && typeof value == 'object' && value !== null) { - deepMerge(target[k], value); - } else { - target[k] = deepCopy(value); - } - } - } - return target; -} - - -function sessionStorageHash(name) { - return { - name, - value: tryCatch(JSON.parse, sessionStorage[name]) || {}, - set(k, v) { - this.value[k] = v; - this.updateStorage(); - }, - unset(k) { - delete this.value[k]; - this.updateStorage(); - }, - updateStorage() { - sessionStorage[this.name] = JSON.stringify(this.value); - } - }; -} - - -function onBackgroundReady() { - return BG && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) { - chrome.runtime.sendMessage({method: 'healthCheck'}, health => { - if (health !== undefined) { - BG = chrome.extension.getBackgroundPage(); - resolve(); - } else { - setTimeout(ping, 0, resolve); - } - }); - }); -} - - -// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage -function getStylesSafe(options) { - return onBackgroundReady() - .then(() => BG.getStyles(options)); -} - - -function saveStyleSafe(style) { - return onBackgroundReady() - .then(() => BG.saveStyle(BG.deepCopy(style))) - .then(savedStyle => { - if (style.notify === false) { - handleUpdate(savedStyle, style); - } - return savedStyle; - }); -} - - -function deleteStyleSafe({id, notify = true} = {}) { - return onBackgroundReady() - .then(() => BG.deleteStyle({id, notify})) - .then(() => { - if (!notify) { - handleDelete(id); - } - return id; - }); -} - - -function download(url) { - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.timeout = 10e3; - xhr.onloadend = () => (xhr.status == 200 - ? resolve(xhr.responseText) - : reject(xhr.status)); - const [mainUrl, query] = url.split('?'); - xhr.open(query ? 'POST' : 'GET', mainUrl, true); - xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); - xhr.send(query); - }); -} - - -function doTimeout(ms = 0, ...args) { - return ms > 0 - ? () => new Promise(resolve => setTimeout(resolve, ms, ...args)) - : new Promise(resolve => setTimeout(resolve, 0, ...args)); -} - - -function invokeOrPostpone(isInvoke, fn, ...args) { - return isInvoke - ? fn(...args) - : setTimeout(invokeOrPostpone, 0, true, fn, ...args); -} +/* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */ +'use strict'; + +// keep message channel open for sendResponse in chrome.runtime.onMessage listener +const KEEP_CHANNEL_OPEN = true; + +const FIREFOX = /Firefox/.test(navigator.userAgent); +const OPERA = /OPR/.test(navigator.userAgent); + +const URLS = { + ownOrigin: chrome.runtime.getURL(''), + + optionsUI: [ + chrome.runtime.getURL('options.html'), + 'chrome://extensions/?options=' + chrome.runtime.id, + ], + + configureCommands: + OPERA ? 'opera://settings/configureCommands' + : 'chrome://extensions/configureCommands', + + // CWS cannot be scripted in chromium, see ChromeExtensionsClient::IsScriptableURL + // https://cs.chromium.org/chromium/src/chrome/common/extensions/chrome_extensions_client.cc + chromeWebStore: FIREFOX ? 'https://addons.mozilla.org/' : ( + OPERA ? 'https://addons.opera.com/' : 'https://chrome.google.com/webstore/' + ), + + supported: new RegExp( + '^(file|ftps?|http)://|' + + `^https://${FIREFOX ? '(?!addons\\.mozilla\\.org)' : ( + OPERA ? '(?!addons\\.opera\\.com)' : '(?!chrome\\.google\\.com/webstore)' + )}|` + + '^' + chrome.runtime.getURL('')), +}; + +let BG = chrome.extension.getBackgroundPage(); + +if (!BG || BG !== window) { + document.documentElement.classList.toggle('firefox', FIREFOX); + document.documentElement.classList.toggle('opera', OPERA); + // TODO: remove once our manifest's minimum_chrome_version is 50+ + // Chrome 49 doesn't report own extension pages in webNavigation apparently + if (navigator.userAgent.includes('Chrome/49.')) { + getActiveTab().then(BG.updateIcon); + } +} + +function notifyAllTabs(msg) { + const originalMessage = msg; + if (msg.method === 'styleUpdated' || msg.method === 'styleAdded') { + // apply/popup/manage use only meta for these two methods, + // editor may need the full code but can fetch it directly, + // so we send just the meta to avoid spamming lots of tabs with huge styles + msg = Object.assign({}, msg, { + style: getStyleWithNoCode(msg.style) + }); + } + const affectsAll = !msg.affects || msg.affects.all; + const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager); + const affectsTabs = affectsAll || affectsOwnOriginOnly; + const affectsIcon = affectsAll || msg.affects.icon; + const affectsPopup = affectsAll || msg.affects.popup; + const affectsSelf = affectsPopup || msg.prefs; + if (affectsTabs || affectsIcon) { + const notifyTab = tab => { + // own pages will be notified via runtime.sendMessage later + if ((affectsTabs || URLS.optionsUI.includes(tab.url)) + && !(affectsSelf && tab.url.startsWith(URLS.ownOrigin)) + // skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF + && (!FIREFOX || tab.width)) { + chrome.tabs.sendMessage(tab.id, msg); + } + if (affectsIcon && BG) { + BG.updateIcon(tab); + } + }; + // list all tabs including chrome-extension:// which can be ours + Promise.all([ + queryTabs(affectsOwnOriginOnly ? {url: URLS.ownOrigin + '*'} : {}), + getActiveTab(), + ]).then(([tabs, activeTab]) => { + const activeTabId = activeTab && activeTab.id; + for (const tab of tabs) { + invokeOrPostpone(tab.id === activeTabId, notifyTab, tab); + } + }); + } + // notify self: the message no longer is sent to the origin in new Chrome + if (typeof onRuntimeMessage !== 'undefined') { + onRuntimeMessage(originalMessage); + } + // notify apply.js on own pages + if (typeof applyOnMessage !== 'undefined') { + applyOnMessage(originalMessage); + } + // notify background page and all open popups + if (affectsSelf) { + chrome.runtime.sendMessage(msg); + } +} + + +function queryTabs(options = {}) { + return new Promise(resolve => + chrome.tabs.query(options, tabs => + resolve(tabs))); +} + + +function getTab(id) { + return new Promise(resolve => + chrome.tabs.get(id, tab => + !chrome.runtime.lastError && resolve(tab))); +} + + +function getOwnTab() { + return new Promise(resolve => + chrome.tabs.getCurrent(tab => resolve(tab))); +} + + +function getActiveTab() { + return queryTabs({currentWindow: true, active: true}) + .then(tabs => tabs[0]); +} + + +function getActiveTabRealURL() { + return getActiveTab() + .then(getTabRealURL); +} + + +function getTabRealURL(tab) { + return new Promise(resolve => { + if (tab.url !== 'chrome://newtab/') { + resolve(tab.url); + } else { + chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, frame => { + resolve(frame && frame.url || ''); + }); + } + }); +} + + +// opens a tab or activates the already opened one, +// reuses the New Tab page if it's focused now +function openURL({url, currentWindow = true}) { + if (!url.includes('://')) { + url = chrome.runtime.getURL(url); + } + return new Promise(resolve => { + // [some] chromium forks don't handle their fake branded protocols + url = url.replace(/^(opera|vivaldi)/, 'chrome'); + // FF doesn't handle moz-extension:// URLs (bug) + // API doesn't handle the hash-fragment part + const urlQuery = url.startsWith('moz-extension') ? undefined : url.replace(/#.*/, ''); + queryTabs({url: urlQuery, currentWindow}).then(tabs => { + for (const tab of tabs) { + if (tab.url === url) { + activateTab(tab).then(resolve); + return; + } + } + getActiveTab().then(tab => { + if (tab && tab.url === 'chrome://newtab/' + // prevent redirecting incognito NTP to a chrome URL as it crashes Chrome + && (!url.startsWith('chrome') || !tab.incognito)) { + chrome.tabs.update({url}, resolve); + } else { + chrome.tabs.create(tab && !FIREFOX ? {url, openerTabId: tab.id} : {url}, resolve); + } + }); + }); + }); +} + + +function activateTab(tab) { + return Promise.all([ + new Promise(resolve => { + chrome.tabs.update(tab.id, {active: true}, resolve); + }), + new Promise(resolve => { + chrome.windows.update(tab.windowId, {focused: true}, resolve); + }), + ]); +} + + +function stringAsRegExp(s, flags) { + return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=*!|]/g, '\\$&'), flags); +} + + +function ignoreChromeError() { + chrome.runtime.lastError; // eslint-disable-line no-unused-expressions +} + + +function getStyleWithNoCode(style) { + const stripped = Object.assign({}, style, {sections: []}); + for (const section of style.sections) { + stripped.sections.push(Object.assign({}, section, {code: null})); + } + return stripped; +} + + +// js engine can't optimize the entire function if it contains try-catch +// so we should keep it isolated from normal code in a minimal wrapper +// Update: might get fixed in V8 TurboFan in the future +function tryCatch(func, ...args) { + try { + return func(...args); + } catch (e) {} +} + + +function tryRegExp(regexp) { + try { + return new RegExp(regexp); + } catch (e) {} +} + + +function tryJSONparse(jsonString) { + try { + return JSON.parse(jsonString); + } catch (e) {} +} + + +const debounce = Object.assign((fn, delay, ...args) => { + clearTimeout(debounce.timers.get(fn)); + debounce.timers.set(fn, setTimeout(debounce.run, delay, fn, ...args)); +}, { + timers: new Map(), + run(fn, ...args) { + debounce.timers.delete(fn); + fn(...args); + }, + unregister(fn) { + clearTimeout(debounce.timers.get(fn)); + debounce.timers.delete(fn); + }, +}); + + +function deepCopy(obj) { + return obj !== null && obj !== undefined && typeof obj === 'object' + ? deepMerge(typeof obj.slice === 'function' ? [] : {}, obj) + : obj; +} + + +function deepMerge(target, ...args) { + const isArray = typeof target.slice === 'function'; + for (const obj of args) { + if (isArray && obj !== null && obj !== undefined) { + for (const element of obj) { + target.push(deepCopy(element)); + } + continue; + } + for (const k in obj) { + const value = obj[k]; + if (k in target && typeof value === 'object' && value !== null) { + deepMerge(target[k], value); + } else { + target[k] = deepCopy(value); + } + } + } + return target; +} + + +function sessionStorageHash(name) { + return { + name, + value: tryCatch(JSON.parse, sessionStorage[name]) || {}, + set(k, v) { + this.value[k] = v; + this.updateStorage(); + }, + unset(k) { + delete this.value[k]; + this.updateStorage(); + }, + updateStorage() { + sessionStorage[this.name] = JSON.stringify(this.value); + } + }; +} + + +function onBackgroundReady() { + return BG && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) { + chrome.runtime.sendMessage({method: 'healthCheck'}, health => { + if (health !== undefined) { + BG = chrome.extension.getBackgroundPage(); + resolve(); + } else { + setTimeout(ping, 0, resolve); + } + }); + }); +} + + +// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage +function getStylesSafe(options) { + return onBackgroundReady() + .then(() => BG.getStyles(options)); +} + + +function saveStyleSafe(style) { + return onBackgroundReady() + .then(() => BG.saveStyle(BG.deepCopy(style))) + .then(savedStyle => { + if (style.notify === false) { + handleUpdate(savedStyle, style); + } + return savedStyle; + }); +} + + +function deleteStyleSafe({id, notify = true} = {}) { + return onBackgroundReady() + .then(() => BG.deleteStyle({id, notify})) + .then(() => { + if (!notify) { + handleDelete(id); + } + return id; + }); +} + + +function download(url) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.timeout = 10e3; + xhr.onloadend = () => (xhr.status === 200 + ? resolve(xhr.responseText) + : reject(xhr.status)); + const [mainUrl, query] = url.split('?'); + xhr.open(query ? 'POST' : 'GET', mainUrl, true); + xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + xhr.send(query); + }); +} + + +function doTimeout(ms = 0, ...args) { + return ms > 0 + ? () => new Promise(resolve => setTimeout(resolve, ms, ...args)) + : new Promise(resolve => setTimeout(resolve, 0, ...args)); +} + + +function invokeOrPostpone(isInvoke, fn, ...args) { + return isInvoke + ? fn(...args) + : setTimeout(invokeOrPostpone, 0, true, fn, ...args); +} diff --git a/prefs.js b/js/prefs.js similarity index 93% rename from prefs.js rename to js/prefs.js index c34a0546..2326036b 100644 --- a/prefs.js +++ b/js/prefs.js @@ -115,10 +115,10 @@ var prefs = new function Prefs() { defineReadonlyProperty(this.readOnlyValues, key, value); const hasChanged = !equal(value, oldValue); if (!fromBroadcast) { - if (BG && BG != window) { + if (BG && BG !== window) { BG.prefs.set(key, BG.deepCopy(value), {broadcast, sync}); } else { - localStorage[key] = typeof defaults[key] == 'object' + localStorage[key] = typeof defaults[key] === 'object' ? JSON.stringify(value) : value; if (broadcast && hasChanged) { @@ -166,7 +166,7 @@ var prefs = new function Prefs() { for (const key in defaults) { const defaultValue = defaults[key]; let value = localStorage[key]; - if (typeof value == 'string') { + if (typeof value === 'string') { switch (typeof defaultValue) { case 'boolean': value = value.toLowerCase() === 'true'; @@ -181,7 +181,7 @@ var prefs = new function Prefs() { } else { value = defaultValue; } - if (BG == window) { + if (BG === window) { // when in bg page, .set() will write to localStorage this.set(key, value, {broadcast: false, sync: false}); } else { @@ -190,13 +190,13 @@ var prefs = new function Prefs() { } } - if (!BG || BG == window) { + if (!BG || BG === window) { affectsIcon.forEach(key => this.broadcast(key, values[key], {sync: false})); getSync().get('settings', ({settings: synced} = {}) => { if (synced) { for (const key in defaults) { - if (key == 'popupWidth' && synced[key] != values.popupWidth) { + if (key === 'popupWidth' && synced[key] !== values.popupWidth) { // this is a fix for the period when popupWidth wasn't synced // TODO: remove it in a couple of months continue; @@ -209,7 +209,7 @@ var prefs = new function Prefs() { }); chrome.storage.onChanged.addListener((changes, area) => { - if (area == 'sync' && 'settings' in changes) { + if (area === 'sync' && 'settings' in changes) { const synced = changes.settings.newValue; if (synced) { for (const key in defaults) { @@ -283,21 +283,21 @@ var prefs = new function Prefs() { function defineReadonlyProperty(obj, key, value) { const copy = deepCopy(value); - if (typeof copy == 'object') { + if (typeof copy === 'object') { Object.freeze(copy); } Object.defineProperty(obj, key, {value: copy, configurable: true}); } function equal(a, b) { - if (!a || !b || typeof a != 'object' || typeof b != 'object') { + if (!a || !b || typeof a !== 'object' || typeof b !== 'object') { return a === b; } - if (Object.keys(a).length != Object.keys(b).length) { + if (Object.keys(a).length !== Object.keys(b).length) { return false; } for (const k in a) { - if (typeof a[k] == 'object') { + if (typeof a[k] === 'object') { if (!equal(a[k], b[k])) { return false; } @@ -315,7 +315,7 @@ var prefs = new function Prefs() { // Chrome and co. /Safari\/[\d.]+$/.test(navigator.userAgent) && // skip forks with Flash as those are likely to have the menu e.g. CentBrowser - !Array.from(navigator.plugins).some(p => p.name == 'Shockwave Flash') + !Array.from(navigator.plugins).some(p => p.name === 'Shockwave Flash') ); } }(); @@ -330,7 +330,7 @@ function setupLivePrefs( const checkedProps = {}; for (const id of IDs) { const element = document.getElementById(id); - checkedProps[id] = element.type == 'checkbox' ? 'checked' : 'value'; + checkedProps[id] = element.type === 'checkbox' ? 'checked' : 'value'; updateElement({id, element, force: true}); element.addEventListener('change', onChange); } @@ -338,7 +338,7 @@ function setupLivePrefs( function onChange() { const value = this[checkedProps[this.id]]; - if (prefs.get(this.id) != value) { + if (prefs.get(this.id) !== value) { prefs.set(this.id, value); } } @@ -349,7 +349,7 @@ function setupLivePrefs( force, }) { const prop = checkedProps[id]; - if (force || element[prop] != value) { + if (force || element[prop] !== value) { element[prop] = value; element.dispatchEvent(new Event('change', {bubbles: true, cancelable: true})); } diff --git a/manage.html b/manage.html index 5fc34411..b3bb0545 100644 --- a/manage.html +++ b/manage.html @@ -3,7 +3,7 @@ - + @@ -121,12 +121,12 @@ - - - - - - + + + + + + @@ -216,7 +216,7 @@
- + diff --git a/backup/fileSaveLoad.js b/manage/fileSaveLoad.js similarity index 91% rename from backup/fileSaveLoad.js rename to manage/fileSaveLoad.js index 7626a1b2..4a8397ec 100644 --- a/backup/fileSaveLoad.js +++ b/manage/fileSaveLoad.js @@ -1,395 +1,395 @@ -/* global messageBox, handleUpdate, applyOnMessage */ -'use strict'; - -const STYLISH_DUMP_FILE_EXT = '.txt'; -const STYLUS_BACKUP_FILE_EXT = '.json'; - - -function importFromFile({fileTypeFilter, file} = {}) { - return new Promise(resolve => { - const fileInput = document.createElement('input'); - if (file) { - readFile(); - return; - } - fileInput.style.display = 'none'; - fileInput.type = 'file'; - fileInput.accept = fileTypeFilter || STYLISH_DUMP_FILE_EXT; - fileInput.acceptCharset = 'utf-8'; - - document.body.appendChild(fileInput); - fileInput.initialValue = fileInput.value; - fileInput.onchange = readFile; - fileInput.click(); - - function readFile() { - if (file || fileInput.value !== fileInput.initialValue) { - file = file || fileInput.files[0]; - if (file.size > 100e6) { - console.warn("100MB backup? I don't believe you."); - importFromString('').then(resolve); - return; - } - document.body.style.cursor = 'wait'; - const fReader = new FileReader(); - fReader.onloadend = event => { - fileInput.remove(); - importFromString(event.target.result).then(numStyles => { - document.body.style.cursor = ''; - resolve(numStyles); - }); - }; - fReader.readAsText(file, 'utf-8'); - } - } - }); -} - - -function importFromString(jsonString) { - if (!BG) { - onBackgroundReady().then(() => importFromString(jsonString)); - return; - } - // create objects in background context - const json = BG.tryJSONparse(jsonString) || []; - if (typeof json.slice != 'function') { - json.length = 0; - } - const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []); - const oldStylesByName = json.length && new Map( - oldStyles.map(style => [style.name.trim(), style])); - - const stats = { - added: {names: [], ids: [], legend: 'importReportLegendAdded'}, - unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'}, - metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth'}, - metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta'}, - codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode'}, - invalid: {names: [], legend: 'importReportLegendInvalid'}, - }; - - let index = 0; - let lastRenderTime = performance.now(); - const renderQueue = []; - const RENDER_NAP_TIME_MAX = 1000; // ms - const RENDER_QUEUE_MAX = 50; // number of styles - const SAVE_OPTIONS = {reason: 'import', notify: false}; - - return new Promise(proceed); - - function proceed(resolve) { - while (index < json.length) { - const item = json[index++]; - const info = analyze(item); - if (info) { - // using saveStyle directly since json was parsed in background page context - return BG.saveStyle(Object.assign(item, SAVE_OPTIONS)) - .then(style => account({style, info, resolve})); - } - } - renderQueue.forEach(style => handleUpdate(style, {reason: 'import'})); - renderQueue.length = 0; - done(resolve); - } - - function analyze(item) { - if (!item || !item.name || !item.name.trim() || typeof item != 'object' - || (item.sections && typeof item.sections.slice != 'function')) { - stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`); - return; - } - item.name = item.name.trim(); - const byId = BG.cachedStyles.byId.get(item.id); - const byName = oldStylesByName.get(item.name); - oldStylesByName.delete(item.name); - let oldStyle; - if (byId) { - if (sameStyle(byId, item)) { - oldStyle = byId; - } else { - item.id = null; - } - } - if (!oldStyle && byName) { - item.id = byName.id; - oldStyle = byName; - } - const oldStyleKeys = oldStyle && Object.keys(oldStyle); - const metaEqual = oldStyleKeys && - oldStyleKeys.length == Object.keys(item).length && - oldStyleKeys.every(k => k == 'sections' || oldStyle[k] === item[k]); - const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item); - if (metaEqual && codeEqual) { - stats.unchanged.names.push(oldStyle.name); - stats.unchanged.ids.push(oldStyle.id); - return; - } - return {oldStyle, metaEqual, codeEqual}; - } - - function sameStyle(oldStyle, newStyle) { - return oldStyle.name.trim() === newStyle.name.trim() || - ['updateUrl', 'originalMd5', 'originalDigest'] - .some(field => oldStyle[field] && oldStyle[field] == newStyle[field]); - } - - function account({style, info, resolve}) { - renderQueue.push(style); - if (performance.now() - lastRenderTime > RENDER_NAP_TIME_MAX - || renderQueue.length > RENDER_QUEUE_MAX) { - renderQueue.forEach(style => handleUpdate(style, {reason: 'import'})); - setTimeout(scrollElementIntoView, 0, $('#style-' + renderQueue.pop().id)); - renderQueue.length = 0; - lastRenderTime = performance.now(); - } - setTimeout(proceed, 0, resolve); - const {oldStyle, metaEqual, codeEqual} = info; - if (!oldStyle) { - stats.added.names.push(style.name); - stats.added.ids.push(style.id); - return; - } - if (!metaEqual && !codeEqual) { - stats.metaAndCode.names.push(reportNameChange(oldStyle, style)); - stats.metaAndCode.ids.push(style.id); - return; - } - if (!codeEqual) { - stats.codeOnly.names.push(style.name); - stats.codeOnly.ids.push(style.id); - return; - } - stats.metaOnly.names.push(reportNameChange(oldStyle, style)); - stats.metaOnly.ids.push(style.id); - } - - function done(resolve) { - const numChanged = stats.metaAndCode.names.length + - stats.metaOnly.names.length + - stats.codeOnly.names.length + - stats.added.names.length; - Promise.resolve(numChanged && refreshAllTabs()).then(() => { - const report = Object.keys(stats) - .filter(kind => stats[kind].names.length) - .map(kind => { - const {ids, names, legend} = stats[kind]; - const listItemsWithId = (name, i) => - $element({dataset: {id: ids[i]}, textContent: name}); - const listItems = name => - $element({textContent: name}); - const block = - $element({tag: 'details', dataset: {id: kind}, appendChild: [ - $element({tag: 'summary', appendChild: - $element({tag: 'b', textContent: names.length + ' ' + t(legend)}) - }), - $element({tag: 'small', appendChild: - names.map(ids ? listItemsWithId : listItems) - }), - ]}); - return block; - }); - scrollTo(0, 0); - messageBox({ - title: t('importReportTitle'), - contents: report.length ? report : t('importReportUnchanged'), - buttons: [t('confirmOK'), numChanged && t('undo')], - onshow: bindClick, - }).then(({button, enter, esc}) => { - if (button == 1) { - undo(); - } - }); - resolve(numChanged); - }); - } - - function undo() { - const oldStylesById = new Map(oldStyles.map(style => [style.id, style])); - const newIds = [ - ...stats.metaAndCode.ids, - ...stats.metaOnly.ids, - ...stats.codeOnly.ids, - ...stats.added.ids, - ]; - let resolve; - index = 0; - return new Promise(resolve_ => { - resolve = resolve_; - undoNextId(); - }).then(refreshAllTabs) - .then(() => messageBox({ - title: t('importReportUndoneTitle'), - contents: newIds.length + ' ' + t('importReportUndone'), - buttons: [t('confirmOK')], - })); - function undoNextId() { - if (index == newIds.length) { - resolve(); - return; - } - const id = newIds[index++]; - deleteStyleSafe({id, notify: false}).then(id => { - const oldStyle = oldStylesById.get(id); - if (oldStyle) { - saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS)) - .then(undoNextId); - } else { - undoNextId(); - } - }); - } - } - - function bindClick(box) { - const highlightElement = event => { - const styleElement = $('#style-' + event.target.dataset.id); - if (styleElement) { - scrollElementIntoView(styleElement); - animateElement(styleElement); - } - }; - for (const block of $$('details')) { - if (block.dataset.id != 'invalid') { - block.style.cursor = 'pointer'; - block.onclick = highlightElement; - } - } - } - - function limitString(s, limit = 100) { - return s.length <= limit ? s : s.substr(0, limit) + '...'; - } - - function reportNameChange(oldStyle, newStyle) { - return newStyle.name != oldStyle.name - ? oldStyle.name + ' —> ' + newStyle.name - : oldStyle.name; - } - - function refreshAllTabs() { - return Promise.all([ - getActiveTab(), - getOwnTab(), - ]).then(([activeTab, ownTab]) => new Promise(resolve => { - // list all tabs including chrome-extension:// which can be ours - queryTabs().then(tabs => { - const lastTab = tabs[tabs.length - 1]; - for (const tab of tabs) { - // skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF - if (FIREFOX && !tab.width) { - if (tab == lastTab) { - resolve(); - } - continue; - } - getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => { - const message = {method: 'styleReplaceAll', styles}; - if (tab.id == ownTab.id) { - applyOnMessage(message); - } else { - invokeOrPostpone(tab.id == activeTab.id, - chrome.tabs.sendMessage, tab.id, message, ignoreChromeError); - } - setTimeout(BG.updateIcon, 0, tab, styles); - if (tab == lastTab) { - resolve(); - } - }); - } - }); - })); - } -} - - -$('#file-all-styles').onclick = () => { - getStylesSafe().then(styles => { - const text = JSON.stringify(styles, null, '\t'); - const url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text); - return url; - // for long URLs; https://github.com/schomery/stylus/issues/13#issuecomment-284582600 - }).then(fetch) - .then(res => res.blob()) - .then(blob => { - const objectURL = URL.createObjectURL(blob); - let link = $element({ - tag:'a', - href: objectURL, - type: 'application/json', - download: generateFileName(), - }); - // TODO: remove the fallback when FF multi-process bug is fixed - if (!FIREFOX) { - link.dispatchEvent(new MouseEvent('click')); - setTimeout(() => URL.revokeObjectURL(objectURL)); - } else { - const iframe = document.body.appendChild($element({ - tag: 'iframe', - style: 'width: 0; height: 0; position: fixed; opacity: 0;'.replace(/;/g, '!important;'), - })); - doTimeout().then(() => { - link = iframe.contentDocument.importNode(link, true); - iframe.contentDocument.body.appendChild(link); - }) - .then(doTimeout) - .then(() => link.dispatchEvent(new MouseEvent('click'))) - .then(doTimeout(1000)) - .then(() => { - URL.revokeObjectURL(objectURL); - iframe.remove(); - }); - } - }); - - function generateFileName() { - const today = new Date(); - const dd = ('0' + today.getDate()).substr(-2); - const mm = ('0' + (today.getMonth() + 1)).substr(-2); - const yyyy = today.getFullYear(); - return `stylus-${yyyy}-${mm}-${dd}${STYLUS_BACKUP_FILE_EXT}`; - } -}; - - -$('#unfile-all-styles').onclick = () => { - importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT}); -}; - -Object.assign(document.body, { - ondragover(event) { - const hasFiles = event.dataTransfer.types.includes('Files'); - event.dataTransfer.dropEffect = hasFiles || event.target.type == 'search' ? 'copy' : 'none'; - this.classList.toggle('dropzone', hasFiles); - if (hasFiles) { - event.preventDefault(); - clearTimeout(this.fadeoutTimer); - this.classList.remove('fadeout'); - } - }, - ondragend(event) { - animateElement(this, {className: 'fadeout', removeExtraClasses: ['dropzone']}).then(() => { - this.style.animationDuration = ''; - }); - }, - ondragleave(event) { - try { - // in Firefox event.target could be XUL browser and hence there is no permission to access it - if (event.target === this) { - this.ondragend(); - } - } catch (e) { - this.ondragend(); - } - }, - ondrop(event) { - this.ondragend(); - if (event.dataTransfer.files.length) { - event.preventDefault(); - if ($('#onlyUpdates input').checked) { - $('#onlyUpdates input').click(); - } - importFromFile({file: event.dataTransfer.files[0]}); - } - }, -}); +/* global messageBox, handleUpdate, applyOnMessage */ +'use strict'; + +const STYLISH_DUMP_FILE_EXT = '.txt'; +const STYLUS_BACKUP_FILE_EXT = '.json'; + + +function importFromFile({fileTypeFilter, file} = {}) { + return new Promise(resolve => { + const fileInput = document.createElement('input'); + if (file) { + readFile(); + return; + } + fileInput.style.display = 'none'; + fileInput.type = 'file'; + fileInput.accept = fileTypeFilter || STYLISH_DUMP_FILE_EXT; + fileInput.acceptCharset = 'utf-8'; + + document.body.appendChild(fileInput); + fileInput.initialValue = fileInput.value; + fileInput.onchange = readFile; + fileInput.click(); + + function readFile() { + if (file || fileInput.value !== fileInput.initialValue) { + file = file || fileInput.files[0]; + if (file.size > 100e6) { + console.warn("100MB backup? I don't believe you."); + importFromString('').then(resolve); + return; + } + document.body.style.cursor = 'wait'; + const fReader = new FileReader(); + fReader.onloadend = event => { + fileInput.remove(); + importFromString(event.target.result).then(numStyles => { + document.body.style.cursor = ''; + resolve(numStyles); + }); + }; + fReader.readAsText(file, 'utf-8'); + } + } + }); +} + + +function importFromString(jsonString) { + if (!BG) { + onBackgroundReady().then(() => importFromString(jsonString)); + return; + } + // create objects in background context + const json = BG.tryJSONparse(jsonString) || []; + if (typeof json.slice !== 'function') { + json.length = 0; + } + const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []); + const oldStylesByName = json.length && new Map( + oldStyles.map(style => [style.name.trim(), style])); + + const stats = { + added: {names: [], ids: [], legend: 'importReportLegendAdded'}, + unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'}, + metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth'}, + metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta'}, + codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode'}, + invalid: {names: [], legend: 'importReportLegendInvalid'}, + }; + + let index = 0; + let lastRenderTime = performance.now(); + const renderQueue = []; + const RENDER_NAP_TIME_MAX = 1000; // ms + const RENDER_QUEUE_MAX = 50; // number of styles + const SAVE_OPTIONS = {reason: 'import', notify: false}; + + return new Promise(proceed); + + function proceed(resolve) { + while (index < json.length) { + const item = json[index++]; + const info = analyze(item); + if (info) { + // using saveStyle directly since json was parsed in background page context + return BG.saveStyle(Object.assign(item, SAVE_OPTIONS)) + .then(style => account({style, info, resolve})); + } + } + renderQueue.forEach(style => handleUpdate(style, {reason: 'import'})); + renderQueue.length = 0; + done(resolve); + } + + function analyze(item) { + if (!item || !item.name || !item.name.trim() || typeof item !== 'object' + || (item.sections && typeof item.sections.slice !== 'function')) { + stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`); + return; + } + item.name = item.name.trim(); + const byId = BG.cachedStyles.byId.get(item.id); + const byName = oldStylesByName.get(item.name); + oldStylesByName.delete(item.name); + let oldStyle; + if (byId) { + if (sameStyle(byId, item)) { + oldStyle = byId; + } else { + item.id = null; + } + } + if (!oldStyle && byName) { + item.id = byName.id; + oldStyle = byName; + } + const oldStyleKeys = oldStyle && Object.keys(oldStyle); + const metaEqual = oldStyleKeys && + oldStyleKeys.length === Object.keys(item).length && + oldStyleKeys.every(k => k === 'sections' || oldStyle[k] === item[k]); + const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item); + if (metaEqual && codeEqual) { + stats.unchanged.names.push(oldStyle.name); + stats.unchanged.ids.push(oldStyle.id); + return; + } + return {oldStyle, metaEqual, codeEqual}; + } + + function sameStyle(oldStyle, newStyle) { + return oldStyle.name.trim() === newStyle.name.trim() || + ['updateUrl', 'originalMd5', 'originalDigest'] + .some(field => oldStyle[field] && oldStyle[field] === newStyle[field]); + } + + function account({style, info, resolve}) { + renderQueue.push(style); + if (performance.now() - lastRenderTime > RENDER_NAP_TIME_MAX + || renderQueue.length > RENDER_QUEUE_MAX) { + renderQueue.forEach(style => handleUpdate(style, {reason: 'import'})); + setTimeout(scrollElementIntoView, 0, $('#style-' + renderQueue.pop().id)); + renderQueue.length = 0; + lastRenderTime = performance.now(); + } + setTimeout(proceed, 0, resolve); + const {oldStyle, metaEqual, codeEqual} = info; + if (!oldStyle) { + stats.added.names.push(style.name); + stats.added.ids.push(style.id); + return; + } + if (!metaEqual && !codeEqual) { + stats.metaAndCode.names.push(reportNameChange(oldStyle, style)); + stats.metaAndCode.ids.push(style.id); + return; + } + if (!codeEqual) { + stats.codeOnly.names.push(style.name); + stats.codeOnly.ids.push(style.id); + return; + } + stats.metaOnly.names.push(reportNameChange(oldStyle, style)); + stats.metaOnly.ids.push(style.id); + } + + function done(resolve) { + const numChanged = stats.metaAndCode.names.length + + stats.metaOnly.names.length + + stats.codeOnly.names.length + + stats.added.names.length; + Promise.resolve(numChanged && refreshAllTabs()).then(() => { + const report = Object.keys(stats) + .filter(kind => stats[kind].names.length) + .map(kind => { + const {ids, names, legend} = stats[kind]; + const listItemsWithId = (name, i) => + $element({dataset: {id: ids[i]}, textContent: name}); + const listItems = name => + $element({textContent: name}); + const block = + $element({tag: 'details', dataset: {id: kind}, appendChild: [ + $element({tag: 'summary', appendChild: + $element({tag: 'b', textContent: names.length + ' ' + t(legend)}) + }), + $element({tag: 'small', appendChild: + names.map(ids ? listItemsWithId : listItems) + }), + ]}); + return block; + }); + scrollTo(0, 0); + messageBox({ + title: t('importReportTitle'), + contents: report.length ? report : t('importReportUnchanged'), + buttons: [t('confirmOK'), numChanged && t('undo')], + onshow: bindClick, + }).then(({button}) => { + if (button === 1) { + undo(); + } + }); + resolve(numChanged); + }); + } + + function undo() { + const oldStylesById = new Map(oldStyles.map(style => [style.id, style])); + const newIds = [ + ...stats.metaAndCode.ids, + ...stats.metaOnly.ids, + ...stats.codeOnly.ids, + ...stats.added.ids, + ]; + let resolve; + index = 0; + return new Promise(resolve_ => { + resolve = resolve_; + undoNextId(); + }).then(refreshAllTabs) + .then(() => messageBox({ + title: t('importReportUndoneTitle'), + contents: newIds.length + ' ' + t('importReportUndone'), + buttons: [t('confirmOK')], + })); + function undoNextId() { + if (index === newIds.length) { + resolve(); + return; + } + const id = newIds[index++]; + deleteStyleSafe({id, notify: false}).then(id => { + const oldStyle = oldStylesById.get(id); + if (oldStyle) { + saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS)) + .then(undoNextId); + } else { + undoNextId(); + } + }); + } + } + + function bindClick() { + const highlightElement = event => { + const styleElement = $('#style-' + event.target.dataset.id); + if (styleElement) { + scrollElementIntoView(styleElement); + animateElement(styleElement); + } + }; + for (const block of $$('details')) { + if (block.dataset.id !== 'invalid') { + block.style.cursor = 'pointer'; + block.onclick = highlightElement; + } + } + } + + function limitString(s, limit = 100) { + return s.length <= limit ? s : s.substr(0, limit) + '...'; + } + + function reportNameChange(oldStyle, newStyle) { + return newStyle.name !== oldStyle.name + ? oldStyle.name + ' —> ' + newStyle.name + : oldStyle.name; + } + + function refreshAllTabs() { + return Promise.all([ + getActiveTab(), + getOwnTab(), + ]).then(([activeTab, ownTab]) => new Promise(resolve => { + // list all tabs including chrome-extension:// which can be ours + queryTabs().then(tabs => { + const lastTab = tabs[tabs.length - 1]; + for (const tab of tabs) { + // skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF + if (FIREFOX && !tab.width) { + if (tab === lastTab) { + resolve(); + } + continue; + } + getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => { + const message = {method: 'styleReplaceAll', styles}; + if (tab.id === ownTab.id) { + applyOnMessage(message); + } else { + invokeOrPostpone(tab.id === activeTab.id, + chrome.tabs.sendMessage, tab.id, message, ignoreChromeError); + } + setTimeout(BG.updateIcon, 0, tab, styles); + if (tab === lastTab) { + resolve(); + } + }); + } + }); + })); + } +} + + +$('#file-all-styles').onclick = () => { + getStylesSafe().then(styles => { + const text = JSON.stringify(styles, null, '\t'); + const url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text); + return url; + // for long URLs; https://github.com/schomery/stylus/issues/13#issuecomment-284582600 + }).then(fetch) + .then(res => res.blob()) + .then(blob => { + const objectURL = URL.createObjectURL(blob); + let link = $element({ + tag:'a', + href: objectURL, + type: 'application/json', + download: generateFileName(), + }); + // TODO: remove the fallback when FF multi-process bug is fixed + if (!FIREFOX) { + link.dispatchEvent(new MouseEvent('click')); + setTimeout(() => URL.revokeObjectURL(objectURL)); + } else { + const iframe = document.body.appendChild($element({ + tag: 'iframe', + style: 'width: 0; height: 0; position: fixed; opacity: 0;'.replace(/;/g, '!important;'), + })); + doTimeout().then(() => { + link = iframe.contentDocument.importNode(link, true); + iframe.contentDocument.body.appendChild(link); + }) + .then(doTimeout) + .then(() => link.dispatchEvent(new MouseEvent('click'))) + .then(doTimeout(1000)) + .then(() => { + URL.revokeObjectURL(objectURL); + iframe.remove(); + }); + } + }); + + function generateFileName() { + const today = new Date(); + const dd = ('0' + today.getDate()).substr(-2); + const mm = ('0' + (today.getMonth() + 1)).substr(-2); + const yyyy = today.getFullYear(); + return `stylus-${yyyy}-${mm}-${dd}${STYLUS_BACKUP_FILE_EXT}`; + } +}; + + +$('#unfile-all-styles').onclick = () => { + importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT}); +}; + +Object.assign(document.body, { + ondragover(event) { + const hasFiles = event.dataTransfer.types.includes('Files'); + event.dataTransfer.dropEffect = hasFiles || event.target.type === 'search' ? 'copy' : 'none'; + this.classList.toggle('dropzone', hasFiles); + if (hasFiles) { + event.preventDefault(); + clearTimeout(this.fadeoutTimer); + this.classList.remove('fadeout'); + } + }, + ondragend() { + animateElement(this, {className: 'fadeout', removeExtraClasses: ['dropzone']}).then(() => { + this.style.animationDuration = ''; + }); + }, + ondragleave(event) { + try { + // in Firefox event.target could be XUL browser and hence there is no permission to access it + if (event.target === this) { + this.ondragend(); + } + } catch (e) { + this.ondragend(); + } + }, + ondrop(event) { + this.ondragend(); + if (event.dataTransfer.files.length) { + event.preventDefault(); + if ($('#onlyUpdates input').checked) { + $('#onlyUpdates input').click(); + } + importFromFile({file: event.dataTransfer.files[0]}); + } + }, +}); diff --git a/manage.css b/manage/manage.css similarity index 100% rename from manage.css rename to manage/manage.css diff --git a/manage.js b/manage/manage.js similarity index 91% rename from manage.js rename to manage/manage.js index da268c6b..3d11308a 100644 --- a/manage.js +++ b/manage/manage.js @@ -73,7 +73,7 @@ function initGlobalEvents() { // focus search field on / key document.onkeypress = event => { - if ((event.keyCode || event.which) == 47 + if ((event.keyCode || event.which) === 47 && !event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.target.matches('[type="text"], [type="search"]')) { event.preventDefault(); @@ -114,7 +114,7 @@ function initGlobalEvents() { 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)); + .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; @@ -128,8 +128,11 @@ function showStyles(styles = []) { function renderStyles() { const t0 = performance.now(); let rendered = 0; - while (index < sorted.length - && (shouldRenderAll || ++rendered < 10 || performance.now() - t0 < 10)) { + 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}); @@ -225,18 +228,18 @@ function createStyleTargetsElement({entry, style, postponeFavicons}) { displayed.add(targetValue); const element = template.appliesToTarget.cloneNode(true); if (!newUI.enabled) { - if (numTargets == 10) { + 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') { + if (type === 'domains') { favicon = GET_FAVICON_URL + targetValue; } else if (targetValue.startsWith('chrome-extension:')) { favicon = OWN_ICON; - } else if (type != 'regexps') { + } else if (type !== 'regexps') { favicon = targetValue.includes('://') && targetValue.match(/^.*?:\/\/([^/]+)/); favicon = favicon ? GET_FAVICON_URL + favicon[1] : ''; } @@ -289,7 +292,7 @@ Object.assign(handleEvent, { 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) { + 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); @@ -304,8 +307,8 @@ Object.assign(handleEvent, { } event.preventDefault(); event.stopPropagation(); - const left = event.button == 0; - const middle = event.button == 1; + const left = event.button === 0; + const middle = event.button === 1; const shift = event.shiftKey; const ctrl = event.ctrlKey; const openWindow = left && shift && !ctrl; @@ -357,8 +360,8 @@ Object.assign(handleEvent, { className: 'danger center', buttons: [t('confirmDelete'), t('confirmCancel')], }) - .then(({button, enter, esc}) => { - if (button == 0 || enter) { + .then(({button, enter}) => { + if (button === 0 || enter) { deleteStyleSafe({id}); } }); @@ -383,10 +386,10 @@ Object.assign(handleEvent, { }, filterOnChange({target: el, forceRefilter}) { - const getValue = el => (el.type == 'checkbox' ? el.checked : el.value.trim()); + const getValue = el => (el.type === 'checkbox' ? el.checked : el.value.trim()); if (!forceRefilter) { const value = getValue(el); - if (value == el.lastValue) { + if (value === el.lastValue) { return; } el.lastValue = value; @@ -412,22 +415,22 @@ Object.assign(handleEvent, { function handleUpdate(style, {reason, method} = {}) { let entry; let oldEntry = $(ENTRY_ID_PREFIX + style.id); - if (oldEntry && method == 'styleUpdated') { + if (oldEntry && method === 'styleUpdated') { handleToggledOrCodeOnly(); } entry = entry || createStyleElement({style}); if (oldEntry) { - if (oldEntry.styleNameLowerCase == entry.styleNameLowerCase) { + if (oldEntry.styleNameLowerCase === entry.styleNameLowerCase) { installed.replaceChild(entry, oldEntry); } else { oldEntry.remove(); } } - if (reason == 'update' && entry.matches('.updatable')) { + if (reason === 'update' && entry.matches('.updatable')) { handleUpdateInstalled(); } filterAndAppend({entry}); - if (!entry.matches('.hidden') && reason != 'import') { + if (!entry.matches('.hidden') && reason !== 'import') { animateElement(entry); scrollElementIntoView(entry); } @@ -435,12 +438,12 @@ function handleUpdate(style, {reason, method} = {}) { function handleToggledOrCodeOnly() { const newStyleMeta = getStyleWithNoCode(style); const diff = objectDiff(oldEntry.styleMeta, newStyleMeta); - if (diff.length == 0) { + if (diff.length === 0) { // only code was modified entry = oldEntry; oldEntry = null; } - if (diff.length == 1 && diff[0].key == 'enabled') { + 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)); @@ -478,8 +481,8 @@ function switchUI({styleOnly} = {}) { // 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); + 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; @@ -565,7 +568,7 @@ function checkUpdateAll() { $('#apply-all-updates').classList.add('hidden'); $('#update-all-no-updates').classList.add('hidden'); - const ignoreDigest = this && this.id == 'check-all-updates-force'; + const ignoreDigest = this && this.id === 'check-all-updates-force'; $$('.updatable:not(.can-update)' + (ignoreDigest ? '' : ':not(.update-problem)')) .map(el => checkUpdate(el, {single: false})); @@ -582,7 +585,7 @@ function checkUpdateAll() { total = value; break; case BG.updater.UPDATED: - if (++updated == 1) { + if (++updated === 1) { $('#apply-all-updates').disabled = true; $('#apply-all-updates').classList.remove('hidden'); } @@ -590,7 +593,7 @@ function checkUpdateAll() { // fallthrough case BG.updater.SKIPPED: checked++; - if (details == BG.updater.EDITED || details == BG.updater.MAYBE_EDITED) { + if (details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED) { skippedEdited++; } reportUpdateState(state, value, details); @@ -603,13 +606,13 @@ function checkUpdateAll() { function done() { document.body.classList.remove('update-in-progress'); - $('#check-all-updates').disabled = total == 0; + $('#check-all-updates').disabled = total === 0; $('#apply-all-updates').disabled = false; renderUpdatesOnlyFilter({check: updated + skippedEdited > 0}); if (!updated) { $('#update-all-no-updates').dataset.skippedEdited = skippedEdited > 0; $('#update-all-no-updates').classList.remove('hidden'); - $('#check-all-updates-force').classList.toggle('hidden', skippedEdited == 0); + $('#check-all-updates-force').classList.toggle('hidden', skippedEdited === 0); } } } @@ -645,16 +648,16 @@ function reportUpdateState(state, style, details) { if (entry.classList.contains('can-update')) { break; } - const same = details == BG.updater.SAME_MD5 || details == BG.updater.SAME_CODE; - const edited = details == BG.updater.EDITED || details == BG.updater.MAYBE_EDITED; + const same = details === BG.updater.SAME_MD5 || details === BG.updater.SAME_CODE; + const edited = details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED; entry.dataset.details = details; if (!details) { details = t('updateCheckFailServerUnreachable'); - } else if (typeof details == 'number') { + } else if (typeof details === 'number') { details = t('updateCheckFailBadResponseCode', [details]); - } else if (details == BG.updater.EDITED) { + } else if (details === BG.updater.EDITED) { details = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); - } else if (details == BG.updater.MAYBE_EDITED) { + } else if (details === BG.updater.MAYBE_EDITED) { details = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); } const message = same ? t('updateCheckSucceededNoUpdate') : details; @@ -716,7 +719,7 @@ function searchStyles({immediately, container}) { const searchElement = $('#search'); const query = searchElement.value.toLocaleLowerCase(); const queryPrev = searchElement.lastValue || ''; - if (query == queryPrev && !immediately && !container) { + if (query === queryPrev && !immediately && !container) { return; } if (!immediately) { @@ -738,7 +741,7 @@ function searchStyles({immediately, container}) { style.url && isMatchingText(style.url) || isMatchingStyle(style))); } - if (entry.classList.contains('not-matching') != !isMatching) { + if (entry.classList.contains('not-matching') !== !isMatching) { entry.classList.toggle('not-matching', !isMatching); needsRefilter = true; } @@ -847,7 +850,7 @@ function reapplyFilter(container = installed) { shuffle(false); setTimeout(shuffle, 0, true); // single-element job from handleEvent(): add the last wraith - if (toHide.length == 1 && toHide[0].parentElement != installed) { + if (toHide.length === 1 && toHide[0].parentElement !== installed) { installed.appendChild(toHide[0]); } return; @@ -882,7 +885,7 @@ function reapplyFilter(container = installed) { const skipGroup = state => { const start = i; const first = entry; - while (entry && entry.classList.contains('hidden') == state) { + while (entry && entry.classList.contains('hidden') === state) { entry = entry.nextElementSibling; i++; } @@ -900,7 +903,7 @@ function reapplyFilter(container = installed) { // 3. move the shortest group; repeat 2-3 if (hidden.len < visible.len && (fullPass || hidden.len % 2)) { // 3a. move hidden under the horizon - for (let j = 0; j < (fullPass ? hidden.len : 1); j++) { + for (let j = 0; j < (fullPass ? hidden.len : 1); j++) { const entry = entries[hidden.start]; installed.insertBefore(entry, horizon); horizon = entry; @@ -975,14 +978,19 @@ function objectDiff(first, second, path = '') { diff.push({path, key, values: [a], type: 'removed'}); continue; } - if (a && typeof a.filter == 'function' && b && typeof b.filter == 'function') { - if (a.length != b.length - || a.some((el, i) => !el || typeof el != 'object' ? el != b[i] - : objectDiff(el, b[i], path + key + '[' + i + '].').length) + if (a && typeof a.filter === 'function' && b && typeof b.filter === 'function') { + if ( + a.length !== b.length || + a.some((el, i) => { + const result = !el || typeof el !== 'object' + ? el !== b[i] + : objectDiff(el, b[i], path + key + '[' + i + '].').length; + return result; + }) ) { diff.push({path, key, values: [a, b], type: 'changed'}); } - } else if (typeof a == 'object' && typeof b == 'object') { + } else if (typeof a === 'object' && typeof b === 'object') { diff.push(...objectDiff(a, b, path + key + '.')); } else { diff.push({path, key, values: [a, b], type: 'changed'}); diff --git a/manifest.json b/manifest.json index 4f2ccca1..958f5a5e 100644 --- a/manifest.json +++ b/manifest.json @@ -19,7 +19,13 @@ "" ], "background": { - "scripts": ["messaging.js", "storage.js", "prefs.js", "background.js", "update.js"] + "scripts": [ + "js/messaging.js", + "background/storage.js", + "js/prefs.js", + "background/background.js", + "background/update.js" + ] }, "commands": { "openManage": { @@ -35,13 +41,13 @@ "run_at": "document_start", "all_frames": true, "match_about_blank": true, - "js": ["apply.js"] + "js": ["content/apply.js"] }, { "matches": ["http://userstyles.org/*", "https://userstyles.org/*"], "run_at": "document_start", "all_frames": false, - "js": ["install.js"] + "js": ["content/install.js"] } ], "browser_action": { @@ -56,7 +62,7 @@ }, "default_locale": "en", "options_ui": { - "page": "options/index.html", + "page": "options.html", "chrome_style": true } } diff --git a/msgbox/msgbox.js b/msgbox/msgbox.js index a7ebbe2a..8faf8d60 100644 --- a/msgbox/msgbox.js +++ b/msgbox/msgbox.js @@ -30,9 +30,9 @@ function messageBox({ key(event) { const keyCode = event.keyCode || event.which; if (!event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey - && (keyCode == 13 || keyCode == 27)) { + && (keyCode === 13 || keyCode === 27)) { event.preventDefault(); - resolveWith(keyCode == 13 ? {enter: true} : {esc: true}); + resolveWith(keyCode === 13 ? {enter: true} : {esc: true}); } }, scroll() { @@ -52,7 +52,7 @@ function messageBox({ unbindAndRemoveSelf(); } const id = 'message-box'; - const putAs = typeof contents == 'string' ? 'innerHTML' : 'appendChild'; + const putAs = typeof contents === 'string' ? 'innerHTML' : 'appendChild'; messageBox.element = $element({id, className, appendChild: [ $element({appendChild: [ $element({id: `${id}-title`, innerHTML: title}), diff --git a/options/index.html b/options.html similarity index 92% rename from options/index.html rename to options.html index 3a7318be..c673ac44 100644 --- a/options/index.html +++ b/options.html @@ -2,12 +2,12 @@ Stylus - - - - - - + + + + + + @@ -128,6 +128,6 @@ - + diff --git a/options/index.css b/options/index.css index fbc0e803..1b9508f7 100644 --- a/options/index.css +++ b/options/index.css @@ -81,6 +81,7 @@ label:not([disabled]) > :first-child { label:not([disabled]):hover > :first-child { text-shadow: 0 0 0.01px rgba(0, 0, 0, .25); + cursor: pointer; } button, diff --git a/options/index.js b/options/index.js index decc038d..10404e5b 100644 --- a/options/index.js +++ b/options/index.js @@ -15,7 +15,7 @@ document.onclick = e => { switch (target.dataset.cmd) { case 'open-manage': - openURL({url: '/manage.html'}); + openURL({url: 'manage.html'}); break; case 'check-updates': @@ -65,9 +65,9 @@ function checkUpdates() { function setupRadioButtons() { const sets = {}; - const onChange = function() { + const onChange = function () { const newValue = sets[this.name].indexOf(this); - if (newValue >= 0 && prefs.get(this.name) != newValue) { + if (newValue >= 0 && prefs.get(this.name) !== newValue) { prefs.set(this.name, newValue); } }; diff --git a/popup.html b/popup.html index 7684eb05..933df669 100644 --- a/popup.html +++ b/popup.html @@ -2,7 +2,7 @@ - +