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