From df2b08a75aa3f2eec532e66a7f11ccc6d8f31374 Mon Sep 17 00:00:00 2001 From: Rob Garrison Date: Mon, 19 Nov 2018 09:07:14 -0600 Subject: [PATCH 01/17] Remove soft-hyphens in popup. Closes #562 --- popup/popup.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/popup/popup.css b/popup/popup.css index 99bc18de..9eca352a 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -554,7 +554,8 @@ body.blocked .actions > .main-controls { } .blocked-info { - hyphens: auto; + hyphens: none; + word-wrap: break-word; } .blocked-info label { From 99951efc80b3a9e5a592dc08a0256fa558de1e0f Mon Sep 17 00:00:00 2001 From: Rob Garrison Date: Mon, 19 Nov 2018 22:55:38 -0600 Subject: [PATCH 02/17] Show error in tooltip (#566) * Show error in tooltip * Move error.message check --- manage/updater-ui.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manage/updater-ui.js b/manage/updater-ui.js index 038eb774..5141969b 100644 --- a/manage/updater-ui.js +++ b/manage/updater-ui.js @@ -148,6 +148,9 @@ function reportUpdateState({updated, style, error, STATES}) { error = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); } else if (error === STATES.MAYBE_EDITED) { error = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); + } else if (typeof error === 'object' && error.message) { + // UserCSS meta errors provide an object + error = error.message; } const message = same ? t('updateCheckSucceededNoUpdate') : error; newClasses.set('no-update', true); From 33df061b5307816c658bba3c99e258b968aeb645 Mon Sep 17 00:00:00 2001 From: Rob Garrison Date: Tue, 20 Nov 2018 17:06:20 -0600 Subject: [PATCH 03/17] Allow /*! in UserCSS metadata. Closes #571 --- edit/linter-meta.js | 2 +- js/usercss.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/edit/linter-meta.js b/edit/linter-meta.js index d99282ee..bd146a52 100644 --- a/edit/linter-meta.js +++ b/edit/linter-meta.js @@ -12,7 +12,7 @@ function createMetaCompiler(cm) { if (_cm !== cm) { return; } - const match = text.match(/\/\*\s*==userstyle==[\s\S]*?==\/userstyle==\s*\*\//i); + const match = text.match(/\/\*\!?\s*==userstyle==[\s\S]*?==\/userstyle==\s*\*\//i); if (!match) { return []; } diff --git a/js/usercss.js b/js/usercss.js index 0e9b95a5..27bc9c92 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -10,7 +10,7 @@ const usercss = (() => { // updateURL: 'updateUrl', name: undefined, }; - const RX_META = /\/\*\s*==userstyle==[\s\S]*?==\/userstyle==\s*\*\//i; + const RX_META = /\/\*\!?\s*==userstyle==[\s\S]*?==\/userstyle==\s*\*\//i; const ERR_ARGS_IS_LIST = new Set(['missingMandatory', 'missingChar']); return {buildMeta, buildCode, assignVars}; From 17339e933baa18e8799909d8d1f74e6d3f8bb1c8 Mon Sep 17 00:00:00 2001 From: Rob Garrison Date: Tue, 20 Nov 2018 17:32:03 -0600 Subject: [PATCH 04/17] Show Dropbox unavailable in dev mode (#568) --- _locales/en/messages.json | 3 +++ background/background.js | 8 ++++++++ sync/import-export-dropbox.js | 10 ++++++++-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 2b2c111d..63f59b62 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1477,6 +1477,9 @@ "connectingDropbox": { "message": "Connecting Dropbox..." }, + "connectingDropboxNotAllowed": { + "message": "Connecting to Dropbox is only available in apps installed directly from the webstore" + }, "gettingStyles": { "message": "Getting all styles..." }, diff --git a/background/background.js b/background/background.js index 4d246045..420287c2 100644 --- a/background/background.js +++ b/background/background.js @@ -51,6 +51,12 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, { .then(() => new Promise(resolve => setTimeout(resolve, 100))) .then(() => msg.broadcastExtension({method: 'optionsCustomizeHotkeys'})); }, + + installType() { + // types: "admin", "development", "normal", "sideload" & "other" + // "normal" = addon installed from webstore + return chrome.management.getSelf(info => localStorage.installType = info.installType); + } }); // eslint-disable-next-line no-var @@ -144,6 +150,8 @@ chrome.runtime.onInstalled.addListener(({reason}) => { }); // themes may change delete localStorage.codeMirrorThemes; + // save install type + window.API_METHODS.installType(); }); // ************************************************************************* diff --git a/sync/import-export-dropbox.js b/sync/import-export-dropbox.js index e7782568..f442c3d7 100644 --- a/sync/import-export-dropbox.js +++ b/sync/import-export-dropbox.js @@ -46,8 +46,11 @@ function uploadFileDropbox(client, stylesText) { } $('#sync-dropbox-export').onclick = () => { + const mode = localStorage.installType; const title = t('syncDropboxStyles'); - messageProgressBar({title: title, text: t('connectingDropbox')}); + const text = mode === 'normal' ? t('connectingDropbox') : t('connectingDropboxNotAllowed'); + messageProgressBar({title, text}); + if (mode !== 'normal') return; hasDropboxAccessToken() .then(token => token || requestDropboxAccessToken()) @@ -116,8 +119,11 @@ $('#sync-dropbox-export').onclick = () => { }; $('#sync-dropbox-import').onclick = () => { + const mode = localStorage.installType; const title = t('retrieveDropboxSync'); - messageProgressBar({title: title, text: t('connectingDropbox')}); + const text = mode === 'normal' ? t('connectingDropbox') : t('connectingDropboxNotAllowed'); + messageProgressBar({title, text}); + if (mode !== 'normal') return; hasDropboxAccessToken() .then(token => token || requestDropboxAccessToken()) From 81d27288f25db333c9c0c87d920a595f34d1179f Mon Sep 17 00:00:00 2001 From: Rob Garrison Date: Tue, 20 Nov 2018 18:56:33 -0600 Subject: [PATCH 05/17] Remove duplicate rules in issue modal --- edit/linter-help-dialog.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/edit/linter-help-dialog.js b/edit/linter-help-dialog.js index f1c6d18c..ff57ce3b 100644 --- a/edit/linter-help-dialog.js +++ b/edit/linter-help-dialog.js @@ -33,18 +33,18 @@ function createLinterHelpDialog(getIssues) { }; } else { headerLink = $createLink(baseUrl, 'stylelint'); - template = ({rule}) => + template = rule => $create('li', rule === 'CssSyntaxError' ? rule : $createLink(baseUrl + rule, rule)); } const header = t('linterIssuesHelp', '\x01').split('\x01'); - const activeRules = getIssues(); + const activeRules = new Set([...getIssues()].map(issue => issue.rule)); Promise.resolve(linter === 'csslint' && prepareCsslintRules()) .then(() => showHelp(t('linterIssues'), $create([ header[0], headerLink, header[1], - $create('ul.rules', [...activeRules.values()].map(template)), + $create('ul.rules', [...activeRules].map(template)), ]) ) ); From 21b64292058c0dd17e0708aea0a6112260560ab0 Mon Sep 17 00:00:00 2001 From: Rob Garrison Date: Tue, 20 Nov 2018 19:41:31 -0600 Subject: [PATCH 06/17] Beautify: stop new line injection before comment. Fixes #564 --- vendor-overwrites/beautify/beautify-css-mod.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/vendor-overwrites/beautify/beautify-css-mod.js b/vendor-overwrites/beautify/beautify-css-mod.js index 85c16422..1c39ea84 100644 --- a/vendor-overwrites/beautify/beautify-css-mod.js +++ b/vendor-overwrites/beautify/beautify-css-mod.js @@ -402,10 +402,6 @@ if (!ch) { break; } else if (ch === '/' && peek() === '*') { /* css comment */ - if (isAfterNewline) { - print.newLine(); - } - print.text(eatComment()); if (peek() !== ';') print.newLine(); } else if (ch === '/' && peek() === '/') { // single line comment From 9250d5c62406265d5fed06ad94fe92dfa72c71a5 Mon Sep 17 00:00:00 2001 From: Rob Garrison Date: Tue, 20 Nov 2018 19:48:52 -0600 Subject: [PATCH 07/17] Remove unnecessary API method --- background/background.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/background/background.js b/background/background.js index 420287c2..0ea3e0cf 100644 --- a/background/background.js +++ b/background/background.js @@ -50,12 +50,6 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, { return browser.runtime.openOptionsPage() .then(() => new Promise(resolve => setTimeout(resolve, 100))) .then(() => msg.broadcastExtension({method: 'optionsCustomizeHotkeys'})); - }, - - installType() { - // types: "admin", "development", "normal", "sideload" & "other" - // "normal" = addon installed from webstore - return chrome.management.getSelf(info => localStorage.installType = info.installType); } }); @@ -150,8 +144,9 @@ chrome.runtime.onInstalled.addListener(({reason}) => { }); // themes may change delete localStorage.codeMirrorThemes; - // save install type - window.API_METHODS.installType(); + // save install type: "admin", "development", "normal", "sideload" or "other" + // "normal" = addon installed from webstore + chrome.management.getSelf(info => localStorage.installType = info.installType); }); // ************************************************************************* From 25fb5acabee7450126e7e045bee21c96c994796b Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 21 Nov 2018 23:47:28 +0800 Subject: [PATCH 08/17] Fix: cycle through editors (#572) * Fix: cycle through editors * Fix: command is broken --- edit/codemirror-default.js | 2 +- edit/sections-editor.js | 32 ++++++++++++-------------------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/edit/codemirror-default.js b/edit/codemirror-default.js index 2885b08b..d0f6b8c2 100644 --- a/edit/codemirror-default.js +++ b/edit/codemirror-default.js @@ -230,7 +230,7 @@ // editor commands for (const name of ['save', 'toggleStyle', 'nextEditor', 'prevEditor']) { - CodeMirror.commands[name] = () => editor[name](); + CodeMirror.commands[name] = (...args) => editor[name](...args); } // CodeMirror convenience commands diff --git a/edit/sections-editor.js b/edit/sections-editor.js index a06223d3..79605374 100644 --- a/edit/sections-editor.js +++ b/edit/sections-editor.js @@ -201,35 +201,27 @@ function createSectionsEditor(style) { } function nextEditor(cm, cycle = true) { - if (!cycle) { - for (const section of sections) { - if (section.isRemoved()) { - continue; - } - if (cm === section.cm) { - return; - } - break; - } + if (!cycle && findLast(sections, s => !s.isRemoved()).cm === cm) { + return; } return nextPrevEditor(cm, 1); } function prevEditor(cm, cycle = true) { - if (!cycle) { - for (let i = sections.length - 1; i >= 0; i--) { - if (sections[i].isRemoved()) { - continue; - } - if (cm === sections[i].cm) { - return; - } - break; - } + if (!cycle && sections.find(s => !s.isRemoved()).cm === cm) { + return; } return nextPrevEditor(cm, -1); } + function findLast(arr, match) { + for (let i = arr.length - 1; i >= 0; i--) { + if (match(arr[i])) { + return arr[i]; + } + } + } + function nextPrevEditor(cm, direction) { const editors = getEditors(); cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length]; From 05ec2fb1c77ca1310ca56ead0a19b7f398d5963e Mon Sep 17 00:00:00 2001 From: eight Date: Thu, 22 Nov 2018 01:09:48 +0800 Subject: [PATCH 09/17] Fix: detect style CSP (#573) --- content/apply.js | 57 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/content/apply.js b/content/apply.js index b118d363..6a83d4e5 100644 --- a/content/apply.js +++ b/content/apply.js @@ -86,30 +86,57 @@ const APPLY = (() => { function injectPageScript() { const scriptContent = EVENT_NAME => { document.currentScript.remove(); - window.addEventListener(EVENT_NAME, function handler(e) { - const {method, id, content} = e.detail; - if (method === 'setStyleContent') { - const el = document.getElementById(id); - if (!el) { - return; + const available = checkStyleApplied(); + if (available) { + window.addEventListener(EVENT_NAME, function handler(e) { + const {method, id, content} = e.detail; + if (method === 'setStyleContent') { + const el = document.getElementById(id); + if (!el) { + return; + } + const disabled = el.disabled; + el.textContent = content; + el.disabled = disabled; + } else if (method === 'orphan') { + window.removeEventListener(EVENT_NAME, handler); } - const disabled = el.disabled; - el.textContent = content; - el.disabled = disabled; - } else if (method === 'orphan') { - window.removeEventListener(EVENT_NAME, handler); - } - }, true); + }, true); + } + window.dispatchEvent(new CustomEvent(EVENT_NAME, {detail: { + method: 'init', + available + }})); + + function checkStyleApplied() { + const style = document.createElement('style'); + style.textContent = ':root{--stylus-applied:1}'; + document.documentElement.appendChild(style); + const applied = getComputedStyle(document.documentElement) + .getPropertyValue('--stylus-applied'); + style.remove(); + return Boolean(applied); + } }; const code = `(${scriptContent})(${JSON.stringify(EVENT_NAME)})`; const src = `data:application/javascript;base64,${btoa(code)}`; const script = document.createElement('script'); const {resolve, promise} = deferred(); script.src = src; - script.onload = () => resolve(true); script.onerror = () => resolve(false); + window.addEventListener(EVENT_NAME, handleInit); document.documentElement.appendChild(script); - return promise; + return promise.then(result => { + script.remove(); + window.removeEventListener(EVENT_NAME, handleInit); + return result; + }); + + function handleInit(e) { + if (e.detail.method === 'init') { + resolve(e.detail.available); + } + } } } From 764fe399f311836b58f2ae9123a5605559b76c00 Mon Sep 17 00:00:00 2001 From: Rob Garrison Date: Sat, 24 Nov 2018 06:03:01 -0600 Subject: [PATCH 10/17] Fix inaccessible file message. Closes #574 (#575) * Fix inaccessible file message. Closes #574 * Reword inaccessible message --- _locales/en/messages.json | 4 ++++ popup.html | 1 - popup/popup.js | 12 +++++++++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 63f59b62..d0cd0136 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1373,6 +1373,10 @@ "message": "Stylus can access file:// URLs only if you enable the corresponding checkbox for Stylus extension on chrome://extensions page.", "description": "Note in the toolbar popup for file:// URLs" }, + "InaccessibleFileHint": { + "message": "Stylus can not access some file types (e.g. pdf & json files).", + "description": "Note in the toolbar popup for some file types that cannot be accessed" + }, "updateAllCheckSucceededNoUpdate": { "message": "No updates found.", "description": "Text that displays when an update all check completed and no updates are available" diff --git a/popup.html b/popup.html index 93eadd4e..1149d95d 100644 --- a/popup.html +++ b/popup.html @@ -80,7 +80,6 @@ diff --git a/popup/popup.js b/popup/popup.js index 610b7d4e..60f0a8a1 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -130,6 +130,10 @@ function initPopup() { return; } const info = template.unreachableInfo; + if (!FIREFOX) { + // Chrome "Allow access to file URLs" in chrome://extensions message + info.appendChild($create('p', t('unreachableFileHint'))); + } if (FIREFOX && tabURL.startsWith(URLS.browserWebStore)) { $('label', info).textContent = t('unreachableAMO'); const note = (FIREFOX < 59 ? t('unreachableAMOHintOldFF') : t('unreachableAMOHint')) + @@ -137,9 +141,11 @@ function initPopup() { const renderToken = s => s[0] === '<' ? $create('b', tWordBreak(s.slice(1, -1))) : s; const renderLine = line => $create('p', line.split(/(<.*?>)/).map(renderToken)); const noteNode = $create('fragment', note.split('\n').map(renderLine)); - const target = $('p', info); - target.parentNode.insertBefore(noteNode, target); - target.remove(); + info.appendChild(noteNode); + } + // Inaccessible locally hosted file type, e.g. JSON, PDF, etc. + if (tabURL.length - tabURL.lastIndexOf(".") <= 5) { + info.appendChild($create('p', t('InaccessibleFileHint'))); } document.body.classList.add('unreachable'); document.body.insertBefore(info, document.body.firstChild); From 319ec320c73b59050a27a72d9e61aa5789a27f4f Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 25 Nov 2018 08:13:33 +0300 Subject: [PATCH 11/17] specify the end token for simple block in _expr() (#580) --- vendor-overwrites/csslint/parserlib.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/vendor-overwrites/csslint/parserlib.js b/vendor-overwrites/csslint/parserlib.js index 8c72404e..e53c1c7a 100644 --- a/vendor-overwrites/csslint/parserlib.js +++ b/vendor-overwrites/csslint/parserlib.js @@ -4777,7 +4777,7 @@ self.parserlib = (() => { return result; } - _expr(inFunction) { + _expr(inFunction, endToken = Tokens.RPAREN) { const stream = this._tokenStream; const values = []; @@ -4786,7 +4786,7 @@ self.parserlib = (() => { if (!value && !values.length) return null; // get everything inside the parens and let validateProperty handle that - if (!value && inFunction && stream.peek() !== Tokens.RPAREN) { + if (!value && inFunction && stream.peek() !== endToken) { stream.get(); value = new PropertyValuePart(stream._token); } else if (!value) { @@ -4914,8 +4914,9 @@ self.parserlib = (() => { inFunction && Tokens.LBRACE, ])) { const token = stream._token; - token.expr = this._expr(inFunction); - stream.mustMatch(Tokens.type(token.endChar)); + const endToken = Tokens.type(token.endChar); + token.expr = this._expr(inFunction, endToken); + stream.mustMatch(endToken); return finalize(token, token.value + (token.expr || '') + token.endChar); } From 412090795718793894a1d83953636cdb626b06a1 Mon Sep 17 00:00:00 2001 From: eight Date: Sun, 25 Nov 2018 21:27:11 +0800 Subject: [PATCH 12/17] Fix: failed to find the old style when the name/namespace is changed (#581) --- background/usercss-helper.js | 4 +++- edit/source-editor.js | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/background/usercss-helper.js b/background/usercss-helper.js index f1c6419c..546f1172 100644 --- a/background/usercss-helper.js +++ b/background/usercss-helper.js @@ -75,6 +75,7 @@ * @returns {Promise<{style, dup:Boolean?}>} */ function build({ + styleId, sourceCode, checkDup, metaOnly, @@ -83,7 +84,8 @@ }) { return usercss.buildMeta(sourceCode) .then(style => { - const findDup = checkDup || assignVars ? find(style) : null; + const findDup = checkDup || assignVars ? + find(styleId ? {id: styleId} : style) : Promise.resolve(); return Promise.all([ metaOnly ? style : doBuild(style, findDup), findDup diff --git a/edit/source-editor.js b/edit/source-editor.js index dcd43a13..59209c5b 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -82,6 +82,7 @@ function createSourceEditor(style) { function preprocess(style) { return API.buildUsercss({ + styleId: style.id, sourceCode: style.sourceCode, assignVars: true }) From e97a3ef2692b60d2c83af4c0b818b6e141246145 Mon Sep 17 00:00:00 2001 From: eight Date: Sun, 25 Nov 2018 21:28:37 +0800 Subject: [PATCH 13/17] Change: modify match-highlighter plugin (#578) * Change: modify match-highlighter plugin * Fix: boundary character should only be used when the query starts/ends with alphabet --- edit.html | 5 +- edit/codemirror-factory.js | 10 +- edit/edit.css | 10 +- edit/match-highlighter-helper.js | 223 ------------------ .../codemirror-addon}/match-highlighter.js | 30 ++- 5 files changed, 40 insertions(+), 238 deletions(-) delete mode 100644 edit/match-highlighter-helper.js rename {vendor/codemirror/addon/search => vendor-overwrites/codemirror-addon}/match-highlighter.js (89%) diff --git a/edit.html b/edit.html index c064d89f..da439c2b 100644 --- a/edit.html +++ b/edit.html @@ -31,7 +31,6 @@ - @@ -62,6 +61,8 @@ + + @@ -77,8 +78,6 @@ - - diff --git a/edit/codemirror-factory.js b/edit/codemirror-factory.js index 68996bb9..887e6c08 100644 --- a/edit/codemirror-factory.js +++ b/edit/codemirror-factory.js @@ -32,12 +32,14 @@ const cmFactory = (() => { if (value === 'token') { cm.setOption('highlightSelectionMatches', { showToken: /[#.\-\w]/, - annotateScrollbar: true + annotateScrollbar: true, + onUpdate: updateMatchHighlightCount }); } else if (value === 'selection') { cm.setOption('highlightSelectionMatches', { showToken: false, - annotateScrollbar: true + annotateScrollbar: true, + onUpdate: updateMatchHighlightCount }); } else { cm.setOption('highlightSelectionMatches', null); @@ -80,6 +82,10 @@ const cmFactory = (() => { }); return {create, destroy, setOption}; + function updateMatchHighlightCount(cm, state) { + cm.display.wrapper.dataset.matchHighlightCount = state.matchesonscroll.matches.length; + } + function configureMouseFn(cm, repeat) { return repeat === 'double' ? {unit: selectTokenOnDoubleclick} : diff --git a/edit/edit.css b/edit/edit.css index 907cac06..22d9a110 100644 --- a/edit/edit.css +++ b/edit/edit.css @@ -364,14 +364,14 @@ input:invalid { .resize-grip-enabled .CodeMirror-scrollbar-filler { bottom: 7px; /* make space for resize-grip */ } -body[data-match-highlight="token"] .cm-matchhighlight-approved .cm-matchhighlight, -body[data-match-highlight="token"] .CodeMirror-selection-highlight-scrollbar { +.cm-matchhighlight, +.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); +[data-match-highlight-count="1"] .cm-matchhighlight, +[data-match-highlight-count="1"] .CodeMirror-selection-highlight-scrollbar { + animation: none; } @-webkit-keyframes highlight { from { diff --git a/edit/match-highlighter-helper.js b/edit/match-highlighter-helper.js deleted file mode 100644 index 8396a7ed..00000000 --- a/edit/match-highlighter-helper.js +++ /dev/null @@ -1,223 +0,0 @@ -/* global CodeMirror prefs */ -'use strict'; - -(() => { - /* - The original match-highlighter addon always recreates the highlight overlay - even if the token under cursor hasn't changed, which is terribly ineffective - (the entire view is re-rendered) and makes our animated token highlight effect - restart on every cursor movement. - - Invocation sequence of our hooks: - - 1. removeOverlayForHighlighter() - The original addon removes the overlay unconditionally - so this hook saves the state if the token hasn't changed. - - 2. addOverlayForHighlighter() - Restores the saved state instead of creating a new overlay, - installs a hook to count occurrences. - - 3. matchesOnScrollbar() - Saves the query regexp passed from the original addon in our helper object, - and in case removeOverlayForHighlighter() decided to keep the overlay - only rewrites the regexp without invoking the original constructor. - */ - - const HL_APPROVED = 'cm-matchhighlight-approved'; - const SEARCH_MATCH_TOKEN_NAME = 'searching'; - - const originalAddOverlay = CodeMirror.prototype.addOverlay; - const originalRemoveOverlay = CodeMirror.prototype.removeOverlay; - const originalMatchesOnScrollbar = CodeMirror.prototype.showMatchesOnScrollbar; - const originalSetOption = CodeMirror.prototype.setOption; - let originalGetOption; - - CodeMirror.prototype.addOverlay = addOverlay; - CodeMirror.prototype.removeOverlay = removeOverlay; - CodeMirror.prototype.showMatchesOnScrollbar = matchesOnScrollbar; - CodeMirror.prototype.setOption = setOption; - - let enabled = Boolean(prefs.get('editor.matchHighlight')); - - return; - - function setOption(option, value) { - enabled = option === 'highlightSelectionMatches' ? value : enabled; - return originalSetOption.apply(this, arguments); - } - - function shouldIntercept(overlay) { - const hlState = this.state.matchHighlighter || {}; - return overlay === hlState.overlay && (hlState.options || {}).showToken; - } - - function addOverlay() { - return enabled && shouldIntercept.apply(this, arguments) && - addOverlayForHighlighter.apply(this, arguments) || - originalAddOverlay.apply(this, arguments); - } - - function removeOverlay() { - return enabled && shouldIntercept.apply(this, arguments) && - removeOverlayForHighlighter.apply(this, arguments) || - originalRemoveOverlay.apply(this, arguments); - } - - function addOverlayForHighlighter(overlay) { - const state = this.state.matchHighlighter || {}; - const helper = state.highlightHelper = state.highlightHelper || {}; - - helper.rewriteScrollbarQuery = true; - - // since we're here the original addon decided there's something to highlight, - // so we cancel removeOverlayIfExpired() scheduled in our removeOverlay hook - clearTimeout(helper.hookTimer); - - // the original addon just removed its overlays, which was intercepted by removeOverlayForHighlighter, - // which decided to restore it and saved the previous overlays in our helper object, - // so here we are now, restoring them - if (helper.skipMatchesOnScrollbar) { - state.matchesonscroll = helper.matchesonscroll; - state.overlay = helper.overlay; - return true; - } - - // hook the newly created overlay's token() to count the occurrences - if (overlay.token !== tokenHook) { - overlay.highlightHelper = { - token: overlay.token, - occurrences: 0, - }; - overlay.token = tokenHook; - } - - // speed up rendering of scrollbar marks 4 times: we don't need ultimate precision there - // so for the duration of this event loop cycle we spoof the "lineWrapping" option - // and restore it in the next event loop cycle - if (this.options.lineWrapping && CodeMirror.prototype.getOption !== spoofLineWrappingOption) { - originalGetOption = CodeMirror.prototype.getOption; - CodeMirror.prototype.getOption = spoofLineWrappingOption; - setTimeout(() => (CodeMirror.prototype.getOption = originalGetOption)); - } - } - - function spoofLineWrappingOption(option) { - return option !== 'lineWrapping' && originalGetOption.apply(this, arguments); - } - - function tokenHook(stream) { - // we don't highlight a single match in case 'editor.matchHighlight' option is 'token' - // so this hook counts the occurrences and toggles HL_APPROVED class on CM's wrapper element - const style = this.highlightHelper.token.call(this, stream); - if (style !== 'matchhighlight') { - return style; - } - - const tokens = stream.lineOracle.baseTokens; - const tokenIndex = tokens.indexOf(stream.pos, 1); - if (tokenIndex > 0) { - const tokenStart = tokenIndex > 2 ? tokens[tokenIndex - 2] : 0; - const token = tokenStart === stream.start && tokens[tokenIndex + 1]; - const index = token && token.indexOf(SEARCH_MATCH_TOKEN_NAME); - if (token && index >= 0 && - (token[index - 1] || ' ') === ' ' && - (token[index + SEARCH_MATCH_TOKEN_NAME.length] || ' ') === ' ') { - return; - } - } - - const num = ++this.highlightHelper.occurrences; - if (num === 1) { - stream.lineOracle.doc.cm.display.wrapper.classList.remove(HL_APPROVED); - } else if (num === 2) { - stream.lineOracle.doc.cm.display.wrapper.classList.add(HL_APPROVED); - } - return style; - } - - function removeOverlayForHighlighter() { - const state = this.state.matchHighlighter || {}; - const helper = state.highlightHelper; - const {query, originalToken} = helper || state.matchesonscroll || {}; - // no current query means nothing to preserve => remove the overlay - if (!query || !originalToken) { - return; - } - const sel = this.getSelection(); - // current query differs from the selected text => remove the overlay - if (sel && sel.toLowerCase() !== originalToken.toLowerCase()) { - helper.query = helper.originalToken = sel; - return; - } - // if token under cursor has changed => remove the overlay - if (!sel) { - const {line, ch} = this.getCursor(); - const queryLen = originalToken.length; - const start = Math.max(0, ch - queryLen + 1); - const end = ch + queryLen; - const string = this.getLine(line); - const area = string.slice(start, end); - const i = area.indexOf(query); - const startInArea = i < 0 ? NaN : i; - if (isNaN(startInArea) || start + startInArea > ch || - state.options.showToken.test(string[start + startInArea - 1] || '') || - state.options.showToken.test(string[start + startInArea + queryLen] || '')) { - // pass the displayed instance back to the original code to remove it - state.matchesonscroll = state.matchesonscroll || helper && helper.matchesonscroll; - return; - } - } - // since the same token is under cursor we prevent the highlighter from rerunning - // by saving current overlays in a helper object so that it's restored in addOverlayForHighlighter() - state.highlightHelper = { - overlay: state.overlay, - matchesonscroll: state.matchesonscroll || (helper || {}).matchesonscroll, - // instruct our matchesOnScrollbar hook to preserve current scrollbar annotations - skipMatchesOnScrollbar: true, - // in case the original addon won't highlight anything we need to actually remove the overlays - // by setting a timer that runs in the next event loop cycle and can be canceled in this cycle - hookTimer: setTimeout(removeOverlayIfExpired, 0, this, state), - originalToken, - query, - }; - // fool the original addon so it won't invoke state.matchesonscroll.clear() - state.matchesonscroll = null; - return true; - } - - function removeOverlayIfExpired(self, state) { - const {overlay, matchesonscroll} = state.highlightHelper || {}; - if (overlay) { - originalRemoveOverlay.call(self, overlay); - } - if (matchesonscroll) { - matchesonscroll.clear(); - } - state.highlightHelper = null; - } - - function matchesOnScrollbar(query, ...args) { - if (!enabled) { - return originalMatchesOnScrollbar.call(this, query, ...args); - } - const state = this.state.matchHighlighter; - const helper = state.highlightHelper = state.highlightHelper || {}; - // rewrite the \btoken\b regexp so it matches .token and #token and --token - if (helper.rewriteScrollbarQuery && /^\\b.*?\\b$/.test(query.source)) { - helper.rewriteScrollbarQuery = undefined; - helper.originalToken = query.source.slice(2, -2); - const notToken = '(?!' + state.options.showToken.source + ').'; - query = new RegExp(`(^|${notToken})` + helper.originalToken + `(${notToken}|$)`); - } - // save the query for future use in removeOverlayForHighlighter - helper.query = query; - // if removeOverlayForHighlighter() decided to keep the overlay - if (helper.skipMatchesOnScrollbar) { - helper.skipMatchesOnScrollbar = undefined; - return; - } else { - return originalMatchesOnScrollbar.call(this, query, ...args); - } - } -})(); diff --git a/vendor/codemirror/addon/search/match-highlighter.js b/vendor-overwrites/codemirror-addon/match-highlighter.js similarity index 89% rename from vendor/codemirror/addon/search/match-highlighter.js rename to vendor-overwrites/codemirror-addon/match-highlighter.js index b344ac79..cf2a53b0 100644 --- a/vendor/codemirror/addon/search/match-highlighter.js +++ b/vendor-overwrites/codemirror-addon/match-highlighter.js @@ -36,7 +36,8 @@ wordsOnly: false, annotateScrollbar: false, showToken: false, - trim: true + trim: true, + onUpdate: () => {} } function State(options) { @@ -46,6 +47,7 @@ this.overlay = this.timeout = null; this.matchesonscroll = null; this.active = false; + this.query = null; } CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) { @@ -88,12 +90,24 @@ function addOverlay(cm, query, hasBoundary, style) { var state = cm.state.matchHighlighter; + if (state.query === query) { + return; + } + removeOverlay(cm); + state.query = query; cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style)); if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) { - var searchFor = hasBoundary ? new RegExp("\\b" + query.replace(/[\\\[.+*?(){|^$]/g, "\\$&") + "\\b") : query; + var searchFor = hasBoundary ? + new RegExp( + (/[a-z]/i.test(query[0]) ? "\\b" : "") + + query.replace(/[\\\[.+*?(){|^$]/g, "\\$&") + + (/[a-z]/i.test(query[query.length - 1]) ? "\\b" : ""), + "m" + ) : query; state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false, {className: "CodeMirror-selection-highlight-scrollbar"}); } + state.options.onUpdate(cm, state); } function removeOverlay(cm) { @@ -106,19 +120,22 @@ state.matchesonscroll = null; } } + state.query = null; } function highlightMatches(cm) { cm.operation(function() { var state = cm.state.matchHighlighter; - removeOverlay(cm); if (!cm.somethingSelected() && state.options.showToken) { var re = state.options.showToken === true ? /[\w$]/ : state.options.showToken; var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start; while (start && re.test(line.charAt(start - 1))) --start; while (end < line.length && re.test(line.charAt(end))) ++end; - if (start < end) + if (start < end) { addOverlay(cm, line.slice(start, end), re, state.options.style); + } else { + removeOverlay(cm); + } return; } var from = cm.getCursor("from"), to = cm.getCursor("to"); @@ -126,8 +143,11 @@ if (state.options.wordsOnly && !isWord(cm, from, to)) return; var selection = cm.getRange(from, to) if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "") - if (selection.length >= state.options.minChars) + if (selection.length >= state.options.minChars) { addOverlay(cm, selection, false, state.options.style); + } else { + removeOverlay(cm); + } }); } From 237d5c0c061b782a9553fea8a980a7446938753b Mon Sep 17 00:00:00 2001 From: narcolepticinsomniac Date: Tue, 27 Nov 2018 23:39:35 -0500 Subject: [PATCH 14/17] Fix inline search (#588) For some unknown reason, USO now requires a new param. --- popup/search-results.js | 1 + 1 file changed, 1 insertion(+) diff --git a/popup/search-results.js b/popup/search-results.js index 3ed5c679..c834c7cb 100755 --- a/popup/search-results.js +++ b/popup/search-results.js @@ -689,6 +689,7 @@ window.addEventListener('showStyles:done', function _() { '/api/v1/styles/subcategory' + '?search=' + encodeURIComponent(category) + '&page=' + searchCurrentPage + + '&per_page=10' + '&country=NA'; const cacheKey = category + '/' + searchCurrentPage; From eb0b9f58f504469e83d1958b03167ddbb4c05626 Mon Sep 17 00:00:00 2001 From: narcolepticinsomniac Date: Tue, 27 Nov 2018 23:48:45 -0500 Subject: [PATCH 15/17] Fix search highlight conflict (#587) * Fix search highlight conflict Regular highlight styling and search highlight styling shouldn't both be applied at the same time. Search highlight styling should also be removed when search is closed. This PR resolves those conflicts. * Remove unnecessary dummy animation Not sure what the point of it ever was, but I'm pretty sure it should go. --- edit/codemirror-default.css | 8 -------- edit/edit.css | 8 ++++---- edit/global-search.css | 26 +++++++++++++++++++++----- edit/global-search.js | 10 ++++++++-- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/edit/codemirror-default.css b/edit/codemirror-default.css index 83085a6d..61dd10f2 100644 --- a/edit/codemirror-default.css +++ b/edit/codemirror-default.css @@ -37,14 +37,6 @@ .cm-uso-variable { font-weight: bold; } -.cm-searching.cm-matchhighlight { - /* tokens found by manual search should not animate by cm-matchhighlight */ - animation-name: search-and-match-highlighter !important; -} -@keyframes search-and-match-highlighter { - from { background-color: rgba(255, 255, 0, .4); } /* search color */ - to { background-color: rgba(100, 255, 100, .4); } /* sarch + highlight */ -} .CodeMirror-activeline .applies-to:before { background-color: hsla(214, 100%, 90%, 0.15); diff --git a/edit/edit.css b/edit/edit.css index 22d9a110..6276fac1 100644 --- a/edit/edit.css +++ b/edit/edit.css @@ -364,13 +364,13 @@ input:invalid { .resize-grip-enabled .CodeMirror-scrollbar-filler { bottom: 7px; /* make space for resize-grip */ } -.cm-matchhighlight, -.CodeMirror-selection-highlight-scrollbar { +body:not(.find-open) .cm-matchhighlight, +body:not(.find-open) .CodeMirror-selection-highlight-scrollbar { animation: fadein-match-highlighter 1s cubic-bezier(.97,.01,.42,.98); animation-fill-mode: both; } -[data-match-highlight-count="1"] .cm-matchhighlight, -[data-match-highlight-count="1"] .CodeMirror-selection-highlight-scrollbar { +body:not(.find-open) [data-match-highlight-count="1"] .cm-matchhighlight, +body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-highlight-scrollbar { animation: none; } @-webkit-keyframes highlight { diff --git a/edit/global-search.css b/edit/global-search.css index 51ec9111..4c84c0e0 100644 --- a/edit/global-search.css +++ b/edit/global-search.css @@ -181,17 +181,33 @@ opacity: 1; } -/*********** CodeMirror ****************/ - -.search-target-editor { - outline: 1px solid darkorange; +/*********** CM search highlight restyling, which shouldn't need color variables ****************/ +body.find-open .search-target-editor { + outline-color: darkorange !important; } -#stylus .search-target-match { +body.find-open .cm-searching { + background-color: rgba(255, 255, 0, .4); +} + +body.find-open .cm-searching.search-target-match { background-color: darkorange; color: black; } +body.find-open .CodeMirror-search-match { + background: gold; + border-top: 1px solid orange; + border-bottom: 1px solid orange; +} + +/* hide default CM search highlight styling */ +body .cm-searching, +body .CodeMirror-search-match { + background-color: transparent; + border-color: transparent; +} + @media (max-width: 500px) { #search-replace-dialog { left: 0; diff --git a/edit/global-search.js b/edit/global-search.js index c160de16..d6f18d13 100644 --- a/edit/global-search.js +++ b/edit/global-search.js @@ -752,8 +752,14 @@ onDOMready().then(() => { function makeTargetVisible(element) { const old = $('.' + TARGET_CLASS); if (old !== element) { - if (old) old.classList.remove(TARGET_CLASS); - if (element) element.classList.add(TARGET_CLASS); + if (old) { + old.classList.remove(TARGET_CLASS); + document.body.classList.remove('find-open'); + } + if (element) { + element.classList.add(TARGET_CLASS); + document.body.classList.add('find-open'); + } } } From a1b17bb55375b97d93ab9c1cf84c1151829f16cd Mon Sep 17 00:00:00 2001 From: Rob Garrison Date: Tue, 27 Nov 2018 22:54:36 -0600 Subject: [PATCH 16/17] Fix dirty style updating. Closes #585 (#586) * Fix dirty style updating. Closes #585 * Move common code to edit.js * init updateDirty --- edit/edit.js | 28 +++++++++++++++++++++++----- edit/sections-editor.js | 17 ++++------------- edit/source-editor.js | 20 +++----------------- 3 files changed, 30 insertions(+), 35 deletions(-) diff --git a/edit/edit.js b/edit/edit.js index 8551381a..78b6e78c 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -174,12 +174,30 @@ preinit(); $('#beautify').onclick = () => beautify(editor.getEditors()); $('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true}); window.addEventListener('resize', () => debounce(rememberWindowSize, 100)); - editor = usercss ? createSourceEditor(style) : createSectionsEditor(style); - if (editor.ready) { - return editor.ready(); - } + editor = (usercss ? createSourceEditor : createSectionsEditor)({ + style, + onTitleChanged: updateTitle + }); + editor.dirty.onChange(updateDirty); + return Promise.resolve(editor.ready && editor.ready()) + .then(updateDirty); }); } + + function updateTitle() { + if (editor) { + const styleName = editor.getStyle().name; + const isDirty = editor.dirty.isDirty(); + document.title = (isDirty ? '* ' : '') + styleName; + } + } + + function updateDirty() { + const isDirty = editor.dirty.isDirty(); + document.body.classList.toggle('dirty', isDirty); + $('#save-button').disabled = !isDirty; + updateTitle(); + } })(); function preinit() { @@ -306,7 +324,7 @@ function beforeUnload(e) { // refocus if unloading was canceled setTimeout(() => activeElement.focus()); } - if (editor && editor.isDirty()) { + if (editor && editor.dirty.isDirty()) { // neither confirm() nor custom messages work in modern browsers but just in case e.returnValue = t('styleChangesNotSaved'); } diff --git a/edit/sections-editor.js b/edit/sections-editor.js index 79605374..c6ef5c1c 100644 --- a/edit/sections-editor.js +++ b/edit/sections-editor.js @@ -6,10 +6,9 @@ /* exported createSectionsEditor */ 'use strict'; -function createSectionsEditor(style) { +function createSectionsEditor({style, onTitleChanged}) { let INC_ID = 0; // an increment id that is used by various object to track the order const dirty = dirtyReporter(); - dirty.onChange(updateTitle); const container = $('#sections'); const sections = []; @@ -18,7 +17,7 @@ function createSectionsEditor(style) { nameEl.addEventListener('input', () => { dirty.modify('name', style.name, nameEl.value); style.name = nameEl.value; - updateTitle(); + onTitleChanged(); }); const enabledEl = $('#enabled'); @@ -64,7 +63,7 @@ function createSectionsEditor(style) { return { ready: () => initializing, replaceStyle, - isDirty: dirty.isDirty, + dirty, getStyle: () => style, getEditors, scrollToEditor, @@ -413,7 +412,7 @@ function createSectionsEditor(style) { nameEl.value = style.name || ''; enabledEl.checked = style.enabled !== false; $('#url').href = style.url || ''; - updateTitle(); + onTitleChanged(); } function updateLivePreview() { @@ -424,14 +423,6 @@ function createSectionsEditor(style) { livePreview.update(getModel()); } - function updateTitle() { - const name = style.name; - const clean = !dirty.isDirty(); - const title = !style.id ? t('addStyleTitle') : name; - document.title = (clean ? '' : '* ') + title; - $('#save-button').disabled = clean; - } - function initSection({ sections: originalSections, total = originalSections.length, diff --git a/edit/source-editor.js b/edit/source-editor.js index 59209c5b..6a4658f5 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -6,7 +6,7 @@ /* exported createSourceEditor */ 'use strict'; -function createSourceEditor(style) { +function createSourceEditor({style, onTitleChanged}) { $('#name').disabled = true; $('#save-button').disabled = true; $('#mozilla-format-container').remove(); @@ -16,12 +16,6 @@ function createSourceEditor(style) { $('#sections').appendChild($create('.single-editor')); const dirty = dirtyReporter(); - dirty.onChange(() => { - const isDirty = dirty.isDirty(); - document.body.classList.toggle('dirty', isDirty); - $('#save-button').disabled = !isDirty; - updateTitle(); - }); // normalize style if (!style.id) setupNewStyle(style); @@ -171,18 +165,10 @@ function createSourceEditor(style) { $('#name').value = style.name; $('#enabled').checked = style.enabled; $('#url').href = style.url; - updateTitle(); + onTitleChanged(); return cm.setPreprocessor((style.usercssData || {}).preprocessor); } - function updateTitle() { - const newTitle = (dirty.isDirty() ? '* ' : '') + - (style.id ? style.name : t('addStyleTitle')); - if (document.title !== newTitle) { - document.title = newTitle; - } - } - function replaceStyle(newStyle, codeIsUpdated) { const sameCode = newStyle.sourceCode === cm.getValue(); if (sameCode) { @@ -385,7 +371,7 @@ function createSourceEditor(style) { return { replaceStyle, - isDirty: dirty.isDirty, + dirty, getStyle: () => style, getEditors: () => [cm], scrollToEditor: () => {}, From 43a4671c64dff36f287b2415019520778407a91a Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 28 Nov 2018 12:57:44 +0800 Subject: [PATCH 17/17] Change: defer page script injection. Try to inject to head (#584) --- content/apply.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/content/apply.js b/content/apply.js index 6a83d4e5..bbb6a338 100644 --- a/content/apply.js +++ b/content/apply.js @@ -65,10 +65,9 @@ const APPLY = (() => { // Since it's easy to spoof the browser version in pre-Quantum FF we're checking // for getPreventDefault which got removed in FF59 https://bugzil.la/691151 const EVENT_NAME = chrome.runtime.id; - const usePageScript = CHROME || isOwnPage || Event.prototype.getPreventDefault ? - Promise.resolve(false) : injectPageScript(); + let ready; return (el, content) => - usePageScript.then(ok => { + checkPageScript().then(ok => { if (!ok) { const disabled = el.disabled; el.textContent = content; @@ -83,6 +82,14 @@ const APPLY = (() => { } }); + function checkPageScript() { + if (!ready) { + ready = CHROME || isOwnPage || Event.prototype.getPreventDefault ? + Promise.resolve(false) : injectPageScript(); + } + return ready; + } + function injectPageScript() { const scriptContent = EVENT_NAME => { document.currentScript.remove(); @@ -125,7 +132,7 @@ const APPLY = (() => { script.src = src; script.onerror = () => resolve(false); window.addEventListener(EVENT_NAME, handleInit); - document.documentElement.appendChild(script); + (document.head || document.documentElement).appendChild(script); return promise.then(result => { script.remove(); window.removeEventListener(EVENT_NAME, handleInit);