Merge branch 'master' into dev-chrome-49

This commit is contained in:
Rob Garrison 2018-11-27 23:03:30 -06:00 committed by GitHub
commit 37dca280f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 213 additions and 350 deletions

View File

@ -1373,6 +1373,10 @@
"message": "Stylus can access file:// URLs only if you enable the corresponding checkbox for Stylus extension on chrome://extensions page.", "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" "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": { "updateAllCheckSucceededNoUpdate": {
"message": "No updates found.", "message": "No updates found.",
"description": "Text that displays when an update all check completed and no updates are available" "description": "Text that displays when an update all check completed and no updates are available"
@ -1477,6 +1481,9 @@
"connectingDropbox": { "connectingDropbox": {
"message": "Connecting Dropbox..." "message": "Connecting Dropbox..."
}, },
"connectingDropboxNotAllowed": {
"message": "Connecting to Dropbox is only available in apps installed directly from the webstore"
},
"gettingStyles": { "gettingStyles": {
"message": "Getting all styles..." "message": "Getting all styles..."
}, },

View File

@ -56,7 +56,7 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
return browser.runtime.openOptionsPage() return browser.runtime.openOptionsPage()
.then(() => new Promise(resolve => setTimeout(resolve, 100))) .then(() => new Promise(resolve => setTimeout(resolve, 100)))
.then(() => msg.broadcastExtension({method: 'optionsCustomizeHotkeys'})); .then(() => msg.broadcastExtension({method: 'optionsCustomizeHotkeys'}));
}, }
}); });
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
@ -150,6 +150,9 @@ chrome.runtime.onInstalled.addListener(({reason}) => {
}); });
// themes may change // themes may change
delete localStorage.codeMirrorThemes; 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);
}); });
// ************************************************************************* // *************************************************************************

View File

