diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 2b2c111d..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" @@ -1477,6 +1481,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 7b1f425f..ac2927e7 100644 --- a/background/background.js +++ b/background/background.js @@ -56,7 +56,7 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, { return browser.runtime.openOptionsPage() .then(() => new Promise(resolve => setTimeout(resolve, 100))) .then(() => msg.broadcastExtension({method: 'optionsCustomizeHotkeys'})); - }, + } }); // eslint-disable-next-line no-var @@ -150,6 +150,9 @@ chrome.runtime.onInstalled.addListener(({reason}) => { }); // themes may change delete localStorage.codeMirrorThemes; + // save install type: "admin", "development", "normal", "sideload" or "other" + // "normal" = addon installed from webstore + chrome.management.getSelf(info => localStorage.installType = info.installType); }); // ************************************************************************* 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/content/apply.js b/content/apply.js index b118d363..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,33 +82,68 @@ 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(); - 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); - document.documentElement.appendChild(script); - return promise; + window.addEventListener(EVENT_NAME, handleInit); + (document.head || document.documentElement).appendChild(script); + 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); + } + } } } diff --git a/edit.html b/edit.html index 81423113..9185bd42 100644 --- a/edit.html +++ b/edit.html @@ -31,7 +31,6 @@ - @@ -62,6 +61,8 @@ + + @@ -78,8 +79,6 @@ - - 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/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/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..6276fac1 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 { +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; } -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); +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 { from { 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/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'); + } } } 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)), ]) ) ); 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/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/edit/sections-editor.js b/edit/sections-editor.js index a06223d3..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, @@ -201,35 +200,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]; @@ -421,7 +412,7 @@ function createSectionsEditor(style) { nameEl.value = style.name || ''; enabledEl.checked = style.enabled !== false; $('#url').href = style.url || ''; - updateTitle(); + onTitleChanged(); } function updateLivePreview() { @@ -432,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 dcd43a13..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); @@ -82,6 +76,7 @@ function createSourceEditor(style) { function preprocess(style) { return API.buildUsercss({ + styleId: style.id, sourceCode: style.sourceCode, assignVars: true }) @@ -170,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) { @@ -384,7 +371,7 @@ function createSourceEditor(style) { return { replaceStyle, - isDirty: dirty.isDirty, + dirty, getStyle: () => style, getEditors: () => [cm], scrollToEditor: () => {}, diff --git a/js/usercss.js b/js/usercss.js index 4c9d9ff8..03e37c18 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}; 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); diff --git a/popup.html b/popup.html index 35c51cba..4890c313 100644 --- a/popup.html +++ b/popup.html @@ -80,7 +80,6 @@ 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 { 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); 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; 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()) 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 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); + } }); } 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); }