From 58b1c5be8199c1ae028216000bd0fd4706972001 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 30 Aug 2017 16:20:49 +0300 Subject: [PATCH 01/51] fixup 508f71f2: revert to 'function' to use the correct 'this' --- edit/edit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/edit/edit.js b/edit/edit.js index d28b15f4..b2f72a7b 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -902,9 +902,9 @@ function setupGlobalSearch() { let query; let replacement; activeCM = focusClosestCM(activeCM); - customizeOpenDialog(activeCM, template[all ? 'replaceAll' : 'replace'], txt => { + customizeOpenDialog(activeCM, template[all ? 'replaceAll' : 'replace'], function (txt) { query = txt; - customizeOpenDialog(activeCM, template.replaceWith, txt => { + customizeOpenDialog(activeCM, template.replaceWith, function (txt) { replacement = txt; queue = editors.rotate(-editors.indexOf(activeCM)); if (all) { From 3c5d20f8626e8aec2e50ae5d6534762fd18e0c1d Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 30 Aug 2017 17:50:32 +0300 Subject: [PATCH 02/51] proper partial match detection and display in showRegExpTester closes #180 --- edit/edit.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/edit/edit.js b/edit/edit.js index b2f72a7b..237244ab 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -1816,7 +1816,8 @@ function showRegExpTester(event, section = getSectionForChild(this)) { const rxData = Object.assign({text}, cachedRegexps.get(text)); if (!rxData.urls) { cachedRegexps.set(text, Object.assign(rxData, { - rx: tryRegExp(text), + // imitate buggy Stylish-for-chrome, see detectSloppyRegexps() + rx: tryRegExp('^' + text + '$'), urls: new Map(), })); } @@ -1882,6 +1883,7 @@ function showRegExpTester(event, section = getSectionForChild(this)) { } else { partial.push($element({appendChild: [ icon, + url.substr(0, match.index), $element({tag: 'mark', textContent: match}), url.substr(match.length), ]})); From c88438c6ebb0defcd6a3df683dfa12aff0b4561d Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 30 Aug 2017 18:05:22 +0300 Subject: [PATCH 03/51] use tryCatch to simplify 7e5396a1 --- background/background.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/background/background.js b/background/background.js index 1b3dbc70..454239fa 100644 --- a/background/background.js +++ b/background/background.js @@ -306,13 +306,7 @@ function updateIcon(tab, styles) { function onRuntimeMessage(request, sender, sendResponse) { // prevent browser exception bug on sending a response to a closed tab - sendResponse = (sendResponseOriginal => - data => { - try { - sendResponseOriginal(data); - } catch (e) {} - } - )(sendResponse); + sendResponse = (send => data => tryCatch(send, data))(sendResponse); switch (request.method) { case 'getStyles': getStyles(request).then(sendResponse); From bf4619fc2ee14f86ac08ff81ce152fcb2b0c7cff Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 30 Aug 2017 20:44:41 +0300 Subject: [PATCH 04/51] exponentially speedup getEditorInSight to avoid delaying 'find' related: #178 --- edit/edit.js | 74 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/edit/edit.js b/edit/edit.js index 237244ab..7e515efa 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -1076,28 +1076,62 @@ function getEditorInSight(nearbyElement) { } 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) { + // closest editor should have at least 2 lines visible + const lineHeight = editors[0].defaultTextHeight(); + const scrollY = window.scrollY; + const windowBottom = scrollY + window.innerHeight - 2 * lineHeight; + const allSectionsContainerTop = scrollY + $('#sections').getBoundingClientRect().top; + const distances = []; + const alreadyInView = cm && offscreenDistance(null, cm) === 0; + return alreadyInView ? cm : findClosest(); + + function offscreenDistance(index, cm) { + if (index >= 0 && distances[index] !== undefined) { + return distances[index]; + } + const section = (cm || editors[index]).getSection(); + const top = allSectionsContainerTop + section.offsetTop; + if (top < scrollY + lineHeight) { + return Math.max(0, scrollY - top - lineHeight); + } + if (top < windowBottom) { + return 0; + } + const distance = top - windowBottom + section.offsetHeight; + if (index >= 0) { + distances[index] = distance; + } + return distance; + } + + function findClosest() { + // side-effect: sets 'cm' of the parent function + let a = 0; + let b = editors.length - 1; + let c; + let cm, distance; + while (a < b - 1) { + c = (a + b) / 2 | 0; + distance = offscreenDistance(c); + if (!distance || !c) { + break; + } + const distancePrev = offscreenDistance(c - 1); + const distanceNext = c <= b ? offscreenDistance(c + 1) : 1e20; + if (distancePrev <= distance && distance <= distanceNext) { + b = c; + } else { + a = c; + } + } + while (b && offscreenDistance(b - 1) <= offscreenDistance(b)) { + b--; + } + cm = editors[b]; + if (distances[b] > 0) { makeSectionVisible(cm); } - } - return cm; - - function offscreenDistance(cm) { - // closest editor should have at least # lines visible - const LINES_VISIBLE = 2; - 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; - } + return cm; } } From 916a3bced3d749ef26203a2240e38d05cb618c1e Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 30 Aug 2017 20:54:03 +0300 Subject: [PATCH 05/51] fixup bf4619fc possible OOB --- edit/edit.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/edit/edit.js b/edit/edit.js index 7e515efa..2e2b25a6 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -1105,9 +1105,9 @@ function getEditorInSight(nearbyElement) { } function findClosest() { - // side-effect: sets 'cm' of the parent function + const last = editors.length - 1; let a = 0; - let b = editors.length - 1; + let b = last; let c; let cm, distance; while (a < b - 1) { @@ -1117,7 +1117,7 @@ function getEditorInSight(nearbyElement) { break; } const distancePrev = offscreenDistance(c - 1); - const distanceNext = c <= b ? offscreenDistance(c + 1) : 1e20; + const distanceNext = c < last ? offscreenDistance(c + 1) : 1e20; if (distancePrev <= distance && distance <= distanceNext) { b = c; } else { From 8960a9f5ce41e96a06152ce1dbc8d988f364ff34 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 30 Aug 2017 21:19:53 +0300 Subject: [PATCH 06/51] fixup 3c5d20f8: show detection results properly fixes #180 --- edit/edit.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/edit/edit.js b/edit/edit.js index 2e2b25a6..ba4f5c12 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -1866,6 +1866,8 @@ function showRegExpTester(event, section = getSectionForChild(this)) { chrome.tabs.onUpdated.removeListener(_); } }); + const getMatchInfo = m => m && {text: m[0], pos: m.index}; + queryTabs().then(tabs => { const supported = tabs.map(tab => tab.url) .filter(url => URLS.supported(url)); @@ -1875,7 +1877,7 @@ function showRegExpTester(event, section = getSectionForChild(this)) { if (rx) { const urlsNow = new Map(); for (const url of unique) { - const match = urls.get(url) || (url.match(rx) || [])[0]; + const match = urls.get(url) || getMatchInfo(url.match(rx)); if (match) { urlsNow.set(url, match); } @@ -1909,7 +1911,7 @@ function showRegExpTester(event, section = getSectionForChild(this)) { ? OWN_ICON : GET_FAVICON_URL + new URL(url).hostname; const icon = $element({tag: 'img', src: faviconUrl}); - if (match.length === url.length) { + if (match.text.length === url.length) { full.push($element({appendChild: [ icon, url, @@ -1917,9 +1919,9 @@ function showRegExpTester(event, section = getSectionForChild(this)) { } else { partial.push($element({appendChild: [ icon, - url.substr(0, match.index), - $element({tag: 'mark', textContent: match}), - url.substr(match.length), + url.substr(0, match.pos), + $element({tag: 'mark', textContent: match.text}), + url.substr(match.pos + match.text.length), ]})); } } From 58b52b0bf0e3ee7264a56e35f0ebc70df2cc26e4 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 31 Aug 2017 13:11:34 +0300 Subject: [PATCH 07/51] make #confirm popup more flexible kinda fixes #175 --- popup/popup.css | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/popup/popup.css b/popup/popup.css index c666221b..ebb958c2 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -332,13 +332,9 @@ body.blocked .actions > .left-gutter { /* confirm */ -#confirm, -#confirm > div > span { +#confirm { align-items: center; justify-content: center; -} - -#confirm { z-index: 2147483647; display: none; position: absolute; @@ -358,6 +354,7 @@ body.blocked .actions > .left-gutter { animation-fill-mode: both; } +#confirm.lights-on, #confirm.lights-on > div { display: none; } @@ -368,30 +365,20 @@ body.blocked .actions > .left-gutter { #confirm > div { width: 80%; - height: 100px; max-height: 80%; - min-height: 8em; + min-height: 6em; + padding: 1em; background-color: #fff; display: flex; flex-direction: column; border: solid 2px rgba(0, 0, 0, 0.5); } -#confirm > div > span { - display: flex; - flex: 1; - padding: 0 10px; -} - -#confirm > div > b { - padding: 10px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; +#confirm > div > *:not(:last-child) { + padding-bottom: .5em; } #confirm > div > div { - padding: 10px; text-align: center; } @@ -400,6 +387,12 @@ body.blocked .actions > .left-gutter { text-align: right; } +#confirm > button { + /* add a gap between buttons both for horizontal + or vertical (when the label is wide) layout */ + margin: 0 .25em .25em 0; +} + .unreachable .entry { opacity: .25; } From fa5ebb89583b9cf63e33c94cce0c764026d20c40 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 31 Aug 2017 13:23:12 +0300 Subject: [PATCH 08/51] use callbacks in animateElement to avoid re-animation --- js/dom.js | 8 ++++---- msgbox/msgbox.js | 6 ++++-- popup/popup.js | 6 ++++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/js/dom.js b/js/dom.js index 091f58be..d3b09859 100644 --- a/js/dom.js +++ b/js/dom.js @@ -169,7 +169,7 @@ function animateElement( element, { className = 'highlight', removeExtraClasses = [], - remove = false, + onComplete, } = {}) { return element && new Promise(resolve => { element.addEventListener('animationend', function _() { @@ -180,9 +180,9 @@ function animateElement( // This is helpful to clean-up on the same frame ...removeExtraClasses ); - // TODO: investigate why animation restarts if the elements is removed in .then() - if (remove) { - element.remove(); + // TODO: investigate why animation restarts for 'display' modification in .then() + if (typeof onComplete === 'function') { + onComplete.call(element); } resolve(); }); diff --git a/msgbox/msgbox.js b/msgbox/msgbox.js index 4e418832..468ecb96 100644 --- a/msgbox/msgbox.js +++ b/msgbox/msgbox.js @@ -45,8 +45,10 @@ function messageBox({ function resolveWith(value) { unbindGlobalListeners(); setTimeout(messageBox.resolve, 0, value); - animateElement(messageBox.element, {className: 'fadeout', remove: true}) - .then(removeSelf); + animateElement(messageBox.element, { + className: 'fadeout', + onComplete: removeSelf, + }); } function createElement() { diff --git a/popup/popup.js b/popup/popup.js index 4b4f2ad1..40b4a2f6 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -312,8 +312,10 @@ Object.assign(handleEvent, { }; function confirm(ok) { window.onkeydown = null; - animateElement(box, {className: 'lights-on'}) - .then(() => (box.dataset.display = false)); + animateElement(box, { + className: 'lights-on', + onComplete: () => (box.dataset.display = false), + }); if (ok) { deleteStyleSafe({id}).then(() => { // update view with 'No styles installed for this site' message From 35615eb2d46d56e1c6c3e3d4bf8fb415b9c976dc Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 31 Aug 2017 13:28:56 +0300 Subject: [PATCH 09/51] make message-box.danger close icon match the red title --- msgbox/msgbox.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/msgbox/msgbox.css b/msgbox/msgbox.css index afd3af50..17b30e30 100644 --- a/msgbox/msgbox.css +++ b/msgbox/msgbox.css @@ -80,6 +80,10 @@ font-weight: bold; } +#message-box.danger #message-box-close-icon svg { + fill: maroon; +} + #message-box.danger #message-box-close-icon svg:hover { fill: #600; } From 4c616442f649f56ea51e4bf9df8174f4b886118d Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 31 Aug 2017 13:41:48 +0300 Subject: [PATCH 10/51] correctly show/hide #no-styles message in the popup --- popup/popup.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/popup/popup.js b/popup/popup.js index 40b4a2f6..093d2478 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -318,6 +318,8 @@ Object.assign(handleEvent, { }); if (ok) { deleteStyleSafe({id}).then(() => { + // don't wait for the async notifyAllTabs as we check the children right away + handleDelete(id); // update view with 'No styles installed for this site' message if (!installed.children.length) { showStyles([]); @@ -402,7 +404,7 @@ function handleUpdate(style) { // Add an entry when a new style for the current url is installed if (tabURL && BG.getApplicableSections({style, matchUrl: tabURL, stopOnFirst: true}).length) { document.body.classList.remove('blocked'); - $$('.blocked-info').forEach(el => el.remove()); + $$('.blocked-info, #no-styles').forEach(el => el.remove()); createStyleElement({style}); } } From 0189ae1d6468f92e47c372653463e3a10698e465 Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 29 Aug 2017 23:36:56 +0300 Subject: [PATCH 11/51] show lint report on open earlier; lint on import --- edit/edit.js | 27 ++++++++++++--------------- edit/lint.js | 29 +++++++++++++++++------------ 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/edit/edit.js b/edit/edit.js index ba4f5c12..9c84b5b6 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -1287,19 +1287,18 @@ function initWithStyle({style, codeIsUpdated}) { 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(); + const t0 = performance.now(); // after 100ms the sections will be added asynchronously - while (new Date().getTime() - queueStart <= 100 && queue.length) { + while (performance.now() - t0 <= 100 && queue.length) { add(); } (function processQueue() { if (queue.length) { add(); - setTimeout(processQueue, 0); + setTimeout(processQueue); } })(); initHooks(); @@ -1307,12 +1306,8 @@ function initWithStyle({style, codeIsUpdated}) { function add() { const sectionDiv = addSection(null, queue.shift()); maximizeCodeHeight(sectionDiv, !queue.length); - const cm = sectionDiv.CodeMirror; - if (CodeMirror.lint) { - setTimeout(() => { - cm.setOption('lint', CodeMirror.defaults.lint); - updateLintReport(cm, 0); - }, prefs.get('editor.lintDelay')); + if (!queue.length) { + editors.last.state.renderLintReportNow = true; } } } @@ -1451,9 +1446,9 @@ function validate() { return null; } -function updateLintReportIfEnabled(cm, time) { - if (CodeMirror.lint) { - updateLintReport(cm, time); +function updateLintReportIfEnabled(...args) { + if (CodeMirror.defaults.lint) { + updateLintReport(...args); } } @@ -1568,8 +1563,10 @@ function fromMozillaFormat() { function doImport(event) { // parserlib contained in CSSLint-worker.js - onDOMscripted(['vendor-overwrites/csslint/csslint-worker.js']) - .then(() => doImportWhenReady(event.target)); + onDOMscripted(['vendor-overwrites/csslint/csslint-worker.js']).then(() => { + doImportWhenReady(event.target); + editors.last.state.renderLintReportNow = true; + }); } function doImportWhenReady(target) { diff --git a/edit/lint.js b/edit/lint.js index e8cc1fc9..febdddf6 100644 --- a/edit/lint.js +++ b/edit/lint.js @@ -191,13 +191,24 @@ function updateLinter({immediately} = {}) { } function updateLintReport(cm, delay) { + if (cm && !cm.options.lint) { + // add 'lint' option back to the freshly created section + setTimeout(() => { + if (!cm.options.lint) { + cm.setOption('lint', linterConfig.getForCodeMirror()); + } + }); + } + const state = cm && cm.state && cm.state.lint || {}; if (delay === 0) { // immediately show pending csslint/stylelint messages in onbeforeunload and save + clearTimeout(state.lintTimeout); update(cm); return; } if (delay > 0) { - setTimeout(cm => { + clearTimeout(state.lintTimeout); + state.lintTimeout = setTimeout(cm => { if (cm.performLint) { cm.performLint(); update(cm); @@ -205,15 +216,10 @@ function updateLintReport(cm, delay) { }, 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.reportTimeout = setTimeout(update, (state.options || {}).delay + 100, cm); state.postponeNewIssues = delay === undefined || delay === null; function update(cm) { @@ -251,19 +257,18 @@ function updateLintReport(cm, delay) { }).join('') + ''; scopedState.markedLast = newMarkers; fixedOldIssues |= scopedState.reportDisplayed && Object.keys(oldMarkers).length > 0; - if (scopedState.html !== html) { + if ((scopedState.html || '') !== html) { scopedState.html = html; changed = true; } }); if (changed) { clearTimeout(state ? state.renderTimeout : undefined); - if (!state || !state.postponeNewIssues || fixedOldIssues) { + if (!state || !state.postponeNewIssues || fixedOldIssues || editors.last.state.renderLintReportNow) { + editors.last.state.renderLintReportNow = false; renderLintReport(true); } else { - state.renderTimeout = setTimeout(() => { - renderLintReport(true); - }, CodeMirror.defaults.lintReportDelay); + state.renderTimeout = setTimeout(renderLintReport, CodeMirror.defaults.lintReportDelay, true); } } } From 799108e88074d7080ea0341153e73babf8faa7a5 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 30 Aug 2017 01:30:45 +0300 Subject: [PATCH 12/51] create code box in write-new-style editor immediately --- edit/edit.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/edit/edit.js b/edit/edit.js index 9c84b5b6..8435deb8 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -1242,14 +1242,11 @@ function init() { section[CssToProperty[i]] = [params[i]]; } } - window.onload = () => { - window.onload = null; - addSection(null, section); - editors[0].setOption('lint', CodeMirror.defaults.lint); - // default to enabled - $('#enabled').checked = true; - initHooks(); - }; + addSection(null, section); + editors[0].setOption('lint', CodeMirror.defaults.lint); + // default to enabled + $('#enabled').checked = true; + initHooks(); return; } // This is an edit From ab97108e6f5be989c0c7b514ed07050d21fa9441 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 30 Aug 2017 01:49:03 +0300 Subject: [PATCH 13/51] properly replace the editors in initWithStyle --- edit/edit.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/edit/edit.js b/edit/edit.js index 8435deb8..b66ecb76 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -1247,6 +1247,8 @@ function init() { // default to enabled $('#enabled').checked = true; initHooks(); + setCleanGlobal(); + updateTitle(); return; } // This is an edit @@ -1285,9 +1287,11 @@ function initWithStyle({style, codeIsUpdated}) { return; } // if this was done in response to an update, we need to clear existing sections - getSections().forEach(div => { div.remove(); }); + editors.length = 0; + getSections().forEach(div => div.remove()); const queue = style.sections.length ? style.sections.slice() : [{code: ''}]; const t0 = performance.now(); + maximizeCodeHeight.stats = null; // after 100ms the sections will be added asynchronously while (performance.now() - t0 <= 100 && queue.length) { add(); @@ -1299,6 +1303,8 @@ function initWithStyle({style, codeIsUpdated}) { } })(); initHooks(); + setCleanGlobal(); + updateTitle(); function add() { const sectionDiv = addSection(null, queue.shift()); @@ -1310,6 +1316,10 @@ function initWithStyle({style, codeIsUpdated}) { } function initHooks() { + if (initHooks.alreadyDone) { + return; + } + initHooks.alreadyDone = true; $$('#header .style-contributor').forEach(node => { node.addEventListener('change', onChange); node.addEventListener('input', onChange); @@ -1341,8 +1351,6 @@ function initHooks() { }); setupGlobalSearch(); - setCleanGlobal(); - updateTitle(); } From 7f9c377d33795bb3ccf05931e4da36d6c89a3d95 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 31 Aug 2017 21:13:49 +0300 Subject: [PATCH 14/51] use 'changes' event to reduce overhead upon replaceAll --- edit/edit.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/edit/edit.js b/edit/edit.js index b66ecb76..e7367620 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -355,7 +355,7 @@ function acmeEventListener(event) { case 'autocompleteOnTyping': editors.forEach(cm => { const onOff = el.checked ? 'on' : 'off'; - cm[onOff]('change', autocompleteOnTyping); + cm[onOff]('changes', autocompleteOnTyping); cm[onOff]('pick', autocompletePicked); }); return; @@ -380,9 +380,9 @@ function setupCodeMirror(textarea, index) { const cm = CodeMirror.fromTextArea(textarea, {lint: null}); const wrapper = cm.display.wrapper; - cm.on('change', indicateCodeChange); + cm.on('changes', cm => debounce(indicateCodeChange, 200, cm)); if (prefs.get('editor.autocompleteOnTyping')) { - cm.on('change', autocompleteOnTyping); + cm.on('changes', autocompleteOnTyping); cm.on('pick', autocompletePicked); } cm.on('blur', () => { @@ -1019,7 +1019,7 @@ function toggleSectionHeight(cm) { } } -function autocompleteOnTyping(cm, info, debounced) { +function autocompleteOnTyping(cm, [info], debounced) { if ( cm.state.completionActive || info.origin && !info.origin.includes('input') || @@ -1032,7 +1032,7 @@ function autocompleteOnTyping(cm, info, debounced) { return; } if (!debounced) { - debounce(autocompleteOnTyping, 100, cm, info, true); + debounce(autocompleteOnTyping, 100, cm, [info], true); return; } if (info.text.last.match(/[-\w!]+$/)) { From ec01914f170c74ae376fb28338c53067b9e4079f Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 31 Aug 2017 21:36:18 +0300 Subject: [PATCH 15/51] separate 'rule' property in lint annotations --- edit/lint-codemirror-helper.js | 7 +++++-- edit/lint.js | 8 +++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/edit/lint-codemirror-helper.js b/edit/lint-codemirror-helper.js index 588a202c..6106ca1c 100644 --- a/edit/lint-codemirror-helper.js +++ b/edit/lint-codemirror-helper.js @@ -6,7 +6,8 @@ CodeMirror.registerHelper('lint', 'csslint', code => .messages.map(message => ({ from: CodeMirror.Pos(message.line - 1, message.col - 1), to: CodeMirror.Pos(message.line - 1, message.col), - message: message.message + ` (${message.rule.id})`, + message: message.message, + rule: message.rule.id, severity : message.type })) ); @@ -24,7 +25,9 @@ CodeMirror.registerHelper('lint', 'stylelint', code => to: CodeMirror.Pos(warning.line - 1, warning.column), message: warning.text .replace('Unexpected ', '') - .replace(/^./, firstLetter => firstLetter.toUpperCase()), + .replace(/^./, firstLetter => firstLetter.toUpperCase()) + .replace(/\s*\([^(]+\)$/, ''), // strip the rule, + rule: warning.text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'), severity : warning.severity })); }) diff --git a/edit/lint.js b/edit/lint.js index febdddf6..eb97d1c4 100644 --- a/edit/lint.js +++ b/edit/lint.js @@ -237,16 +237,14 @@ function updateLintReport(cm, delay) { const pos = isActiveLine ? 'cursor' : (info.from.line + ',' + info.from.ch); // rule name added in parentheses at the end; extract it out for the info popup const text = info.message; - const parenPos = text.endsWith(')') ? text.lastIndexOf('(') : text.length; - const ruleName = text.slice(parenPos + 1, -1); - const title = escapeHtml(text); - const message = escapeHtml(text.substr(0, Math.min(100, parenPos)), {limit: 100}); + const title = escapeHtml(text + `\n(${info.rule})`, {limit: 1000}); + const message = escapeHtml(text, {limit: 100}); if (isActiveLine || oldMarkers[pos] === message) { delete oldMarkers[pos]; } newMarkers[pos] = message; return ` - +
${info.severity}
${info.from.line + 1} From 85a5702fe08c74c066c393df55256e15e1b6ea1f Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 31 Aug 2017 22:25:05 +0300 Subject: [PATCH 16/51] refactor lint report stuff, use $element --- edit/edit.js | 3 +- edit/lint.js | 158 +++++++++++++++++++++++++++++---------------------- js/dom.js | 6 ++ 3 files changed, 97 insertions(+), 70 deletions(-) diff --git a/edit/edit.js b/edit/edit.js index e7367620..9b25a902 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -154,7 +154,7 @@ function setCleanSection(section) { const cm = section.CodeMirror; if (cm) { section.savedValue = cm.changeGeneration(); - indicateCodeChange(cm); + updateTitle(); } } @@ -1570,6 +1570,7 @@ function fromMozillaFormat() { // parserlib contained in CSSLint-worker.js onDOMscripted(['vendor-overwrites/csslint/csslint-worker.js']).then(() => { doImportWhenReady(event.target); + editors.forEach(cm => updateLintReportIfEnabled(cm, 1)); editors.last.state.renderLintReportNow = true; }); } diff --git a/edit/lint.js b/edit/lint.js index eb97d1c4..a91622a9 100644 --- a/edit/lint.js +++ b/edit/lint.js @@ -152,9 +152,9 @@ function updateLinter({immediately} = {}) { if (guttersOption) { cm.setOption('guttersOption', guttersOption); updateGutters(cm, guttersOption); + cm.refresh(); } - cm.refresh(); - updateLintReport(cm); + setTimeout(updateLintReport, 0, cm); }); } @@ -188,6 +188,9 @@ function updateLinter({immediately} = {}) { updateEditors(); }); $('#linter-settings').style.display = !linter ? 'none' : 'inline-block'; + if (!linter) { + $('#lint > div').textContent = ''; + } } function updateLintReport(cm, delay) { @@ -203,7 +206,7 @@ function updateLintReport(cm, delay) { if (delay === 0) { // immediately show pending csslint/stylelint messages in onbeforeunload and save clearTimeout(state.lintTimeout); - update(cm); + updateLintReportInternal(cm); return; } if (delay > 0) { @@ -211,73 +214,79 @@ function updateLintReport(cm, delay) { state.lintTimeout = setTimeout(cm => { if (cm.performLint) { cm.performLint(); - update(cm); + updateLintReportInternal(cm); } }, delay, cm); 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; + if (state.options) { + clearTimeout(state.reportTimeout); + const delay = cm && cm.state.renderLintReportNow ? 0 : state.options.delay + 100; + state.reportTimeout = setTimeout(updateLintReportInternal, delay, cm, { + 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 => { +function updateLintReportInternal(scope, {postponeNewIssues} = {}) { + scope = scope ? [scope] : editors; + let changed = false; + let fixedOldIssues = false; + const clipString = (str, limit) => + str.length <= limit ? str : str.substr(0, limit) + '...'; + scope.forEach(cm => { + const lintState = cm.state.lint || {}; + const oldMarkers = lintState.markedLast || new Map(); + const newMarkers = lintState.markedLast = new Map(); + const marked = lintState.marked || {}; + const activeLine = cm.getCursor().line; + if (marked.length) { + const body = $element({tag: 'tbody', + appendChild: 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); - // rule name added in parentheses at the end; extract it out for the info popup - const text = info.message; - const title = escapeHtml(text + `\n(${info.rule})`, {limit: 1000}); - const message = escapeHtml(text, {limit: 100}); + const {line, ch} = info.from; + const isActiveLine = line === activeLine; + const pos = isActiveLine ? 'cursor' : (line + ',' + ch); + const title = clipString(info.message, 1000) + `\n(${info.rule})`; + const message = clipString(info.message, 100); if (isActiveLine || oldMarkers[pos] === message) { - delete oldMarkers[pos]; + oldMarkers.delete(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; + newMarkers.set(pos, message); + return $element({tag: 'tr', + className: info.severity, + appendChild: [ + $element({tag: 'td', + attributes: {role: 'severity'}, + dataset: {rule: info.rule}, + appendChild: $element({ + className: 'CodeMirror-lint-marker-' + info.severity, + textContent: info.severity, + }), + }), + $element({tag: 'td', attributes: {role: 'line'}, textContent: line + 1}), + $element({tag: 'td', attributes: {role: 'sep'}, textContent: ':'}), + $element({tag: 'td', attributes: {role: 'col'}, textContent: ch + 1}), + $element({tag: 'td', attributes: {role: 'message'}, textContent: message, title}), + ], + }); + }) + }); + const text = body.textContentCached = body.textContent; + if (text !== ((lintState.body || {}).textContentCached || '')) { + lintState.body = body; changed = true; } - }); - if (changed) { - clearTimeout(state ? state.renderTimeout : undefined); - if (!state || !state.postponeNewIssues || fixedOldIssues || editors.last.state.renderLintReportNow) { - editors.last.state.renderLintReportNow = false; - renderLintReport(true); - } else { - state.renderTimeout = setTimeout(renderLintReport, CodeMirror.defaults.lintReportDelay, true); - } } - } - function escapeHtml(html, {limit} = {}) { - const chars = {'&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/'}; - let ellipsis = ''; - if (limit && html.length > limit) { - html = html.substr(0, limit); - ellipsis = '...'; + fixedOldIssues |= lintState.reportDisplayed && oldMarkers.size; + }); + if (changed) { + if (!postponeNewIssues || fixedOldIssues || editors.last.state.renderLintReportNow) { + editors.last.state.renderLintReportNow = false; + renderLintReport(true); + } else { + debounce(renderLintReport, CodeMirror.defaults.lintReportDelay, true); } - return html.replace(/[&<>"'/]/g, char => chars[char]) + ellipsis; } } @@ -288,18 +297,29 @@ function renderLintReport(someBlockChanged) { const newContent = content.cloneNode(false); let issueCount = 0; editors.forEach((cm, index) => { - if (cm.state.lint && cm.state.lint.html) { - const html = '' + label + ' ' + (index + 1) + '' + cm.state.lint.html; - const newBlock = newContent.appendChild(tHTML(html, 'table')); - - 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; + const lintState = cm.state.lint || {}; + const body = lintState.body; + if (!body) { + return; } + const newBlock = $element({ + tag: 'table', + appendChild: [ + $element({tag: 'caption', textContent: label + ' ' + (index + 1)}), + body, + ], + cm, + }); + newContent.appendChild(newBlock); + issueCount += newBlock.rows.length; + + const block = content.children[newContent.children.length - 1]; + const blockChanged = + !block || + block.cm !== cm || + body.textContentCached !== block.textContentCached; + someBlockChanged |= blockChanged; + lintState.reportDisplayed = blockChanged; }); if (someBlockChanged || newContent.children.length !== content.children.length) { $('#issue-count').textContent = issueCount; diff --git a/js/dom.js b/js/dom.js index d3b09859..659c2e4d 100644 --- a/js/dom.js +++ b/js/dom.js @@ -244,6 +244,12 @@ function $element(opt) { Object.assign(element.dataset, opt.dataset); delete opt.dataset; } + if (opt.attributes) { + for (const attr in opt.attributes) { + element.setAttribute(attr, opt.attributes[attr]); + } + delete opt.attributes; + } if (ns) { for (const attr in opt) { element.setAttributeNS(null, attr, opt[attr]); From 86ebca5e1a39b3bdebf5f799784a62bca1f33ce3 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 1 Sep 2017 00:52:38 +0300 Subject: [PATCH 17/51] editor: show progress bar while opening huge styles --- edit/edit.css | 17 +++++++++++++++++ edit/edit.js | 19 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/edit/edit.css b/edit/edit.css index 5e54a2b1..27b669ed 100644 --- a/edit/edit.css +++ b/edit/edit.css @@ -2,6 +2,23 @@ body { margin: 0; font: 12px arial,sans-serif; } + +#global-progress { + position: fixed; + height: 4px; + top: 0; + left: 0; + right: 0; + background-color: hsla(180, 66%, 36%, .25); + border-left: 0 solid darkcyan; + z-index: 2147483647; + opacity: 0; + transition: opacity 2s; +} +#global-progress[title] { + opacity: 1; +} + /************ header ************/ #header { width: 280px; diff --git a/edit/edit.js b/edit/edit.js index 9b25a902..b1a99462 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -1300,6 +1300,11 @@ function initWithStyle({style, codeIsUpdated}) { if (queue.length) { add(); setTimeout(processQueue); + if (performance.now() - t0 > 500) { + setGlobalProgress(editors.length, style.sections.length); + } + } else { + setGlobalProgress(); } })(); initHooks(); @@ -2164,3 +2169,17 @@ function getCodeMirrorThemes() { }); }); } + +function setGlobalProgress(done, total) { + const progressElement = $('#global-progress') || + total && document.body.appendChild($element({id: 'global-progress'})); + if (total) { + const progress = (done / Math.max(done, total) * 100).toFixed(1); + progressElement.style.borderLeftWidth = progress + 'vw'; + setTimeout(() => { + progressElement.title = progress + '%'; + }); + } else if (progressElement) { + progressElement.remove(); + } +} From 27ad478f8cd462fc521efc18915df74627856c47 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 1 Sep 2017 11:04:24 +0300 Subject: [PATCH 18/51] fixup fb7f7d54: isMatching must be boolean --- manage/filters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manage/filters.js b/manage/filters.js index 8d96b601..1d0eecce 100644 --- a/manage/filters.js +++ b/manage/filters.js @@ -339,7 +339,7 @@ function searchStyles({immediately, container}) { if (!isMatching) { const style = urlMode ? siteStyleIds.has(entry.styleId) : BG.cachedStyles.byId.get(entry.styleId) || {}; - isMatching = urlMode ? style : Boolean(style && ( + isMatching = Boolean(style && (urlMode || isMatchingText(style.name) || style.url && isMatchingText(style.url) || isMatchingStyle(style))); From 0a5d9d86bf4c8e6a845c2db60922413c8ed2aba6 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 1 Sep 2017 11:08:48 +0300 Subject: [PATCH 19/51] editor: on disabling CSS linter option, hide the report entirely --- edit/lint.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edit/lint.js b/edit/lint.js index a91622a9..05414293 100644 --- a/edit/lint.js +++ b/edit/lint.js @@ -189,7 +189,7 @@ function updateLinter({immediately} = {}) { }); $('#linter-settings').style.display = !linter ? 'none' : 'inline-block'; if (!linter) { - $('#lint > div').textContent = ''; + $('#lint').style.display = 'none'; } } From 88093842646b01b0113ec86bed6d75cadd1c02df Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 1 Sep 2017 11:16:57 +0300 Subject: [PATCH 20/51] debounce indicateCodeChange per cm --- edit/edit.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/edit/edit.js b/edit/edit.js index b1a99462..046bcf6f 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -380,7 +380,7 @@ function setupCodeMirror(textarea, index) { const cm = CodeMirror.fromTextArea(textarea, {lint: null}); const wrapper = cm.display.wrapper; - cm.on('changes', cm => debounce(indicateCodeChange, 200, cm)); + cm.on('changes', indicateCodeChangeDebounced); if (prefs.get('editor.autocompleteOnTyping')) { cm.on('changes', autocompleteOnTyping); cm.on('pick', autocompletePicked); @@ -469,6 +469,11 @@ function indicateCodeChange(cm) { updateLintReportIfEnabled(cm); } +function indicateCodeChangeDebounced(cm, ...args) { + clearTimeout(cm.state.stylusOnChangeTimer); + cm.state.stylusOnChangeTimer = setTimeout(indicateCodeChange, 200, cm, ...args); +} + function getSectionForChild(e) { return e.closest('#sections > div'); } From 96b47e7bddfd9e0f0d135bf2605e3e44a0bcbb27 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 1 Sep 2017 12:23:49 +0300 Subject: [PATCH 21/51] editor: autoswitch to next/prev editor on cursor movement --- edit/edit.css | 6 ++++++ edit/edit.js | 54 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/edit/edit.css b/edit/edit.css index 27b669ed..877a45fd 100644 --- a/edit/edit.css +++ b/edit/edit.css @@ -146,6 +146,12 @@ h2 .svg-icon, label .svg-icon { #sections { counter-reset: codebox; } +#sections > div > label { + animation: 2s highlight; + animation-play-state: paused; + animation-direction: reverse; + animation-fill-mode: both; +} #sections > div > label::after { counter-increment: codebox; content: counter(codebox); diff --git a/edit/edit.js b/edit/edit.js index 046bcf6f..9f634379 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -196,8 +196,8 @@ function initCodeMirror() { // additional commands CM.commands.jumpToLine = jumpToLine; - CM.commands.nextEditor = cm => { nextPrevEditor(cm, 1); }; - CM.commands.prevEditor = cm => { nextPrevEditor(cm, -1); }; + 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}); @@ -385,6 +385,7 @@ function setupCodeMirror(textarea, index) { cm.on('changes', autocompleteOnTyping); cm.on('pick', autocompletePicked); } + wrapper.addEventListener('keydown', event => nextPrevEditorOnKeydown(cm, event), true); cm.on('blur', () => { editors.lastActive = cm; hotkeyRerouter.setState(true); @@ -1071,6 +1072,55 @@ function nextPrevEditor(cm, direction) { cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length]; makeSectionVisible(cm); cm.focus(); + return cm; +} + +function nextPrevEditorOnKeydown(cm, event) { + const key = event.which; + if (key < 37 || key > 40 || event.shiftKey || event.altKey || event.metaKey) { + return; + } + const {line, ch} = cm.getCursor(); + switch (key) { + case 37: + // arrow Left + if (line || ch) { + return; + } + // fallthrough to arrow Up + case 38: + // arrow Up + if (line > 0 || cm === editors[0]) { + return; + } + event.preventDefault(); + event.stopPropagation(); + cm = nextPrevEditor(cm, -1); + cm.setCursor(cm.doc.size - 1, key === 37 ? 1e20 : ch); + break; + case 39: + // arrow Right + if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) { + return; + } + // fallthrough to arrow Down + case 40: + // arrow Down + if (line < cm.doc.size - 1 || cm === editors.last) { + return; + } + event.preventDefault(); + event.stopPropagation(); + cm = nextPrevEditor(cm, 1); + cm.setCursor(0, 0); + break; + } + const animation = (cm.getSection().firstElementChild.getAnimations() || [])[0]; + if (animation) { + animation.playbackRate = -1; + animation.currentTime = 2000; + animation.play(); + } } function getEditorInSight(nearbyElement) { From 874088e067182254d121d49c8296228a42de1636 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 1 Sep 2017 12:25:40 +0300 Subject: [PATCH 22/51] edit: focus the first section upon opening --- edit/edit.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/edit/edit.js b/edit/edit.js index 9f634379..af23297d 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -1299,6 +1299,7 @@ function init() { } addSection(null, section); editors[0].setOption('lint', CodeMirror.defaults.lint); + editors[0].focus(); // default to enabled $('#enabled').checked = true; initHooks(); @@ -1362,6 +1363,7 @@ function initWithStyle({style, codeIsUpdated}) { setGlobalProgress(); } })(); + editors[0].focus(); initHooks(); setCleanGlobal(); updateTitle(); From 1f237a98eb0d46351cdd7e40e3b47415e6125858 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 1 Sep 2017 12:27:59 +0300 Subject: [PATCH 23/51] editor: focus the name input when saving a nameless style --- edit/edit.js | 1 + 1 file changed, 1 insertion(+) diff --git a/edit/edit.js b/edit/edit.js index af23297d..36451969 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -1484,6 +1484,7 @@ function updateTitle() { function validate() { const name = $('#name').value; if (name === '') { + $('#name').focus(); return t('styleMissingName'); } // validate the regexps From d3b7b45452e12d94034808db9769c75368b27825 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 1 Sep 2017 12:46:41 +0300 Subject: [PATCH 24/51] editor: toggle #options block upon clicking its title --- edit.html | 7 ++++--- edit/edit.css | 9 +++++++++ edit/edit.js | 7 ++++++- js/prefs.js | 1 + 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/edit.html b/edit.html index b8d2b243..52a7d86e 100644 --- a/edit.html +++ b/edit.html @@ -144,8 +144,8 @@ -
-

+
+

@@ -195,7 +195,8 @@   -
+ +

:

diff --git a/edit/edit.css b/edit/edit.css index 877a45fd..27e78a88 100644 --- a/edit/edit.css +++ b/edit/edit.css @@ -118,6 +118,15 @@ h2 .svg-icon, label .svg-icon { margin-bottom: 0.5rem; } /* options */ +#options summary { + align-items: center; + margin-left: -13px; + cursor: pointer; + outline: none; +} +#options summary h2 { + display: inline-block; +} #options [type="number"] { max-width: 2.5rem; text-align: right; diff --git a/edit/edit.js b/edit/edit.js index 36451969..2e9e1430 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -1282,7 +1282,7 @@ function beautify(event) { } } -document.addEventListener('DOMContentLoaded', init); +onDOMready().then(init); function init() { initCodeMirror(); @@ -1395,6 +1395,11 @@ function initHooks() { $('#sections-help').addEventListener('click', showSectionHelp, false); $('#keyMap-help').addEventListener('click', showKeyMapHelp, false); $('#cancel-button').addEventListener('click', goBackToManage); + $('#options').open = prefs.get('editor.options.expanded'); + $('#options h2').addEventListener('click', () => { + setTimeout(() => prefs.set('editor.options.expanded', $('#options').open)); + }); + initLint(); if (!FIREFOX) { diff --git a/js/prefs.js b/js/prefs.js index 7567c787..e308bbfd 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -25,6 +25,7 @@ var prefs = new function Prefs() { 'manage.newUI.targets': 3, // max number of applies-to targets visible: 0 = none 'editor.options': {}, // CodeMirror.defaults.* + 'editor.options.expanded': true,// UI element state: expanded/collapsed 'editor.lineWrapping': true, // word wrap 'editor.smartIndent': true, // 'smart' indent 'editor.indentWithTabs': false, // smart indent with tabs From cf2f64436670cb32778ef522c91d3ba4a13d10bf Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 1 Sep 2017 13:30:58 +0300 Subject: [PATCH 25/51] code cosmetics --- manage/filters.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manage/filters.js b/manage/filters.js index 1d0eecce..088abd90 100644 --- a/manage/filters.js +++ b/manage/filters.js @@ -339,7 +339,8 @@ function searchStyles({immediately, container}) { if (!isMatching) { const style = urlMode ? siteStyleIds.has(entry.styleId) : BG.cachedStyles.byId.get(entry.styleId) || {}; - isMatching = Boolean(style && (urlMode || + isMatching = Boolean(style && ( + urlMode || isMatchingText(style.name) || style.url && isMatchingText(style.url) || isMatchingStyle(style))); From 72e8213bd7a5f45faa6d15f26f3ae54bcac1700c Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 2 Sep 2017 08:51:36 +0300 Subject: [PATCH 26/51] followup 9a55e64b: suppress CSS transitions bug in FF while loading fixes #177 --- manage.html | 6 ++++++ manage/filters.js | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/manage.html b/manage.html index bd6f73d8..6548ed03 100644 --- a/manage.html +++ b/manage.html @@ -7,6 +7,12 @@ + - * **Browser**: * **Operating System**: * **Screenshot**: + + From 0c205df108876a2ea3029b40dd2fc272f77879a9 Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 3 Sep 2017 19:36:33 +0300 Subject: [PATCH 30/51] fixup 85a5702f: refactor; properly clear renderLintReportNow --- edit/lint.js | 113 +++++++++++++++++++++++++-------------------------- 1 file changed, 56 insertions(+), 57 deletions(-) diff --git a/edit/lint.js b/edit/lint.js index 05414293..44b3899b 100644 --- a/edit/lint.js +++ b/edit/lint.js @@ -229,64 +229,62 @@ function updateLintReport(cm, delay) { } function updateLintReportInternal(scope, {postponeNewIssues} = {}) { - scope = scope ? [scope] : editors; - let changed = false; - let fixedOldIssues = false; - const clipString = (str, limit) => - str.length <= limit ? str : str.substr(0, limit) + '...'; - scope.forEach(cm => { - const lintState = cm.state.lint || {}; - const oldMarkers = lintState.markedLast || new Map(); - const newMarkers = lintState.markedLast = new Map(); - const marked = lintState.marked || {}; - const activeLine = cm.getCursor().line; - if (marked.length) { - const body = $element({tag: 'tbody', - appendChild: marked.map(mark => { - const info = mark.__annotation; - const {line, ch} = info.from; - const isActiveLine = line === activeLine; - const pos = isActiveLine ? 'cursor' : (line + ',' + ch); - const title = clipString(info.message, 1000) + `\n(${info.rule})`; - const message = clipString(info.message, 100); - if (isActiveLine || oldMarkers[pos] === message) { - oldMarkers.delete(pos); - } - newMarkers.set(pos, message); - return $element({tag: 'tr', - className: info.severity, - appendChild: [ - $element({tag: 'td', - attributes: {role: 'severity'}, - dataset: {rule: info.rule}, - appendChild: $element({ - className: 'CodeMirror-lint-marker-' + info.severity, - textContent: info.severity, - }), - }), - $element({tag: 'td', attributes: {role: 'line'}, textContent: line + 1}), - $element({tag: 'td', attributes: {role: 'sep'}, textContent: ':'}), - $element({tag: 'td', attributes: {role: 'col'}, textContent: ch + 1}), - $element({tag: 'td', attributes: {role: 'message'}, textContent: message, title}), - ], - }); - }) - }); - const text = body.textContentCached = body.textContent; - if (text !== ((lintState.body || {}).textContentCached || '')) { - lintState.body = body; - changed = true; - } - } - fixedOldIssues |= lintState.reportDisplayed && oldMarkers.size; - }); + const {changed, fixedSome} = (scope ? [scope] : editors).reduce(process, {}); if (changed) { - if (!postponeNewIssues || fixedOldIssues || editors.last.state.renderLintReportNow) { - editors.last.state.renderLintReportNow = false; - renderLintReport(true); - } else { - debounce(renderLintReport, CodeMirror.defaults.lintReportDelay, true); - } + const renderNow = editors.last.state.renderLintReportNow = + !postponeNewIssues || fixedSome || editors.last.state.renderLintReportNow; + debounce(renderLintReport, renderNow ? 0 : CodeMirror.defaults.lintReportDelay, true); + } + + function process(result, cm) { + const lintState = cm.state.lint || {}; + const oldMarkers = lintState.stylusMarkers || new Map(); + const newMarkers = lintState.stylusMarkers = new Map(); + const oldText = (lintState.body || {}).textContentCached || ''; + const activeLine = cm.getCursor().line; + const body = !(lintState.marked || {}).length ? {} : $element({ + tag: 'tbody', + appendChild: lintState.marked.map(mark => { + const info = mark.__annotation; + const {line, ch} = info.from; + const isActiveLine = line === activeLine; + const pos = isActiveLine ? 'cursor' : (line + ',' + ch); + const title = clipString(info.message, 1000) + `\n(${info.rule})`; + const message = clipString(info.message, 100); + if (isActiveLine || oldMarkers[pos] === message) { + oldMarkers.delete(pos); + } + newMarkers.set(pos, message); + return $element({ + tag: 'tr', + className: info.severity, + appendChild: [ + $element({ + tag: 'td', + attributes: {role: 'severity'}, + dataset: {rule: info.rule}, + appendChild: $element({ + className: 'CodeMirror-lint-marker-' + info.severity, + textContent: info.severity, + }), + }), + $element({tag: 'td', attributes: {role: 'line'}, textContent: line + 1}), + $element({tag: 'td', attributes: {role: 'sep'}, textContent: ':'}), + $element({tag: 'td', attributes: {role: 'col'}, textContent: ch + 1}), + $element({tag: 'td', attributes: {role: 'message'}, textContent: message, title}), + ], + }); + }) + }); + body.textContentCached = body.textContent || ''; + lintState.body = body.textContentCached && body; + result.changed |= oldText !== body.textContentCached; + result.fixedSome |= lintState.reportDisplayed && oldMarkers.size; + return result; + } + + function clipString(str, limit) { + return str.length <= limit ? str : str.substr(0, limit) + '...'; } } @@ -297,6 +295,7 @@ function renderLintReport(someBlockChanged) { const newContent = content.cloneNode(false); let issueCount = 0; editors.forEach((cm, index) => { + cm.state.renderLintReportNow = false; const lintState = cm.state.lint || {}; const body = lintState.body; if (!body) { From 519d745f59ff1a6d80bcd5114efcdbc95a835141 Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 3 Sep 2017 11:12:18 +0300 Subject: [PATCH 31/51] globally disable CSS transitions for a moment during page opening the problem we fix is that since we add the styles asynchronously, the browsers, esp. Firefox, sometimes apply transitions from the null/default state to the one specified in the injected CSS. supersedes 72e8213b and 4dbca46b --- background/background.js | 6 ++- background/storage.js | 85 ++++++++++++++++++++++++++++++++++++++++ content/apply.js | 24 ++++++++++-- manage.html | 6 --- manage/filters.js | 4 -- 5 files changed, 111 insertions(+), 14 deletions(-) diff --git a/background/background.js b/background/background.js index 454239fa..87ad49eb 100644 --- a/background/background.js +++ b/background/background.js @@ -1,4 +1,5 @@ /* global dbExec, getStyles, saveStyle */ +/* global handleCssTransitionBug */ 'use strict'; // eslint-disable-next-line no-var @@ -211,7 +212,10 @@ contextMenus = Object.assign({ function webNavigationListener(method, {url, tabId, frameId}) { getStyles({matchUrl: url, enabled: true, asHash: true}).then(styles => { - if (method && !url.startsWith('chrome:') && tabId >= 0) { + if (method && URLS.supported(url) && tabId >= 0) { + if (method === 'styleApply') { + handleCssTransitionBug(tabId, frameId, styles); + } chrome.tabs.sendMessage(tabId, { method, // ping own page so it retrieves the styles directly diff --git a/background/storage.js b/background/storage.js index bc566874..d4554e75 100644 --- a/background/storage.js +++ b/background/storage.js @@ -8,6 +8,11 @@ const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g; // eslint-disable-next-line no-var var SLOPPY_REGEXP_PREFIX = '\0'; +// CSS transition bug workaround: since we insert styles asynchronously, +// the browsers, especially Firefox, may apply all transitions on page load +const CSS_TRANSITION_SUPPRESSOR = '* { transition: none !important; }'; +const RX_CSS_TRANSITION_DETECTOR = /([\s\n;/{]|-webkit-|-moz-)transition[\s\n]*:[\s\n]*(?!none)/; + // Note, only 'var'-declared variables are visible from another extension page // eslint-disable-next-line no-var var cachedStyles = { @@ -16,6 +21,7 @@ var cachedStyles = { filters: new Map(), // filterStyles() parameters mapped to the returned results, 10k max regexps: new Map(), // compiled style regexps urlDomains: new Map(), // getDomain() results for 100 last checked urls + needTransitionPatch: new Map(), // FF bug workaround mutex: { inProgress: false, // while getStyles() is reading IndexedDB all subsequent calls onDone: [], // to getStyles() are queued and resolved when the first one finishes @@ -517,6 +523,7 @@ function invalidateCache({added, updated, deletedId} = {}) { if (cached) { Object.assign(cached, updated); cachedStyles.filters.clear(); + cachedStyles.needTransitionPatch.delete(id); return; } else { added = updated; @@ -527,6 +534,7 @@ function invalidateCache({added, updated, deletedId} = {}) { cachedStyles.list.push(added); cachedStyles.byId.set(added.id, added); cachedStyles.filters.clear(); + cachedStyles.needTransitionPatch.delete(id); } return; } @@ -536,11 +544,13 @@ function invalidateCache({added, updated, deletedId} = {}) { cachedStyles.list.splice(cachedIndex, 1); cachedStyles.byId.delete(deletedId); cachedStyles.filters.clear(); + cachedStyles.needTransitionPatch.delete(id); return; } } cachedStyles.list = null; cachedStyles.filters.clear(); + cachedStyles.needTransitionPatch.clear(id); } @@ -612,3 +622,78 @@ function calcStyleDigest(style) { return parts.join(''); } } + + +function handleCssTransitionBug(tabId, frameId, styles) { + for (let id in styles) { + id |= 0; + if (!id) { + continue; + } + let need = cachedStyles.needTransitionPatch.get(id); + if (need === false) { + continue; + } + if (need !== true) { + need = styles[id].some(sectionContainsTransitions); + cachedStyles.needTransitionPatch.set(id, need); + if (!need) { + continue; + } + } + if (FIREFOX) { + patchFirefox(); + } else { + styles.needTransitionPatch = true; + } + break; + } + + function patchFirefox() { + browser.tabs.insertCSS(tabId, { + frameId, + code: CSS_TRANSITION_SUPPRESSOR, + cssOrigin: 'user', + runAt: 'document_start', + matchAboutBlank: true, + }).then(() => setTimeout(() => { + browser.tabs.removeCSS(tabId, { + frameId, + code: CSS_TRANSITION_SUPPRESSOR, + cssOrigin: 'user', + matchAboutBlank: true, + }).catch(ignoreChromeError); + })).catch(ignoreChromeError); + } + + function sectionContainsTransitions(section) { + let code = section.code; + const firstTransition = code.indexOf('transition'); + if (firstTransition < 0) { + return false; + } + const firstCmt = code.indexOf('/*'); + // check the part before the first comment + if (firstCmt < 0 || firstTransition < firstCmt) { + if (quickCheckAround(code, firstTransition)) { + return true; + } else if (firstCmt < 0) { + return false; + } + } + // check the rest + const lastCmt = code.lastIndexOf('*/'); + if (lastCmt < firstCmt) { + // the comment is unclosed and we already checked the preceding part + return false; + } + let mid = code.slice(firstCmt, lastCmt + 2); + mid = mid.indexOf('*/') === mid.length - 2 ? '' : mid.replace(RX_CSS_COMMENTS, ''); + code = mid + code.slice(lastCmt + 2); + return quickCheckAround(code) || RX_CSS_TRANSITION_DETECTOR.test(code); + } + + function quickCheckAround(code, pos = code.indexOf('transition')) { + return RX_CSS_TRANSITION_DETECTOR.test(code.substr(Math.max(0, pos - 10), 50)); + } +} diff --git a/content/apply.js b/content/apply.js index f29b6e4a..c93ebc7d 100644 --- a/content/apply.js +++ b/content/apply.js @@ -199,8 +199,25 @@ function applyStyles(styles) { // which is already autogenerated at this moment ROOT = document.head; } + if (styles.needTransitionPatch) { + // CSS transition bug workaround: since we insert styles asynchronously, + // the browsers, especially Firefox, may apply all transitions on page load + delete styles.needTransitionPatch; + const className = chrome.runtime.id + '-transition-bug-fix'; + const docId = document.documentElement.id ? '#' + document.documentElement.id : ''; + document.documentElement.classList.add(className); + applySections(0, ` + ${docId}.${className}:root * { + transition: none !important; + } + `); + setTimeout(() => { + removeStyle({id: 0}); + document.documentElement.classList.remove(className); + }); + } for (const id in styles) { - applySections(id, styles[id]); + applySections(id, styles[id].map(section => section.code).join('\n')); } initDocRewriteObserver(); initDocRootObserver(); @@ -215,7 +232,7 @@ function applyStyles(styles) { } -function applySections(styleId, sections) { +function applySections(styleId, code) { let el = document.getElementById(ID_PREFIX + styleId); if (el) { return; @@ -234,11 +251,12 @@ function applySections(styleId, sections) { id: ID_PREFIX + styleId, className: 'stylus', type: 'text/css', - textContent: sections.map(section => section.code).join('\n'), + textContent: code, }); addStyleElement(el); styleElements.set(el.id, el); disabledElements.delete(Number(styleId)); + return el; } diff --git a/manage.html b/manage.html index 6548ed03..bd6f73d8 100644 --- a/manage.html +++ b/manage.html @@ -7,12 +7,6 @@ -