@ -75,6 +75,7 @@
* @returns {Promise<{style, dup:Boolean?}>} * @returns {Promise<{style, dup:Boolean?}>}
*/ */
function build({ function build({
styleId,
sourceCode, sourceCode,
checkDup, checkDup,
metaOnly, metaOnly,
@ -83,7 +84,8 @@
}) { }) {
return usercss.buildMeta(sourceCode) return usercss.buildMeta(sourceCode)
.then(style => { .then(style => {
const findDup = checkDup || assignVars ? find(style) : null; const findDup = checkDup || assignVars ?
find(styleId ? {id: styleId} : style) : Promise.resolve();
return Promise.all([ return Promise.all([
metaOnly ? style : doBuild(style, findDup), metaOnly ? style : doBuild(style, findDup),
findDup findDup

View File

@ -65,10 +65,9 @@ const APPLY = (() => {
// Since it's easy to spoof the browser version in pre-Quantum FF we're checking // 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 // for getPreventDefault which got removed in FF59 https://bugzil.la/691151
const EVENT_NAME = chrome.runtime.id; const EVENT_NAME = chrome.runtime.id;
const usePageScript = CHROME || isOwnPage || Event.prototype.getPreventDefault ? let ready;
Promise.resolve(false) : injectPageScript();
return (el, content) => return (el, content) =>
usePageScript.then(ok => { checkPageScript().then(ok => {
if (!ok) { if (!ok) {
const disabled = el.disabled; const disabled = el.disabled;
el.textContent = content; 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() { function injectPageScript() {
const scriptContent = EVENT_NAME => { const scriptContent = EVENT_NAME => {
document.currentScript.remove(); document.currentScript.remove();
window.addEventListener(EVENT_NAME, function handler(e) { const available = checkStyleApplied();
const {method, id, content} = e.detail; if (available) {
if (method === 'setStyleContent') { window.addEventListener(EVENT_NAME, function handler(e) {
const el = document.getElementById(id); const {method, id, content} = e.detail;
if (!el) { if (method === 'setStyleContent') {
return; 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; }, true);
el.textContent = content; }
el.disabled = disabled; window.dispatchEvent(new CustomEvent(EVENT_NAME, {detail: {
} else if (method === 'orphan') { method: 'init',
window.removeEventListener(EVENT_NAME, handler); available
} }}));
}, true);
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 code = `(${scriptContent})(${JSON.stringify(EVENT_NAME)})`;
const src = `data:application/javascript;base64,${btoa(code)}`; const src = `data:application/javascript;base64,${btoa(code)}`;
const script = document.createElement('script'); const script = document.createElement('script');
const {resolve, promise} = deferred(); const {resolve, promise} = deferred();
script.src = src; script.src = src;
script.onload = () => resolve(true);
script.onerror = () => resolve(false); script.onerror = () => resolve(false);
document.documentElement.appendChild(script); window.addEventListener(EVENT_NAME, handleInit);
return promise; (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);
}
}
} }
} }

View File

@ -31,7 +31,6 @@
<link href="vendor/codemirror/addon/search/matchesonscrollbar.css" rel="stylesheet"> <link href="vendor/codemirror/addon/search/matchesonscrollbar.css" rel="stylesheet">
<script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script> <script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
<script src="vendor/codemirror/addon/scroll/annotatescrollbar.js"></script> <script src="vendor/codemirror/addon/scroll/annotatescrollbar.js"></script>
<script src="vendor/codemirror/addon/search/match-highlighter.js"></script>
<script src="vendor/codemirror/addon/search/searchcursor.js"></script> <script src="vendor/codemirror/addon/search/searchcursor.js"></script>
<script src="vendor/codemirror/addon/comment/comment.js"></script> <script src="vendor/codemirror/addon/comment/comment.js"></script>
@ -62,6 +61,8 @@
<script src="vendor-overwrites/colorpicker/colorpicker.js"></script> <script src="vendor-overwrites/colorpicker/colorpicker.js"></script>
<script src="vendor-overwrites/colorpicker/colorview.js"></script> <script src="vendor-overwrites/colorpicker/colorview.js"></script>
<script src="vendor-overwrites/codemirror-addon/match-highlighter.js"></script>
<script src="js/polyfill.js"></script> <script src="js/polyfill.js"></script>
<script src="js/promisify.js"></script> <script src="js/promisify.js"></script>
<script src="js/dom.js"></script> <script src="js/dom.js"></script>
@ -78,8 +79,6 @@
<link href="edit/global-search.css" rel="stylesheet"> <link href="edit/global-search.css" rel="stylesheet">
<script src="edit/global-search.js"></script> <script src="edit/global-search.js"></script>
<script src="edit/match-highlighter-helper.js"></script>
<link href="edit/codemirror-default.css" rel="stylesheet"> <link href="edit/codemirror-default.css" rel="stylesheet">
<script src="edit/codemirror-default.js"></script> <script src="edit/codemirror-default.js"></script>

View File

@ -37,14 +37,6 @@
.cm-uso-variable { .cm-uso-variable {
font-weight: bold; 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 { .CodeMirror-activeline .applies-to:before {
background-color: hsla(214, 100%, 90%, 0.15); background-color: hsla(214, 100%, 90%, 0.15);

View File

@ -230,7 +230,7 @@
// editor commands // editor commands
for (const name of ['save', 'toggleStyle', 'nextEditor', 'prevEditor']) { for (const name of ['save', 'toggleStyle', 'nextEditor', 'prevEditor']) {
CodeMirror.commands[name] = () => editor[name](); CodeMirror.commands[name] = (...args) => editor[name](...args);
} }
// CodeMirror convenience commands // CodeMirror convenience commands

View File

@ -32,12 +32,14 @@ const cmFactory = (() => {
if (value === 'token') { if (value === 'token') {
cm.setOption('highlightSelectionMatches', { cm.setOption('highlightSelectionMatches', {
showToken: /[#.\-\w]/, showToken: /[#.\-\w]/,
annotateScrollbar: true annotateScrollbar: true,
onUpdate: updateMatchHighlightCount
}); });
} else if (value === 'selection') { } else if (value === 'selection') {
cm.setOption('highlightSelectionMatches', { cm.setOption('highlightSelectionMatches', {
showToken: false, showToken: false,
annotateScrollbar: true annotateScrollbar: true,
onUpdate: updateMatchHighlightCount
}); });
} else { } else {
cm.setOption('highlightSelectionMatches', null); cm.setOption('highlightSelectionMatches', null);
@ -80,6 +82,10 @@ const cmFactory = (() => {
}); });
return {create, destroy, setOption}; return {create, destroy, setOption};
function updateMatchHighlightCount(cm, state) {
cm.display.wrapper.dataset.matchHighlightCount = state.matchesonscroll.matches.length;
}
function configureMouseFn(cm, repeat) { function configureMouseFn(cm, repeat) {
return repeat === 'double' ? return repeat === 'double' ?
{unit: selectTokenOnDoubleclick} : {unit: selectTokenOnDoubleclick} :

View File

@ -364,14 +364,14 @@ input:invalid {
.resize-grip-enabled .CodeMirror-scrollbar-filler { .resize-grip-enabled .CodeMirror-scrollbar-filler {
bottom: 7px; /* make space for resize-grip */ bottom: 7px; /* make space for resize-grip */
} }
body[data-match-highlight="token"] .cm-matchhighlight-approved .cm-matchhighlight, body:not(.find-open) .cm-matchhighlight,
body[data-match-highlight="token"] .CodeMirror-selection-highlight-scrollbar { body:not(.find-open) .CodeMirror-selection-highlight-scrollbar {
animation: fadein-match-highlighter 1s cubic-bezier(.97,.01,.42,.98); animation: fadein-match-highlighter 1s cubic-bezier(.97,.01,.42,.98);
animation-fill-mode: both; animation-fill-mode: both;
} }
body[data-match-highlight="selection"] .cm-matchhighlight-approved .cm-matchhighlight, body:not(.find-open) [data-match-highlight-count="1"] .cm-matchhighlight,
body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar { body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-highlight-scrollbar {
background-color: rgba(1, 151, 193, 0.1); animation: none;
} }
@-webkit-keyframes highlight { @-webkit-keyframes highlight {
from { from {

View File

@ -174,12 +174,30 @@ preinit();
$('#beautify').onclick = () => beautify(editor.getEditors()); $('#beautify').onclick = () => beautify(editor.getEditors());
$('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true}); $('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true});
window.addEventListener('resize', () => debounce(rememberWindowSize, 100)); window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
editor = usercss ? createSourceEditor(style) : createSectionsEditor(style); editor = (usercss ? createSourceEditor : createSectionsEditor)({
if (editor.ready) { style,
return editor.ready(); 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() { function preinit() {
@ -306,7 +324,7 @@ function beforeUnload(e) {
// refocus if unloading was canceled // refocus if unloading was canceled
setTimeout(() => activeElement.focus()); 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 // neither confirm() nor custom messages work in modern browsers but just in case
e.returnValue = t('styleChangesNotSaved'); e.returnValue = t('styleChangesNotSaved');
} }

View File

@ -181,17 +181,33 @@
opacity: 1; opacity: 1;
} }
/*********** CodeMirror ****************/ /*********** CM search highlight restyling, which shouldn't need color variables ****************/
body.find-open .search-target-editor {
.search-target-editor { outline-color: darkorange !important;
outline: 1px solid darkorange;
} }
#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; background-color: darkorange;
color: black; 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) { @media (max-width: 500px) {
#search-replace-dialog { #search-replace-dialog {
left: 0; left: 0;

View File

@ -752,8 +752,14 @@ onDOMready().then(() => {
function makeTargetVisible(element) { function makeTargetVisible(element) {
const old = $('.' + TARGET_CLASS); const old = $('.' + TARGET_CLASS);
if (old !== element) { if (old !== element) {
if (old) old.classList.remove(TARGET_CLASS); if (old) {
if (element) element.classList.add(TARGET_CLASS); old.classList.remove(TARGET_CLASS);
document.body.classList.remove('find-open');
}
if (element) {
element.classList.add(TARGET_CLASS);
document.body.classList.add('find-open');
}
} }
} }

View File

@ -33,18 +33,18 @@ function createLinterHelpDialog(getIssues) {
}; };
} else { } else {
headerLink = $createLink(baseUrl, 'stylelint'); headerLink = $createLink(baseUrl, 'stylelint');
template = ({rule}) => template = rule =>
$create('li', $create('li',
rule === 'CssSyntaxError' ? rule : $createLink(baseUrl + rule, rule)); rule === 'CssSyntaxError' ? rule : $createLink(baseUrl + rule, rule));
} }
const header = t('linterIssuesHelp', '\x01').split('\x01'); const header = t('linterIssuesHelp', '\x01').split('\x01');
const activeRules = getIssues(); const activeRules = new Set([...getIssues()].map(issue => issue.rule));
Promise.resolve(linter === 'csslint' && prepareCsslintRules()) Promise.resolve(linter === 'csslint' && prepareCsslintRules())
.then(() => .then(() =>
showHelp(t('linterIssues'), showHelp(t('linterIssues'),
$create([ $create([
header[0], headerLink, header[1], header[0], headerLink, header[1],
$create('ul.rules', [...activeRules.values()].map(template)), $create('ul.rules', [...activeRules].map(template)),
]) ])
) )
); );

View File

@ -12,7 +12,7 @@ function createMetaCompiler(cm) {
if (_cm !== cm) { if (_cm !== cm) {
return; 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) { if (!match) {
return []; return [];
} }

View File

@ -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);
}
}
})();

View File

@ -6,10 +6,9 @@
/* exported createSectionsEditor */ /* exported createSectionsEditor */
'use strict'; '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 let INC_ID = 0; // an increment id that is used by various object to track the order
const dirty = dirtyReporter(); const dirty = dirtyReporter();
dirty.onChange(updateTitle);
const container = $('#sections'); const container = $('#sections');
const sections = []; const sections = [];
@ -18,7 +17,7 @@ function createSectionsEditor(style) {
nameEl.addEventListener('input', () => { nameEl.addEventListener('input', () => {
dirty.modify('name', style.name, nameEl.value); dirty.modify('name', style.name, nameEl.value);
style.name = nameEl.value; style.name = nameEl.value;
updateTitle(); onTitleChanged();
}); });
const enabledEl = $('#enabled'); const enabledEl = $('#enabled');
@ -64,7 +63,7 @@ function createSectionsEditor(style) {
return { return {
ready: () => initializing, ready: () => initializing,
replaceStyle, replaceStyle,
isDirty: dirty.isDirty, dirty,
getStyle: () => style, getStyle: () => style,
getEditors, getEditors,
scrollToEditor, scrollToEditor,
@ -201,35 +200,27 @@ function createSectionsEditor(style) {
} }
function nextEditor(cm, cycle = true) { function nextEditor(cm, cycle = true) {
if (!cycle) { if (!cycle && findLast(sections, s => !s.isRemoved()).cm === cm) {
for (const section of sections) { return;
if (section.isRemoved()) {
continue;
}
if (cm === section.cm) {
return;
}
break;
}
} }
return nextPrevEditor(cm, 1); return nextPrevEditor(cm, 1);
} }
function prevEditor(cm, cycle = true) { function prevEditor(cm, cycle = true) {
if (!cycle) { if (!cycle && sections.find(s => !s.isRemoved()).cm === cm) {
for (let i = sections.length - 1; i >= 0; i--) { return;
if (sections[i].isRemoved()) {
continue;
}
if (cm === sections[i].cm) {
return;
}
break;
}
} }
return nextPrevEditor(cm, -1); 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) { function nextPrevEditor(cm, direction) {
const editors = getEditors(); const editors = getEditors();
cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length]; cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
@ -421,7 +412,7 @@ function createSectionsEditor(style) {
nameEl.value = style.name || ''; nameEl.value = style.name || '';
enabledEl.checked = style.enabled !== false; enabledEl.checked = style.enabled !== false;
$('#url').href = style.url || ''; $('#url').href = style.url || '';
updateTitle(); onTitleChanged();
} }
function updateLivePreview() { function updateLivePreview() {
@ -432,14 +423,6 @@ function createSectionsEditor(style) {
livePreview.update(getModel()); 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({ function initSection({
sections: originalSections, sections: originalSections,
total = originalSections.length, total = originalSections.length,

View File

@ -6,7 +6,7 @@
/* exported createSourceEditor */ /* exported createSourceEditor */
'use strict'; 'use strict';
function createSourceEditor(style) { function createSourceEditor({style, onTitleChanged}) {
$('#name').disabled = true; $('#name').disabled = true;
$('#save-button').disabled = true; $('#save-button').disabled = true;
$('#mozilla-format-container').remove(); $('#mozilla-format-container').remove();
@ -16,12 +16,6 @@ function createSourceEditor(style) {
$('#sections').appendChild($create('.single-editor')); $('#sections').appendChild($create('.single-editor'));
const dirty = dirtyReporter(); const dirty = dirtyReporter();
dirty.onChange(() => {
const isDirty = dirty.isDirty();
document.body.classList.toggle('dirty', isDirty);
$('#save-button').disabled = !isDirty;
updateTitle();
});
// normalize style // normalize style
if (!style.id) setupNewStyle(style); if (!style.id) setupNewStyle(style);
@ -82,6 +76,7 @@ function createSourceEditor(style) {
function preprocess(style) { function preprocess(style) {
return API.buildUsercss({ return API.buildUsercss({
styleId: style.id,
sourceCode: style.sourceCode, sourceCode: style.sourceCode,
assignVars: true assignVars: true
}) })
@ -170,18 +165,10 @@ function createSourceEditor(style) {
$('#name').value = style.name; $('#name').value = style.name;
$('#enabled').checked = style.enabled; $('#enabled').checked = style.enabled;
$('#url').href = style.url; $('#url').href = style.url;
updateTitle(); onTitleChanged();
return cm.setPreprocessor((style.usercssData || {}).preprocessor); 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) { function replaceStyle(newStyle, codeIsUpdated) {
const sameCode = newStyle.sourceCode === cm.getValue(); const sameCode = newStyle.sourceCode === cm.getValue();
if (sameCode) { if (sameCode) {
@ -384,7 +371,7 @@ function createSourceEditor(style) {
return { return {
replaceStyle, replaceStyle,
isDirty: dirty.isDirty, dirty,
getStyle: () => style, getStyle: () => style,
getEditors: () => [cm], getEditors: () => [cm],
scrollToEditor: () => {}, scrollToEditor: () => {},

View File

@ -10,7 +10,7 @@ const usercss = (() => {
// updateURL: 'updateUrl', // updateURL: 'updateUrl',
name: undefined, 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']); const ERR_ARGS_IS_LIST = new Set(['missingMandatory', 'missingChar']);
return {buildMeta, buildCode, assignVars}; return {buildMeta, buildCode, assignVars};

View File

@ -148,6 +148,9 @@ function reportUpdateState({updated, style, error, STATES}) {
error = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); error = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
} else if (error === STATES.MAYBE_EDITED) { } else if (error === STATES.MAYBE_EDITED) {
error = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); 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; const message = same ? t('updateCheckSucceededNoUpdate') : error;
newClasses.set('no-update', true); newClasses.set('no-update', true);

View File

@ -80,7 +80,6 @@
<template data-id="unreachableInfo"> <template data-id="unreachableInfo">
<div class="blocked-info"> <div class="blocked-info">
<label i18n-text="unreachableContentScript"></label> <label i18n-text="unreachableContentScript"></label>
<p i18n-text="unreachableFileHint"></p>
</div> </div>
</template> </template>

View File

@ -554,7 +554,8 @@ body.blocked .actions > .main-controls {
} }
.blocked-info { .blocked-info {
hyphens: auto; hyphens: none;
word-wrap: break-word;
} }
.blocked-info label { .blocked-info label {

View File

@ -130,6 +130,10 @@ function initPopup() {
return; return;
} }
const info = template.unreachableInfo; 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)) { if (FIREFOX && tabURL.startsWith(URLS.browserWebStore)) {
$('label', info).textContent = t('unreachableAMO'); $('label', info).textContent = t('unreachableAMO');
const note = (FIREFOX < 59 ? t('unreachableAMOHintOldFF') : t('unreachableAMOHint')) + 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 renderToken = s => s[0] === '<' ? $create('b', tWordBreak(s.slice(1, -1))) : s;
const renderLine = line => $create('p', line.split(/(<.*?>)/).map(renderToken)); const renderLine = line => $create('p', line.split(/(<.*?>)/).map(renderToken));
const noteNode = $create('fragment', note.split('\n').map(renderLine)); const noteNode = $create('fragment', note.split('\n').map(renderLine));
const target = $('p', info); info.appendChild(noteNode);
target.parentNode.insertBefore(noteNode, target); }
target.remove(); // 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.classList.add('unreachable');
document.body.insertBefore(info, document.body.firstChild); document.body.insertBefore(info, document.body.firstChild);

View File

@ -689,6 +689,7 @@ window.addEventListener('showStyles:done', function _() {
'/api/v1/styles/subcategory' + '/api/v1/styles/subcategory' +
'?search=' + encodeURIComponent(category) + '?search=' + encodeURIComponent(category) +
'&page=' + searchCurrentPage + '&page=' + searchCurrentPage +
'&per_page=10' +
'&country=NA'; '&country=NA';
const cacheKey = category + '/' + searchCurrentPage; const cacheKey = category + '/' + searchCurrentPage;

View File

@ -46,8 +46,11 @@ function uploadFileDropbox(client, stylesText) {
} }
$('#sync-dropbox-export').onclick = () => { $('#sync-dropbox-export').onclick = () => {
const mode = localStorage.installType;
const title = t('syncDropboxStyles'); 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() hasDropboxAccessToken()
.then(token => token || requestDropboxAccessToken()) .then(token => token || requestDropboxAccessToken())
@ -116,8 +119,11 @@ $('#sync-dropbox-export').onclick = () => {
}; };
$('#sync-dropbox-import').onclick = () => { $('#sync-dropbox-import').onclick = () => {
const mode = localStorage.installType;
const title = t('retrieveDropboxSync'); 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() hasDropboxAccessToken()
.then(token => token || requestDropboxAccessToken()) .then(token => token || requestDropboxAccessToken())

View File

@ -402,10 +402,6 @@
if (!ch) { if (!ch) {
break; break;
} else if (ch === '/' && peek() === '*') { /* css comment */ } else if (ch === '/' && peek() === '*') { /* css comment */
if (isAfterNewline) {
print.newLine();
}
print.text(eatComment()); print.text(eatComment());
if (peek() !== ';') print.newLine(); if (peek() !== ';') print.newLine();
} else if (ch === '/' && peek() === '/') { // single line comment } else if (ch === '/' && peek() === '/') { // single line comment

View File

@ -36,7 +36,8 @@
wordsOnly: false, wordsOnly: false,
annotateScrollbar: false, annotateScrollbar: false,
showToken: false, showToken: false,
trim: true trim: true,
onUpdate: () => {}
} }
function State(options) { function State(options) {
@ -46,6 +47,7 @@
this.overlay = this.timeout = null; this.overlay = this.timeout = null;
this.matchesonscroll = null; this.matchesonscroll = null;
this.active = false; this.active = false;
this.query = null;
} }
CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) { CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
@ -88,12 +90,24 @@
function addOverlay(cm, query, hasBoundary, style) { function addOverlay(cm, query, hasBoundary, style) {
var state = cm.state.matchHighlighter; var state = cm.state.matchHighlighter;
if (state.query === query) {
return;
}
removeOverlay(cm);
state.query = query;
cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style)); cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) { 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, state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,
{className: "CodeMirror-selection-highlight-scrollbar"}); {className: "CodeMirror-selection-highlight-scrollbar"});
} }
state.options.onUpdate(cm, state);
} }
function removeOverlay(cm) { function removeOverlay(cm) {
@ -106,19 +120,22 @@
state.matchesonscroll = null; state.matchesonscroll = null;
} }
} }
state.query = null;
} }
function highlightMatches(cm) { function highlightMatches(cm) {
cm.operation(function() { cm.operation(function() {
var state = cm.state.matchHighlighter; var state = cm.state.matchHighlighter;
removeOverlay(cm);
if (!cm.somethingSelected() && state.options.showToken) { if (!cm.somethingSelected() && state.options.showToken) {
var re = state.options.showToken === true ? /[\w$]/ : 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; var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;
while (start && re.test(line.charAt(start - 1))) --start; while (start && re.test(line.charAt(start - 1))) --start;
while (end < line.length && re.test(line.charAt(end))) ++end; 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); addOverlay(cm, line.slice(start, end), re, state.options.style);
} else {
removeOverlay(cm);
}
return; return;
} }
var from = cm.getCursor("from"), to = cm.getCursor("to"); var from = cm.getCursor("from"), to = cm.getCursor("to");
@ -126,8 +143,11 @@
if (state.options.wordsOnly && !isWord(cm, from, to)) return; if (state.options.wordsOnly && !isWord(cm, from, to)) return;
var selection = cm.getRange(from, to) var selection = cm.getRange(from, to)
if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "") 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); addOverlay(cm, selection, false, state.options.style);
} else {
removeOverlay(cm);
}
}); });
} }

View File

@ -4777,7 +4777,7 @@ self.parserlib = (() => {
return result; return result;
} }
_expr(inFunction) { _expr(inFunction, endToken = Tokens.RPAREN) {
const stream = this._tokenStream; const stream = this._tokenStream;
const values = []; const values = [];
@ -4786,7 +4786,7 @@ self.parserlib = (() => {
if (!value && !values.length) return null; if (!value && !values.length) return null;
// get everything inside the parens and let validateProperty handle that // 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(); stream.get();
value = new PropertyValuePart(stream._token); value = new PropertyValuePart(stream._token);
} else if (!value) { } else if (!value) {
@ -4914,8 +4914,9 @@ self.parserlib = (() => {
inFunction && Tokens.LBRACE, inFunction && Tokens.LBRACE,
])) { ])) {
const token = stream._token; const token = stream._token;
token.expr = this._expr(inFunction); const endToken = Tokens.type(token.endChar);
stream.mustMatch(Tokens.type(token.endChar)); token.expr = this._expr(inFunction, endToken);
stream.mustMatch(endToken);
return finalize(token, token.value + (token.expr || '') + token.endChar); return finalize(token, token.value + (token.expr || '') + token.endChar);
} }