From a0172c262c533984172f1b2ed258adf61a930f0c Mon Sep 17 00:00:00 2001 From: eight Date: Mon, 17 Aug 2020 21:06:52 +0800 Subject: [PATCH 01/55] Fix: enable linter after processing metadata (#1018) --- edit/source-editor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/edit/source-editor.js b/edit/source-editor.js index 7119cfa5..a9f8bf22 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -50,10 +50,10 @@ function createSourceEditor({style, onTitleChanged}) { updateMeta(); }); - linter.enableForEditor(cm); - updateMeta().then(() => { + linter.enableForEditor(cm); + let prevMode = NaN; cm.on('optionChange', (cm, option) => { if (option !== 'mode') return; From 56f1574433757e15ff122a88ed7b2ed7d4a26c45 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 19 Aug 2020 22:33:04 +0300 Subject: [PATCH 02/55] fix styling of options frame in FF (#1022) --- content/apply.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/apply.js b/content/apply.js index 57a630d3..555535a2 100644 --- a/content/apply.js +++ b/content/apply.js @@ -64,7 +64,7 @@ self.INJECTED !== 1 && (() => { function getMatchUrl() { let matchUrl = location.href; - if (!matchUrl.match(/^(http|file|chrome|ftp)/)) { + if (!chrome.tabs && !matchUrl.match(/^(http|file|chrome|ftp)/)) { // dynamic about: and javascript: iframes don't have an URL yet // so we'll try the parent frame which is guaranteed to have a real URL try { From db3747d5f07c1b1af9881642592d8359c8317766 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 19 Jul 2020 11:23:54 +0000 Subject: [PATCH 03/55] Bump lodash from 4.17.15 to 4.17.19 Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19) Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5a767709..60af9c56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5065,9 +5065,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, "lodash.defaults": { From e0aadb752af32ed25542883f42dc9c6ee8f1e50e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2020 08:08:44 +0000 Subject: [PATCH 04/55] Bump minimist from 1.2.0 to 1.2.5 Bumps [minimist](https://github.com/substack/minimist) from 1.2.0 to 1.2.5. - [Release notes](https://github.com/substack/minimist/releases) - [Commits](https://github.com/substack/minimist/compare/1.2.0...1.2.5) Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 60af9c56..ebf1ae98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5324,9 +5324,9 @@ } }, "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, "minipass": { @@ -5425,14 +5425,6 @@ "dev": true, "requires": { "minimist": "^1.2.5" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - } } }, "moment": { From 5109f9abb36976cda64109d2f61574ec6116764e Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 23 Aug 2020 18:44:27 +0300 Subject: [PATCH 05/55] CodeMirror 5.57 (#1023) * codemirror 5.57 * dedupe the props defined in new codemirror --- edit/codemirror-default.js | 7 --- package-lock.json | 6 +-- package.json | 2 +- vendor/codemirror/README.md | 2 +- vendor/codemirror/addon/comment/comment.js | 4 +- .../addon/scroll/annotatescrollbar.js | 10 +++- vendor/codemirror/keymap/vim.js | 47 ++++++++++++++++++- vendor/codemirror/lib/codemirror.js | 6 +-- vendor/codemirror/mode/css/css.js | 35 +++++++------- 9 files changed, 84 insertions(+), 35 deletions(-) diff --git a/edit/codemirror-default.js b/edit/codemirror-default.js index 6d97573d..8576d9e0 100644 --- a/edit/codemirror-default.js +++ b/edit/codemirror-default.js @@ -105,14 +105,7 @@ } Object.assign(CodeMirror.mimeModes['text/css'].propertyKeywords, { - 'background-position-x': true, - 'background-position-y': true, - 'contain': true, - 'mask-image': true, - 'mix-blend-mode': true, 'overscroll-behavior': true, - 'rotate': true, - 'isolation': true, }); Object.assign(CodeMirror.mimeModes['text/css'].colorKeywords, { 'darkgrey': true, diff --git a/package-lock.json b/package-lock.json index ebf1ae98..4872bfa8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1917,9 +1917,9 @@ "dev": true }, "codemirror": { - "version": "5.56.0", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.56.0.tgz", - "integrity": "sha512-MfKVmYgifXjQpLSgpETuih7A7WTTIsxvKfSLGseTY5+qt0E1UD1wblZGM6WLenORo8sgmf+3X+WTe2WF7mufyw==" + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.57.0.tgz", + "integrity": "sha512-WGc6UL7Hqt+8a6ZAsj/f1ApQl3NPvHY/UQSzG6fB6l4BjExgVdhFaxd7mRTw1UCiYe/6q86zHP+kfvBQcZGvUg==" }, "collection-visit": { "version": "1.0.0", diff --git a/package.json b/package.json index f79dceca..49d423d4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "repository": "openstyles/stylus", "author": "Stylus Team", "dependencies": { - "codemirror": "^5.56.0", + "codemirror": "^5.57.0", "db-to-cloud": "^0.4.5", "jsonlint": "^1.6.3", "less-bundle": "github:openstyles/less-bundle#v0.1.0", diff --git a/vendor/codemirror/README.md b/vendor/codemirror/README.md index 2e35ecfa..f838d71e 100644 --- a/vendor/codemirror/README.md +++ b/vendor/codemirror/README.md @@ -1,4 +1,4 @@ -## codemirror v5.56.0 +## codemirror v5.57.0 Following files are copied from npm (node_modules): diff --git a/vendor/codemirror/addon/comment/comment.js b/vendor/codemirror/addon/comment/comment.js index 8394e85a..dac48d03 100644 --- a/vendor/codemirror/addon/comment/comment.js +++ b/vendor/codemirror/addon/comment/comment.js @@ -13,7 +13,7 @@ var noOptions = {}; var nonWS = /[^\s\u00a0]/; - var Pos = CodeMirror.Pos; + var Pos = CodeMirror.Pos, cmp = CodeMirror.cmpPos; function firstNonWS(str) { var found = str.search(nonWS); @@ -126,7 +126,9 @@ if (i != end || lastLineHasText) self.replaceRange(lead + pad, Pos(i, 0)); } else { + var atCursor = cmp(self.getCursor("to"), to) == 0, empty = !self.somethingSelected() self.replaceRange(endString, to); + if (atCursor) self.setSelection(empty ? to : self.getCursor("from"), to) self.replaceRange(startString, from); } }); diff --git a/vendor/codemirror/addon/scroll/annotatescrollbar.js b/vendor/codemirror/addon/scroll/annotatescrollbar.js index 9fe61ec1..c12e44cd 100644 --- a/vendor/codemirror/addon/scroll/annotatescrollbar.js +++ b/vendor/codemirror/addon/scroll/annotatescrollbar.js @@ -72,10 +72,16 @@ var wrapping = cm.getOption("lineWrapping"); var singleLineH = wrapping && cm.defaultTextHeight() * 1.5; var curLine = null, curLineObj = null; + function getY(pos, top) { if (curLine != pos.line) { - curLine = pos.line; - curLineObj = cm.getLineHandle(curLine); + curLine = pos.line + curLineObj = cm.getLineHandle(pos.line) + var visual = cm.getLineHandleVisualStart(curLineObj) + if (visual != curLineObj) { + curLine = cm.getLineNumber(visual) + curLineObj = visual + } } if ((curLineObj.widgets && curLineObj.widgets.length) || (wrapping && curLineObj.height > singleLineH)) diff --git a/vendor/codemirror/keymap/vim.js b/vendor/codemirror/keymap/vim.js index bca6d46d..5a4860c6 100644 --- a/vendor/codemirror/keymap/vim.js +++ b/vendor/codemirror/keymap/vim.js @@ -2069,6 +2069,8 @@ if (operatorArgs) { operatorArgs.linewise = true; } tmp.end.line--; } + } else if (character === 't') { + tmp = expandTagUnderCursor(cm, head, inclusive); } else { // No text object defined for this, don't move. return null; @@ -3295,6 +3297,49 @@ return { start: Pos(cur.line, start), end: Pos(cur.line, end) }; } + /** + * Depends on the following: + * + * - editor mode should be htmlmixedmode / xml + * - mode/xml/xml.js should be loaded + * - addon/fold/xml-fold.js should be loaded + * + * If any of the above requirements are not true, this function noops. + * + * This is _NOT_ a 100% accurate implementation of vim tag text objects. + * The following caveats apply (based off cursory testing, I'm sure there + * are other discrepancies): + * + * - Does not work inside comments: + * ``` + * + * ``` + * - Does not work when tags have different cases: + * ``` + *
broken
+ * ``` + * - Does not work when cursor is inside a broken tag: + * ``` + *
+ * ``` + */ + function expandTagUnderCursor(cm, head, inclusive) { + var cur = head; + if (!CodeMirror.findMatchingTag || !CodeMirror.findEnclosingTag) { + return { start: cur, end: cur }; + } + + var tags = CodeMirror.findMatchingTag(cm, head) || CodeMirror.findEnclosingTag(cm, head); + if (!tags || !tags.open || !tags.close) { + return { start: cur, end: cur }; + } + + if (inclusive) { + return { start: tags.open.from, end: tags.close.to }; + } + return { start: tags.open.to, end: tags.close.from }; + } + function recordJumpPosition(cm, oldCur, newCur) { if (!cursorEqual(oldCur, newCur)) { vimGlobalState.jumpList.add(cm, oldCur, newCur); @@ -3836,7 +3881,7 @@ return Pos(curr_index.ln, curr_index.pos); } - // TODO: perhaps this finagling of start and end positions belonds + // TODO: perhaps this finagling of start and end positions belongs // in codemirror/replaceRange? function selectCompanionObject(cm, head, symb, inclusive) { var cur = head, start, end; diff --git a/vendor/codemirror/lib/codemirror.js b/vendor/codemirror/lib/codemirror.js index 06f0f868..7cee4096 100644 --- a/vendor/codemirror/lib/codemirror.js +++ b/vendor/codemirror/lib/codemirror.js @@ -6641,7 +6641,7 @@ 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 145: "ScrollLock", 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", - 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", + 221: "]", 222: "'", 224: "Mod", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert" }; @@ -6776,7 +6776,7 @@ var base = name; if (event.altKey && base != "Alt") { name = "Alt-" + name; } if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") { name = "Ctrl-" + name; } - if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") { name = "Cmd-" + name; } + if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Mod") { name = "Cmd-" + name; } if (!noShift && event.shiftKey && base != "Shift") { name = "Shift-" + name; } return name } @@ -9771,7 +9771,7 @@ addLegacyProps(CodeMirror); - CodeMirror.version = "5.56.0"; + CodeMirror.version = "5.57.0"; return CodeMirror; diff --git a/vendor/codemirror/mode/css/css.js b/vendor/codemirror/mode/css/css.js index 441ba4ab..77ca0c10 100644 --- a/vendor/codemirror/mode/css/css.js +++ b/vendor/codemirror/mode/css/css.js @@ -442,17 +442,18 @@ CodeMirror.defineMode("css", function(config, parserConfig) { "monochrome", "min-monochrome", "max-monochrome", "resolution", "min-resolution", "max-resolution", "scan", "grid", "orientation", "device-pixel-ratio", "min-device-pixel-ratio", "max-device-pixel-ratio", - "pointer", "any-pointer", "hover", "any-hover" + "pointer", "any-pointer", "hover", "any-hover", "prefers-color-scheme" ], mediaFeatures = keySet(mediaFeatures_); var mediaValueKeywords_ = [ "landscape", "portrait", "none", "coarse", "fine", "on-demand", "hover", - "interlace", "progressive" + "interlace", "progressive", + "dark", "light" ], mediaValueKeywords = keySet(mediaValueKeywords_); var propertyKeywords_ = [ "align-content", "align-items", "align-self", "alignment-adjust", - "alignment-baseline", "anchor-point", "animation", "animation-delay", + "alignment-baseline", "all", "anchor-point", "animation", "animation-delay", "animation-direction", "animation-duration", "animation-fill-mode", "animation-iteration-count", "animation-name", "animation-play-state", "animation-timing-function", "appearance", "azimuth", "backdrop-filter", @@ -503,7 +504,9 @@ CodeMirror.defineMode("css", function(config, parserConfig) { "list-style-image", "list-style-position", "list-style-type", "margin", "margin-bottom", "margin-left", "margin-right", "margin-top", "marks", "marquee-direction", "marquee-loop", "marquee-play-count", "marquee-speed", - "marquee-style", "max-block-size", "max-height", "max-inline-size", + "marquee-style", "mask-clip", "mask-composite", "mask-image", "mask-mode", + "mask-origin", "mask-position", "mask-repeat", "mask-size","mask-type", + "max-block-size", "max-height", "max-inline-size", "max-width", "min-block-size", "min-height", "min-inline-size", "min-width", "mix-blend-mode", "move-to", "nav-down", "nav-index", "nav-left", "nav-right", "nav-up", "object-fit", "object-position", "offset", "offset-anchor", @@ -540,7 +543,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) { "text-height", "text-indent", "text-justify", "text-orientation", "text-outline", "text-overflow", "text-rendering", "text-shadow", "text-size-adjust", "text-space-collapse", "text-transform", - "text-underline-position", "text-wrap", "top", "transform", "transform-origin", + "text-underline-position", "text-wrap", "top", "touch-action", "transform", "transform-origin", "transform-style", "transition", "transition-delay", "transition-duration", "transition-property", "transition-timing-function", "translate", "unicode-bidi", "user-select", "vertical-align", "visibility", "voice-balance", @@ -552,11 +555,11 @@ CodeMirror.defineMode("css", function(config, parserConfig) { "flood-opacity", "lighting-color", "stop-color", "stop-opacity", "pointer-events", "color-interpolation", "color-interpolation-filters", "color-rendering", "fill", "fill-opacity", "fill-rule", "image-rendering", - "marker", "marker-end", "marker-mid", "marker-start", "shape-rendering", "stroke", + "marker", "marker-end", "marker-mid", "marker-start", "paint-order", "shape-rendering", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering", "baseline-shift", "dominant-baseline", "glyph-orientation-horizontal", - "glyph-orientation-vertical", "text-anchor", "writing-mode" + "glyph-orientation-vertical", "text-anchor", "writing-mode", ], propertyKeywords = keySet(propertyKeywords_); var nonStandardPropertyKeywords_ = [ @@ -623,7 +626,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) { "after-white-space", "ahead", "alias", "all", "all-scroll", "alphabetic", "alternate", "always", "amharic", "amharic-abegede", "antialiased", "appworkspace", "arabic-indic", "armenian", "asterisks", "attr", "auto", "auto-flow", "avoid", "avoid-column", "avoid-page", - "avoid-region", "background", "backwards", "baseline", "below", "bidi-override", "binary", + "avoid-region", "axis-pan", "background", "backwards", "baseline", "below", "bidi-override", "binary", "bengali", "blink", "block", "block-axis", "bold", "bolder", "border", "border-box", "both", "bottom", "break", "break-all", "break-word", "bullets", "button", "button-bevel", "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "calc", "cambodian", @@ -647,7 +650,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) { "ethiopic-halehame-sid-et", "ethiopic-halehame-so-et", "ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", "ethiopic-halehame-tig", "ethiopic-numeric", "ew-resize", "exclusion", "expanded", "extends", "extra-condensed", - "extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "flex", "flex-end", "flex-start", "footnotes", + "extra-expanded", "fantasy", "fast", "fill", "fill-box", "fixed", "flat", "flex", "flex-end", "flex-start", "footnotes", "forwards", "from", "geometricPrecision", "georgian", "graytext", "grid", "groove", "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hard-light", "hebrew", "help", "hidden", "hide", "higher", "highlight", "highlighttext", @@ -662,7 +665,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) { "line-through", "linear", "linear-gradient", "lines", "list-item", "listbox", "listitem", "local", "logical", "loud", "lower", "lower-alpha", "lower-armenian", "lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian", - "lower-roman", "lowercase", "ltr", "luminosity", "malayalam", "match", "matrix", "matrix3d", + "lower-roman", "lowercase", "ltr", "luminosity", "malayalam", "manipulation", "match", "matrix", "matrix3d", "media-controls-background", "media-current-time-display", "media-fullscreen-button", "media-mute-button", "media-play-button", "media-return-to-realtime-button", "media-rewind-button", @@ -671,13 +674,13 @@ CodeMirror.defineMode("css", function(config, parserConfig) { "media-volume-slider-container", "media-volume-sliderthumb", "medium", "menu", "menulist", "menulist-button", "menulist-text", "menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic", - "mix", "mongolian", "monospace", "move", "multiple", "multiply", "myanmar", "n-resize", + "mix", "mongolian", "monospace", "move", "multiple", "multiple_mask_images", "multiply", "myanmar", "n-resize", "narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop", "no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap", "ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "opacity", "open-quote", "optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset", "outside", "outside-shape", "overlay", "overline", "padding", "padding-box", - "painted", "page", "paused", "persian", "perspective", "plus-darker", "plus-lighter", + "painted", "page", "paused", "persian", "perspective", "pinch-zoom", "plus-darker", "plus-lighter", "pointer", "polygon", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d", "progress", "push-button", "radial-gradient", "radio", "read-only", "read-write", "read-write-plaintext-only", "rectangle", "region", @@ -695,8 +698,8 @@ CodeMirror.defineMode("css", function(config, parserConfig) { "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow", "small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali", "source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "space-evenly", "spell-out", "square", - "square-button", "start", "static", "status-bar", "stretch", "stroke", "sub", - "subpixel-antialiased", "super", "sw-resize", "symbolic", "symbols", "system-ui", "table", + "square-button", "start", "static", "status-bar", "stretch", "stroke", "stroke-box", "sub", + "subpixel-antialiased", "svg_masks", "super", "sw-resize", "symbolic", "symbols", "system-ui", "table", "table-caption", "table-cell", "table-column", "table-column-group", "table-footer-group", "table-header-group", "table-row", "table-row-group", "tamil", @@ -706,10 +709,10 @@ CodeMirror.defineMode("css", function(config, parserConfig) { "tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top", "trad-chinese-formal", "trad-chinese-informal", "transform", "translate", "translate3d", "translateX", "translateY", "translateZ", - "transparent", "ultra-condensed", "ultra-expanded", "underline", "unset", "up", + "transparent", "ultra-condensed", "ultra-expanded", "underline", "unidirectional-pan", "unset", "up", "upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal", "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url", - "var", "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted", + "var", "vertical", "vertical-text", "view-box", "visible", "visibleFill", "visiblePainted", "visibleStroke", "visual", "w-resize", "wait", "wave", "wider", "window", "windowframe", "windowtext", "words", "wrap", "wrap-reverse", "x-large", "x-small", "xor", "xx-large", "xx-small" From 01cfb435f67a73e4c637776bfc266397ed9c81a9 Mon Sep 17 00:00:00 2001 From: narcolepticinsomniac Date: Mon, 24 Aug 2020 12:27:23 -0400 Subject: [PATCH 06/55] Remove deprecated dropbox and add sync button (#1025) * Remove deprecated dropbox and add sync button * re-use existing message --- manage.html | 27 ++---------------- manage/import-export.js | 10 ++----- manage/manage.css | 62 ++--------------------------------------- manage/manage.js | 5 ++-- 4 files changed, 9 insertions(+), 95 deletions(-) diff --git a/manage.html b/manage.html index d168af14..dad506b6 100644 --- a/manage.html +++ b/manage.html @@ -368,30 +368,9 @@

- - - - + + +
diff --git a/manage/import-export.js b/manage/import-export.js index 0c1c3bd8..7d08fc89 100644 --- a/manage/import-export.js +++ b/manage/import-export.js @@ -11,14 +11,8 @@ let bulkChangeQueue = []; let bulkChangeTime = 0; onDOMready().then(() => { - $('#file-all-styles').onclick = event => { - event.preventDefault(); - exportToFile(); - }; - $('#unfile-all-styles').onclick = event => { - event.preventDefault(); - importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT}); - }; + $('#file-all-styles').onclick = () => exportToFile(); + $('#unfile-all-styles').onclick = () => importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT}); Object.assign(document.body, { ondragover(event) { diff --git a/manage/manage.css b/manage/manage.css index 74375b25..31f2b325 100644 --- a/manage/manage.css +++ b/manage/manage.css @@ -291,7 +291,8 @@ a:hover { padding-top: .1rem; } -#options-buttons button, +#options-buttons > a, +#options-buttons > button, #backup-buttons button { margin: 0 .2rem .5rem 0; } @@ -1046,54 +1047,6 @@ input[id^="manage.newUI"] { text-overflow: ellipsis; } -/* export/import buttons */ -#backup-buttons .dropbtn { - padding: 3px 7px; - cursor: pointer; - text-overflow: inherit; -} - -#backup-buttons .dropbtn span { - display: inline-block; - margin-right: 12px; -} - -#backup-buttons .dropdown { - position: relative; - display: inline-block; -} - -#backup-buttons .dropdown-content { - display: none; - position: absolute; - background-color: #f9f9f9; - min-width: 160px; - box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); - z-index: 1; -} - -#backup-buttons .dropdown-content a { - color: black; - padding: 8px; - text-decoration: none; - display: block; -} - -#backup-buttons .dropdown-content a:hover { - /* background-color: #f2f2f2 */ - background-color: #e9e9e9 -} - -#backup-buttons .dropdown:hover .dropdown-content { - display: block; -} - -#backup-buttons .dropdown:hover .dropbtn { - background-color: hsl(0, 0%, 95%); - border-color: hsl(0, 0%, 52%); - /* background-color: #3e8e41; */ -} - /* sort font */ @font-face { font-family: 'sorticon'; @@ -1302,14 +1255,3 @@ input[id^="manage.newUI"] { margin-left: -2px; } } - -/* Deprecated dropbox backup (dropbox-sync) */ -#sync-dropbox-export, -#sync-dropbox-import { - opacity: 0.5; - cursor: not-allowed; -} -#backup-buttons .dropdown-content #sync-dropbox-export:hover, -#backup-buttons .dropdown-content #sync-dropbox-import:hover { - background: transparent; -} diff --git a/manage/manage.js b/manage/manage.js index 26c26c01..d7478b63 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -86,9 +86,8 @@ function onRuntimeMessage(msg) { function initGlobalEvents() { installed = $('#installed'); installed.onclick = handleEvent.entryClicked; - $('#manage-options-button').onclick = () => { - router.updateHash('#stylus-options'); - }; + $('#manage-options-button').onclick = () => router.updateHash('#stylus-options'); + $('#sync-styles').onclick = () => router.updateHash('#stylus-options'); { const btn = $('#manage-shortcuts-button'); btn.onclick = btn.onclick || (() => openURL({url: URLS.configureCommands})); From 07ba44cc2c369770b9e675d0b9ceaa46e809d8d8 Mon Sep 17 00:00:00 2001 From: eight Date: Mon, 31 Aug 2020 16:38:18 +0800 Subject: [PATCH 07/55] Change: switch to launchWebAuthFlow polyfill (#1017) * WIP: add webextLaunchWebAuthFlow * Change: switch to webextLaunchWebAuthFlow * Bump dependencies * Fix: use minimized version * Fix: wrong call to promisifyChrome --- background/token-manager.js | 12 +++++----- manifest.json | 1 + package-lock.json | 5 +++++ package.json | 3 ++- tools/build-vendor.js | 3 +++ vendor/webext-launch-web-auth-flow/LICENSE | 22 +++++++++++++++++++ vendor/webext-launch-web-auth-flow/README.md | 5 +++++ .../webext-launch-web-auth-flow.min.js | 2 ++ 8 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 vendor/webext-launch-web-auth-flow/LICENSE create mode 100644 vendor/webext-launch-web-auth-flow/README.md create mode 100644 vendor/webext-launch-web-auth-flow/webext-launch-web-auth-flow.min.js diff --git a/background/token-manager.js b/background/token-manager.js index 8be21874..63f69652 100644 --- a/background/token-manager.js +++ b/background/token-manager.js @@ -1,10 +1,11 @@ -/* global chromeLocal promisifyChrome FIREFOX */ +/* global chromeLocal promisifyChrome webextLaunchWebAuthFlow FIREFOX */ /* exported tokenManager */ 'use strict'; const tokenManager = (() => { promisifyChrome({ - identity: ['launchWebAuthFlow'], + 'windows': ['create', 'update', 'remove'], + 'tabs': ['create', 'update', 'remove'] }); const AUTH = { dropbox: { @@ -160,9 +161,10 @@ const tokenManager = (() => { Object.assign(query, provider.authQuery); } const url = `${provider.authURL}?${stringifyQuery(query)}`; - return browser.identity.launchWebAuthFlow({ + return webextLaunchWebAuthFlow({ url, - interactive + interactive, + redirect_uri: query.redirect_uri }) .then(url => { const params = new URLSearchParams( @@ -185,7 +187,7 @@ const tokenManager = (() => { code, grant_type: 'authorization_code', client_id: provider.clientId, - redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL() + redirect_uri: query.redirect_uri }; if (provider.clientSecret) { body.client_secret = provider.clientSecret; diff --git a/manifest.json b/manifest.json index 89a62be7..9d59db44 100644 --- a/manifest.json +++ b/manifest.json @@ -38,6 +38,7 @@ "vendor/semver-bundle/semver.js", "vendor/db-to-cloud/db-to-cloud.min.js", "vendor/uuid/uuid.min.js", + "vendor/webext-launch-web-auth-flow/webext-launch-web-auth-flow.min.js", "background/token-manager.js", "background/sync.js", "background/content-scripts.js", diff --git a/package-lock.json b/package-lock.json index 4872bfa8..8ecf3451 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7934,6 +7934,11 @@ } } }, + "webext-launch-web-auth-flow": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/webext-launch-web-auth-flow/-/webext-launch-web-auth-flow-0.1.0.tgz", + "integrity": "sha512-3W8ANT9/6uL6NX5SiaKQee439dfiS1NT8wSc+vmjly/2MmH7FBqGFBXLfBFw296w8OOqHNPnEdNcBkDGJQkDgg==" + }, "webext-tx-fix": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/webext-tx-fix/-/webext-tx-fix-0.3.3.tgz", diff --git a/package.json b/package.json index 49d423d4..09ef363a 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "stylelint-bundle": "^8.0.0", "stylus-lang-bundle": "^0.54.5", "usercss-meta": "^0.9.0", - "uuid": "^8.1.0" + "uuid": "^8.1.0", + "webext-launch-web-auth-flow": "^0.1.0" }, "devDependencies": { "archiver": "^4.0.1", diff --git a/tools/build-vendor.js b/tools/build-vendor.js index 1e3d5c24..639a74ab 100644 --- a/tools/build-vendor.js +++ b/tools/build-vendor.js @@ -62,6 +62,9 @@ const files = { ], 'uuid': [ 'dist/umd/uuidv4.min.js → uuid.min.js' + ], + 'webext-launch-web-auth-flow': [ + 'dist/webext-launch-web-auth-flow.min.js → webext-launch-web-auth-flow.min.js' ] }; diff --git a/vendor/webext-launch-web-auth-flow/LICENSE b/vendor/webext-launch-web-auth-flow/LICENSE new file mode 100644 index 00000000..792e1731 --- /dev/null +++ b/vendor/webext-launch-web-auth-flow/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2020 eight + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/webext-launch-web-auth-flow/README.md b/vendor/webext-launch-web-auth-flow/README.md new file mode 100644 index 00000000..821ceade --- /dev/null +++ b/vendor/webext-launch-web-auth-flow/README.md @@ -0,0 +1,5 @@ +## webext-launch-web-auth-flow v0.1.0 + +Following files are copied from npm (node_modules): + +* webext-launch-web-auth-flow.min.js: dist\webext-launch-web-auth-flow.min.js diff --git a/vendor/webext-launch-web-auth-flow/webext-launch-web-auth-flow.min.js b/vendor/webext-launch-web-auth-flow/webext-launch-web-auth-flow.min.js new file mode 100644 index 00000000..7e8f7587 --- /dev/null +++ b/vendor/webext-launch-web-auth-flow/webext-launch-web-auth-flow.min.js @@ -0,0 +1,2 @@ +var webextLaunchWebAuthFlow=function(){"use strict";function e(e,r,t,n,o,i,s){try{var u=e[i](s),a=u.value}catch(e){return void t(e)}u.done?r(a):Promise.resolve(a).then(n,o)}function r(r){return function(){var t=this,n=arguments;return new Promise((function(o,i){var s=r.apply(t,n);function u(r){e(s,o,i,u,a,"next",r)}function a(r){e(s,o,i,u,a,"throw",r)}u(void 0)}))}}function t(e){return n.apply(this,arguments)}function n(){return(n=r((function*(e){if(browser.windows)return yield browser.windows.create(e);const r={active:"minimized"!==e.state,url:e.url};return{tabs:[yield browser.tabs.create(r)]}}))).apply(this,arguments)}function o(e,r,t){return i.apply(this,arguments)}function i(){return(i=r((function*(e,r,t){return e?yield browser.windows.update(e,t):yield browser.tabs.update(r,{active:t.focused})}))).apply(this,arguments)}function s(e,r){return u.apply(this,arguments)}function u(){return(u=r((function*(e,r){return e?yield browser.windows.remove(e):yield browser.tabs.remove(r)}))).apply(this,arguments)}function a(){const e={};return e.promise=new Promise((r,t)=>{e.resolve=r,e.reject=t}),e}function c(){return(c=r((function*({url:e,redirect_uri:r,interactive:n=!1}){const i=yield t({type:"popup",url:e,state:"minimized"}),u=i.id,c=i.tabs[0].id,d=a(),l=d.promise,w=d.resolve,b=d.reject;browser.webRequest.onBeforeRequest.addListener(f,{urls:["*://*/*"],tabId:c,types:["main_frame"]},["blocking"]),browser.webNavigation.onDOMContentLoaded.addListener(p),browser.tabs.onRemoved.addListener(v);try{return yield l}finally{browser.webRequest.onBeforeRequest.removeListener(f),browser.webNavigation.onDOMContentLoaded.removeListener(p),browser.tabs.onRemoved.removeListener(v),s(u,c).catch(e=>console.error(e))}function f(e){if(!e.frameId&&e.tabId===c&&e.url.startsWith(r))return w(e.url),{cancel:!0}}function p(e){e.frameId||e.tabId!==c||(n?o(u,c,{focused:!0,state:"normal"}).catch(e=>console.error(e)):b(new Error("User interaction required")),browser.webNavigation.onDOMContentLoaded.removeListener(p))}function v(e){e===c&&b(new Error("Canceled by user"))}}))).apply(this,arguments)}return function(e){return c.apply(this,arguments)}}(); +//# sourceMappingURL=webext-launch-web-auth-flow.min.js.map From aa4350747822c184acaaa2ba7de73707f6846712 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 27 Aug 2020 22:39:49 +0300 Subject: [PATCH 08/55] parserlib: consume unknown @-rules per CSS grammar --- vendor-overwrites/csslint/parserlib.js | 141 ++++++++++++++++++------- 1 file changed, 100 insertions(+), 41 deletions(-) diff --git a/vendor-overwrites/csslint/parserlib.js b/vendor-overwrites/csslint/parserlib.js index 480aef97..7a92912f 100644 --- a/vendor-overwrites/csslint/parserlib.js +++ b/vendor-overwrites/csslint/parserlib.js @@ -2722,6 +2722,10 @@ self.parserlib = (() => { constructor(input) { this._reader = new StringReader(input ? input.toString() : ''); + this.resetLT(); + } + + resetLT() { // Token object for the last consumed token. this._token = null; // Lookahead token buffer. @@ -3148,8 +3152,7 @@ self.parserlib = (() => { */ atRuleToken(first, pos) { this._reader.mark(); - const ident = this.readName(); - let rule = first + ident; + let rule = first + this.readName(); let tt = Tokens.type(lower(rule)); // if it's not valid, use the first character only and reset the reader if (tt === Tokens.CHAR || tt === Tokens.UNKNOWN) { @@ -5296,19 +5299,62 @@ self.parserlib = (() => { throw new SyntaxError('Unknown @ rule.', lt0); } - this.fire({ - type: 'error', - error: null, - message: 'Unknown @ rule: ' + lt0.value + '.', - }, lt0); + this._ws(); + const simpleValue = + stream.match([Tokens.IDENT, Tokens.CUSTOM_PROP]) && SyntaxUnit.fromToken(stream._token) || + stream.peek() === Tokens.FUNCTION && this._function({asText: true}) || + this._unknownBlock([Tokens.LBRACKET, Tokens.LPAREN]); - // skip {} block - let count = 0; - do { - const brace = stream.advance([Tokens.LBRACE, Tokens.RBRACE]); - count += brace === Tokens.LBRACE ? 1 : -1; - } while (count > 0 && !stream._reader.eof()); - if (count < 0) stream.unget(); + this._ws(); + const blockValue = this._unknownBlock(); + if (!blockValue) { + stream.match(Tokens.SEMICOLON); + } + + this.fire({ + type: 'unknown-at-rule', + name: lt0.value, + simpleValue, + blockValue, + }, lt0); + this._ws(); + } + + _unknownBlock(canStartWith = [Tokens.LBRACE]) { + const stream = this._tokenStream; + if (!canStartWith.includes(stream.peek())) { + return null; + } + stream.get(); + const start = stream._token; + const reader = stream._reader; + reader.mark(); + reader._cursor = start.offset; + reader._line = start.startLine; + reader._col = start.startCol; + const value = []; + const endings = []; + let blockEnd; + while (!reader.eof()) { + const chunk = reader.readMatch(/[^{}()[\]]*[{}()[\]]?/y); + const c = chunk.slice(-1); + value.push(chunk); + if (c === '{' || c === '(' || c === '[') { + endings.push(blockEnd); + blockEnd = c === '{' ? '}' : c === '(' ? ')' : ']'; + } else if (c === '}' || c === ')' || c === ']') { + if (c !== blockEnd) { + break; + } + blockEnd = endings.pop(); + if (!blockEnd) { + stream.resetLT(); + return new SyntaxUnit(value.join(''), start); + } + } + } + reader.reset(); + return null; } _unexpectedToken(token) { @@ -5406,18 +5452,28 @@ self.parserlib = (() => { Object.assign(Parser.prototype, TYPES); Parser.prototype._readWhitespace = Parser.prototype._ws; + const symDocument = [Tokens.DOCUMENT_SYM, Parser.prototype._document]; + const symDocMisplaced = [Tokens.DOCUMENT_SYM, Parser.prototype._documentMisplaced]; + const symFontFace = [Tokens.FONT_FACE_SYM, Parser.prototype._fontFace]; + const symKeyframes = [Tokens.KEYFRAMES_SYM, Parser.prototype._keyframes]; + const symMedia = [Tokens.MEDIA_SYM, Parser.prototype._media]; + const symPage = [Tokens.PAGE_SYM, Parser.prototype._page]; + const symSupports = [Tokens.SUPPORTS_SYM, Parser.prototype._supports]; + const symUnknown = [Tokens.UNKNOWN_SYM, Parser.prototype._unknownSym]; + const symViewport = [Tokens.VIEWPORT_SYM, Parser.prototype._viewport]; + Parser.ACTIONS = { stylesheet: new Map([ - [Tokens.MEDIA_SYM, Parser.prototype._media], - [Tokens.DOCUMENT_SYM, Parser.prototype._document], - [Tokens.SUPPORTS_SYM, Parser.prototype._supports], - [Tokens.PAGE_SYM, Parser.prototype._page], - [Tokens.FONT_FACE_SYM, Parser.prototype._fontFace], - [Tokens.KEYFRAMES_SYM, Parser.prototype._keyframes], - [Tokens.VIEWPORT_SYM, Parser.prototype._viewport], + symMedia, + symDocument, + symSupports, + symPage, + symFontFace, + symKeyframes, + symViewport, + symUnknown, [Tokens.S, Parser.prototype._ws], - [Tokens.UNKNOWN_SYM, Parser.prototype._unknownSym], ]), stylesheetMisplaced: new Map([ @@ -5427,31 +5483,34 @@ self.parserlib = (() => { ]), document: new Map([ - [Tokens.MEDIA_SYM, Parser.prototype._media], - [Tokens.DOCUMENT_SYM, Parser.prototype._documentMisplaced], - [Tokens.SUPPORTS_SYM, Parser.prototype._supports], - [Tokens.PAGE_SYM, Parser.prototype._page], - [Tokens.FONT_FACE_SYM, Parser.prototype._fontFace], - [Tokens.VIEWPORT_SYM, Parser.prototype._viewport], - [Tokens.KEYFRAMES_SYM, Parser.prototype._keyframes], + symMedia, + symDocMisplaced, + symSupports, + symPage, + symFontFace, + symViewport, + symKeyframes, + symUnknown, ]), supports: new Map([ - [Tokens.KEYFRAMES_SYM, Parser.prototype._keyframes], - [Tokens.MEDIA_SYM, Parser.prototype._media], - [Tokens.SUPPORTS_SYM, Parser.prototype._supports], - [Tokens.DOCUMENT_SYM, Parser.prototype._documentMisplaced], - [Tokens.VIEWPORT_SYM, Parser.prototype._viewport], + symKeyframes, + symMedia, + symSupports, + symDocMisplaced, + symViewport, + symUnknown, ]), media: new Map([ - [Tokens.KEYFRAMES_SYM, Parser.prototype._keyframes], - [Tokens.MEDIA_SYM, Parser.prototype._media], - [Tokens.DOCUMENT_SYM, Parser.prototype._documentMisplaced], - [Tokens.SUPPORTS_SYM, Parser.prototype._supports], - [Tokens.PAGE_SYM, Parser.prototype._page], - [Tokens.FONT_FACE_SYM, Parser.prototype._fontFace], - [Tokens.VIEWPORT_SYM, Parser.prototype._viewport], + symKeyframes, + symMedia, + symDocMisplaced, + symSupports, + symPage, + symFontFace, + symViewport, + symUnknown, ]), simpleSelectorSequence: new Map([ From fa1496ecb8054f454f3aaecb16d6f2c0eb46881e Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 22 Sep 2020 13:54:48 +0300 Subject: [PATCH 09/55] use tab.pendingUrl (#1040) --- background/background.js | 2 +- background/content-scripts.js | 4 ++-- background/navigator-util.js | 5 +++-- edit/regexp-tester.js | 3 +-- js/messaging.js | 7 +++---- js/msg.js | 7 ++++--- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/background/background.js b/background/background.js index 77034880..6851006e 100644 --- a/background/background.js +++ b/background/background.js @@ -328,7 +328,7 @@ function openManage({options = false, search} = {}) { if (tab) { return Promise.all([ activateTab(tab), - tab.url !== url && msg.sendTab(tab.id, {method: 'pushState', url}) + (tab.pendingUrl || tab.url) !== url && msg.sendTab(tab.id, {method: 'pushState', url}) .catch(console.error) ]); } diff --git a/background/content-scripts.js b/background/content-scripts.js index dfd744f2..d3c93b40 100644 --- a/background/content-scripts.js +++ b/background/content-scripts.js @@ -58,13 +58,13 @@ const contentScripts = (() => { return browser.tabs.query({}).then(tabs => { for (const tab of tabs) { // skip unloaded/discarded/chrome tabs - if (!tab.width || tab.discarded || !URLS.supported(tab.url)) continue; + if (!tab.width || tab.discarded || !URLS.supported(tab.pendingUrl || tab.url)) continue; // our content scripts may still be pending injection at browser start so it's too early to ping them if (tab.status === 'loading') { trackBusyTab(tab.id, true); } else { injectToTab({ - url: tab.url, + url: tab.pendingUrl || tab.url, tabId: tab.id }); } diff --git a/background/navigator-util.js b/background/navigator-util.js index 67fdc1e7..c1b702c6 100644 --- a/background/navigator-util.js +++ b/background/navigator-util.js @@ -49,8 +49,9 @@ const navigatorUtil = (() => { } return browser.tabs.get(data.tabId) .then(tab => { - if (tab.url === 'chrome://newtab/') { - data.url = tab.url; + const url = tab.pendingUrl || tab.url; + if (url === 'chrome://newtab/') { + data.url = url; } }); } diff --git a/edit/regexp-tester.js b/edit/regexp-tester.js index 8f001102..5b7bf13e 100644 --- a/edit/regexp-tester.js +++ b/edit/regexp-tester.js @@ -67,8 +67,7 @@ const regExpTester = (() => { }); const getMatchInfo = m => m && {text: m[0], pos: m.index}; browser.tabs.query({}).then(tabs => { - const supported = tabs.map(tab => tab.url) - .filter(url => URLS.supported(url)); + const supported = tabs.map(tab => tab.pendingUrl || tab.url).filter(URLS.supported); const unique = [...new Set(supported).values()]; for (const rxData of regexps) { const {rx, urls} = rxData; diff --git a/js/messaging.js b/js/messaging.js index 642b20dc..e7f0e9da 100644 --- a/js/messaging.js +++ b/js/messaging.js @@ -132,7 +132,7 @@ function findExistingTab({url, currentWindow, ignoreHash = true, ignoreSearch = .then(tabs => tabs.find(matchTab)); function matchTab(tab) { - const tabUrl = new URL(tab.url); + const tabUrl = new URL(tab.pendingUrl || tab.url); return tabUrl.protocol === url.protocol && tabUrl.username === url.username && tabUrl.password === url.password && @@ -175,7 +175,7 @@ function openURL({ index, openerTabId, // when hash is different we can only set `url` if it has # otherwise the tab would reload - url: url !== tab.url && url.includes('#') ? url : undefined, + url: url !== (tab.pendingUrl || tab.url) && url.includes('#') ? url : undefined, }); } if (newWindow && browser.windows) { @@ -200,10 +200,9 @@ function openURL({ // except when new URL is chrome:// or chrome-extension:// and the empty tab is // in incognito function isTabReplaceable(tab, newUrl) { - if (!tab || !URLS.emptyTab.includes(tab.url)) { + if (!tab || !URLS.emptyTab.includes(tab.pendingUrl || tab.url)) { return false; } - // FIXME: but why? if (tab.incognito && newUrl.startsWith('chrome')) { return false; } diff --git a/js/msg.js b/js/msg.js index d52438e4..26dba45d 100644 --- a/js/msg.js +++ b/js/msg.js @@ -106,12 +106,13 @@ self.msg = self.INJECTED === 1 ? self.msg : (() => { .then(tabs => { const requests = []; for (const tab of tabs) { - const isExtension = tab.url.startsWith(EXTENSION_URL); + const tabUrl = tab.pendingUrl || tab.url; + const isExtension = tabUrl.startsWith(EXTENSION_URL); if ( tab.discarded || // FIXME: use `URLS.supported`? - !/^(http|ftp|file)/.test(tab.url) && - !tab.url.startsWith('chrome://newtab/') && + !/^(http|ftp|file)/.test(tabUrl) && + !tabUrl.startsWith('chrome://newtab/') && !isExtension || isExtension && ignoreExtension || filter && !filter(tab) From 30983db679f82d000d293cad5dbe37be7149d7f3 Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 22 Sep 2020 13:56:53 +0300 Subject: [PATCH 10/55] Scroll Anchoring L1 (ED 2020-09-18) (#1038) --- edit/codemirror-default.js | 1 + vendor-overwrites/csslint/parserlib.js | 1 + 2 files changed, 2 insertions(+) diff --git a/edit/codemirror-default.js b/edit/codemirror-default.js index 8576d9e0..94b156e7 100644 --- a/edit/codemirror-default.js +++ b/edit/codemirror-default.js @@ -105,6 +105,7 @@ } Object.assign(CodeMirror.mimeModes['text/css'].propertyKeywords, { + 'overflow-anchor': true, 'overscroll-behavior': true, }); Object.assign(CodeMirror.mimeModes['text/css'].colorKeywords, { diff --git a/vendor-overwrites/csslint/parserlib.js b/vendor-overwrites/csslint/parserlib.js index 7a92912f..263fb54d 100644 --- a/vendor-overwrites/csslint/parserlib.js +++ b/vendor-overwrites/csslint/parserlib.js @@ -508,6 +508,7 @@ self.parserlib = (() => { 'outline-style': ' | auto', 'outline-width': '', 'overflow': '{1,2}', + 'overflow-anchor': 'auto | none', 'overflow-block': '', 'overflow-inline': '', 'overflow-style': 1, From 2b149f97a54cae68856268d58567f7767b4e83bb Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 22 Sep 2020 14:03:31 +0300 Subject: [PATCH 11/55] CodeMirror 5.58.0 (#1037) --- package-lock.json | 6 +- package.json | 2 +- vendor/codemirror/README.md | 2 +- vendor/codemirror/addon/lint/lint.css | 24 ++- vendor/codemirror/addon/lint/lint.js | 8 +- vendor/codemirror/keymap/vim.js | 155 ++++++++++++++++-- vendor/codemirror/lib/codemirror.css | 1 + vendor/codemirror/lib/codemirror.js | 42 +++-- .../codemirror/mode/javascript/javascript.js | 2 +- 9 files changed, 188 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8ecf3451..86c23d51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1917,9 +1917,9 @@ "dev": true }, "codemirror": { - "version": "5.57.0", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.57.0.tgz", - "integrity": "sha512-WGc6UL7Hqt+8a6ZAsj/f1ApQl3NPvHY/UQSzG6fB6l4BjExgVdhFaxd7mRTw1UCiYe/6q86zHP+kfvBQcZGvUg==" + "version": "5.58.0", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.58.0.tgz", + "integrity": "sha512-OUK+7EgaYnLyC0F09UWjckLWvviy02IDDGTW5Zmj60a3gdGnFtUM6rVsqrfl5+YSylQVQBNfAGG4KF7tQOb4/Q==" }, "collection-visit": { "version": "1.0.0", diff --git a/package.json b/package.json index 09ef363a..1e00242e 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "repository": "openstyles/stylus", "author": "Stylus Team", "dependencies": { - "codemirror": "^5.57.0", + "codemirror": "^5.58.0", "db-to-cloud": "^0.4.5", "jsonlint": "^1.6.3", "less-bundle": "github:openstyles/less-bundle#v0.1.0", diff --git a/vendor/codemirror/README.md b/vendor/codemirror/README.md index f838d71e..9b880841 100644 --- a/vendor/codemirror/README.md +++ b/vendor/codemirror/README.md @@ -1,4 +1,4 @@ -## codemirror v5.57.0 +## codemirror v5.58.0 Following files are copied from npm (node_modules): diff --git a/vendor/codemirror/addon/lint/lint.css b/vendor/codemirror/addon/lint/lint.css index f097cfe3..08718659 100644 --- a/vendor/codemirror/addon/lint/lint.css +++ b/vendor/codemirror/addon/lint/lint.css @@ -25,22 +25,20 @@ -ms-transition: opacity .4s; } -.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning { +.CodeMirror-lint-mark { background-position: left bottom; background-repeat: repeat-x; } -.CodeMirror-lint-mark-error { - background-image: - url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==") - ; -} - .CodeMirror-lint-mark-warning { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); } -.CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning { +.CodeMirror-lint-mark-error { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg=="); +} + +.CodeMirror-lint-marker { background-position: center center; background-repeat: no-repeat; cursor: pointer; @@ -51,20 +49,20 @@ position: relative; } -.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { +.CodeMirror-lint-message { padding-left: 18px; background-position: top left; background-repeat: no-repeat; } -.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); -} - .CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII="); } +.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); +} + .CodeMirror-lint-marker-multiple { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC"); background-repeat: no-repeat; diff --git a/vendor/codemirror/addon/lint/lint.js b/vendor/codemirror/addon/lint/lint.js index 5bc1af18..963f2cf2 100644 --- a/vendor/codemirror/addon/lint/lint.js +++ b/vendor/codemirror/addon/lint/lint.js @@ -83,10 +83,10 @@ function makeMarker(cm, labels, severity, multiple, tooltips) { var marker = document.createElement("div"), inner = marker; - marker.className = "CodeMirror-lint-marker-" + severity; + marker.className = "CodeMirror-lint-marker CodeMirror-lint-marker-" + severity; if (multiple) { inner = marker.appendChild(document.createElement("div")); - inner.className = "CodeMirror-lint-marker-multiple"; + inner.className = "CodeMirror-lint-marker CodeMirror-lint-marker-multiple"; } if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) { @@ -114,7 +114,7 @@ var severity = ann.severity; if (!severity) severity = "error"; var tip = document.createElement("div"); - tip.className = "CodeMirror-lint-message-" + severity; + tip.className = "CodeMirror-lint-message CodeMirror-lint-message-" + severity; if (typeof ann.messageHTML != 'undefined') { tip.innerHTML = ann.messageHTML; } else { @@ -183,7 +183,7 @@ if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann)); if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, { - className: "CodeMirror-lint-mark-" + severity, + className: "CodeMirror-lint-mark CodeMirror-lint-mark-" + severity, __annotation: ann })); } diff --git a/vendor/codemirror/keymap/vim.js b/vendor/codemirror/keymap/vim.js index 5a4860c6..789e1e55 100644 --- a/vendor/codemirror/keymap/vim.js +++ b/vendor/codemirror/keymap/vim.js @@ -8,7 +8,7 @@ * Supported Ex commands: * Refer to defaultExCommandMap below. * - * Registers: unnamed, -, a-z, A-Z, 0-9 + * Registers: unnamed, -, ., :, /, _, a-z, A-Z, 0-9 * (Does not respect the special case for number registers when delete * operator is made with these commands: %, (, ), , /, ?, n, N, {, } ) * TODO: Implement the remaining registers. @@ -141,6 +141,8 @@ { keys: 'gU', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, isEdit: true }, { keys: 'n', type: 'motion', motion: 'findNext', motionArgs: { forward: true, toJumplist: true }}, { keys: 'N', type: 'motion', motion: 'findNext', motionArgs: { forward: false, toJumplist: true }}, + { keys: 'gn', type: 'motion', motion: 'findAndSelectNextInclusive', motionArgs: { forward: true }}, + { keys: 'gN', type: 'motion', motion: 'findAndSelectNextInclusive', motionArgs: { forward: false }}, // Operator-Motion dual commands { keys: 'x', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorMotionArgs: { visualLine: false }}, { keys: 'X', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }}, @@ -416,7 +418,7 @@ var lowerCaseAlphabet = makeKeyRange(97, 26); var numbers = makeKeyRange(48, 10); var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']); - var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"', '.', ':', '/']); + var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"', '.', ':', '_', '/']); function isLine(cm, line) { return line >= cm.firstLine() && line <= cm.lastLine(); @@ -1128,6 +1130,8 @@ } RegisterController.prototype = { pushText: function(registerName, operator, text, linewise, blockwise) { + // The black hole register, "_, means delete/yank to nowhere. + if (registerName === '_') return; if (linewise && text.charAt(text.length - 1) !== '\n'){ text += '\n'; } @@ -1574,7 +1578,7 @@ motionArgs.repeat = repeat; clearInputState(cm); if (motion) { - var motionResult = motions[motion](cm, origHead, motionArgs, vim); + var motionResult = motions[motion](cm, origHead, motionArgs, vim, inputState); vim.lastMotion = motions[motion]; if (!motionResult) { return; @@ -1772,6 +1776,87 @@ highlightSearchMatches(cm, query); return findNext(cm, prev/** prev */, query, motionArgs.repeat); }, + /** + * Find and select the next occurrence of the search query. If the cursor is currently + * within a match, then find and select the current match. Otherwise, find the next occurrence in the + * appropriate direction. + * + * This differs from `findNext` in the following ways: + * + * 1. Instead of only returning the "from", this returns a "from", "to" range. + * 2. If the cursor is currently inside a search match, this selects the current match + * instead of the next match. + * 3. If there is no associated operator, this will turn on visual mode. + */ + findAndSelectNextInclusive: function(cm, _head, motionArgs, vim, prevInputState) { + var state = getSearchState(cm); + var query = state.getQuery(); + + if (!query) { + return; + } + + var prev = !motionArgs.forward; + prev = (state.isReversed()) ? !prev : prev; + + // next: [from, to] | null + var next = findNextFromAndToInclusive(cm, prev, query, motionArgs.repeat, vim); + + // No matches. + if (!next) { + return; + } + + // If there's an operator that will be executed, return the selection. + if (prevInputState.operator) { + return next; + } + + // At this point, we know that there is no accompanying operator -- let's + // deal with visual mode in order to select an appropriate match. + + var from = next[0]; + // For whatever reason, when we use the "to" as returned by searchcursor.js directly, + // the resulting selection is extended by 1 char. Let's shrink it so that only the + // match is selected. + var to = Pos(next[1].line, next[1].ch - 1); + + if (vim.visualMode) { + // If we were in visualLine or visualBlock mode, get out of it. + if (vim.visualLine || vim.visualBlock) { + vim.visualLine = false; + vim.visualBlock = false; + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: ""}); + } + + // If we're currently in visual mode, we should extend the selection to include + // the search result. + var anchor = vim.sel.anchor; + if (anchor) { + if (state.isReversed()) { + if (motionArgs.forward) { + return [anchor, from]; + } + + return [anchor, to]; + } else { + if (motionArgs.forward) { + return [anchor, to]; + } + + return [anchor, from]; + } + } + } else { + // Let's turn visual mode on. + vim.visualMode = true; + vim.visualLine = false; + vim.visualBlock = false; + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: ""}); + } + + return prev ? [to, from] : [from, to]; + }, goToMark: function(cm, _head, motionArgs, vim) { var pos = getMarkPos(cm, vim, motionArgs.selectedCharacter); if (pos) { @@ -1867,8 +1952,8 @@ // move to previous/next line is triggered. if (line < first && cur.line == first){ return this.moveToStartOfLine(cm, head, motionArgs, vim); - }else if (line > last && cur.line == last){ - return this.moveToEol(cm, head, motionArgs, vim, true); + } else if (line > last && cur.line == last){ + return moveToEol(cm, head, motionArgs, vim, true); } if (motionArgs.toFirstChar){ endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line)); @@ -1970,16 +2055,8 @@ vim.lastHSPos = cm.charCoords(head,'div').left; return moveToColumn(cm, repeat); }, - moveToEol: function(cm, head, motionArgs, vim, keepHPos) { - var cur = head; - var retval= Pos(cur.line + motionArgs.repeat - 1, Infinity); - var end=cm.clipPos(retval); - end.ch--; - if (!keepHPos) { - vim.lastHPos = Infinity; - vim.lastHSPos = cm.charCoords(end,'div').left; - } - return retval; + moveToEol: function(cm, head, motionArgs, vim) { + return moveToEol(cm, head, motionArgs, vim, false); }, moveToFirstNonWhiteSpaceCharacter: function(cm, head) { // Go to the start of the line where the text begins, or the end for @@ -3607,6 +3684,18 @@ } } + function moveToEol(cm, head, motionArgs, vim, keepHPos) { + var cur = head; + var retval= Pos(cur.line + motionArgs.repeat - 1, Infinity); + var end=cm.clipPos(retval); + end.ch--; + if (!keepHPos) { + vim.lastHPos = Infinity; + vim.lastHSPos = cm.charCoords(end,'div').left; + } + return retval; + } + function moveToCharacter(cm, repeat, forward, character) { var cur = cm.getCursor(); var start = cur.ch; @@ -4348,6 +4437,42 @@ return cursor.from(); }); } + /** + * Pretty much the same as `findNext`, except for the following differences: + * + * 1. Before starting the search, move to the previous search. This way if our cursor is + * already inside a match, we should return the current match. + * 2. Rather than only returning the cursor's from, we return the cursor's from and to as a tuple. + */ + function findNextFromAndToInclusive(cm, prev, query, repeat, vim) { + if (repeat === undefined) { repeat = 1; } + return cm.operation(function() { + var pos = cm.getCursor(); + var cursor = cm.getSearchCursor(query, pos); + + // Go back one result to ensure that if the cursor is currently a match, we keep it. + var found = cursor.find(!prev); + + // If we haven't moved, go back one more (similar to if i==0 logic in findNext). + if (!vim.visualMode && found && cursorEqual(cursor.from(), pos)) { + cursor.find(!prev); + } + + for (var i = 0; i < repeat; i++) { + found = cursor.find(prev); + if (!found) { + // SearchCursor may have returned null because it hit EOF, wrap + // around and try again. + cursor = cm.getSearchCursor(query, + (prev) ? Pos(cm.lastLine()) : Pos(cm.firstLine(), 0) ); + if (!cursor.find(prev)) { + return; + } + } + } + return [cursor.from(), cursor.to()]; + }); + } function clearSearchHighlight(cm) { var state = getSearchState(cm); cm.removeOverlay(getSearchState(cm).getOverlay()); diff --git a/vendor/codemirror/lib/codemirror.css b/vendor/codemirror/lib/codemirror.css index 56896500..a64f97c7 100644 --- a/vendor/codemirror/lib/codemirror.css +++ b/vendor/codemirror/lib/codemirror.css @@ -184,6 +184,7 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} position: absolute; z-index: 6; display: none; + outline: none; } .CodeMirror-vscrollbar { right: 0; top: 0; diff --git a/vendor/codemirror/lib/codemirror.js b/vendor/codemirror/lib/codemirror.js index 7cee4096..30f166a1 100644 --- a/vendor/codemirror/lib/codemirror.js +++ b/vendor/codemirror/lib/codemirror.js @@ -1841,7 +1841,7 @@ } } builder.trailingSpace = displayText.charCodeAt(text.length - 1) == 32; - if (style || startStyle || endStyle || mustWrap || css) { + if (style || startStyle || endStyle || mustWrap || css || attributes) { var fullStyle = style || ""; if (startStyle) { fullStyle += startStyle; } if (endStyle) { fullStyle += endStyle; } @@ -3276,8 +3276,10 @@ var on = true; display.cursorDiv.style.visibility = ""; if (cm.options.cursorBlinkRate > 0) - { display.blinker = setInterval(function () { return display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; }, - cm.options.cursorBlinkRate); } + { display.blinker = setInterval(function () { + if (!cm.hasFocus()) { onBlur(cm); } + display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; + }, cm.options.cursorBlinkRate); } else if (cm.options.cursorBlinkRate < 0) { display.cursorDiv.style.visibility = "hidden"; } } @@ -7002,7 +7004,7 @@ goGroupRight: function (cm) { return cm.moveH(1, "group"); }, goGroupLeft: function (cm) { return cm.moveH(-1, "group"); }, goWordRight: function (cm) { return cm.moveH(1, "word"); }, - delCharBefore: function (cm) { return cm.deleteH(-1, "char"); }, + delCharBefore: function (cm) { return cm.deleteH(-1, "codepoint"); }, delCharAfter: function (cm) { return cm.deleteH(1, "char"); }, delWordBefore: function (cm) { return cm.deleteH(-1, "word"); }, delWordAfter: function (cm) { return cm.deleteH(1, "word"); }, @@ -7877,7 +7879,9 @@ attachDoc(this, doc); if ((options.autofocus && !mobile) || this.hasFocus()) - { setTimeout(bind(onFocus, this), 20); } + { setTimeout(function () { + if (this$1.hasFocus() && !this$1.state.focused) { onFocus(this$1); } + }, 20); } else { onBlur(this); } @@ -8640,14 +8644,14 @@ } // Used for horizontal relative motion. Dir is -1 or 1 (left or - // right), unit can be "char", "column" (like char, but doesn't - // cross line boundaries), "word" (across next word), or "group" (to - // the start of next group of word or non-word-non-whitespace - // chars). The visually param controls whether, in right-to-left - // text, direction 1 means to move towards the next index in the - // string, or towards the character to the right of the current - // position. The resulting position will have a hitSide=true - // property if it reached the end of the document. + // right), unit can be "codepoint", "char", "column" (like char, but + // doesn't cross line boundaries), "word" (across next word), or + // "group" (to the start of next group of word or + // non-word-non-whitespace chars). The visually param controls + // whether, in right-to-left text, direction 1 means to move towards + // the next index in the string, or towards the character to the right + // of the current position. The resulting position will have a + // hitSide=true property if it reached the end of the document. function findPosH(doc, pos, dir, unit, visually) { var oldPos = pos; var origDir = dir; @@ -8661,7 +8665,12 @@ } function moveOnce(boundToLine) { var next; - if (visually) { + if (unit == "codepoint") { + var ch = lineObj.text.charCodeAt(pos.ch + (unit > 0 ? 0 : -1)); + if (isNaN(ch)) { next = null; } + else { next = new Pos(pos.line, Math.max(0, Math.min(lineObj.text.length, pos.ch + dir * (ch >= 0xD800 && ch < 0xDC00 ? 2 : 1))), + -dir); } + } else if (visually) { next = moveVisually(doc.cm, lineObj, pos, dir); } else { next = moveLogically(lineObj, pos, dir); @@ -8677,7 +8686,7 @@ return true } - if (unit == "char") { + if (unit == "char" || unit == "codepoint") { moveOnce(); } else if (unit == "column") { moveOnce(true); @@ -9621,6 +9630,7 @@ TextareaInput.prototype.readOnlyChanged = function (val) { if (!val) { this.reset(); } this.textarea.disabled = val == "nocursor"; + this.textarea.readOnly = !!val; }; TextareaInput.prototype.setUneditable = function () {}; @@ -9771,7 +9781,7 @@ addLegacyProps(CodeMirror); - CodeMirror.version = "5.57.0"; + CodeMirror.version = "5.58.0"; return CodeMirror; diff --git a/vendor/codemirror/mode/javascript/javascript.js b/vendor/codemirror/mode/javascript/javascript.js index 9c751d23..66e5a308 100644 --- a/vendor/codemirror/mode/javascript/javascript.js +++ b/vendor/codemirror/mode/javascript/javascript.js @@ -111,7 +111,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { if (ch != ">" || !state.lexical || state.lexical.type != ">") { if (stream.eat("=")) { if (ch == "!" || ch == "=") stream.eat("=") - } else if (/[<>*+\-]/.test(ch)) { + } else if (/[<>*+\-|&?]/.test(ch)) { stream.eat(ch) if (ch == ">") stream.eat(ch) } From ee30aa1407a2abee39ba9683fecd1ddc77036321 Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 22 Sep 2020 14:15:40 +0300 Subject: [PATCH 12/55] convert colors in `uso` preprocessor to match USO site (#997) #rrggbb for /*[[color]]*/ r,g,b for /*[[color-rgb]]*/ (no alpha channel) --- background/background-worker.js | 47 ++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/background/background-worker.js b/background/background-worker.js index 81387aac..f44013e5 100644 --- a/background/background-worker.js +++ b/background/background-worker.js @@ -31,7 +31,11 @@ createAPI({ }); function compileUsercss(preprocessor, code, vars) { - loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js'); + loadScript( + '/vendor-overwrites/csslint/parserlib.js', + '/vendor-overwrites/colorpicker/colorconverter.js', + '/js/moz-parser.js' + ); const builder = getUsercssCompiler(preprocessor); vars = simpleVars(vars); return Promise.resolve(builder.preprocess ? builder.preprocess(code, vars) : code) @@ -122,28 +126,39 @@ function getUsercssCompiler(preprocessor) { const pool = new Map(); return Promise.resolve(doReplace(source)); - function getValue(name, rgb) { + function getValue(name, rgbName) { if (!vars.hasOwnProperty(name)) { if (name.endsWith('-rgb')) { - return getValue(name.slice(0, -4), true); + return getValue(name.slice(0, -4), name); } return null; } - if (rgb) { - if (vars[name].type === 'color') { - const color = colorConverter.parse(vars[name].value); - if (!color) return null; - const {r, g, b} = color; - return `${r}, ${g}, ${b}`; + const {type, value} = vars[name]; + switch (type) { + case 'color': { + let color = pool.get(rgbName || name); + if (color == null) { + color = colorConverter.parse(value); + if (color) { + if (color.type === 'hsl') { + color = colorConverter.HSVtoRGB(colorConverter.HSLtoHSV(color)); + } + const {r, g, b} = color; + color = rgbName + ? `${r}, ${g}, ${b}` + : `#${(0x1000000 + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + } + // the pool stores `false` for bad colors to differentiate from a yet unknown color + pool.set(rgbName || name, color || false); + } + return color || null; } - return null; + case 'dropdown': + case 'select': // prevent infinite recursion + pool.set(name, ''); + return doReplace(value); } - if (vars[name].type === 'dropdown' || vars[name].type === 'select') { - // prevent infinite recursion - pool.set(name, ''); - return doReplace(vars[name].value); - } - return vars[name].value; + return value; } function doReplace(text) { From 038629517e4bbfd9d89f12c2b7de0e09176d514d Mon Sep 17 00:00:00 2001 From: narcolepticinsomniac Date: Tue, 22 Sep 2020 12:04:19 -0400 Subject: [PATCH 13/55] remove dropbox disabled (#1041) --- options/options.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/options/options.js b/options/options.js index 0d99be13..a9752248 100644 --- a/options/options.js +++ b/options/options.js @@ -9,13 +9,6 @@ setupRadioButtons(); enforceInputRange($('#popupWidth')); setTimeout(splitLongTooltips); -// https://github.com/openstyles/stylus/issues/822 -if (!FIREFOX && CHROME >= 76 && CHROME <= 81) { - const dropboxOption = $('option[value="dropbox"]'); - dropboxOption.disabled = true; - dropboxOption.setAttribute('title', t('hostDisabled')); -} - if (CHROME_HAS_BORDER_BUG) { const borderOption = $('.chrome-no-popup-border'); if (borderOption) { From e0a7372f4f0365cf2c10802232f4b948e82e08ae Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 24 Sep 2020 16:22:28 +0300 Subject: [PATCH 14/55] enable starHack option --- js/moz-parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/moz-parser.js b/js/moz-parser.js index 32a73afb..d79c4789 100644 --- a/js/moz-parser.js +++ b/js/moz-parser.js @@ -18,7 +18,7 @@ function parseMozFormat({code, styleId}) { 'regexp': 'regexps', }; const hasSingleEscapes = /([^\\]|^)\\([^\\]|$)/; - const parser = new parserlib.css.Parser(); + const parser = new parserlib.css.Parser({starHack: true}); const sectionStack = [{code: '', start: 0}]; const errors = []; const sections = []; From 3f6c85637c26c35f7a2ea64c61df13834a9f26a6 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 25 Sep 2020 13:51:03 +0300 Subject: [PATCH 15/55] Containment Module L2 (WD, 2020-06-03) --- edit/codemirror-default.js | 1 + vendor-overwrites/csslint/parserlib.js | 1 + 2 files changed, 2 insertions(+) diff --git a/edit/codemirror-default.js b/edit/codemirror-default.js index 94b156e7..e83deeca 100644 --- a/edit/codemirror-default.js +++ b/edit/codemirror-default.js @@ -105,6 +105,7 @@ } Object.assign(CodeMirror.mimeModes['text/css'].propertyKeywords, { + 'content-visibility': true, 'overflow-anchor': true, 'overscroll-behavior': true, }); diff --git a/vendor-overwrites/csslint/parserlib.js b/vendor-overwrites/csslint/parserlib.js index 263fb54d..41f831da 100644 --- a/vendor-overwrites/csslint/parserlib.js +++ b/vendor-overwrites/csslint/parserlib.js @@ -286,6 +286,7 @@ self.parserlib = (() => { 'columns': 1, 'contain': 'none | strict | content | [ size || layout || style || paint ]', 'content': 'normal | none | [ / ]?', + 'content-visibility': 'visible | auto | hidden', 'counter-increment': 1, 'counter-reset': 1, 'crop': 'rect() | inset-rect() | auto', From c416fa7ca0929012805f4df04959e48486f73bd4 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 2 Oct 2020 18:10:52 +0300 Subject: [PATCH 16/55] rework and move newUI+theme to options.html (#1050) * rework and move newUI+theme to options.html * rephrase/clarify the find styles label * switch to USO-archive * search for 'Stylus' keyword to filter out Stylish crud * use archive's default search order --- _locales/bg/messages.json | 6 +-- _locales/cs/messages.json | 6 +-- _locales/de/messages.json | 6 +-- _locales/en/messages.json | 7 ++-- _locales/es/messages.json | 6 +-- _locales/et/messages.json | 6 +-- _locales/fr/messages.json | 6 +-- _locales/he/messages.json | 6 +-- _locales/hu/messages.json | 6 +-- _locales/it/messages.json | 6 +-- _locales/ja/messages.json | 6 +-- _locales/ko/messages.json | 6 +-- _locales/nl/messages.json | 6 +-- _locales/pl/messages.json | 6 +-- _locales/pt_PT/messages.json | 6 +-- _locales/ro/messages.json | 6 +-- _locales/ru/messages.json | 6 +-- _locales/sv/messages.json | 6 +-- _locales/tr/messages.json | 6 +-- _locales/zh_CN/messages.json | 6 +-- _locales/zh_TW/messages.json | 6 +-- js/prefs.js | 2 +- manage.html | 56 ++++------------------------ manage/manage.css | 71 ++---------------------------------- manage/manage.js | 47 +++++++++++------------- options.html | 49 +++++++++++++++++++++++++ options/options.js | 2 +- 27 files changed, 106 insertions(+), 248 deletions(-) diff --git a/_locales/bg/messages.json b/_locales/bg/messages.json index 0e02bbf8..fce7dc43 100644 --- a/_locales/bg/messages.json +++ b/_locales/bg/messages.json @@ -217,10 +217,6 @@ } } }, - "editorStylesButton": { - "message": "Стилове за редактора", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Включване", "description": "Label for the button to enable a style" @@ -741,4 +737,4 @@ "message": "този адрес", "description": "Text for link in toolbar pop-up to write a new style for the current URL" } -} \ No newline at end of file +} diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index 51c2f253..4bd340b5 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -305,10 +305,6 @@ } } }, - "editorStylesButton": { - "message": "Najít styly pro editor", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Povolit", "description": "Label for the button to enable a style" @@ -1306,4 +1302,4 @@ "message": "Nahrávání souboru…", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 6521b6f3..32253e1b 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -309,10 +309,6 @@ } } }, - "editorStylesButton": { - "message": "Editor Styles finden", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Aktivieren", "description": "Label for the button to enable a style" @@ -1600,4 +1596,4 @@ "message": "Lade Styles hoch...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6ed037fb..95cf61d4 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -319,10 +319,6 @@ }, "description": "Title of the page for editing styles" }, - "editorStylesButton": { - "message": "Find editor styles", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Enable", "description": "Label for the button to enable a style" @@ -1012,6 +1008,9 @@ "optionsResetButton": { "message": "Reset options" }, + "optionsStylusThemes": { + "message": "Find a Stylus UI theme" + }, "optionsSubheading": { "message": "More Options", "description": "Subheading for options section on manage page." diff --git a/_locales/es/messages.json b/_locales/es/messages.json index ffb6ba7a..ceb3dd1d 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -313,10 +313,6 @@ } } }, - "editorStylesButton": { - "message": "Buscar estilos del editor", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Activar", "description": "Label for the button to enable a style" @@ -1568,4 +1564,4 @@ "message": "Subiendo el archivo....", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/et/messages.json b/_locales/et/messages.json index e2568512..57066641 100644 --- a/_locales/et/messages.json +++ b/_locales/et/messages.json @@ -313,10 +313,6 @@ } } }, - "editorStylesButton": { - "message": "Leia redaktori stiile", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Luba", "description": "Label for the button to enable a style" @@ -1486,4 +1482,4 @@ "message": "Faili üleslaadimine...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index d96e9a3a..e2aad9cd 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -321,10 +321,6 @@ } } }, - "editorStylesButton": { - "message": "Trouver des styles pour l’éditeur", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Activer", "description": "Label for the button to enable a style" @@ -1616,4 +1612,4 @@ "message": "Envoi du fichier…", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/he/messages.json b/_locales/he/messages.json index 22266d83..c9b25a9a 100644 --- a/_locales/he/messages.json +++ b/_locales/he/messages.json @@ -321,10 +321,6 @@ } } }, - "editorStylesButton": { - "message": "מצא עיצובים לעורך", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "אפשר", "description": "Label for the button to enable a style" @@ -1381,4 +1377,4 @@ "message": "מעלה קובץ...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/hu/messages.json b/_locales/hu/messages.json index 1ab582b4..1195eb83 100644 --- a/_locales/hu/messages.json +++ b/_locales/hu/messages.json @@ -309,10 +309,6 @@ } } }, - "editorStylesButton": { - "message": "A szerkesztő stílusainak keresése", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Engedélyezés", "description": "Label for the button to enable a style" @@ -1604,4 +1600,4 @@ "message": "Fájl feltöltése...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 1ccff03a..0be3fef9 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -273,10 +273,6 @@ } } }, - "editorStylesButton": { - "message": "Cerca stili editor", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Attiva", "description": "Label for the button to enable a style" @@ -1058,4 +1054,4 @@ "message": "questo URL", "description": "Text for link in toolbar pop-up to write a new style for the current URL" } -} \ No newline at end of file +} diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 1b79ccb7..b76c741f 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -313,10 +313,6 @@ } } }, - "editorStylesButton": { - "message": "エディタのスタイルを見つける", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "有効化", "description": "Label for the button to enable a style" @@ -1636,4 +1632,4 @@ "message": "スタイルをアップロード中...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/ko/messages.json b/_locales/ko/messages.json index 9b207cd9..26a35640 100644 --- a/_locales/ko/messages.json +++ b/_locales/ko/messages.json @@ -317,10 +317,6 @@ } } }, - "editorStylesButton": { - "message": "편집기 스타일 찾기", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "활성화", "description": "Label for the button to enable a style" @@ -1636,4 +1632,4 @@ "message": "파일 업로드 중...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/nl/messages.json b/_locales/nl/messages.json index daa2a292..677c39cd 100644 --- a/_locales/nl/messages.json +++ b/_locales/nl/messages.json @@ -317,10 +317,6 @@ } } }, - "editorStylesButton": { - "message": "Editorstijlen zoeken", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Inschakelen", "description": "Label for the button to enable a style" @@ -1620,4 +1616,4 @@ "message": "Bestand uploaden...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index a5f8ad21..f24ce9e9 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -321,10 +321,6 @@ } } }, - "editorStylesButton": { - "message": "Znajdź style edytora", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Włącz", "description": "Label for the button to enable a style" @@ -1644,4 +1640,4 @@ "message": "Wysyłanie stylów...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/pt_PT/messages.json b/_locales/pt_PT/messages.json index a54ba31e..cf3cee05 100644 --- a/_locales/pt_PT/messages.json +++ b/_locales/pt_PT/messages.json @@ -301,10 +301,6 @@ } } }, - "editorStylesButton": { - "message": "Encontrar estilos para o editor", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Ativar", "description": "Label for the button to enable a style" @@ -1224,4 +1220,4 @@ "message": "este URL", "description": "Text for link in toolbar pop-up to write a new style for the current URL" } -} \ No newline at end of file +} diff --git a/_locales/ro/messages.json b/_locales/ro/messages.json index 0a34d235..703d0f54 100644 --- a/_locales/ro/messages.json +++ b/_locales/ro/messages.json @@ -269,10 +269,6 @@ } } }, - "editorStylesButton": { - "message": "Găsiți teme pentru editor", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Activați", "description": "Label for the button to enable a style" @@ -1140,4 +1136,4 @@ "message": "acest URL", "description": "Text for link in toolbar pop-up to write a new style for the current URL" } -} \ No newline at end of file +} diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 69985610..2334e357 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -321,10 +321,6 @@ } } }, - "editorStylesButton": { - "message": "Сменить тему Стилус", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Включить", "description": "Label for the button to enable a style" @@ -1644,4 +1640,4 @@ "message": "Загрузка файла...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index 91ff2c39..d800b825 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -309,10 +309,6 @@ } } }, - "editorStylesButton": { - "message": "Hitta redaktörsstilar", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Aktivera", "description": "Label for the button to enable a style" @@ -1522,4 +1518,4 @@ "message": "Skickar filen...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/tr/messages.json b/_locales/tr/messages.json index 941cc153..1dedf52b 100644 --- a/_locales/tr/messages.json +++ b/_locales/tr/messages.json @@ -309,10 +309,6 @@ } } }, - "editorStylesButton": { - "message": "Editör stili bul", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Etkinleştir", "description": "Label for the button to enable a style" @@ -896,4 +892,4 @@ "message": "Dosya Yükleniyor...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 50c1336b..04e56f37 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -321,10 +321,6 @@ } } }, - "editorStylesButton": { - "message": "查找编辑器样式", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "启用", "description": "Label for the button to enable a style" @@ -1644,4 +1640,4 @@ "message": "正在上传文件...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json index 0886cb37..2551fb69 100644 --- a/_locales/zh_TW/messages.json +++ b/_locales/zh_TW/messages.json @@ -321,10 +321,6 @@ } } }, - "editorStylesButton": { - "message": "找到編輯器樣式", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "啟用", "description": "Label for the button to enable a style" @@ -1644,4 +1640,4 @@ "message": "正在上傳檔案……", "description": "" } -} \ No newline at end of file +} diff --git a/js/prefs.js b/js/prefs.js index 52e2ad7a..3b39cf85 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -28,9 +28,9 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => { 'manage.onlyLocal.invert': false, // display only externally installed styles 'manage.onlyUsercss.invert': false, // display only non-usercss (standard) styles // UI element state: expanded/collapsed + 'manage.actions.expanded': true, 'manage.backup.expanded': true, 'manage.filters.expanded': true, - 'manage.options.expanded': true, // the new compact layout doesn't look good on Android yet 'manage.newUI': !navigator.appVersion.includes('Android'), 'manage.newUI.favicons': false, // show favicons for the sites in applies-to diff --git a/manage.html b/manage.html index dad506b6..6744d802 100644 --- a/manage.html +++ b/manage.html @@ -278,8 +278,11 @@ + -
-
- -

- - - -
-
- - -
- -
- -
- - - -
- -
-

diff --git a/manage/manage.css b/manage/manage.css index 31f2b325..ef4d0279 100644 --- a/manage/manage.css +++ b/manage/manage.css @@ -285,14 +285,6 @@ a:hover { margin-top: .5rem; } -#options-buttons { - display: flex; - flex-wrap: wrap; - padding-top: .1rem; -} - -#options-buttons > a, -#options-buttons > button, #backup-buttons button { margin: 0 .2rem .5rem 0; } @@ -695,65 +687,6 @@ a:hover { filter: grayscale(0); } -#newUIoptions { - display: none; -} - -.newUI #newUIoptions { - display: initial; -} - -#newUIoptions > * { - display: flex; - align-items: center; - margin-bottom: auto; - flex-wrap: wrap; - position: relative; -} - -#newUIoptions input[type="number"] { - width: 3em; - margin-right: .5em; -} - -#newUIoptions [data-toggle-on-click="#faviconsHelp"] { - width: 14px; - height: 14px; - display: inline-block; - vertical-align: middle; - position: relative; - top: -1px; -} - -#newUIoptions [data-toggle-on-click] > svg { - position: static; -} - -#newUIoptions [data-toggle-on-click]:not([open]) > svg { - /* note: without ">" FF52 also transforms the nested svg inside */ - transform: rotate(-90deg); -} - -html:not(.newUI) #newUIoptions + * { - margin-top: .5em; -} - -input[id^="manage.newUI"] { - margin-left: 0; -} - -#faviconsHelp { - overflow-y: auto; - font-size: 90%; - padding: 1ex 0 2ex 16px; -} - -#faviconsHelp div { - display: flex; - align-items: center; - margin-top: 1ex; -} - /* Default, no update buttons */ .updater-icons .update, .updater-icons .check-update { @@ -1078,6 +1011,10 @@ input[id^="manage.newUI"] { animation: fadeout .25s ease-in-out; } +.settings-column { + margin-top: 1rem; +} + @keyframes fadein { from { opacity: 0; diff --git a/manage/manage.js b/manage/manage.js index d7478b63..956fe526 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -5,7 +5,7 @@ global messageBox getStyleWithNoCode objectDiff configDialog sorter msg prefs API onDOMready $ $$ $create template setupLivePrefs - URLS enforceInputRange t tWordBreak formatDate + t tWordBreak formatDate getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce scrollElementIntoView CHROME VIVALDI FIREFOX router bulkChangeTime:true bulkChangeQueue @@ -19,15 +19,24 @@ const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW; const BULK_THROTTLE_MS = 100; +// define pref-mapped ids separately const newUI = { - enabled: prefs.get('manage.newUI'), - favicons: prefs.get('manage.newUI.favicons'), - faviconsGray: prefs.get('manage.newUI.faviconsGray'), - targets: prefs.get('manage.newUI.targets'), - renderClass() { - document.documentElement.classList.toggle('newUI', newUI.enabled); - }, + enabled: null, // the global option should come first + favicons: null, + faviconsGray: null, + targets: null, }; +// ...add utility functions +Object.assign(newUI, { + ids: Object.keys(newUI), + prefGroup: 'manage.newUI', + prefKeyForId: id => id === 'enabled' ? newUI.prefGroup : `${newUI.prefGroup}.${id}`, + renderClass: () => document.documentElement.classList.toggle('newUI', newUI.enabled), +}); +// ...read the actual values +for (const id of newUI.ids) { + newUI[id] = prefs.get(newUI.prefKeyForId(id)); +} newUI.renderClass(); const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps']; @@ -88,10 +97,6 @@ function initGlobalEvents() { installed.onclick = handleEvent.entryClicked; $('#manage-options-button').onclick = () => router.updateHash('#stylus-options'); $('#sync-styles').onclick = () => router.updateHash('#stylus-options'); - { - const btn = $('#manage-shortcuts-button'); - btn.onclick = btn.onclick || (() => openURL({url: URLS.configureCommands})); - } $$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external)); // show date installed & last update on hover installed.addEventListener('mouseover', handleEvent.lazyAddEntryTitle); @@ -113,19 +118,11 @@ function initGlobalEvents() { }; }); - // triggered automatically by setupLivePrefs() below - enforceInputRange($('#manage.newUI.targets')); - // N.B. triggers existing onchange listeners setupLivePrefs(); sorter.init(); - prefs.subscribe([ - 'manage.newUI', - 'manage.newUI.favicons', - 'manage.newUI.faviconsGray', - 'manage.newUI.targets', - ], () => switchUI()); + prefs.subscribe(newUI.ids.map(newUI.prefKeyForId), () => switchUI()); switchUI({styleOnly: true}); @@ -488,7 +485,7 @@ Object.assign(handleEvent, { const y = Math.max(0, top); const first = document.elementFromPoint(x, y); const lastOffset = first.offsetTop + window.innerHeight; - const numTargets = prefs.get('manage.newUI.targets'); + const numTargets = newUI.targets; let entry = first && first.closest('.entry') || installed.children[0]; while (entry && entry.offsetTop <= lastOffset) { favicons.push(...$$('img', entry).slice(0, numTargets).filter(img => img.dataset.src)); @@ -618,10 +615,8 @@ function switchUI({styleOnly} = {}) { const current = {}; const changed = {}; let someChanged = false; - // ensure the global option is processed first - for (const el of [$('#manage.newUI'), ...$$('[id^="manage.newUI."]')]) { - const id = el.id.replace(/^manage\.newUI\.?/, '') || 'enabled'; - const value = el.type === 'checkbox' ? el.checked : Number(el.value); + for (const id of newUI.ids) { + const value = prefs.get(newUI.prefKeyForId(id)); const valueChanged = value !== newUI[id] && (id === 'enabled' || current.enabled); current[id] = value; changed[id] = valueChanged; diff --git a/options.html b/options.html index 6ea9a78b..718a8d31 100644 --- a/options.html +++ b/options.html @@ -41,6 +41,16 @@
+
+

+
+ +
+
+

@@ -125,6 +135,45 @@
+
+

+
+ + + + +
+
+

diff --git a/options/options.js b/options/options.js index a9752248..dff74940 100644 --- a/options/options.js +++ b/options/options.js @@ -6,7 +6,7 @@ setupLivePrefs(); setupRadioButtons(); -enforceInputRange($('#popupWidth')); +$$('input[min], input[max]').forEach(enforceInputRange); setTimeout(splitLongTooltips); if (CHROME_HAS_BORDER_BUG) { From e7a6e86b6c5204a4a37da94822539e465ecd34ba Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 5 Oct 2020 13:05:35 +0300 Subject: [PATCH 17/55] fixup for c416fa7c: remove the leftovers --- manage/manage.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/manage/manage.js b/manage/manage.js index 956fe526..ec13839b 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -58,11 +58,6 @@ Promise.all([ if (!VIVALDI) { $$('#header select').forEach(el => el.adjustWidth()); } - if (FIREFOX && 'update' in (chrome.commands || {})) { - const btn = $('#manage-shortcuts-button'); - btn.classList.remove('chromium-only'); - btn.onclick = API.optionsCustomizeHotkeys; - } }), ]).then(args => { showStyles(...args); From 15d854f913687355ab999297790806c01c9f36b8 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 5 Oct 2020 16:19:18 +0300 Subject: [PATCH 18/55] fixup for c416fa7c: remove the leftovers, take 2 --- manage/manage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manage/manage.js b/manage/manage.js index ec13839b..0df3ea8d 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -7,7 +7,7 @@ global messageBox getStyleWithNoCode sorter msg prefs API onDOMready $ $$ $create template setupLivePrefs t tWordBreak formatDate getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce - scrollElementIntoView CHROME VIVALDI FIREFOX router + scrollElementIntoView CHROME VIVALDI router bulkChangeTime:true bulkChangeQueue */ 'use strict'; From 7c89f7b21d876d62d456f2b51ba3287e176b505b Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 5 Oct 2020 21:08:30 +0300 Subject: [PATCH 19/55] autosort style elements on own pages too --- content/style-injector.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/content/style-injector.js b/content/style-injector.js index 5c9fffd0..8630bf05 100644 --- a/content/style-injector.js +++ b/content/style-injector.js @@ -8,7 +8,6 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ const PATCH_ID = 'transition-patch'; // styles are out of order if any of these elements is injected between them const ORDERED_TAGS = new Set(['head', 'body', 'frameset', 'style', 'link']); - const IS_OWN_PAGE = Boolean(chrome.tabs); // detect Chrome 65 via a feature it added since browser version can be spoofed const isChromePre65 = chrome.app && typeof Worklet !== 'function'; const docRewriteObserver = RewriteObserver(_sort); @@ -159,7 +158,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ } function _emitUpdate(value) { - _toggleObservers(!IS_OWN_PAGE && list.length); + _toggleObservers(list.length); onUpdate(); return value; } From cb89be8682dc3c036d88eabf52e74df771aae2b3 Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 4 Oct 2020 13:42:06 +0300 Subject: [PATCH 20/55] ignore empty code only in global (non-targeted) sections --- background/style-manager.js | 14 +++----------- js/sections-util.js | 10 +++++++++- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/background/style-manager.js b/background/style-manager.js index a1dee182..ff18192d 100644 --- a/background/style-manager.js +++ b/background/style-manager.js @@ -1,5 +1,5 @@ /* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */ -/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty +/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty styleSectionGlobal getStyleWithNoCode msg sync uuidv4 */ /* exported styleManager */ 'use strict'; @@ -445,7 +445,7 @@ const styleManager = (() => { excluded = true; } for (const section of data.sections) { - if (styleCodeEmpty(section.code)) { + if (styleSectionGlobal(section) && styleCodeEmpty(section.code)) { continue; } const match = urlMatchSection(query, section); @@ -612,15 +612,7 @@ const styleManager = (() => { return 'sloppy'; } // TODO: check for invalid regexps? - if ( - (!section.regexps || !section.regexps.length) && - (!section.urlPrefixes || !section.urlPrefixes.length) && - (!section.urls || !section.urls.length) && - (!section.domains || !section.domains.length) - ) { - return true; - } - return false; + return styleSectionGlobal(section); } function createCompiler(compile) { diff --git a/js/sections-util.js b/js/sections-util.js index f44b52a2..48a2f9c1 100644 --- a/js/sections-util.js +++ b/js/sections-util.js @@ -1,4 +1,4 @@ -/* exported styleSectionsEqual styleCodeEmpty calcStyleDigest styleJSONseemsValid */ +/* exported styleSectionsEqual styleCodeEmpty styleSectionGlobal calcStyleDigest styleJSONseemsValid */ 'use strict'; function styleCodeEmpty(code) { @@ -14,6 +14,14 @@ function styleCodeEmpty(code) { return false; } +/** Checks if section is global i.e. has no targets at all */ +function styleSectionGlobal(section) { + return (!section.regexps || !section.regexps.length) && + (!section.urlPrefixes || !section.urlPrefixes.length) && + (!section.urls || !section.urls.length) && + (!section.domains || !section.domains.length); +} + /** * @param {Style} a - first style object * @param {Style} b - second style object From 6b2dff668730791b9c7fe78c3f52e85ddf44cb9f Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 3 Oct 2020 20:17:59 +0300 Subject: [PATCH 21/55] treat empty url-prefix() as non-matching --- background/style-manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/style-manager.js b/background/style-manager.js index ff18192d..4ddb6411 100644 --- a/background/style-manager.js +++ b/background/style-manager.js @@ -586,7 +586,7 @@ const styleManager = (() => { ) { return true; } - if (section.urlPrefixes && section.urlPrefixes.some(p => query.url.startsWith(p))) { + if (section.urlPrefixes && section.urlPrefixes.some(p => p && query.url.startsWith(p))) { return true; } // as per spec the fragment portion is ignored in @-moz-document: From 7c205880d2fcc295fdf5fb7a7098f7dc541aee21 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 7 Oct 2020 22:19:30 +0300 Subject: [PATCH 22/55] require Chrome 55 and allow native async/await syntax --- .eslintrc.yml | 2 +- manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 87af0074..0871bcad 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,7 +1,7 @@ # https://github.com/eslint/eslint/blob/master/docs/rules/README.md parserOptions: - ecmaVersion: 2015 + ecmaVersion: 2017 env: browser: true diff --git a/manifest.json b/manifest.json index 9d59db44..ad3859f2 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "Stylus", "version": "1.5.13", - "minimum_chrome_version": "49", + "minimum_chrome_version": "55", "description": "__MSG_description__", "homepage_url": "https://add0n.com/stylus.html", "manifest_version": 2, From a01bd3cd618f2cc125fc0f4ee279434d7f4cd476 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 7 Oct 2020 23:27:38 +0300 Subject: [PATCH 23/55] update polyfill for Chrome>=55 --- background/background-worker.js | 2 - edit/editor-worker.js | 1 - js/polyfill.js | 122 ++++++++------------------------ 3 files changed, 28 insertions(+), 97 deletions(-) diff --git a/background/background-worker.js b/background/background-worker.js index f44013e5..ddb33d53 100644 --- a/background/background-worker.js +++ b/background/background-worker.js @@ -12,7 +12,6 @@ createAPI({ compileUsercss, parseUsercssMeta(text, indexOffset = 0) { loadScript( - '/js/polyfill.js', '/vendor/usercss-meta/usercss-meta.min.js', '/vendor-overwrites/colorpicker/colorconverter.js', '/js/meta-parser.js' @@ -21,7 +20,6 @@ createAPI({ }, nullifyInvalidVars(vars) { loadScript( - '/js/polyfill.js', '/vendor/usercss-meta/usercss-meta.min.js', '/vendor-overwrites/colorpicker/colorconverter.js', '/js/meta-parser.js' diff --git a/edit/editor-worker.js b/edit/editor-worker.js index 6ef51eef..62ef380c 100644 --- a/edit/editor-worker.js +++ b/edit/editor-worker.js @@ -16,7 +16,6 @@ createAPI({ }, metalint: code => { loadScript( - '/js/polyfill.js', '/vendor/usercss-meta/usercss-meta.min.js', '/vendor-overwrites/colorpicker/colorconverter.js', '/js/meta-parser.js' diff --git a/js/polyfill.js b/js/polyfill.js index 3b22c96d..859de6e2 100644 --- a/js/polyfill.js +++ b/js/polyfill.js @@ -3,27 +3,33 @@ // eslint-disable-next-line no-unused-expressions self.INJECTED !== 1 && (() => { - // this part runs in workers, content scripts, our extension pages + //#region for content scripts and our extension pages - if (!Object.entries) { - Object.entries = obj => Object.keys(obj).map(k => [k, obj[k]]); + if (!window.browser || !browser.runtime) { + const createTrap = (base, parent) => { + const target = typeof base === 'function' ? () => {} : {}; + target.isTrap = true; + return new Proxy(target, { + get: (target, prop) => { + if (target[prop]) return target[prop]; + if (base[prop] && (typeof base[prop] === 'object' || typeof base[prop] === 'function')) { + target[prop] = createTrap(base[prop], base); + return target[prop]; + } + return base[prop]; + }, + apply: (target, thisArg, args) => base.apply(parent, args) + }); + }; + window.browser = createTrap(chrome, null); } - if (!Object.values) { - Object.values = obj => Object.keys(obj).map(k => obj[k]); - } - - // don't use self.chrome. It is undefined in Firefox - if (typeof chrome !== 'object') return; - // the rest is for content scripts and our extension pages - - self.browser = polyfillBrowser(); /* Promisifies the specified `chrome` methods into `browser`. The definitions is an object like this: { 'storage.sync': ['get', 'set'], // if deeper than one level, combine the path via `.` windows: ['create', 'update'], // items and sub-objects will only be created if present in `chrome` } */ - self.promisifyChrome = definitions => { + window.promisifyChrome = definitions => { for (const [scopeName, methods] of Object.entries(definitions)) { const path = scopeName.split('.'); const src = path.reduce((obj, p) => obj && obj[p], chrome); @@ -43,90 +49,18 @@ self.INJECTED !== 1 && (() => { }; if (!chrome.tabs) return; - // the rest is for our extension pages - if (typeof document === 'object') { - const ELEMENT_METH = { - append: { - base: [Element, Document, DocumentFragment], - fn: (node, frag) => { - node.appendChild(frag); - } - }, - prepend: { - base: [Element, Document, DocumentFragment], - fn: (node, frag) => { - node.insertBefore(frag, node.firstChild); - } - }, - before: { - base: [Element, CharacterData, DocumentType], - fn: (node, frag) => { - node.parentNode.insertBefore(frag, node); - } - }, - after: { - base: [Element, CharacterData, DocumentType], - fn: (node, frag) => { - node.parentNode.insertBefore(frag, node.nextSibling); - } - } - }; + //#endregion + //#region for our extension pages - for (const [key, {base, fn}] of Object.entries(ELEMENT_METH)) { - for (const cls of base) { - if (cls.prototype[key]) { - continue; - } - cls.prototype[key] = function (...nodes) { - const frag = document.createDocumentFragment(); - for (const node of nodes) { - frag.appendChild(typeof node === 'string' ? document.createTextNode(node) : node); - } - fn(this, frag); - }; - } + for (const storage of ['localStorage', 'sessionStorage']) { + try { + window[storage]._access_check = 1; + delete window[storage]._access_check; + } catch (err) { + Object.defineProperty(window, storage, {value: {}}); } } - try { - if (!localStorage) { - throw new Error('localStorage is null'); - } - localStorage._access_check = 1; - delete localStorage._access_check; - } catch (err) { - Object.defineProperty(self, 'localStorage', {value: {}}); - } - try { - if (!sessionStorage) { - throw new Error('sessionStorage is null'); - } - sessionStorage._access_check = 1; - delete sessionStorage._access_check; - } catch (err) { - Object.defineProperty(self, 'sessionStorage', {value: {}}); - } - function polyfillBrowser() { - if (typeof browser === 'object' && browser.runtime) { - return browser; - } - return createTrap(chrome, null); - - function createTrap(base, parent) { - const target = typeof base === 'function' ? () => {} : {}; - target.isTrap = true; - return new Proxy(target, { - get: (target, prop) => { - if (target[prop]) return target[prop]; - if (base[prop] && (typeof base[prop] === 'object' || typeof base[prop] === 'function')) { - target[prop] = createTrap(base[prop], base); - return target[prop]; - } - return base[prop]; - }, - apply: (target, thisArg, args) => base.apply(parent, args) - }); - } - } + //#endregion })(); From 999481181936634d5fe58db4b715a22a0d8ee441 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 8 Oct 2020 12:19:14 +0300 Subject: [PATCH 24/55] recognize 'backdrop-filter' --- vendor-overwrites/csslint/parserlib.js | 1 + 1 file changed, 1 insertion(+) diff --git a/vendor-overwrites/csslint/parserlib.js b/vendor-overwrites/csslint/parserlib.js index 41f831da..f4dcd2e5 100644 --- a/vendor-overwrites/csslint/parserlib.js +++ b/vendor-overwrites/csslint/parserlib.js @@ -154,6 +154,7 @@ self.parserlib = (() => { 'azimuth': '', // B + 'backdrop-filter': ' | none', 'backface-visibility': 'visible | hidden', 'background': '[ , ]* ', 'background-attachment': '#', From b840d4897d8322f584758c1b8052f221e8e0c171 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 7 Oct 2020 10:47:51 +0300 Subject: [PATCH 25/55] cleanup usoSearchCache + tidy up db.js --- background/background.js | 12 ++- background/db.js | 179 +++++++++++++++------------------------ 2 files changed, 77 insertions(+), 114 deletions(-) diff --git a/background/background.js b/background/background.js index 6851006e..2dd15b0a 100644 --- a/background/background.js +++ b/background/background.js @@ -1,5 +1,5 @@ /* global download prefs openURL FIREFOX CHROME - URLS ignoreChromeError usercssHelper + URLS ignoreChromeError usercssHelper chromeLocal semverCompare styleManager msg navigatorUtil workerUtil contentScripts sync findExistingTab activateTab isTabReplaceable getActiveTab tabManager */ @@ -139,7 +139,7 @@ if (chrome.commands) { } // ************************************************************************* -chrome.runtime.onInstalled.addListener(({reason}) => { +chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => { // save install type: "admin", "development", "normal", "sideload" or "other" // "normal" = addon installed from webstore chrome.management.getSelf(info => { @@ -156,6 +156,14 @@ chrome.runtime.onInstalled.addListener(({reason}) => { }); // themes may change delete localStorage.codeMirrorThemes; + // inline search cache for USO is not needed anymore, TODO: remove this by the middle of 2021 + if (semverCompare(previousVersion, '1.5.13') <= 0) { + setTimeout(async () => { + const del = Object.keys(await chromeLocal.get()) + .filter(key => key.startsWith('usoSearchCache')); + if (del.length) chromeLocal.remove(del); + }, 15e3); + } }); // ************************************************************************* diff --git a/background/db.js b/background/db.js index 2549a3ce..223d3870 100644 --- a/background/db.js +++ b/background/db.js @@ -1,4 +1,4 @@ -/* global chromeLocal ignoreChromeError workerUtil createChromeStorageDB */ +/* global chromeLocal workerUtil createChromeStorageDB */ /* exported db */ /* Initialize a database. There are some problems using IndexedDB in Firefox: @@ -10,29 +10,18 @@ https://www.reddit.com/r/firefox/comments/7ijuaq/firefox_59_webextensions_can_us 'use strict'; const db = (() => { - let exec; - const preparing = prepare(); - return { - exec: (...args) => - preparing.then(() => exec(...args)) + const DATABASE = 'stylish'; + const STORE = 'styles'; + const FALLBACK = 'dbInChromeStorage'; + const dbApi = { + async exec(...args) { + dbApi.exec = await tryUsingIndexedDB().catch(useChromeStorage); + return dbApi.exec(...args); + }, }; + return dbApi; - function prepare() { - return withPromise(shouldUseIndexedDB).then( - ok => { - if (ok) { - useIndexedDB(); - } else { - useChromeStorage(); - } - }, - err => { - useChromeStorage(err); - } - ); - } - - function shouldUseIndexedDB() { + async function tryUsingIndexedDB() { // we use chrome.storage.local fallback if IndexedDB doesn't save data, // which, once detected on the first run, is remembered in chrome.storage.local // for reliablility and in localStorage for fast synchronous access @@ -42,115 +31,81 @@ const db = (() => { if (typeof indexedDB === 'undefined') { throw new Error('indexedDB is undefined'); } - // test localStorage - const fallbackSet = localStorage.dbInChromeStorage; - if (fallbackSet === 'true') { - return false; + switch (await getFallback()) { + case true: throw null; + case false: break; + default: await testDB(); } - if (fallbackSet === 'false') { - return true; - } - // test storage.local - return chromeLocal.get('dbInChromeStorage') - .then(data => { - if (data && data.dbInChromeStorage) { - return false; - } - return testDBSize() - .then(ok => ok || testDBMutation()); - }); + return useIndexedDB(); } - function withPromise(fn) { - try { - return Promise.resolve(fn()); - } catch (err) { - return Promise.reject(err); - } + async function getFallback() { + return localStorage[FALLBACK] === 'true' ? true : + localStorage[FALLBACK] === 'false' ? false : + chromeLocal.getValue(FALLBACK); } - function testDBSize() { - return dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1) - .then(event => ( - event.target.result && - event.target.result.length && - event.target.result[0] - )); - } - - function testDBMutation() { - return dbExecIndexedDB('put', {id: -1}) - .then(() => dbExecIndexedDB('get', -1)) - .then(event => { - if (!event.target.result) { - throw new Error('failed to get previously put item'); - } - if (event.target.result.id !== -1) { - throw new Error('item id is wrong'); - } - return dbExecIndexedDB('delete', -1); - }) - .then(() => true); + async function testDB() { + let e = await dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1); + // throws if result is null + e = e.target.result[0]; + const id = `${performance.now()}.${Math.random()}.${Date.now()}`; + await dbExecIndexedDB('put', {id}); + e = await dbExecIndexedDB('get', id); + // throws if result or id is null + await dbExecIndexedDB('delete', e.target.result.id); } function useChromeStorage(err) { - exec = createChromeStorageDB().exec; - chromeLocal.set({dbInChromeStorage: true}, ignoreChromeError); + chromeLocal.setValue(FALLBACK, true); if (err) { - chromeLocal.setValue('dbInChromeStorageReason', workerUtil.cloneError(err)); + chromeLocal.setValue(FALLBACK + 'Reason', workerUtil.cloneError(err)); console.warn('Failed to access indexedDB. Switched to storage API.', err); } - localStorage.dbInChromeStorage = 'true'; + localStorage[FALLBACK] = 'true'; + return createChromeStorageDB().exec; } function useIndexedDB() { - exec = dbExecIndexedDB; - chromeLocal.set({dbInChromeStorage: false}, ignoreChromeError); - localStorage.dbInChromeStorage = 'false'; + chromeLocal.setValue(FALLBACK, false); + localStorage[FALLBACK] = 'false'; + return dbExecIndexedDB; } - function dbExecIndexedDB(method, ...args) { - return open().then(database => { - if (!method) { - return database; - } - if (method === 'putMany') { - return putMany(database, ...args); - } - const mode = method.startsWith('get') ? 'readonly' : 'readwrite'; - const transaction = database.transaction(['styles'], mode); - const store = transaction.objectStore('styles'); - return storeRequest(store, method, ...args); + async function dbExecIndexedDB(method, ...args) { + const mode = method.startsWith('get') ? 'readonly' : 'readwrite'; + const store = (await open()).transaction([STORE], mode).objectStore(STORE); + const fn = method === 'putMany' ? putMany : storeRequest; + return fn(store, method, ...args); + } + + function storeRequest(store, method, ...args) { + return new Promise((resolve, reject) => { + const request = store[method](...args); + request.onsuccess = resolve; + request.onerror = reject; }); + } - function storeRequest(store, method, ...args) { - return new Promise((resolve, reject) => { - const request = store[method](...args); - request.onsuccess = resolve; - request.onerror = reject; + function putMany(store, _method, items) { + return Promise.all(items.map(item => storeRequest(store, 'put', item))); + } + + function open() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DATABASE, 2); + request.onsuccess = () => resolve(request.result); + request.onerror = reject; + request.onupgradeneeded = create; + }); + } + + function create(event) { + if (event.oldVersion === 0) { + event.target.result.createObjectStore(STORE, { + keyPath: 'id', + autoIncrement: true, }); } - - function open() { - return new Promise((resolve, reject) => { - const request = indexedDB.open('stylish', 2); - request.onsuccess = () => resolve(request.result); - request.onerror = reject; - request.onupgradeneeded = event => { - if (event.oldVersion === 0) { - event.target.result.createObjectStore('styles', { - keyPath: 'id', - autoIncrement: true, - }); - } - }; - }); - } - - function putMany(database, items) { - const transaction = database.transaction(['styles'], 'readwrite'); - const store = transaction.objectStore('styles'); - return Promise.all(items.map(item => storeRequest(store, 'put', item))); - } } })(); From 5196f96ee344f5c73e13a86dddf0b749edf34c4e Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 5 Oct 2020 18:37:47 +0300 Subject: [PATCH 26/55] trigger change on wheeling inside +
+ + +
+
diff --git a/popup/hotkeys.js b/popup/hotkeys.js index fe058ea1..157a34bb 100644 --- a/popup/hotkeys.js +++ b/popup/hotkeys.js @@ -33,7 +33,8 @@ const hotkeys = (() => { } function onKeyDown(event) { - if (event.ctrlKey || event.altKey || event.metaKey || !enabled) { + if (event.ctrlKey || event.altKey || event.metaKey || !enabled || + /^(text|search)$/.test((document.activeElement || {}).type)) { return; } let entry; diff --git a/popup/popup.js b/popup/popup.js index f6aea8c9..fb2b10bf 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -13,7 +13,8 @@ const handleEvent = {}; const ABOUT_BLANK = 'about:blank'; const ENTRY_ID_PREFIX_RAW = 'style-'; -const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW; + +$.entry = styleOrId => $(`#${ENTRY_ID_PREFIX_RAW}${styleOrId.id || styleOrId}`); if (CHROME >= 66 && CHROME <= 69) { // Chrome 66-69 adds a gap, https://crbug.com/821143 document.head.appendChild($create('style', 'html { overflow: overlay }')); @@ -27,7 +28,7 @@ initTabUrls() onDOMready().then(() => initPopup(frames)), ...frames .filter(f => f.url && !f.isDupe) - .map(({url}) => API.getStylesByUrl(url).then(styles => ({styles, url}))), + .map(({url}) => getStyleDataMerged(url).then(styles => ({styles, url}))), ])) .then(([, ...results]) => { if (results[0]) { @@ -53,17 +54,19 @@ if (CHROME_HAS_BORDER_BUG) { } function onRuntimeMessage(msg) { + if (!tabURL) return; + let ready = Promise.resolve(); switch (msg.method) { case 'styleAdded': case 'styleUpdated': if (msg.reason === 'editPreview' || msg.reason === 'editPreviewEnd') return; - handleUpdate(msg); + ready = handleUpdate(msg); break; case 'styleDeleted': handleDelete(msg.style.id); break; } - dispatchEvent(new CustomEvent(msg.method, {detail: msg})); + ready.then(() => dispatchEvent(new CustomEvent(msg.method, {detail: msg}))); } @@ -141,8 +144,7 @@ function initPopup(frames) { } if (!tabURL) { - document.body.classList.add('blocked'); - document.body.insertBefore(template.unavailableInfo, document.body.firstChild); + blockPopup(); return; } @@ -315,24 +317,30 @@ function showStyles(frameResults) { const entries = new Map(); frameResults.forEach(({styles = [], url}, index) => { styles.forEach(style => { - const {id} = style.data; + const {id} = style; if (!entries.has(id)) { style.frameUrl = index === 0 ? '' : url; - entries.set(id, createStyleElement(Object.assign(style.data, style))); + entries.set(id, createStyleElement(style)); } }); }); if (entries.size) { - installed.append(...sortStyles([...entries.values()])); + resortEntries([...entries.values()]); } else { - installed.appendChild(template.noStyles.cloneNode(true)); + installed.appendChild(template.noStyles); } window.dispatchEvent(new Event('showStyles:done')); } +function resortEntries(entries) { + // `entries` is specified only at startup, after that we respect the prefs + if (entries || prefs.get('popup.autoResort')) { + installed.append(...sortStyles(entries || $$('.entry', installed))); + } +} function createStyleElement(style) { - let entry = $(ENTRY_ID_PREFIX + style.id); + let entry = $.entry(style); if (!entry) { entry = template.style.cloneNode(true); entry.setAttribute('style-id', style.id); @@ -469,11 +477,7 @@ Object.assign(handleEvent, { event.stopPropagation(); API .toggleStyle(handleEvent.getClickedStyleId(event), this.checked) - .then(() => { - if (prefs.get('popup.autoResort')) { - installed.append(...sortStyles($$('.entry', installed))); - } - }); + .then(() => resortEntries()); }, toggleExclude(event, type) { @@ -672,38 +676,25 @@ Object.assign(handleEvent, { }); -function handleUpdate({style, reason}) { - if (!tabURL) return; - - fetchStyle() - .then(style => { - if (!style) { - return; - } - if ($(ENTRY_ID_PREFIX + style.id)) { - createStyleElement(style); - return; - } - document.body.classList.remove('blocked'); - $$.remove('.blocked-info, #no-styles'); - createStyleElement(style); - }) - .catch(console.error); - - function fetchStyle() { - if (reason === 'toggle' && $(ENTRY_ID_PREFIX + style.id)) { - return Promise.resolve(style); - } - return API.getStylesByUrl(tabURL, style.id) - .then(([result]) => result && Object.assign(result.data, result)); +async function handleUpdate({style, reason}) { + if (reason !== 'toggle' || !$.entry(style)) { + style = await getStyleDataMerged(tabURL, style.id); + if (!style) return; } + const el = createStyleElement(style); + if (!el.parentNode) { + installed.appendChild(el); + blockPopup(false); + } + resortEntries(); } function handleDelete(id) { - $.remove(ENTRY_ID_PREFIX + id); - if (!$('.entry')) { - installed.appendChild(template.noStyles.cloneNode(true)); + const el = $.entry(id); + if (el) { + el.remove(); + if (!$('.entry')) installed.appendChild(template.noStyles); } } @@ -721,3 +712,21 @@ function waitForTabUrlFF(tab) { ]); }); } + +/* Merges the extra props from API into style data. + * When `id` is specified returns a single object otherwise an array */ +async function getStyleDataMerged(url, id) { + const styles = (await API.getStylesByUrl(url, id)) + .map(r => Object.assign(r.data, r)); + return id ? styles[0] : styles; +} + +function blockPopup(isBlocked = true) { + document.body.classList.toggle('blocked', isBlocked); + if (isBlocked) { + document.body.prepend(template.unavailableInfo); + } else { + template.unavailableInfo.remove(); + template.noStyles.remove(); + } +} diff --git a/popup/search-results.css b/popup/search-results.css index e1c805a6..d4c8d4e9 100755 --- a/popup/search-results.css +++ b/popup/search-results.css @@ -54,21 +54,15 @@ body.search-results-shown { background-color: #fff; } -.search-result .lds-spinner { +#search-results .lds-spinner { transform: scale(.5); filter: invert(1) drop-shadow(1px 1px 3px #000); } -.search-result-empty .lds-spinner { - transform: scale(.5); +#search-results .search-result-empty .lds-spinner { filter: opacity(.2); } -.search-result-fadein { - animation: fadein 1s; - animation-fill-mode: both; -} - .search-result-screenshot { height: 140px; width: 100%; @@ -257,6 +251,18 @@ body.search-results-shown { padding-left: 16px; } +#search-params { + display: flex; + position: relative; + margin-bottom: 1.25rem; +} + +#search-query { + min-width: 3em; + margin-right: .5em; + flex: auto; +} + /* spinner: https://github.com/loadingio/css-spinner */ .lds-spinner { -webkit-user-select: none; diff --git a/popup/search-results.js b/popup/search-results.js index d46982ac..a29b6911 100755 --- a/popup/search-results.js +++ b/popup/search-results.js @@ -1,105 +1,95 @@ -/* global tabURL handleEvent $ $$ prefs template FIREFOX chromeLocal debounce - $create t API tWordBreak formatDate tryCatch tryJSONparse LZString - promisifyChrome download */ +/* global URLS tabURL handleEvent $ $$ prefs template FIREFOX debounce + $create t API tWordBreak formatDate tryCatch download */ 'use strict'; -window.addEventListener('showStyles:done', function _() { - window.removeEventListener('showStyles:done', _); - - if (!tabURL) { - return; - } - - //region Init - - const BODY_CLASS = 'search-results-shown'; +window.addEventListener('showStyles:done', () => { + if (!tabURL) return; const RESULT_ID_PREFIX = 'search-result-'; - - const BASE_URL = 'https://userstyles.org'; - const JSON_URL = BASE_URL + '/styles/chrome/'; - const API_URL = BASE_URL + '/api/v1/styles/'; - const UPDATE_URL = 'https://update.userstyles.org/%.md5'; - + const INDEX_URL = URLS.usoArchiveRaw + 'search-index.json'; const STYLUS_CATEGORY = 'chrome-extension'; - - const DISPLAY_PER_PAGE = 10; - // Millisecs to wait before fetching next batch of search results. - const DELAY_AFTER_FETCHING_STYLES = 0; - // Millisecs to wait before fetching .JSON for next search result. - const DELAY_BEFORE_SEARCHING_STYLES = 0; - - // update USO style install counter - // if the style isn't uninstalled in the popup - const PINGBACK_DELAY = 60e3; - - const BLANK_PIXEL_DATA = 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAA' + - 'C1HAwCAAAAC0lEQVR42mOcXQ8AAbsBHLLDr5MAAAAASUVORK5CYII='; - - const CACHE_SIZE = 1e6; - const CACHE_PREFIX = 'usoSearchCache/'; - const CACHE_DURATION = 24 * 3600e3; - const CACHE_CLEANUP_THROTTLE = 10e3; - const CACHE_CLEANUP_NEEDED = CACHE_PREFIX + 'clean?'; - const CACHE_EXCEPT_PROPS = ['css', 'discussions', 'additional_info']; - - let searchTotalPages; - let searchCurrentPage = 1; - let searchExhausted = 0; // 1: once, 2: twice (first host.jp, then host) - - // currently active USO requests - const xhrSpoofIds = new Set(); - // used as an HTTP header name to identify spoofed requests - const xhrSpoofTelltale = getRandomId(); - - const processedResults = []; - const unprocessedResults = []; - - let loading = false; - // Category for the active tab's URL. - let category; + const PAGE_LENGTH = 10; + // update USO style install counter if the style isn't uninstalled immediately + const PINGBACK_DELAY = 5e3; + const BUSY_DELAY = .5e3; + const USO_AUTO_PIC_SUFFIX = '-after.png'; + const BLANK_PIXEL = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; + const dom = {}; + /** + * @typedef IndexEntry + * @prop {'uso' | 'uso-android'} f - format + * @prop {Number} i - id + * @prop {string} n - name + * @prop {string} c - category + * @prop {Number} u - updatedTime + * @prop {Number} t - totalInstalls + * @prop {Number} w - weeklyInstalls + * @prop {Number} r - rating + * @prop {Number} ai - authorId + * @prop {string} an - authorName + * @prop {string} sn - screenshotName + * @prop {boolean} sa - screenshotArchived + */ + /** @type IndexEntry[] */ + let results; + /** @type IndexEntry[] */ + let index; + let category = ''; + /** @type string[] */ + let query = []; + /** @type 'n' | 'u' | 't' | 'w' | 'r' */ + let order = 't'; let scrollToFirstResult = true; - let displayedPage = 1; let totalPages = 1; - let totalResults = 0; + let ready; - // fade-in when the entry took that long to replace its placeholder - const FADEIN_THRESHOLD = 50; + calcCategory(); - const dom = {}; + const $orNode = (sel, base) => sel instanceof Node ? sel : $(sel, base); + const show = (...args) => $orNode(...args).classList.remove('hidden'); + const hide = (...args) => $orNode(...args).classList.add('hidden'); Object.assign($('#find-styles-link'), { - href: BASE_URL + '/styles/browse/' + getCategory(), + href: URLS.usoArchive, onclick(event) { if (!prefs.get('popup.findStylesInline') || dom.container) { + this.search = `${new URLSearchParams({category, search: $('#search-query').value})}`; handleEvent.openURLandHide.call(this, event); return; } event.preventDefault(); - this.textContent = this.title; this.title = ''; - init(); - load(); + ready = start(); }, }); return; function init() { - promisifyChrome({ - 'storage.local': ['getBytesInUse'], // FF doesn't implement it - }); - setTimeout(() => document.body.classList.add(BODY_CLASS)); - - $('#find-styles-inline-group').classList.add('hidden'); - + setTimeout(() => document.body.classList.add('search-results-shown')); + hide('#find-styles-inline-group'); + $('#search-query').oninput = function () { + query = []; + const text = this.value.trim().toLocaleLowerCase(); + const thisYear = new Date().getFullYear(); + for (let re = /"(.+?)"|(\S+)/g, m; (m = re.exec(text));) { + const n = Number(m[2]); + query.push(n >= 2000 && n <= thisYear ? n : m[1] || m[2]); + } + ready = ready.then(start); + }; + $('#search-order').value = order; + $('#search-order').onchange = function () { + order = this.value; + results.sort(comparator); + render(); + }; + dom.list = $('#search-results-list'); dom.container = $('#search-results'); dom.container.dataset.empty = ''; - dom.error = $('#search-results-error'); - dom.nav = {}; const navOnClick = {prev, next}; for (const place of ['top', 'bottom']) { @@ -113,10 +103,6 @@ window.addEventListener('showStyles:done', function _() { } } - dom.list = $('#search-results-list'); - - addEventListener('scroll', loadMoreIfNeeded, {passive: true}); - if (FIREFOX) { let lastShift; addEventListener('resize', () => { @@ -130,43 +116,24 @@ window.addEventListener('showStyles:done', function _() { } addEventListener('styleDeleted', ({detail: {style: {id}}}) => { - const result = processedResults.find(r => r.installedStyleId === id); + restoreScrollPosition(); + const result = results.find(r => r.installedStyleId === id); if (result) { - result.installed = false; - result.installedStyleId = -1; - window.clearTimeout(result.pingbackTimer); - renderActionButtons($('#' + RESULT_ID_PREFIX + result.id)); + clearTimeout(result.pingbackTimer); + renderActionButtons(result.i, -1); } }); - addEventListener('styleAdded', ({detail: {style: {id, md5Url}}}) => { - const usoId = parseInt(md5Url && md5Url.match(/\d+|$/)[0]); - const result = usoId && processedResults.find(r => r.id === usoId); - if (result) { - result.installed = true; - result.installedStyleId = id; - renderActionButtons($('#' + RESULT_ID_PREFIX + usoId)); + addEventListener('styleAdded', async ({detail: {style}}) => { + restoreScrollPosition(); + const usoId = calcUsoId(style) || + calcUsoId(await API.getStyle(style.id, true)); + if (usoId && results.find(r => r.i === usoId)) { + renderActionButtons(usoId, style.id); } }); - - chromeLocal.getValue(CACHE_CLEANUP_NEEDED).then(value => - value && debounce(cleanupCache, CACHE_CLEANUP_THROTTLE)); } - //endregion - //region Loader - - /** - * Sets loading status of search results. - * @param {Boolean} isLoading If search results are idle (false) or still loading (true). - */ - function setLoading(isLoading) { - if (loading !== isLoading) { - loading = isLoading; - // Refresh elements that depend on `loading` state. - render(); - } - } function showSpinner(parent) { parent = parent instanceof Node ? parent : $(parent); @@ -174,166 +141,92 @@ window.addEventListener('showStyles:done', function _() { new Array(12).fill($create('div')).map(e => e.cloneNode()))); } - /** Increments displayedPage and loads results. */ function next() { - if (loading) { - debounce(next, 100); - return; - } - displayedPage += 1; + displayedPage = Math.min(totalPages, displayedPage + 1); scrollToFirstResult = true; render(); - loadMoreIfNeeded(); } - /** Decrements currentPage and loads results. */ function prev() { - if (loading) { - debounce(next, 100); - return; - } displayedPage = Math.max(1, displayedPage - 1); scrollToFirstResult = true; render(); } - /** - * Display error message to user. - * @param {string} message Message to display to user. - */ function error(reason) { - dom.error.textContent = reason === 404 ? t('searchResultNoneFound') : reason; - dom.error.classList.remove('hidden'); - dom.container.classList.toggle('hidden', !processedResults.length); - document.body.classList.toggle('search-results-shown', processedResults.length > 0); + dom.error.textContent = reason; + show(dom.error); + (results.length ? show : hide)(dom.container); + document.body.classList.toggle('search-results-shown', results.length > 0); if (dom.error.getBoundingClientRect().bottom < 0) { dom.error.scrollIntoView({behavior: 'smooth', block: 'start'}); } } - /** - * Initializes search results container, starts fetching results. - */ - function load() { - if (searchExhausted > 1) { - if (!processedResults.length) { - error(404); + async function start() { + show(dom.container); + hide(dom.error); + results = []; + try { + for (let retry = 0; !results.length && retry <= 2; retry++) { + results = await search({retry}); } - return; - } - - setLoading(true); - dom.container.classList.remove('hidden'); - dom.error.classList.add('hidden'); - - category = category || getCategory(); - - search({category}) - .then(function process(results) { - const data = results.data.filter(sameCategoryNoDupes); - - if (!data.length && searchExhausted <= 1) { - const old = category; - const uso = (processedResults[0] || {}).subcategory; - category = uso !== category && uso || getCategory({retry: true}); - if (category !== old) return search({category, restart: true}).then(process); - } - - const numIrrelevant = results.data.length - data.length; - totalResults += results.current_page === 1 ? results.total_entries : 0; - totalResults = Math.max(0, totalResults - numIrrelevant); - totalPages = Math.ceil(totalResults / DISPLAY_PER_PAGE); - - setLoading(false); - - if (data.length) { - unprocessedResults.push(...data); - processNextResult(); - } else if (numIrrelevant) { - load(); - } else if (!processedResults.length) { - return Promise.reject(404); - } - }) - .catch(error); - } - - function loadMoreIfNeeded(event) { - let pageToPrefetch = displayedPage; - if (event instanceof Event) { - if ((loadMoreIfNeeded.prefetchedPage || 0) <= pageToPrefetch && - document.scrollingElement.scrollTop > document.scrollingElement.scrollHeight / 2) { - loadMoreIfNeeded.prefetchedPage = ++pageToPrefetch; - } else { - return; + if (results.length) { + const installedStyles = await API.getAllStyles(true); + const allUsoIds = new Set(installedStyles.map(calcUsoId)); + results = results.filter(r => !allUsoIds.has(r.i)); } - } - if (processedResults.length < pageToPrefetch * DISPLAY_PER_PAGE) { - setTimeout(load, DELAY_BEFORE_SEARCHING_STYLES); - } - } - - /** - * Processes the next search result in `unprocessedResults` and adds to `processedResults`. - * Skips installed/non-applicable styles. - * Fetches more search results if unprocessedResults is empty. - * Recurses until shouldLoadMore() is false. - */ - function processNextResult() { - const result = unprocessedResults.shift(); - if (!result) { - loadMoreIfNeeded(); - return; - } - const md5Url = UPDATE_URL.replace('%', result.id); - API.styleExists({md5Url}).then(exist => { - if (exist) { - totalResults = Math.max(0, totalResults - 1); - } else { - processedResults.push(result); + if (results.length || $('#search-query').value) { render(); + } else { + error(t('searchResultNoneFound')); } - setTimeout(processNextResult, !exist && DELAY_AFTER_FETCHING_STYLES); - }); + } catch (reason) { + error(reason); + } } - //endregion - //region UI - function render() { - let start = (displayedPage - 1) * DISPLAY_PER_PAGE; - const end = displayedPage * DISPLAY_PER_PAGE; - + totalPages = Math.ceil(results.length / PAGE_LENGTH); + displayedPage = Math.min(displayedPage, totalPages) || 1; + let start = (displayedPage - 1) * PAGE_LENGTH; + const end = displayedPage * PAGE_LENGTH; let plantAt = 0; let slot = dom.list.children[0]; - // keep rendered elements with ids in the range of interest while ( - plantAt < DISPLAY_PER_PAGE && - slot && slot.id === 'search-result-' + (processedResults[start] || {}).id + plantAt < PAGE_LENGTH && + slot && slot.id === 'search-result-' + (results[start] || {}).i ) { slot = slot.nextElementSibling; plantAt++; start++; } - - const plantEntry = entry => { + // add new elements + while (start < Math.min(end, results.length)) { + const entry = createSearchResultNode(results[start++]); if (slot) { dom.list.replaceChild(entry, slot); slot = entry.nextElementSibling; } else { dom.list.appendChild(entry); } - entry.classList.toggle('search-result-fadein', - !slot || performance.now() - slot._plantedTime > FADEIN_THRESHOLD); - return entry; - }; - - while (start < Math.min(end, processedResults.length)) { - plantEntry(createSearchResultNode(processedResults[start++])); plantAt++; } - + // remove extraneous elements + const pageLen = end > results.length && + results.length % PAGE_LENGTH || + Math.min(results.length, PAGE_LENGTH); + while (dom.list.children.length > pageLen) { + dom.list.lastElementChild.remove(); + } + if (results.length && 'empty' in dom.container.dataset) { + delete dom.container.dataset.empty; + } + if (scrollToFirstResult && (!FIREFOX || FIREFOX >= 55)) { + debounce(doScrollToFirstResult); + } + // navigation for (const place in dom.nav) { const nav = dom.nav[place]; nav._prev.disabled = displayedPage <= 1; @@ -341,34 +234,6 @@ window.addEventListener('showStyles:done', function _() { nav._page.textContent = displayedPage; nav._total.textContent = totalPages; } - - // Fill in remaining search results with blank results + spinners - const maxResults = end > totalResults && - totalResults % DISPLAY_PER_PAGE || - DISPLAY_PER_PAGE; - while (plantAt < maxResults) { - if (!slot || slot.id.startsWith(RESULT_ID_PREFIX)) { - const entry = plantEntry(template.emptySearchResult.cloneNode(true)); - entry._plantedTime = performance.now(); - showSpinner(entry); - } - plantAt++; - if (!processedResults.length) { - break; - } - } - - while (dom.list.children.length > maxResults) { - dom.list.lastElementChild.remove(); - } - - if (processedResults.length && 'empty' in dom.container.dataset) { - delete dom.container.dataset.empty; - } - - if (scrollToFirstResult && (!FIREFOX || FIREFOX >= 55)) { - debounce(doScrollToFirstResult); - } } function doScrollToFirstResult() { @@ -379,96 +244,61 @@ window.addEventListener('showStyles:done', function _() { } /** - * Constructs and adds the given search result to the popup's Search Results container. - * @param {Object} result The SearchResult object from userstyles.org + * @param {IndexEntry} result + * @returns {Node} */ function createSearchResultNode(result) { - /* - userstyleSearchResult format: { - id: 100835, - name: "Reddit Flat Dark", - screenshot_url: "19339_after.png", - description: "...", - user: { - id: 48470, - name: "holloh" - }, - style_settings: [...] - } - */ - const entry = template.searchResult.cloneNode(true); - Object.assign(entry, { - _result: result, - id: RESULT_ID_PREFIX + result.id, - }); - + const { + i: id, + n: name, + r: rating, + u: updateTime, + w: weeklyInstalls, + t: totalInstalls, + an: author, + sa: shotArchived, + sn: shotName, + } = entry._result = result; + entry.id = RESULT_ID_PREFIX + id; + // title Object.assign($('.search-result-title', entry), { onclick: handleEvent.openURLandHide, - href: BASE_URL + result.url + href: URLS.usoArchive + `?category=${category}&style=${id}` }); - - const displayedName = result.name.length < 300 ? result.name : result.name.slice(0, 300) + '...'; - $('.search-result-title span', entry).textContent = tWordBreak(displayedName); - - const screenshot = $('.search-result-screenshot', entry); - let url = result.screenshot_url; - if (!url) { - url = BLANK_PIXEL_DATA; - screenshot.classList.add('no-screenshot'); - } else if (/^[0-9]*_after.(jpe?g|png|gif)$/i.test(url)) { - url = BASE_URL + '/style_screenshot_thumbnails/' + url; - } - screenshot.src = url; - if (url !== BLANK_PIXEL_DATA) { - screenshot.classList.add('search-result-fadein'); - screenshot.onload = () => { - screenshot.classList.remove('search-result-fadein'); - }; - } - - const description = result.description - .replace(/<[^>]*>/g, ' ') - .replace(/([^.][.。?!]|[\s,].{50,70})\s+/g, '$1\n') - .replace(/([\r\n]\s*){3,}/g, '\n\n'); - Object.assign($('.search-result-description', entry), { - textContent: description, - title: description, + $('.search-result-title span', entry).textContent = + tWordBreak(name.length < 300 ? name : name.slice(0, 300) + '...'); + // screenshot + const auto = URLS.uso + `auto_style_screenshots/${id}${USO_AUTO_PIC_SUFFIX}`; + Object.assign($('.search-result-screenshot', entry), { + src: shotName && !shotName.endsWith(USO_AUTO_PIC_SUFFIX) + ? `${shotArchived ? URLS.usoArchiveRaw : URLS.uso + 'style_'}screenshots/${shotName}` + : auto, + _src: auto, + onerror: fixScreenshot, }); - + // author Object.assign($('[data-type="author"] a', entry), { - textContent: result.user.name, - title: result.user.name, - href: BASE_URL + '/users/' + result.user.id, + textContent: author, + title: author, + href: URLS.usoArchive + '?author=' + encodeURIComponent(author).replace(/%20/g, '+'), onclick: handleEvent.openURLandHide, }); - - let ratingClass; - let ratingValue = result.rating; - if (ratingValue === null) { - ratingClass = 'none'; - ratingValue = ''; - } else if (ratingValue >= 2.5) { - ratingClass = 'good'; - ratingValue = ratingValue.toFixed(1); - } else if (ratingValue >= 1.5) { - ratingClass = 'okay'; - ratingValue = ratingValue.toFixed(1); - } else { - ratingClass = 'bad'; - ratingValue = ratingValue.toFixed(1); - } - $('[data-type="rating"]', entry).dataset.class = ratingClass; - $('[data-type="rating"] dd', entry).textContent = ratingValue; - + // rating + $('[data-type="rating"]', entry).dataset.class = + !rating ? 'none' : + rating >= 2.5 ? 'good' : + rating >= 1.5 ? 'okay' : + 'bad'; + $('[data-type="rating"] dd', entry).textContent = rating && rating.toFixed(1) || ''; + // time Object.assign($('[data-type="updated"] time', entry), { - dateTime: result.updated, - textContent: formatDate(result.updated) + dateTime: updateTime * 1000, + textContent: formatDate(updateTime * 1000) }); - - $('[data-type="weekly"] dd', entry).textContent = formatNumber(result.weekly_install_count); - $('[data-type="total"] dd', entry).textContent = formatNumber(result.total_install_count); - + // totals + $('[data-type="weekly"] dd', entry).textContent = formatNumber(weeklyInstalls); + $('[data-type="total"] dd', entry).textContent = formatNumber(totalInstalls); renderActionButtons(entry); return entry; } @@ -484,141 +314,123 @@ window.addEventListener('showStyles:done', function _() { ); } - function renderActionButtons(entry) { - if (!entry) { - return; + function fixScreenshot() { + const {_src} = this; + if (_src && _src !== this.src) { + this.src = _src; + delete this._src; + } else { + this.src = BLANK_PIXEL; + this.onerror = null; } - const result = entry._result; + } - if (result.installed && !('installed' in entry.dataset)) { + function renderActionButtons(entry, installedId) { + if (Number(entry)) { + entry = $('#' + RESULT_ID_PREFIX + entry); + } + if (!entry) return; + const result = entry._result; + if (typeof installedId === 'number') { + result.installed = installedId > 0; + result.installedStyleId = installedId; + } + const isInstalled = result.installed; + if (isInstalled && !('installed' in entry.dataset)) { entry.dataset.installed = ''; $('.search-result-status', entry).textContent = t('clickToUninstall'); - } else if (!result.installed && 'installed' in entry.dataset) { + } else if (!isInstalled && 'installed' in entry.dataset) { delete entry.dataset.installed; $('.search-result-status', entry).textContent = ''; + hide('.search-result-customize', entry); } + Object.assign($('.search-result-screenshot', entry), { + onclick: isInstalled ? uninstall : install, + title: isInstalled ? '' : t('installButton'), + }); + $('.search-result-uninstall', entry).onclick = uninstall; + $('.search-result-install', entry).onclick = install; + } - const screenshot = $('.search-result-screenshot', entry); - screenshot.onclick = result.installed ? onUninstallClicked : onInstallClicked; - screenshot.title = result.installed ? '' : t('installButton'); - - const uninstallButton = $('.search-result-uninstall', entry); - uninstallButton.onclick = onUninstallClicked; - - const installButton = $('.search-result-install', entry); - installButton.onclick = onInstallClicked; - if ((result.style_settings || []).length > 0) { - // Style has customizations - installButton.classList.add('customize'); - uninstallButton.classList.add('customize'); - - const customizeButton = $('.search-result-customize', entry); - customizeButton.dataset.href = BASE_URL + result.url; - customizeButton.dataset.sendMessage = JSON.stringify({method: 'openSettings'}); - customizeButton.classList.remove('hidden'); - customizeButton.onclick = function (event) { - event.stopPropagation(); - handleEvent.openURLandHide.call(this, event); - }; + function renderFullInfo(entry, style) { + let {description, vars} = style.usercssData; + // description + description = (description || '') + .replace(/<[^>]*>/g, ' ') + .replace(/([^.][.。?!]|[\s,].{50,70})\s+/g, '$1\n') + .replace(/([\r\n]\s*){3,}/g, '\n\n'); + Object.assign($('.search-result-description', entry), { + textContent: description, + title: description, + }); + // config button + if (vars) { + const btn = $('.search-result-customize', entry); + btn.onclick = () => $('.configure', $.entry(style)).click(); + show(btn); } } - function onUninstallClicked(event) { - event.stopPropagation(); + async function install() { const entry = this.closest('.search-result'); - saveScrollPosition(entry); - API.deleteStyle(entry._result.installedStyleId) - .then(restoreScrollPosition); - } - - /** Installs the current userstyleSearchResult into Stylus. */ - function onInstallClicked(event) { - event.stopPropagation(); - - const entry = this.closest('.search-result'); - const result = entry._result; + const result = /** @type IndexEntry */ entry._result; + const {i: id} = result; const installButton = $('.search-result-install', entry); showSpinner(entry); saveScrollPosition(entry); installButton.disabled = true; entry.style.setProperty('pointer-events', 'none', 'important'); + // FIXME: move this to background page and create an API like installUSOStyle + result.pingbackTimer = setTimeout(download, PINGBACK_DELAY, + `${URLS.uso}/styles/install/${id}?source=stylish-ch`); - // Fetch settings to see if we should display "configure" button - Promise.all([ - fetchStyleJson(result), - fetchStyleSettings(result), - API.download({url: UPDATE_URL.replace('%', result.id)}) - ]) - .then(([style, settings, md5]) => { - pingback(result); - // show a 'config-on-homepage' icon in the popup - style.updateUrl += settings.length ? '?' : ''; - style.originalMd5 = md5; - return API.installStyle(style); - }) - .catch(reason => { - const usoId = result.id; - console.debug('install:saveStyle(usoID:', usoId, ') => [ERROR]: ', reason); - error('Error while downloading usoID:' + usoId + '\nReason: ' + reason); - }) - .then(() => { - $.remove('.lds-spinner', entry); - installButton.disabled = false; - entry.style.pointerEvents = ''; - restoreScrollPosition(); - }); - - function fetchStyleSettings(result) { - return result.style_settings || - fetchStyle(result.id).then(style => { - result.style_settings = style.style_settings || []; - return result.style_settings; - }); + const updateUrl = `${URLS.usoArchiveRaw}usercss/${id}.user.css`; + try { + const sourceCode = await download(updateUrl); + const style = await API.installUsercss({sourceCode, updateUrl}); + renderFullInfo(entry, style); + } catch (reason) { + error(`Error while downloading usoID:${id}\nReason: ${reason}`); } + $.remove('.lds-spinner', entry); + installButton.disabled = false; + entry.style.pointerEvents = ''; } - function pingback(result) { - const wnd = window; - // FIXME: move this to background page and create an API like installUSOStyle - result.pingbackTimer = wnd.setTimeout(wnd.download, PINGBACK_DELAY, - BASE_URL + '/styles/install/' + result.id + '?source=stylish-ch'); + function uninstall() { + const entry = this.closest('.search-result'); + saveScrollPosition(entry); + API.deleteStyle(entry._result.installedStyleId); } function saveScrollPosition(entry) { - dom.scrollPosition = entry.getBoundingClientRect().top; - dom.scrollPositionElement = entry; + dom.scrollPos = entry.getBoundingClientRect().top; + dom.scrollPosElement = entry; } function restoreScrollPosition() { - const t0 = performance.now(); - new MutationObserver((mutations, observer) => { - if (performance.now() - t0 < 1000) { - window.scrollBy(0, dom.scrollPositionElement.getBoundingClientRect().top - dom.scrollPosition); - } - observer.disconnect(); - }).observe(document.body, {childList: true, subtree: true, attributes: true}); + window.scrollBy(0, dom.scrollPosElement.getBoundingClientRect().top - dom.scrollPos); } - //endregion - //region USO API wrapper - /** * Resolves the Userstyles.org "category" for a given URL. + * @returns {boolean} true if the category has actually changed */ - function getCategory({retry} = {}) { + function calcCategory({retry} = {}) { const u = tryCatch(() => new URL(tabURL)); + const old = category; if (!u) { // Invalid URL - return ''; + category = ''; } else if (u.protocol === 'file:') { - return 'file:'; + category = 'file:'; } else if (u.protocol === location.protocol) { - return STYLUS_CATEGORY; + category = STYLUS_CATEGORY; } else { const parts = u.hostname.replace(/\.(?:com?|org)(\.\w{2,3})$/, '$1').split('.'); const [tld, main = u.hostname, third, fourth] = parts.reverse(); - const keepTld = !retry && !( + const keepTld = retry !== 1 && !( tld === 'com' || tld === 'org' && main !== 'userstyles' ); @@ -626,214 +438,61 @@ window.addEventListener('showStyles:done', function _() { fourth || third && third !== 'www' && third !== 'm' ); - return (keepThird && `${third}.` || '') + main + (keepTld || keepThird ? `.${tld}` : ''); + category = (keepThird && `${third}.` || '') + main + (keepTld || keepThird ? `.${tld}` : ''); } + return category !== old; } - function sameCategoryNoDupes(result) { + async function fetchIndex() { + const timer = setTimeout(showSpinner, BUSY_DELAY, dom.list); + index = await download(INDEX_URL, {responseType: 'json'}); + clearTimeout(timer); + $.remove(':scope > .lds-spinner', dom.list); + return index; + } + + async function search({retry} = {}) { + return retry && !calcCategory({retry}) + ? [] + : (index || await fetchIndex()).filter(isResultMatching).sort(comparator); + } + + function isResultMatching(res) { return ( - result.subcategory && - !processedResults.some(pr => pr.id === result.id) && - (category !== STYLUS_CATEGORY || /\bStylus\b/i.test(result.name + result.description)) && - category.split('.').includes(result.subcategory.split('.')[0]) + res.f === 'uso' && + res.c === category && ( + category === STYLUS_CATEGORY + ? /\bStylus\b/.test(res.n) + : !query.length || query.every(isInHaystack, calcHaystack(res)) + ) ); } - /** - * Fetches the JSON style object from userstyles.org (containing code, sections, updateUrl, etc). - * Stores (caches) the JSON within the given result, to avoid unnecessary network usage. - * Style JSON is fetched from the /styles/chrome/{id}.json endpoint. - * @param {Object} result A search result object from userstyles.org - * @returns {Promise} Promises the response as a JSON object. - */ - function fetchStyleJson(result) { - return Promise.resolve( - result.json || - downloadFromUSO(JSON_URL + result.id + '.json').then(json => { - result.json = json; - return json; - })); + /** @this {IndexEntry} haystack */ + function isInHaystack(needle) { + return needle === this._year || this._nLC.includes(needle); } /** - * Fetches style information from userstyles.org's /api/v1/styles/{ID} API. - * @param {number} userstylesId The internal "ID" for a style on userstyles.org - * @returns {Promise} An object containing info about the style, e.g. name, author, etc. + * @param {IndexEntry} a + * @param {IndexEntry} b */ - function fetchStyle(userstylesId) { - return readCache(userstylesId).then(json => - json || - downloadFromUSO(API_URL + userstylesId).then(writeCache)); + function comparator(a, b) { + return ( + order === 'n' + ? a.n < b.n ? -1 : a.n > b.n + : b[order] - a[order] + ) || b.t - a.t; } - /** - * Fetches (and JSON-parses) search results from a userstyles.org search API. - * Automatically sets searchCurrentPage and searchTotalPages. - * @param {string} category The usrestyles.org "category" (subcategory) OR a any search string. - * @return {Object} Response object from userstyles.org - */ - function search({category, restart}) { - if (restart) { - searchCurrentPage = 1; - searchTotalPages = undefined; - } - if (searchTotalPages !== undefined && searchCurrentPage > searchTotalPages) { - return Promise.resolve({'data':[]}); - } - - const searchURL = API_URL + 'subcategory' + - '?search=' + encodeURIComponent(category) + - '&page=' + searchCurrentPage + - '&per_page=10' + - '&country=NA'; - - const cacheKey = category + '/' + searchCurrentPage; - - return readCache(cacheKey) - .then(json => - json || - downloadFromUSO(searchURL).then(writeCache)) - .then(json => { - searchCurrentPage = json.current_page + 1; - searchTotalPages = json.total_pages; - searchExhausted += searchCurrentPage > searchTotalPages; - return json; - }).catch(reason => { - searchExhausted++; - return Promise.reject(reason); - }); + function calcUsoId({md5Url: m, updateUrl}) { + return parseInt(m && m.match(/\d+|$/)[0]) || + URLS.extractUsoArchiveId(updateUrl); } - //endregion - //region Cache - - function readCache(id) { - const key = CACHE_PREFIX + id; - return chromeLocal.getValue(key).then(item => { - if (!cacheItemExpired(item)) { - return chromeLocal.loadLZStringScript().then(() => - tryJSONparse(LZString.decompressFromUTF16(item.payload))); - } else if (item) { - chromeLocal.remove(key); - } - }); + function calcHaystack(res) { + if (!res._nLC) res._nLC = res.n.toLocaleLowerCase(); + if (!res._year) res._year = new Date(res.u * 1000).getFullYear(); + return res; } - - function writeCache(data, debounced) { - data.id = data.id || category + '/' + data.current_page; - for (const prop of CACHE_EXCEPT_PROPS) { - delete data[prop]; - } - if (!debounced) { - // using plain setTimeout because debounce() replaces previous parameters - setTimeout(writeCache, 100, data, true); - return data; - } else { - chromeLocal.setValue(CACHE_CLEANUP_NEEDED, true); - debounce(cleanupCache, CACHE_CLEANUP_THROTTLE); - return chromeLocal.loadLZStringScript().then(() => - chromeLocal.setValue(CACHE_PREFIX + data.id, { - payload: LZString.compressToUTF16(JSON.stringify(data)), - date: Date.now(), - })).then(() => data); - } - } - - function cacheItemExpired(item) { - return !item || !item.date || Date.now() - item.date > CACHE_DURATION; - } - - function cleanupCache() { - chromeLocal.remove(CACHE_CLEANUP_NEEDED); - Promise.resolve(!browser.storage.local.getBytesInUse ? 1e99 : browser.storage.local.getBytesInUse()) - .then(size => size > CACHE_SIZE && chromeLocal.get().then(cleanupCacheInternal)); - } - - function cleanupCacheInternal(storage) { - const sortedByTime = Object.keys(storage) - .filter(key => key.startsWith(CACHE_PREFIX)) - .map(key => Object.assign(storage[key], {key})) - .sort((a, b) => a.date - b.date); - const someExpired = cacheItemExpired(sortedByTime[0]); - const expired = someExpired ? sortedByTime.filter(cacheItemExpired) : - sortedByTime.slice(0, sortedByTime.length / 2); - const toRemove = expired.length ? expired : sortedByTime; - if (toRemove.length) { - chromeLocal.remove(toRemove.map(item => item.key)); - } - } - - //endregion - //region USO referrer spoofing - - function downloadFromUSO(url) { - const requestId = getRandomId(); - xhrSpoofIds.add(requestId); - xhrSpoofStart(); - return download(url, { - body: null, - responseType: 'json', - timeout: 60e3, - headers: { - 'Referrer-Policy': 'origin-when-cross-origin', - [xhrSpoofTelltale]: requestId, - } - }).then(data => { - xhrSpoofDone(requestId); - return data; - }).catch(data => { - xhrSpoofDone(requestId); - return Promise.reject(data); - }); - } - - function xhrSpoofStart() { - if (chrome.webRequest.onBeforeSendHeaders.hasListener(xhrSpoof)) { - return; - } - const urls = [API_URL + '*', JSON_URL + '*']; - const types = ['xmlhttprequest']; - const options = ['blocking', 'requestHeaders']; - // spoofing Referer requires extraHeaders in Chrome 72+ - if (chrome.webRequest.OnBeforeSendHeadersOptions.EXTRA_HEADERS) { - options.push(chrome.webRequest.OnBeforeSendHeadersOptions.EXTRA_HEADERS); - } - chrome.webRequest.onBeforeSendHeaders.addListener(xhrSpoof, {urls, types}, options); - } - - function xhrSpoofDone(requestId) { - xhrSpoofIds.delete(requestId); - if (!xhrSpoofIds.size) { - chrome.webRequest.onBeforeSendHeaders.removeListener(xhrSpoof); - } - } - - function xhrSpoof({requestHeaders}) { - let referer, hasTelltale; - for (let i = requestHeaders.length; --i >= 0;) { - const header = requestHeaders[i]; - if (header.name.toLowerCase() === 'referer') { - referer = header; - } else if (header.name === xhrSpoofTelltale) { - hasTelltale = xhrSpoofIds.has(header.value); - requestHeaders.splice(i, 1); - } - } - if (!hasTelltale) { - // not our request (unlikely but just in case) - return; - } - if (referer) { - referer.value = BASE_URL; - } else { - requestHeaders.push({name: 'Referer', value: BASE_URL}); - } - return {requestHeaders}; - } - - function getRandomId() { - return btoa(Math.random()).replace(/[^a-z]/gi, ''); - } - - //endregion -}); +}, {once: true}); From 78b0e33ba43b825c5e93a1b4a3a2d44efc9ea726 Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 6 Oct 2020 13:49:43 +0300 Subject: [PATCH 28/55] faster install from known sites --- background/background.js | 12 +--- background/usercss-helper.js | 53 +----------------- background/usercss-install-helper.js | 82 ++++++++++++++++++++++++++++ install-usercss/install-usercss.js | 4 +- js/messaging.js | 4 ++ manifest.json | 1 + 6 files changed, 93 insertions(+), 63 deletions(-) create mode 100644 background/usercss-install-helper.js diff --git a/background/background.js b/background/background.js index 2dd15b0a..d03250b6 100644 --- a/background/background.js +++ b/background/background.js @@ -1,8 +1,8 @@ /* global download prefs openURL FIREFOX CHROME - URLS ignoreChromeError usercssHelper chromeLocal semverCompare + URLS ignoreChromeError chromeLocal semverCompare styleManager msg navigatorUtil workerUtil contentScripts sync findExistingTab activateTab isTabReplaceable getActiveTab - tabManager */ +*/ 'use strict'; @@ -111,14 +111,6 @@ navigatorUtil.onUrlChange(({tabId, frameId}, type) => { } }); -tabManager.onUpdate(({tabId, url, oldUrl = ''}) => { - if (usercssHelper.testUrl(url) && !oldUrl.startsWith(URLS.installUsercss)) { - usercssHelper.testContents(tabId, url).then(data => { - if (data.code) usercssHelper.openInstallerPage(tabId, url, data); - }); - } -}); - if (FIREFOX) { // FF misses some about:blank iframes so we inject our content script explicitly navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, { diff --git a/background/usercss-helper.js b/background/usercss-helper.js index 3f6081f6..00b3a99b 100644 --- a/background/usercss-helper.js +++ b/background/usercss-helper.js @@ -1,15 +1,8 @@ -/* global API_METHODS usercss styleManager deepCopy openURL download URLS */ +/* global API_METHODS usercss styleManager deepCopy */ /* exported usercssHelper */ 'use strict'; const usercssHelper = (() => { - const installCodeCache = {}; - const clearInstallCode = url => delete installCodeCache[url]; - const isResponseText = r => /^text\/(css|plain)(;.*?)?$/i.test(r.headers.get('content-type')); - // in Firefox we have to use a content script to read file:// - const fileLoader = !chrome.app && // not relying on navigator.ua which can be spoofed - (tabId => browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}).then(r => r[0])); - API_METHODS.installUsercss = installUsercss; API_METHODS.editSaveUsercss = editSaveUsercss; API_METHODS.configUsercssVars = configUsercssVars; @@ -17,50 +10,6 @@ const usercssHelper = (() => { API_METHODS.buildUsercss = build; API_METHODS.findUsercss = find; - API_METHODS.getUsercssInstallCode = url => { - // when the installer tab is reloaded after the cache is expired, this will throw intentionally - const {code, timer} = installCodeCache[url]; - clearInstallCode(url); - clearTimeout(timer); - return code; - }; - - return { - - testUrl(url) { - return url.includes('.user.') && - /^(https?|file|ftps?):/.test(url) && - /\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]); - }, - - /** @return {Promise<{ code:string, inTab:boolean } | false>} */ - testContents(tabId, url) { - const isFile = url.startsWith('file:'); - const inTab = isFile && Boolean(fileLoader); - return Promise.resolve(isFile || fetch(url, {method: 'HEAD'}).then(isResponseText)) - .then(ok => ok && (inTab ? fileLoader(tabId) : download(url))) - .then(code => /==userstyle==/i.test(code) && {code, inTab}); - }, - - openInstallerPage(tabId, url, {code, inTab} = {}) { - const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`; - if (inTab) { - browser.tabs.get(tabId).then(tab => - openURL({ - url: `${newUrl}&tabId=${tabId}`, - active: tab.active, - index: tab.index + 1, - openerTabId: tabId, - currentWindow: null, - })); - } else { - const timer = setTimeout(clearInstallCode, 10e3, url); - installCodeCache[url] = {code, timer}; - chrome.tabs.update(tabId, {url: newUrl}); - } - }, - }; - function buildMeta(style) { if (style.usercssData) { return Promise.resolve(style); diff --git a/background/usercss-install-helper.js b/background/usercss-install-helper.js new file mode 100644 index 00000000..b854564a --- /dev/null +++ b/background/usercss-install-helper.js @@ -0,0 +1,82 @@ +/* global API_METHODS openURL download URLS tabManager */ +'use strict'; + +(() => { + const installCodeCache = {}; + const clearInstallCode = url => delete installCodeCache[url]; + const isContentTypeText = type => /^text\/(css|plain)(;.*?)?$/i.test(type); + + // in Firefox we have to use a content script to read file:// + const fileLoader = !chrome.app && ( + async tabId => + (await browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}))[0]); + + const urlLoader = + async (tabId, url) => ( + url.startsWith('file:') || + tabManager.get(tabId, isContentTypeText.name) || + isContentTypeText((await fetch(url, {method: 'HEAD'})).headers.get('content-type')) + ) && download(url); + + API_METHODS.getUsercssInstallCode = url => { + // when the installer tab is reloaded after the cache is expired, this will throw intentionally + const {code, timer} = installCodeCache[url]; + clearInstallCode(url); + clearTimeout(timer); + return code; + }; + + // Faster installation on known distribution sites to avoid flicker of css text + chrome.webRequest.onBeforeSendHeaders.addListener(({tabId, url}) => { + openInstallerPage(tabId, url, {}); + // Silently suppressing navigation like it never happened + return {redirectUrl: 'javascript:void 0'}; // eslint-disable-line no-script-url + }, { + urls: [ + URLS.usoArchiveRaw + 'usercss/*.user.css', + '*://greasyfork.org/scripts/*/code/*.user.css', + '*://sleazyfork.org/scripts/*/code/*.user.css', + ], + types: ['main_frame'], + }, ['blocking']); + + // Remember Content-Type to avoid re-fetching of the headers in urlLoader as it can be very slow + chrome.webRequest.onHeadersReceived.addListener(({tabId, responseHeaders}) => { + const h = responseHeaders.find(h => h.name.toLowerCase() === 'content-type'); + tabManager.set(tabId, isContentTypeText.name, h && isContentTypeText(h.value) || undefined); + }, { + urls: '%css,%css?*,%styl,%styl?*'.replace(/%/g, '*://*/*.user.').split(','), + types: ['main_frame'], + }, ['responseHeaders']); + + tabManager.onUpdate(async ({tabId, url, oldUrl = ''}) => { + if (url.includes('.user.') && + /^(https?|file|ftps?):/.test(url) && + /\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]) && + !oldUrl.startsWith(URLS.installUsercss)) { + const inTab = url.startsWith('file:') && Boolean(fileLoader); + const code = await (inTab ? fileLoader : urlLoader)(tabId, url); + if (/==userstyle==/i.test(code)) { + openInstallerPage(tabId, url, {code, inTab}); + } + } + }); + + function openInstallerPage(tabId, url, {code, inTab} = {}) { + const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`; + if (inTab) { + browser.tabs.get(tabId).then(tab => + openURL({ + url: `${newUrl}&tabId=${tabId}`, + active: tab.active, + index: tab.index + 1, + openerTabId: tabId, + currentWindow: null, + })); + } else { + const timer = setTimeout(clearInstallCode, 10e3, url); + installCodeCache[url] = {code, timer}; + chrome.tabs.update(tabId, {url: newUrl}); + } + } +})(); diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js index 82bb8b73..4354483c 100644 --- a/install-usercss/install-usercss.js +++ b/install-usercss/install-usercss.js @@ -318,7 +318,9 @@ let sequence = null; if (tabId < 0) { getData = DirectDownloader(); - sequence = API.getUsercssInstallCode(initialUrl).catch(getData); + sequence = API.getUsercssInstallCode(initialUrl) + .then(code => code || getData()) + .catch(getData); } else { getData = PortDownloader(); sequence = getData({timer: false}); diff --git a/js/messaging.js b/js/messaging.js index 8c215cac..ec51267c 100644 --- a/js/messaging.js +++ b/js/messaging.js @@ -72,6 +72,10 @@ const URLS = { url.startsWith(URLS.usoArchiveRaw) && parseInt(url.match(/\/(\d+)\.user\.css|$/)[1]), + extractGreasyForkId: url => + /^https:\/\/(?:greasy|sleazy)fork\.org\/scripts\/(\d+)[^/]*\/code\/[^/]*\.user\.css$/.test(url) && + RegExp.$1, + supported: url => ( url.startsWith('http') && (FIREFOX || !url.startsWith(URLS.browserWebStore)) || url.startsWith('ftp') || diff --git a/manifest.json b/manifest.json index ad3859f2..a3fc105f 100644 --- a/manifest.json +++ b/manifest.json @@ -51,6 +51,7 @@ "background/icon-manager.js", "background/background.js", "background/usercss-helper.js", + "background/usercss-install-helper.js", "background/style-via-api.js", "background/search-db.js", "background/update.js", From 4913da2e1956bac44dee9f93b32e23671b881657 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 7 Oct 2020 17:59:21 +0300 Subject: [PATCH 29/55] use installation url on known sites as homepage --- background/style-manager.js | 9 ++++++++- edit/source-editor.js | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/background/style-manager.js b/background/style-manager.js index 4ddb6411..4ae2a5c2 100644 --- a/background/style-manager.js +++ b/background/style-manager.js @@ -1,6 +1,6 @@ /* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */ /* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty styleSectionGlobal - getStyleWithNoCode msg sync uuidv4 */ + getStyleWithNoCode msg sync uuidv4 URLS */ /* exported styleManager */ 'use strict'; @@ -226,6 +226,13 @@ const styleManager = (() => { if (!reason) { reason = style ? 'update' : 'install'; } + let url = !data.url && data.updateUrl; + if (url) { + const usoId = URLS.extractUsoArchiveId(url); + url = usoId && `${URLS.usoArchive}?style=${usoId}` || + URLS.extractGreasyForkId(url) && url.match(/^.*?\/\d+/)[0]; + if (url) data.url = data.installationUrl = url; + } // FIXME: update updateDate? what about usercss config? return calcStyleDigest(data) .then(digest => { diff --git a/edit/source-editor.js b/edit/source-editor.js index a9f8bf22..b3292606 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -46,7 +46,7 @@ function createSourceEditor({style, onTitleChanged}) { metaCompiler.onUpdated(meta => { style.usercssData = meta; style.name = meta.name; - style.url = meta.homepageURL; + style.url = meta.homepageURL || style.installationUrl; updateMeta(); }); From 707cd6576f2d930211be50f21362027b6cb9e873 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 9 Oct 2020 14:37:29 +0300 Subject: [PATCH 30/55] process current contents when live-reload is enabled --- content/install-hook-usercss.js | 4 ++-- install-usercss/install-usercss.js | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/content/install-hook-usercss.js b/content/install-hook-usercss.js index 80e837ea..944c235b 100644 --- a/content/install-hook-usercss.js +++ b/content/install-hook-usercss.js @@ -5,10 +5,10 @@ if (typeof self.oldCode !== 'string') { self.oldCode = (document.querySelector('body > pre') || document.body).textContent; chrome.runtime.onConnect.addListener(port => { if (port.name !== 'downloadSelf') return; - port.onMessage.addListener(({id, timer}) => { + port.onMessage.addListener(({id, force}) => { fetch(location.href, {mode: 'same-origin'}) .then(r => r.text()) - .then(code => ({id, code: timer && code === self.oldCode ? null : code})) + .then(code => ({id, code: force || code !== self.oldCode ? code : null})) .catch(error => ({id, error: error.message || `${error}`})) .then(msg => { port.postMessage(msg); diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js index 82bb8b73..c074bf16 100644 --- a/install-usercss/install-usercss.js +++ b/install-usercss/install-usercss.js @@ -331,7 +331,11 @@ onToggled(e) { if (e) isEnabled = e.target.checked; if (installed || installedDup) { - (isEnabled ? start : stop)(); + if (isEnabled) { + check({force: true}); + } else { + stop(); + } $('.install').disabled = isEnabled; Object.assign($('#live-reload-install-hint'), { hidden: !isEnabled, @@ -340,8 +344,8 @@ } }, }; - function check() { - getData() + function check(opts) { + getData(opts) .then(update, logError) .then(() => { timer = 0; @@ -398,10 +402,11 @@ .then(tab => tab.url === initialUrl && location.reload()) .catch(closeCurrentTab); }); - return ({timer = true} = {}) => new Promise((resolve, reject) => { + return (opts = {}) => new Promise((resolve, reject) => { const id = performance.now(); resolvers.set(id, {resolve, reject}); - port.postMessage({id, timer}); + opts.id = id; + port.postMessage(opts); }); } } From 11ce144efb225a1ff3e678fcb918cd4c14f064b4 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 9 Oct 2020 19:40:11 +0300 Subject: [PATCH 31/55] remove the redundant stylus-lang warning filter --- edit/linter-engines.js | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/edit/linter-engines.js b/edit/linter-engines.js index 7f7717b8..3b3ca7a1 100644 --- a/edit/linter-engines.js +++ b/edit/linter-engines.js @@ -20,29 +20,19 @@ } }); - function stylelint(text, config, mode) { + function stylelint(text, config) { return editorWorker.stylelint(text, config) - .then(({results}) => { - if (!results[0]) { - return []; - } - const output = results[0].warnings.map(({line, column: ch, text, severity}) => - ({ - from: {line: line - 1, ch: ch - 1}, - to: {line: line - 1, ch}, - message: text - .replace('Unexpected ', '') - .replace(/^./, firstLetter => firstLetter.toUpperCase()) - .replace(/\s*\([^(]+\)$/, ''), // strip the rule, - rule: text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'), - severity, - }) - ); - return mode !== 'stylus' ? - output : - output.filter(({message}) => - !message.includes('"@css"') || !message.includes('(at-rule-no-unknown)')); - }); + .then(({results}) => !results[0] ? [] : + results[0].warnings.map(({line, column: ch, text, severity}) => ({ + from: {line: line - 1, ch: ch - 1}, + to: {line: line - 1, ch}, + message: text + .replace('Unexpected ', '') + .replace(/^./, firstLetter => firstLetter.toUpperCase()) + .replace(/\s*\([^(]+\)$/, ''), // strip the rule, + rule: text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'), + severity, + }))); } function csslint(text, config) { From af726405e16295fe8451b4c91a36820ee89094ad Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 10 Oct 2020 14:25:43 +0300 Subject: [PATCH 32/55] also search in global styles --- _locales/en/messages.json | 4 ++++ popup.html | 7 +++++++ popup/search-results.css | 8 +++++++- popup/search-results.js | 39 +++++++++++++++++++++++---------------- 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index a4f6eee9..e5f505f3 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1173,6 +1173,10 @@ "message": "Case-sensitive", "description": "Tooltip for the 'Aa' icon that enables case-sensitive search in the editor shown on Ctrl-F" }, + "searchGlobalStyles": { + "message": "Also search global styles", + "description": "Checkbox label in the popup's inline style search, shown when the text to search is entered" + }, "searchNumberOfResults": { "message": "Number of matches", "description": "Tooltip for the number of found search results in the editor shown on Ctrl-F" diff --git a/popup.html b/popup.html index 980eb227..677edb81 100644 --- a/popup.html +++ b/popup.html @@ -265,6 +265,13 @@ +
diff --git a/popup/search-results.css b/popup/search-results.css index d4c8d4e9..a7eb69cf 100755 --- a/popup/search-results.css +++ b/popup/search-results.css @@ -254,13 +254,19 @@ body.search-results-shown { #search-params { display: flex; position: relative; + margin-top: -.5rem; margin-bottom: 1.25rem; + flex-wrap: wrap; +} + +#search-params > * { + margin-top: .5rem; } #search-query { min-width: 3em; margin-right: .5em; - flex: auto; + flex: 1 1 0; } /* spinner: https://github.com/loadingio/css-spinner */ diff --git a/popup/search-results.js b/popup/search-results.js index a29b6911..590a2337 100755 --- a/popup/search-results.js +++ b/popup/search-results.js @@ -34,6 +34,7 @@ window.addEventListener('showStyles:done', () => { /** @type IndexEntry[] */ let index; let category = ''; + let searchGlobals = $('#search-globals').checked; /** @type string[] */ let query = []; /** @type 'n' | 'u' | 't' | 'w' | 'r' */ @@ -45,9 +46,9 @@ window.addEventListener('showStyles:done', () => { calcCategory(); - const $orNode = (sel, base) => sel instanceof Node ? sel : $(sel, base); - const show = (...args) => $orNode(...args).classList.remove('hidden'); - const hide = (...args) => $orNode(...args).classList.add('hidden'); + const $class = sel => (sel instanceof Node ? sel : $(sel)).classList; + const show = sel => $class(sel).remove('hidden'); + const hide = sel => $class(sel).add('hidden'); Object.assign($('#find-styles-link'), { href: URLS.usoArchive, @@ -70,6 +71,10 @@ window.addEventListener('showStyles:done', () => { function init() { setTimeout(() => document.body.classList.add('search-results-shown')); hide('#find-styles-inline-group'); + $('#search-globals').onchange = function () { + searchGlobals = this.checked; + ready = ready.then(start); + }; $('#search-query').oninput = function () { query = []; const text = this.value.trim().toLocaleLowerCase(); @@ -156,8 +161,7 @@ window.addEventListener('showStyles:done', () => { function error(reason) { dom.error.textContent = reason; show(dom.error); - (results.length ? show : hide)(dom.container); - document.body.classList.toggle('search-results-shown', results.length > 0); + hide(dom.list); if (dom.error.getBoundingClientRect().bottom < 0) { dom.error.scrollIntoView({behavior: 'smooth', block: 'start'}); } @@ -165,6 +169,7 @@ window.addEventListener('showStyles:done', () => { async function start() { show(dom.container); + show(dom.list); hide(dom.error); results = []; try { @@ -176,9 +181,9 @@ window.addEventListener('showStyles:done', () => { const allUsoIds = new Set(installedStyles.map(calcUsoId)); results = results.filter(r => !allUsoIds.has(r.i)); } - if (results.length || $('#search-query').value) { - render(); - } else { + render(); + (results.length ? show : hide)(dom.list); + if (!results.length && !$('#search-query').value) { error(t('searchResultNoneFound')); } } catch (reason) { @@ -445,7 +450,8 @@ window.addEventListener('showStyles:done', () => { async function fetchIndex() { const timer = setTimeout(showSpinner, BUSY_DELAY, dom.list); - index = await download(INDEX_URL, {responseType: 'json'}); + index = (await download(INDEX_URL, {responseType: 'json'})) + .filter(res => res.f === 'uso'); clearTimeout(timer); $.remove(':scope > .lds-spinner', dom.list); return index; @@ -459,18 +465,19 @@ window.addEventListener('showStyles:done', () => { function isResultMatching(res) { return ( - res.f === 'uso' && - res.c === category && ( - category === STYLUS_CATEGORY - ? /\bStylus\b/.test(res.n) - : !query.length || query.every(isInHaystack, calcHaystack(res)) - ) + res.c === category || + searchGlobals && res.c === 'global' && (query.length || calcHaystack(res)._nLC.includes(category)) + ) && ( + category === STYLUS_CATEGORY + ? /\bStylus\b/.test(res.n) + : !query.length || query.every(isInHaystack, calcHaystack(res)) ); } /** @this {IndexEntry} haystack */ function isInHaystack(needle) { - return needle === this._year || this._nLC.includes(needle); + return this._year === needle && this.c !== 'global' || + this._nLC.includes(needle); } /** From 4d1110986ceaac00c8d192f5fba5fb68274a4f4a Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 11 Oct 2020 09:44:17 +0300 Subject: [PATCH 33/55] update CSSLint * Scroll Snap L1 (CR 2020-09-18) * dedupe border* * fix font-variation-settings grammar --- vendor-overwrites/csslint/parserlib.js | 101 ++++++++++++++----------- 1 file changed, 55 insertions(+), 46 deletions(-) diff --git a/vendor-overwrites/csslint/parserlib.js b/vendor-overwrites/csslint/parserlib.js index f4dcd2e5..b9f7dc69 100644 --- a/vendor-overwrites/csslint/parserlib.js +++ b/vendor-overwrites/csslint/parserlib.js @@ -177,38 +177,14 @@ self.parserlib = (() => { 'bookmark-level': 'none | ', 'bookmark-state': 'open | closed', 'bookmark-target': 'none | | attr()', - 'border': '', - 'border-block-color': '{1,2}', - 'border-block-end': '', - 'border-block-end-color': '', - 'border-block-end-style': '', - 'border-block-end-width': '', - 'border-block-start': '', - 'border-block-start-color': '', - 'border-block-start-style': '', - 'border-block-start-width': '', - 'border-block-style': '{1,2}', - 'border-block-width': '{1,2}', - 'border-bottom': '', - 'border-bottom-color': '', + 'border-bottom-left-radius': '', 'border-bottom-right-radius': '', - 'border-bottom-style': '', - 'border-bottom-width': '', - 'border-boundary': 'none | parent | display', - 'border-inline-color': '{1,2}', - 'border-inline-end': '', - 'border-inline-end-color': '', - 'border-inline-end-style': '', - 'border-inline-end-width': '', - 'border-inline-start': '', - 'border-inline-start-color': '', - 'border-inline-start-style': '', - 'border-inline-start-width': '', - 'border-inline-style': '{1,2}', - 'border-inline-width': '{1,2}', + 'border-top-left-radius': '', + 'border-top-right-radius': '', + + 'border-boundary': 'none | parent | display', 'border-collapse': 'collapse | separate', - 'border-color': '{1,4}', 'border-image': '[ none | ] || ' + '[ / | / ? / ]? || ' + '', @@ -217,24 +193,8 @@ self.parserlib = (() => { 'border-image-slice': '', 'border-image-source': ' | none', 'border-image-width': '', - 'border-left': '', - 'border-left-color': '', - 'border-left-style': '', - 'border-left-width': '', 'border-radius': '', - 'border-right': '', - 'border-right-color': '', - 'border-right-style': '', - 'border-right-width': '', 'border-spacing': '{1,2}', - 'border-style': '{1,4}', - 'border-top': '', - 'border-top-color': '', - 'border-top-left-radius': '', - 'border-top-right-radius': '', - 'border-top-style': '', - 'border-top-width': '', - 'border-width': '{1,4}', 'bottom': '', 'box-decoration-break': 'slice | clone', 'box-shadow': '', @@ -370,7 +330,7 @@ self.parserlib = (() => { 'font-variant-ligatures': ' | normal | none', 'font-variant-numeric': ' | normal', 'font-variant-position': 'normal | sub | super', - 'font-variation-settings': 'normal | [ ]#', + 'font-variation-settings': 'normal | [ ]#', 'font-weight': '', '-ms-flex-align': 'start | end | center | stretch | baseline', '-ms-flex-order': '', @@ -581,6 +541,34 @@ self.parserlib = (() => { // S 'scale': 'none | {1,3}', + + 'scroll-behavior': 'auto | smooth', + 'scroll-margin': '{1,4}', + 'scroll-margin-bottom': '', + 'scroll-margin-left': '', + 'scroll-margin-right': '', + 'scroll-margin-top': '', + 'scroll-margin-block': '{1,2}', + 'scroll-margin-block-end': '', + 'scroll-margin-block-start': '', + 'scroll-margin-inline': '{1,2}', + 'scroll-margin-inline-end': '', + 'scroll-margin-inline-start': '', + 'scroll-padding': '{1,4}', + 'scroll-padding-left': '', + 'scroll-padding-right': '', + 'scroll-padding-top': '', + 'scroll-padding-bottom': '', + 'scroll-padding-block': '{1,2}', + 'scroll-padding-block-end': '', + 'scroll-padding-block-start': '', + 'scroll-padding-inline': '{1,2}', + 'scroll-padding-inline-end': '', + 'scroll-padding-inline-start': '', + 'scroll-snap-align': '[ none | start | end | center ]{1,2}', + 'scroll-snap-stop': 'normal | always', + 'scroll-snap-type': 'none | [ x | y | block | inline | both ] [ mandatory | proximity ]?', + 'scrollbar-color': 'auto | dark | light | {2}', 'scrollbar-width': 'auto | thin | none', 'shape-inside': 'auto | outside-shape | [ || shape-box ] | | display', @@ -692,6 +680,25 @@ self.parserlib = (() => { '-webkit-text-stroke-width': '', }; + for (const [k, reps] of Object.entries({ + 'border': '{1,4}', + 'border-bottom': '', + 'border-left': '', + 'border-right': '', + 'border-top': '', + 'border-block': '{1,2}', + 'border-block-end': '', + 'border-block-start': '', + 'border-inline': '{1,2}', + 'border-inline-end': '', + 'border-inline-start': '', + })) { + Properties[k] = ''; + Properties[`${k}-color`] = '' + reps; + Properties[`${k}-style`] = '' + reps; + Properties[`${k}-width`] = '' + reps; + } + //endregion //region ValidationTypes - definitions @@ -713,6 +720,8 @@ self.parserlib = (() => { '': 'scroll | fixed | local', + '': 'auto | | ', + '': 'inset() | circle() | ellipse() | polygon()', '': ' | none', From 740a16a5637e53248f25ea4b013a74614a2961e4 Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 11 Oct 2020 14:37:55 +0300 Subject: [PATCH 34/55] disconnect port explicitly in FF --- content/install-hook-usercss.js | 2 ++ install-usercss/install-usercss.js | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/content/install-hook-usercss.js b/content/install-hook-usercss.js index 944c235b..6e8be595 100644 --- a/content/install-hook-usercss.js +++ b/content/install-hook-usercss.js @@ -15,6 +15,8 @@ if (typeof self.oldCode !== 'string') { if (msg.code != null) self.oldCode = msg.code; }); }); + // FF keeps content scripts connected on navigation https://github.com/openstyles/stylus/issues/864 + addEventListener('pagehide', () => port.disconnect(), {once: true}); }); } diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js index c074bf16..a72b8359 100644 --- a/install-usercss/install-usercss.js +++ b/install-usercss/install-usercss.js @@ -398,9 +398,10 @@ } }); port.onDisconnect.addListener(() => { - browser.tabs.get(tabId) - .then(tab => tab.url === initialUrl && location.reload()) - .catch(closeCurrentTab); + chrome.tabs.get(tabId, tab => + !chrome.runtime.lastError && tab.url === initialUrl + ? location.reload() + : closeCurrentTab()); }); return (opts = {}) => new Promise((resolve, reject) => { const id = performance.now(); From 60fc6f2456025c90573a30314b763c8f260d70b3 Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 11 Oct 2020 17:13:25 +0300 Subject: [PATCH 35/55] Editor fixes, make sectioned editor open quickly again (#1061) * make usercss editor full-height again * make sectioned editor open quickly again * remove leftovers * autofocus when add/clone button is clicked * don't fit to content on clicking the add button * scroll the window to show a manually added section entirely * autofocus on a manually added applies-to * disable Save button while loading * use standard CSS for a focused CodeMirror outline * trigger refresh sooner by one viewport in advance * declare refreshOnView as a standard function * run fixedHeader asynchronously to prevent self-triggering * account for header in compact mode when fitting to content * code cosmetics --- edit.html | 3 +- edit/codemirror-default.css | 10 +-- edit/edit.css | 17 +---- edit/edit.js | 2 +- edit/refresh-on-view.js | 29 -------- edit/sections-editor-section.js | 14 ++-- edit/sections-editor.js | 124 +++++++++++++++++--------------- 7 files changed, 76 insertions(+), 123 deletions(-) delete mode 100644 edit/refresh-on-view.js diff --git a/edit.html b/edit.html index 12614c99..b87396fe 100644 --- a/edit.html +++ b/edit.html @@ -91,7 +91,6 @@ - @@ -305,7 +304,7 @@
- +
diff --git a/edit/codemirror-default.css b/edit/codemirror-default.css index 6adbf962..0db190f4 100644 --- a/edit/codemirror-default.css +++ b/edit/codemirror-default.css @@ -15,8 +15,7 @@ -webkit-animation: highlight 3s cubic-bezier(.18, .02, 0, .94); } .CodeMirror-focused { - outline: -webkit-focus-ring-color auto 5px; - outline-offset: -2px; + outline: #7dadd9 auto 1px; /* not using the ring-color hack as it became ugly in new Chrome */ } .CodeMirror-bookmark { background: linear-gradient(to right, currentColor, transparent); @@ -24,13 +23,6 @@ width: 2em; opacity: .5; } -@supports (-moz-appearance:none) { - /* restrict to FF */ - .CodeMirror-focused { - outline: #7dadd9 auto 1px; - outline-offset: -1px; - } -} .CodeMirror-search-field { width: 10em; } diff --git a/edit/edit.css b/edit/edit.css index 3635fa61..8db215ba 100644 --- a/edit/edit.css +++ b/edit/edit.css @@ -63,6 +63,7 @@ label { #sections { padding-left: 280px; min-height: 0; + height: 100%; } #sections h2 { margin-top: 1rem; @@ -278,12 +279,6 @@ input:invalid { .section-editor .section:not(:first-child) { border-top: 2px solid hsl(0, 0%, 80%); } -.section-editor:not(.section-editor-ready) .section { - opacity: 0 !important; -} -.section-editor:not(.section-editor-ready) .CodeMirror { - height: 0; -} .add-section:after { content: attr(short-text); } @@ -817,13 +812,8 @@ body.linter-disabled .hidden-unless-compact { color: #888; } -/* FIXME: remove the ID selector */ -#sections .single-editor { +.single-editor { height: 100%; - margin: 0; - padding: 0; - display: flex; - box-sizing: border-box; } .single-editor .CodeMirror { @@ -843,7 +833,6 @@ body.linter-disabled .hidden-unless-compact { } .usercss.firefox #sections, -.usercss.firefox .single-editor, .usercss.firefox .CodeMirror { height: 100%; } @@ -996,7 +985,7 @@ body.linter-disabled .hidden-unless-compact { flex-direction: column; flex: 1; } - #sections > * { + #sections > :not(.single-editor) { margin: 0 .5rem; padding: .5rem 0; } diff --git a/edit/edit.js b/edit/edit.js index 7d3b49d4..b902a1e3 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -541,7 +541,7 @@ function detectLayout() { body.classList.add('fixed-header'); } }, 250); - window.addEventListener('scroll', fixedHeader); + window.addEventListener('scroll', fixedHeader, {passive: true}); } } else { body.classList.remove('compact-layout'); diff --git a/edit/refresh-on-view.js b/edit/refresh-on-view.js deleted file mode 100644 index ffbc0589..00000000 --- a/edit/refresh-on-view.js +++ /dev/null @@ -1,29 +0,0 @@ -/* global CodeMirror */ -/* -Initialization of the multi-sections editor is slow if there are many editors -e.g. https://github.com/openstyles/stylus/issues/178. So we only refresh the -editor when they were scroll into view. -*/ -'use strict'; - -CodeMirror.defineExtension('refreshOnView', function () { - const cm = this; - if (typeof IntersectionObserver === 'undefined') { - // uh - cm.isRefreshed = true; - cm.refresh(); - return; - } - const wrapper = cm.display.wrapper; - const observer = new IntersectionObserver(entries => { - for (const entry of entries) { - if (entry.isIntersecting) { - // wrapper.style.visibility = 'visible'; - cm.isRefreshed = true; - cm.refresh(); - observer.disconnect(); - } - } - }); - observer.observe(wrapper); -}); diff --git a/edit/sections-editor-section.js b/edit/sections-editor-section.js index 8885cfc6..2735ac2c 100644 --- a/edit/sections-editor-section.js +++ b/edit/sections-editor-section.js @@ -296,19 +296,14 @@ function createSection({ function insertApplyAfter(init, base) { const apply = createApply(init); - if (base) { - const index = appliesTo.indexOf(base); - appliesTo.splice(index + 1, 0, apply); - appliesToContainer.insertBefore(apply.el, base.el.nextSibling); - } else { - appliesTo.push(apply); - appliesToContainer.appendChild(apply.el); - } + appliesTo.splice(base ? appliesTo.indexOf(base) + 1 : appliesTo.length, 0, apply); + appliesToContainer.insertBefore(apply.el, base ? base.el.nextSibling : null); dirty.add(apply, apply); if (appliesTo.length > 1 && appliesTo[0].all) { removeApply(appliesTo[0]); } emitSectionChange(); + return apply; } function removeApply(apply) { @@ -380,7 +375,8 @@ function createSection({ } $('.add-applies-to', el).addEventListener('click', e => { e.preventDefault(); - insertApplyAfter({type, value: ''}, apply); + const newApply = insertApplyAfter({type, value: ''}, apply); + $('input', newApply.el).focus(); }); return apply; diff --git a/edit/sections-editor.js b/edit/sections-editor.js index bcf95148..8a04e06c 100644 --- a/edit/sections-editor.js +++ b/edit/sections-editor.js @@ -29,6 +29,9 @@ function createSectionsEditor({style, onTitleChanged}) { updateLivePreview(); }); + updateHeader(); + rerouteHotkeys(true); + $('#to-mozilla').addEventListener('click', showMozillaFormat); $('#to-mozilla-help').addEventListener('click', showToMozillaHelp); $('#from-mozilla').addEventListener('click', () => showMozillaFormatImport()); @@ -47,17 +50,22 @@ function createSectionsEditor({style, onTitleChanged}) { .forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete)); } - let sectionOrder = ''; - const initializing = new Promise(resolve => initSection({ - sections: style.sections.slice(), - done:() => { - dirty.clear(); - rerouteHotkeys(true); - resolve(); - updateHeader(); - sections.forEach(fitToContent); + const xo = window.IntersectionObserver && new IntersectionObserver(entries => { + for (const {isIntersecting, target} of entries) { + if (isIntersecting) { + target.CodeMirror.refresh(); + xo.unobserve(target); + } } - })); + }, {rootMargin: '100%'}); + const refreshOnView = (cm, force) => + force || !xo ? + cm.refresh() : + xo.observe(cm.display.wrapper); + + let sectionOrder = ''; + let headerOffset; // in compact mode the header is at the top so it reduces the available height + const initializing = initSections(style.sections.slice()); const livePreview = createLivePreview(); livePreview.show(Boolean(style.id)); @@ -83,28 +91,26 @@ function createSectionsEditor({style, onTitleChanged}) { }; function fitToContent(section) { - if (section.cm.isRefreshed) { + const {cm, cm: {display: {wrapper, sizer}}} = section; + if (cm.display.renderedView) { resize(); } else { - section.cm.on('update', resize); + cm.on('update', resize); } function resize() { - let contentHeight = section.el.querySelector('.CodeMirror-sizer').offsetHeight; - if (contentHeight < section.cm.defaultTextHeight()) { + let contentHeight = sizer.offsetHeight; + if (contentHeight < cm.defaultTextHeight()) { return; } - contentHeight += 9; // border & resize grip - section.cm.off('update', resize); - const cmHeight = section.cm.getWrapperElement().offsetHeight; - const maxHeight = cmHeight + window.innerHeight - section.el.offsetHeight; - section.cm.setSize(null, Math.min(contentHeight, maxHeight)); - if (sections.every(s => s.cm.isRefreshed)) { - fitToAvailableSpace(); + if (headerOffset == null) { + headerOffset = wrapper.getBoundingClientRect().top; } - setTimeout(() => { - container.classList.add('section-editor-ready'); - }, 50); + contentHeight += 9; // border & resize grip + cm.off('update', resize); + const cmHeight = wrapper.offsetHeight; + const maxHeight = (window.innerHeight - headerOffset) - (section.el.offsetHeight - cmHeight); + cm.setSize(null, Math.min(contentHeight, maxHeight)); } } @@ -367,7 +373,7 @@ function createSectionsEditor({style, onTitleChanged}) { if (replaceOldStyle) { return replaceSections(sections); } - return new Promise(resolve => initSection({sections, done: resolve, focusOn: false})); + return initSections(sections, {focusOn: false}); }) .then(() => { $('.dismiss').dispatchEvent(new Event('click')); @@ -472,36 +478,33 @@ function createSectionsEditor({style, onTitleChanged}) { livePreview.update(getModel()); } - function initSection({ - sections: originalSections, + function initSections(originalSections, { total = originalSections.length, focusOn = 0, - done - }) { - container.classList.add('hidden'); - chunk(); - - function chunk() { - if (!originalSections.length) { - setGlobalProgress(); - if (focusOn !== false) { - setTimeout(() => sections[focusOn].cm.focus()); - } - container.classList.remove('hidden'); - for (const section of sections) { - section.cm.refreshOnView(); - } - if (done) { - done(); - } - return; - } + } = {}) { + let done; + return new Promise(resolve => { + done = resolve; + chunk(true); + }); + function chunk(forceRefresh) { const t0 = performance.now(); while (originalSections.length && performance.now() - t0 < 100) { - insertSectionAfter(originalSections.shift()); + insertSectionAfter(originalSections.shift(), undefined, forceRefresh); + dirty.clear(); + if (focusOn !== false && sections[focusOn]) { + sections[focusOn].cm.focus(); + focusOn = false; + } } setGlobalProgress(total - originalSections.length, total); - setTimeout(chunk); + if (!originalSections.length) { + setGlobalProgress(); + fitToAvailableSpace(); + done(); + } else { + setTimeout(chunk); + } } } @@ -540,7 +543,7 @@ function createSectionsEditor({style, onTitleChanged}) { updateLivePreview(); } - function insertSectionAfter(init, base) { + function insertSectionAfter(init, base, forceRefresh) { if (!init) { init = {code: '', urlPrefixes: ['http://example.com']}; } @@ -557,15 +560,18 @@ function createSectionsEditor({style, onTitleChanged}) { prevEditor, nextEditor }); - if (base) { - const index = sections.indexOf(base); - sections.splice(index + 1, 0, section); - container.insertBefore(section.el, base.el.nextSibling); - } else { - sections.push(section); - container.appendChild(section.el); + const {cm} = section; + sections.splice(base ? sections.indexOf(base) + 1 : sections.length, 0, section); + container.insertBefore(section.el, base ? base.el.nextSibling : null); + refreshOnView(cm, forceRefresh); + if (!base || init.code) { + // Fit a) during startup or b) when the clone button is clicked on a section with some code + fitToContent(section); + } + if (base) { + cm.focus(); + setTimeout(scrollToEditor, 0, cm); } - section.render(); updateSectionOrder(); section.onChange(updateLivePreview); updateLivePreview(); @@ -599,7 +605,7 @@ function createSectionsEditor({style, onTitleChanged}) { } sections.length = 0; container.textContent = ''; - return new Promise(resolve => initSection({sections: originalSections, done: resolve})); + return initSections(originalSections); } function replaceStyle(newStyle, codeIsUpdated) { From 9e487b03e514c053ead813971f519305348981ca Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 13 Oct 2020 21:14:54 +0300 Subject: [PATCH 36/55] tweak editor (#1063) * also apply live-preview if an unsaved style was disabled * use box-shadow instead of outline for focus everywhere * allow focus outline on click in text/search input or textarea * search inputs should use the same style as text inputs * also use box-shadow focus on delete buttons * remove URLSearchParams workaround, not needed since Chrome 55 * use `once` in addEventListener, available since Chrome 55 * update USO bug workarounds, remove obsolete ones * ping/pong to fix openURL with `message` in FF * use unprefixed CSS filter, available since Chrome 53 * use unprefixed CSS user-select, available since Chrome 54 * focus tweaks * also use text query in inline search for Stylus category * use event.key, available since Chrome 51 Co-authored-by: narcolepticinsomniac --- background/background.js | 21 +- background/token-manager.js | 18 +- content/install-hook-userstyles.js | 235 +++++------------- edit/codemirror-default.css | 9 +- edit/edit.css | 7 + edit/edit.js | 12 +- edit/linter-config-dialog.js | 5 +- edit/live-preview.js | 2 +- edit/sections-editor-section.js | 29 +-- global.css | 18 +- install-usercss/install-usercss.css | 4 - install-usercss/install-usercss.js | 3 +- js/dom.js | 31 ++- js/polyfill.js | 15 ++ js/router.js | 20 +- js/script-loader.js | 3 +- manage/incremental-search.js | 33 +-- manage/manage.css | 8 +- manage/manage.js | 2 - msgbox/msgbox.css | 7 +- msgbox/msgbox.js | 14 +- options/onoffswitch.css | 4 +- options/options.js | 2 +- popup/hotkeys.js | 42 ++-- popup/popup.css | 6 +- popup/popup.js | 17 +- popup/search-results.css | 2 - popup/search-results.js | 20 +- vendor-overwrites/colorpicker/colorpicker.css | 7 - vendor-overwrites/colorpicker/colorpicker.js | 32 +-- 30 files changed, 240 insertions(+), 388 deletions(-) diff --git a/background/background.js b/background/background.js index d03250b6..fa54e208 100644 --- a/background/background.js +++ b/background/background.js @@ -66,11 +66,13 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, { /* Same as openURL, the only extra prop in `opts` is `message` - it'll be sent when the tab is ready, which is needed in the popup, otherwise another extension could force the tab to open in foreground thus auto-closing the popup (in Chrome at least) and preventing the sendMessage code from running */ - openURL(opts) { - const {message} = opts; - return openURL(opts) // will pass the resolved value untouched when `message` is absent or falsy - .then(message && (tab => tab.status === 'complete' ? tab : onTabReady(tab))) - .then(message && (tab => msg.sendTab(tab.id, opts.message))); + async openURL(opts) { + const tab = await openURL(opts); + if (opts.message) { + await onTabReady(tab); + await msg.sendTab(tab.id, opts.message); + } + return tab; function onTabReady(tab) { return new Promise((resolve, reject) => setTimeout(function ping(numTries = 10, delay = 100) { @@ -297,13 +299,10 @@ function openEditor(params) { 'url-prefix'?: String } */ - const searchParams = new URLSearchParams(); - for (const key in params) { - searchParams.set(key, params[key]); - } - const search = searchParams.toString(); + const u = new URL(chrome.runtime.getURL('edit.html')); + u.search = new URLSearchParams(params); return openURL({ - url: 'edit.html' + (search && `?${search}`), + url: `${u}`, newWindow: prefs.get('openEditInWindow'), windowPosition: prefs.get('windowPosition'), currentWindow: null diff --git a/background/token-manager.js b/background/token-manager.js index 63f69652..755fd9fa 100644 --- a/background/token-manager.js +++ b/background/token-manager.js @@ -37,7 +37,7 @@ const tokenManager = (() => { scopes: ['https://www.googleapis.com/auth/drive.appdata'], revoke: token => { const params = {token}; - return postQuery(`https://accounts.google.com/o/oauth2/revoke?${stringifyQuery(params)}`); + return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`); } }, onedrive: { @@ -137,14 +137,6 @@ const tokenManager = (() => { }); } - function stringifyQuery(obj) { - const search = new URLSearchParams(); - for (const key of Object.keys(obj)) { - search.set(key, obj[key]); - } - return search.toString(); - } - function authUser(name, k, interactive = false) { const provider = AUTH[name]; const state = Math.random().toFixed(8).slice(2); @@ -160,7 +152,7 @@ const tokenManager = (() => { if (provider.authQuery) { Object.assign(query, provider.authQuery); } - const url = `${provider.authURL}?${stringifyQuery(query)}`; + const url = `${provider.authURL}?${new URLSearchParams(query)}`; return webextLaunchWebAuthFlow({ url, interactive, @@ -211,11 +203,9 @@ const tokenManager = (() => { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' - } + }, + body: body ? new URLSearchParams(body) : null, }; - if (body) { - options.body = stringifyQuery(body); - } return fetch(url, options) .then(r => { if (r.ok) { diff --git a/content/install-hook-userstyles.js b/content/install-hook-userstyles.js index c0fd8f70..1adc4e89 100644 --- a/content/install-hook-userstyles.js +++ b/content/install-hook-userstyles.js @@ -1,7 +1,11 @@ /* global cloneInto msg API */ 'use strict'; -(() => { +// eslint-disable-next-line no-unused-expressions +/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (() => { + const styleId = RegExp.$1; + const pageEventId = `${performance.now()}${Math.random()}`; + window.dispatchEvent(new CustomEvent(chrome.runtime.id + '-install')); window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true); @@ -17,35 +21,18 @@ }, '*'); }); - let gotBody = false; let currentMd5; - new MutationObserver(observeDOM).observe(document.documentElement, { - childList: true, - subtree: true, - }); - observeDOM(); + const md5Url = getMeta('stylish-md5-url') || `https://update.userstyles.org/${styleId}.md5`; + Promise.all([ + API.findStyle({md5Url}), + getResource(md5Url), + onDOMready(), + ]).then(checkUpdatability); - function observeDOM() { - if (!gotBody) { - if (!document.body) return; - gotBody = true; - // TODO: remove the following statement when USO pagination title is fixed - document.title = document.title.replace(/^(\d+)&\w+=/, '#$1: '); - const md5Url = getMeta('stylish-md5-url') || location.href; - Promise.all([ - API.findStyle({md5Url}), - getResource(md5Url) - ]) - .then(checkUpdatability); - } - if (document.getElementById('install_button')) { - onDOMready().then(() => { - requestAnimationFrame(() => { - sendEvent(sendEvent.lastEvent); - }); - }); - } - } + document.documentElement.appendChild( + Object.assign(document.createElement('script'), { + textContent: `(${inPageContext})('${pageEventId}')`, + })); function onMessage(msg) { switch (msg.method) { @@ -72,7 +59,7 @@ function checkUpdatability([installedStyle, md5]) { // TODO: remove the following statement when USO is fixed - document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', { + document.dispatchEvent(new CustomEvent(pageEventId, { detail: installedStyle && installedStyle.updateUrl, })); currentMd5 = md5; @@ -141,7 +128,6 @@ }); } - function onClick(event) { if (onClick.processing || !orphanCheck()) { return; @@ -227,13 +213,11 @@ } } - function getMeta(name) { const e = document.querySelector(`link[rel="${name}"]`); return e ? e.getAttribute('href') : null; } - function getResource(url, options) { if (url.startsWith('#')) { return Promise.resolve(document.getElementById(url.slice(1)).textContent); @@ -280,7 +264,6 @@ .catch(() => null); } - function styleSectionsEqual({sections: a}, {sections: b}) { if (!a || !b) { return undefined; @@ -318,20 +301,12 @@ } } - function onDOMready() { - if (document.readyState !== 'loading') { - return Promise.resolve(); - } - return new Promise(resolve => { - document.addEventListener('DOMContentLoaded', function _() { - document.removeEventListener('DOMContentLoaded', _); - resolve(); - }); - }); + return document.readyState !== 'loading' + ? Promise.resolve() + : new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, {once: true})); } - function openSettings(countdown = 10e3) { const button = document.querySelector('.customize_button'); if (button) { @@ -349,12 +324,12 @@ } } - function orphanCheck() { - // TODO: switch to install-hook-usercss.js impl, and remove explicit orphanCheck() calls - if (chrome.i18n && chrome.i18n.getUILanguage()) { - return true; - } + try { + if (chrome.i18n.getUILanguage()) { + return true; + } + } catch (e) {} // In Chrome content script is orphaned on an extension update/reload // so we need to detach event listeners window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true); @@ -366,132 +341,56 @@ } })(); -// run in page context -document.documentElement.appendChild(document.createElement('script')).text = '(' + ( - () => { - document.currentScript.remove(); - - // spoof Stylish extension presence in Chrome - if (window.chrome && chrome.app) { - const realImage = window.Image; - window.Image = function Image(...args) { - return new Proxy(new realImage(...args), { - get(obj, key) { - return obj[key]; - }, - set(obj, key, value) { - if (key === 'src' && /^chrome-extension:/i.test(value)) { - setTimeout(() => typeof obj.onload === 'function' && obj.onload()); - } else { - obj[key] = value; - } - return true; - }, - }); - }; - } - - // USO bug workaround: use the actual style settings in API response - let settings; - const originalResponseJson = Response.prototype.json; - document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) { - document.removeEventListener('stylusFixBuggyUSOsettings', _); - // TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425) - settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search.replace(/^\?/, '')); - if (!settings) { - Response.prototype.json = originalResponseJson; +function inPageContext(eventId) { + document.currentScript.remove(); + const origMethods = { + json: Response.prototype.json, + byId: document.getElementById, + }; + let vars; + // USO bug workaround: prevent errors in console after install and busy cursor + document.getElementById = id => + origMethods.byId.call(document, id) || + (/^(stylish-code|stylish-installed-style-installed-\w+|post-install-ad|style-install-unknown)$/.test(id) + ? Object.assign(document.createElement('p'), {className: 'afterdownload-ad'}) + : null); + // USO bug workaround: use the actual image data in customized settings + document.addEventListener(eventId, ({detail}) => { + vars = /\?/.test(detail) && new URL(detail).searchParams; + if (!vars) Response.prototype.json = origMethods.json; + }, {once: true}); + Response.prototype.json = async function () { + const json = await origMethods.json.apply(this, arguments); + if (vars && json && Array.isArray(json.style_settings)) { + Response.prototype.json = origMethods.json; + const images = new Map(); + for (const ss of json.style_settings) { + const value = vars.get('ik-' + ss.install_key); + if (value && ss.setting_type === 'image' && ss.style_setting_options) { + let isListed; + for (const opt of ss.style_setting_options) { + isListed |= opt.default = (opt.value === value); + } + images.set(ss.install_key, {url: value, isListed}); + } } - }); - Response.prototype.json = function (...args) { - return originalResponseJson.call(this, ...args).then(json => { - if (!settings || typeof ((json || {}).style_settings || {}).every !== 'function') { - return json; - } - Response.prototype.json = originalResponseJson; - const images = new Map(); - for (const jsonSetting of json.style_settings) { - let value = settings.get('ik-' + jsonSetting.install_key); - if (!value - || !jsonSetting.style_setting_options - || !jsonSetting.style_setting_options[0]) { - continue; - } - if (value.startsWith('ik-')) { - value = value.replace(/^ik-/, ''); - const defaultItem = jsonSetting.style_setting_options.find(item => item.default); - if (!defaultItem || defaultItem.install_key !== value) { - if (defaultItem) { - defaultItem.default = false; - } - jsonSetting.style_setting_options.some(item => { - if (item.install_key === value) { - item.default = true; - return true; - } - }); - } - } else if (jsonSetting.setting_type === 'image') { - jsonSetting.style_setting_options.some(item => { - if (item.default) { - item.default = false; - return true; - } - }); - images.set(jsonSetting.install_key, value); - } else { - const item = jsonSetting.style_setting_options[0]; - if (item.value !== value && item.install_key === 'placeholder') { - item.value = value; - } - } - } - if (images.size) { - new MutationObserver((_, observer) => { - if (!document.getElementById('style-settings')) { - return; - } + if (images.size) { + new MutationObserver((_, observer) => { + if (document.getElementById('style-settings')) { observer.disconnect(); - for (const [name, url] of images.entries()) { + for (const [name, {url, isListed}] of images) { const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`); - const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url')); + const elUrl = elRadio && + document.getElementById(elRadio.id.replace('url-choice', 'user-url')); if (elUrl) { + elRadio.checked = !isListed; elUrl.value = url; } } - }).observe(document, {childList: true, subtree: true}); - } - return json; - }); - }; - } -) + `)('${chrome.runtime.getURL('').slice(0, -1)}')`; - -// TODO: remove the following statement when USO pagination is fixed -if (location.search.includes('category=')) { - document.addEventListener('DOMContentLoaded', function _() { - document.removeEventListener('DOMContentLoaded', _); - new MutationObserver((_, observer) => { - if (!document.getElementById('pagination')) { - return; + } + }).observe(document, {childList: true, subtree: true}); } - observer.disconnect(); - const category = '&' + location.search.match(/category=[^&]+/)[0]; - const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])'); - for (let i = 0; i < links.length; i++) { - links[i].href += category; - } - }).observe(document, {childList: true, subtree: true}); - }); -} - -if (/^https?:\/\/userstyles\.org\/styles\/\d{3,}/.test(location.href)) { - new MutationObserver((_, observer) => { - const cssButton = document.getElementsByClassName('css_button'); - if (cssButton.length) { - // Click on the "Show CSS Code" button to workaround the JS error - cssButton[0].click(); - cssButton[0].click(); - observer.disconnect(); } - }).observe(document, {childList: true, subtree: true}); + return json; + }; } diff --git a/edit/codemirror-default.css b/edit/codemirror-default.css index 0db190f4..4ae58f6c 100644 --- a/edit/codemirror-default.css +++ b/edit/codemirror-default.css @@ -7,6 +7,12 @@ } .CodeMirror { border: solid #CCC 1px; + transition: box-shadow .1s; +} +#stylus#stylus .CodeMirror { + /* Using a specificity hack to override userstyles */ + /* Not using the ring-color hack as it became ugly in new Chrome */ + outline: none !important; } .CodeMirror-lint-mark-warning { background: none; @@ -14,9 +20,6 @@ .CodeMirror-dialog { -webkit-animation: highlight 3s cubic-bezier(.18, .02, 0, .94); } -.CodeMirror-focused { - outline: #7dadd9 auto 1px; /* not using the ring-color hack as it became ugly in new Chrome */ -} .CodeMirror-bookmark { background: linear-gradient(to right, currentColor, transparent); position: absolute; diff --git a/edit/edit.css b/edit/edit.css index 8db215ba..deb7c0ba 100644 --- a/edit/edit.css +++ b/edit/edit.css @@ -611,6 +611,9 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high right: 4px; top: .5em; } +#help-popup input[type="search"] { + margin: 3px; +} .keymap-list { font-size: 12px; @@ -788,6 +791,10 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high justify-items: normal; } +.usercss .CodeMirror-focused { + box-shadow: none; +} + html:not(.usercss) .usercss-only, .usercss #mozilla-format-container, .usercss #sections > h2 { diff --git a/edit/edit.js b/edit/edit.js index b902a1e3..c310c881 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -352,8 +352,7 @@ function isUsercss(style) { } function initStyleData() { - // TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425) - const params = new URLSearchParams(location.search.replace(/^\?/, '')); + const params = new URLSearchParams(location.search); const id = Number(params.get('id')); const createEmptyStyle = () => ({ name: params.get('domain') || @@ -409,7 +408,7 @@ function showHelp(title = '', body) { !event || event.type === 'click' || ( - event.which === 27 && + event.key === 'Escape' && !event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey && !$('.CodeMirror-hints, #message-box') && ( @@ -470,7 +469,7 @@ function showCodeMirrorPopup(title, html, options) { popup.style.pointerEvents = 'auto'; const onKeyDown = event => { - if (event.which === 9 && !event.ctrlKey && !event.altKey && !event.metaKey) { + if (event.key === 'Tab' && !event.ctrlKey && !event.altKey && !event.metaKey) { const search = $('#search-replace-dialog'); const area = search && search.contains(document.activeElement) ? search : popup; moveFocus(area, event.shiftKey ? -1 : 1); @@ -479,13 +478,12 @@ function showCodeMirrorPopup(title, html, options) { }; window.addEventListener('keydown', onKeyDown, true); - window.addEventListener('closeHelp', function _() { - window.removeEventListener('closeHelp', _); + window.addEventListener('closeHelp', () => { window.removeEventListener('keydown', onKeyDown, true); document.documentElement.style.removeProperty('pointer-events'); rerouteHotkeys(true); cm = popup.codebox = null; - }); + }, {once: true}); return popup; } diff --git a/edit/linter-config-dialog.js b/edit/linter-config-dialog.js index 2bdf941b..c570963a 100644 --- a/edit/linter-config-dialog.js +++ b/edit/linter-config-dialog.js @@ -53,11 +53,10 @@ cm.on('changes', updateButtonState); rerouteHotkeys(false); - window.addEventListener('closeHelp', function _() { - window.removeEventListener('closeHelp', _); + window.addEventListener('closeHelp', () => { rerouteHotkeys(true); cm = null; - }); + }, {once: true}); loadScript([ '/vendor/codemirror/mode/javascript/javascript.js', diff --git a/edit/live-preview.js b/edit/live-preview.js index c2ff3ca4..4372bbd0 100644 --- a/edit/live-preview.js +++ b/edit/live-preview.js @@ -10,7 +10,7 @@ function createLivePreview(preprocess) { const errorContainer = $('#preview-errors'); prefs.subscribe(['editor.livePreview'], (key, value) => { - if (value && data && data.id && data.enabled) { + if (value && data && data.id && (data.enabled || editor.dirty.has('enabled'))) { previewer = createPreviewer(); previewer.update(data); } diff --git a/edit/sections-editor-section.js b/edit/sections-editor-section.js index 2735ac2c..5b07875e 100644 --- a/edit/sections-editor-section.js +++ b/edit/sections-editor-section.js @@ -206,36 +206,32 @@ function createSection({ } function handleKeydown(cm, event) { - const key = event.which; - if (key < 37 || key > 40 || event.shiftKey || event.altKey || event.metaKey) { + if (event.shiftKey || event.altKey || event.metaKey) { return; } + const {key} = event; const {line, ch} = cm.getCursor(); switch (key) { - case 37: - // arrow Left + case 'ArrowLeft': if (line || ch) { return; } - // fallthrough to arrow Up - case 38: - // arrow Up + // fallthrough + case 'ArrowUp': cm = line === 0 && prevEditor(cm, false); if (!cm) { return; } event.preventDefault(); event.stopPropagation(); - cm.setCursor(cm.doc.size - 1, key === 37 ? 1e20 : ch); + cm.setCursor(cm.doc.size - 1, key === 'ArrowLeft' ? 1e20 : ch); break; - case 39: - // arrow Right + case 'ArrowRight': if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) { return; } - // fallthrough to arrow Down - case 40: - // arrow Down + // fallthrough + case 'ArrowDown': cm = line === cm.doc.size - 1 && nextEditor(cm, false); if (!cm) { return; @@ -245,13 +241,6 @@ function createSection({ cm.setCursor(0, 0); break; } - // FIXME: what is this? - // const animation = (cm.getSection().firstElementChild.getAnimations() || [])[0]; - // if (animation) { - // animation.playbackRate = -1; - // animation.currentTime = 2000; - // animation.play(); - // } } function showAppliesToHelp(event) { diff --git a/global.css b/global.css index c010a958..fa5ff877 100644 --- a/global.css +++ b/global.css @@ -54,18 +54,20 @@ button:active { border-color: hsl(0, 0%, 50%); } -input { +input { font: inherit; + border: 1px solid hsl(0, 0%, 66%); + transition: border-color .1s, box-shadow .1s; } -input:not([type]) { +input:not([type]), +input[type=search] { background: #fff; color: #000; height: 22px; min-height: 22px!important; line-height: 22px; padding: 0 3px; - font: inherit; border: 1px solid hsl(0, 0%, 66%); } @@ -208,9 +210,19 @@ select[disabled] + .select-arrow { display: none !important; } +:focus, +.CodeMirror-focused, +[data-focused-via-click] input[type="text"]:focus, +[data-focused-via-click] input[type="number"]:focus { + /* Using box-shadow instead of the ugly outline in new Chrome */ + outline: none; + box-shadow: 0 0 0 1px hsl(180, 100%, 38%), 0 0 3px hsla(180, 100%, 60%, .5); +} + [data-focused-via-click] :focus, [data-focused-via-click]:focus { outline: none; + box-shadow: none; } @supports (-moz-appearance: none) { diff --git a/install-usercss/install-usercss.css b/install-usercss/install-usercss.css index 88d47bd4..6214629e 100644 --- a/install-usercss/install-usercss.css +++ b/install-usercss/install-usercss.css @@ -288,9 +288,7 @@ li { #header:not(.meta-init) > *:not(.lds-spinner), #header.meta-init > .lds-spinner { - -webkit-user-select: none; -moz-user-select: none; - -ms-user-select: none; user-select: none; pointer-events: none; opacity: 0; @@ -299,9 +297,7 @@ li { #header.meta-init > * { opacity: 1; transition: opacity .5s; - -webkit-user-select: auto; -moz-user-select: auto; - -ms-user-select: auto; user-select: auto; } diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js index f78044a7..41ee0ab1 100644 --- a/install-usercss/install-usercss.js +++ b/install-usercss/install-usercss.js @@ -3,8 +3,7 @@ 'use strict'; (() => { - // TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425) - const params = new URLSearchParams(location.search.replace(/^\?/, '')); + const params = new URLSearchParams(location.search); const tabId = params.has('tabId') ? Number(params.get('tabId')) : -1; const initialUrl = params.get('updateUrl'); diff --git a/js/dom.js b/js/dom.js index 4e40bc83..b95627d5 100644 --- a/js/dom.js +++ b/js/dom.js @@ -20,6 +20,9 @@ for (const type of [NodeList, NamedNodeMap, HTMLCollection, HTMLAllCollection]) } } +$.isTextLikeInput = el => + el.localName === 'input' && /^(text|search|number)$/.test(el.type); + $.remove = (selector, base = document) => { const el = selector && typeof selector === 'string' ? $(selector, base) : selector; if (el) { @@ -112,15 +115,9 @@ document.addEventListener('wheel', event => { }); function onDOMready() { - if (document.readyState !== 'loading') { - return Promise.resolve(); - } - return new Promise(resolve => { - document.addEventListener('DOMContentLoaded', function _() { - document.removeEventListener('DOMContentLoaded', _); - resolve(); - }); - }); + return document.readyState !== 'loading' + ? Promise.resolve() + : new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, {once: true})); } @@ -144,8 +141,7 @@ function animateElement( onComplete, } = {}) { return element && new Promise(resolve => { - element.addEventListener('animationend', function _() { - element.removeEventListener('animationend', _); + element.addEventListener('animationend', () => { element.classList.remove( className, // In Firefox, `resolve()` might be called one frame later. @@ -157,7 +153,7 @@ function animateElement( onComplete.call(element); } resolve(); - }); + }, {once: true}); element.classList.add(className); }); } @@ -355,20 +351,23 @@ function focusAccessibility() { 'a', 'button', 'input', - 'textarea', 'label', 'select', 'summary', ]; // try to find a focusable parent for this many parentElement jumps: const GIVE_UP_DEPTH = 4; + // allow outline on text/search inputs in addition to textareas + const isOutlineAllowed = el => + !focusAccessibility.ELEMENTS.includes(el.localName) || + $.isTextLikeInput(el); addEventListener('mousedown', suppressOutlineOnClick, {passive: true}); addEventListener('keydown', keepOutlineOnTab, {passive: true}); function suppressOutlineOnClick({target}) { for (let el = target, i = 0; el && i++ < GIVE_UP_DEPTH; el = el.parentElement) { - if (focusAccessibility.ELEMENTS.includes(el.localName)) { + if (!isOutlineAllowed(el)) { focusAccessibility.lastFocusedViaClick = true; if (el.dataset.focusedViaClick === undefined) { el.dataset.focusedViaClick = ''; @@ -379,7 +378,7 @@ function focusAccessibility() { } function keepOutlineOnTab(event) { - if (event.which === 9) { + if (event.key === 'Tab') { focusAccessibility.lastFocusedViaClick = false; setTimeout(keepOutlineOnTab, 0, true); return; @@ -387,7 +386,7 @@ function focusAccessibility() { return; } let el = document.activeElement; - if (!el || !focusAccessibility.ELEMENTS.includes(el.localName)) { + if (!el || isOutlineAllowed(el)) { return; } if (el.dataset.focusedViaClick !== undefined) { diff --git a/js/polyfill.js b/js/polyfill.js index 859de6e2..e299957f 100644 --- a/js/polyfill.js +++ b/js/polyfill.js @@ -62,5 +62,20 @@ self.INJECTED !== 1 && (() => { } } + if (!(new URLSearchParams({foo: 1})).get('foo')) { + // TODO: remove when minimum_chrome_version >= 61 + window.URLSearchParams = class extends URLSearchParams { + constructor(init) { + if (init && typeof init === 'object') { + super(); + for (const [key, val] of Object.entries(init)) { + this.set(key, val); + } + } else { + super(...arguments); + } + } + }; + } //#endregion })(); diff --git a/js/router.js b/js/router.js index 59566b59..f466eb30 100644 --- a/js/router.js +++ b/js/router.js @@ -31,18 +31,9 @@ const router = (() => { } function updateSearch(key, value) { - const search = new URLSearchParams(location.search.replace(/^\?/, '')); - if (!value) { - search.delete(key); - } else { - search.set(key, value); - } - const finalSearch = search.toString(); - if (finalSearch) { - history.replaceState(history.state, null, `?${finalSearch}${location.hash}`); - } else { - history.replaceState(history.state, null, `${location.pathname}${location.hash}`); - } + const u = new URL(location); + u.searchParams[value ? 'set' : 'delete'](key, value); + history.replaceState(history.state, null, `${u}`); update(true); } @@ -66,7 +57,7 @@ const router = (() => { } function getSearch(key) { - return new URLSearchParams(location.search.replace(/^\?/, '')).get(key); + return new URLSearchParams(location.search).get(key); } function update(replace) { @@ -86,8 +77,7 @@ const router = (() => { if (options.hash) { state = options.hash === location.hash; } else if (options.search) { - // TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425) - const search = new URLSearchParams(location.search.replace(/^\?/, '')); + const search = new URLSearchParams(location.search); state = options.search.map(key => search.get(key)); } if (!deepEqual(state, options.currentState)) { diff --git a/js/script-loader.js b/js/script-loader.js index 0279cae5..ada07824 100644 --- a/js/script-loader.js +++ b/js/script-loader.js @@ -82,7 +82,7 @@ const loadScript = (() => { for (const {addedNodes} of mutations) { for (const n of addedNodes) { if (n.src && getSubscribersForSrc(n.src)) { - n.addEventListener('load', notifySubscribers); + n.addEventListener('load', notifySubscribers, {once: true}); } } } @@ -97,7 +97,6 @@ const loadScript = (() => { } function notifySubscribers(event) { - this.removeEventListener('load', notifySubscribers); for (let data; (data = getSubscribersForSrc(this.src));) { data.listeners.forEach(fn => fn(event)); if (emptyAfterCleanup(data.suffix)) { diff --git a/manage/incremental-search.js b/manage/incremental-search.js index 293371ae..393b3a4d 100644 --- a/manage/incremental-search.js +++ b/manage/incremental-search.js @@ -24,12 +24,12 @@ onDOMready().then(() => { document.body.appendChild(input); window.addEventListener('keydown', maybeRefocus, true); - function incrementalSearch({which}, immediately) { + function incrementalSearch({key}, immediately) { if (!immediately) { debounce(incrementalSearch, 100, {}, true); return; } - const direction = which === 38 ? -1 : which === 40 ? 1 : 0; + const direction = key === 'ArrowUp' ? -1 : key === 'ArrowDown' ? 1 : 0; const text = input.value.toLocaleLowerCase(); if (!text.trim() || !direction && (text === prevText || focusedName.startsWith(text))) { prevText = text; @@ -76,40 +76,31 @@ onDOMready().then(() => { if (event.altKey || event.metaKey || $('#message-box')) { return; } - const inTextInput = event.target.matches('[type=text], [type=search], [type=number]'); - const {which: k, key} = event; - // focus search field on "/" or Ctrl-F key - if (event.ctrlKey - ? (event.code === 'KeyF' || !event.code && k === 70) && !event.shiftKey - : (key === '/' || !key && k === 191 && !event.shiftKey) && !inTextInput) { + const inTextInput = $.isTextLikeInput(event.target); + const {key, code, ctrlKey: ctrl} = event; + // `code` is independent of the current keyboard language + if ((code === 'KeyF' && ctrl && !event.shiftKey) || + (code === 'Slash' || key === '/') && !ctrl && !inTextInput) { + // focus search field on "/" or Ctrl-F key event.preventDefault(); $('#search').focus(); return; } - if (event.ctrlKey || inTextInput) { + if (ctrl || inTextInput) { return; } const time = performance.now(); - if ( - // 0-9 - k >= 48 && k <= 57 || - // a-z - k >= 65 && k <= 90 || - // numpad keys - k >= 96 && k <= 111 || - // marks - k >= 186 - ) { + if (key.length === 1) { input.focus(); if (time - prevTime > 1000) { input.value = ''; } prevTime = time; } else - if (k === 13 && focusedLink) { + if (key === 'Enter' && focusedLink) { focusedLink.dispatchEvent(new MouseEvent('click', {bubbles: true})); } else - if ((k === 38 || k === 40) && !event.shiftKey && + if ((key === 'ArrowUp' || key === 'ArrowDown') && !event.shiftKey && time - prevTime < 5000 && incrementalSearch(event, true)) { prevTime = time; } else diff --git a/manage/manage.css b/manage/manage.css index ef4d0279..5e3f6041 100644 --- a/manage/manage.css +++ b/manage/manage.css @@ -550,8 +550,6 @@ a:hover { .newUI .update-done .updated svg { top: -4px; position: relative; - /* unprefixed since Chrome 53 */ - -webkit-filter: drop-shadow(0 4px 0 currentColor); filter: drop-shadow(0 5px 0 currentColor); } @@ -663,8 +661,6 @@ a:hover { margin-left: -20px; margin-right: 4px; transition: opacity .5s, filter .5s; - /* unprefixed since Chrome 53 */ - -webkit-filter: grayscale(1); filter: grayscale(1); /* workaround for the buggy CSS filter: images in the hidden overflow are shown on Mac */ backface-visibility: hidden; @@ -682,9 +678,7 @@ a:hover { .newUI .entry:hover .target img { opacity: 1; - /* unprefixed since Chrome 53 */ - -webkit-filter: grayscale(0); - filter: grayscale(0); + filter: none; } /* Default, no update buttons */ diff --git a/manage/manage.js b/manage/manage.js index 0df3ea8d..2b856600 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -631,13 +631,11 @@ function switchUI({styleOnly} = {}) { } ` + (newUI.faviconsGray ? ` .newUI .target img { - -webkit-filter: grayscale(1); filter: grayscale(1); opacity: .25; } ` : ` .newUI .target img { - -webkit-filter: none; filter: none; opacity: 1; } diff --git a/msgbox/msgbox.css b/msgbox/msgbox.css index 32e6c865..bbc70622 100644 --- a/msgbox/msgbox.css +++ b/msgbox/msgbox.css @@ -135,12 +135,7 @@ } .danger #message-box-buttons > button:not([data-focused-via-click]):first-child:focus { - outline: red auto 1px; -} - -/* FF ignores color with 'auto' */ -.firefox .danger #message-box-buttons > button:not([data-focused-via-click]):first-child:focus { - outline: red solid 1px; + box-shadow: 0 0 0 1px red; /* Using box-shadow instead of the ugly outline in new Chrome */ } .non-windows #message-box-buttons { diff --git a/msgbox/msgbox.js b/msgbox/msgbox.js index 3cf7a5bf..a537da32 100644 --- a/msgbox/msgbox.js +++ b/msgbox/msgbox.js @@ -62,28 +62,28 @@ function messageBox({ resolveWith({button: this.buttonIndex}); }, key(event) { - const {which, shiftKey, ctrlKey, altKey, metaKey, target} = event; - if (shiftKey && which !== 9 || ctrlKey || altKey || metaKey) { + const {key, shiftKey, ctrlKey, altKey, metaKey, target} = event; + if (shiftKey && key !== 'Tab' || ctrlKey || altKey || metaKey) { return; } - switch (which) { - case 13: + switch (key) { + case 'Enter': if (target.closest(focusAccessibility.ELEMENTS.join(','))) { return; } break; - case 27: + case 'Escape': event.preventDefault(); event.stopPropagation(); break; - case 9: + case 'Tab': moveFocus(messageBox.element, shiftKey ? -1 : 1); event.preventDefault(); return; default: return; } - resolveWith(which === 13 ? {enter: true} : {esc: true}); + resolveWith(key === 'Enter' ? {enter: true} : {esc: true}); }, scroll() { scrollTo(blockScroll.x, blockScroll.y); diff --git a/options/onoffswitch.css b/options/onoffswitch.css index c335c97e..a2fecfbe 100644 --- a/options/onoffswitch.css +++ b/options/onoffswitch.css @@ -3,9 +3,8 @@ .onoffswitch { position: relative; margin: 1ex 0; - -webkit-user-select: none; -moz-user-select: none; - -ms-user-select: none; + user-select: none; } .onoffswitch input { @@ -17,6 +16,7 @@ bottom: -10px; left: -10px; width: calc(100% + 12px); + border: 0; } #message-box .onoffswitch input { diff --git a/options/options.js b/options/options.js index dff74940..d1bbcb36 100644 --- a/options/options.js +++ b/options/options.js @@ -298,7 +298,7 @@ function customizeHotkeys() { } window.onkeydown = event => { - if (event.keyCode === 27) { + if (event.key === 'Escape') { top.dispatchEvent(new CustomEvent('closeOptions')); } }; diff --git a/popup/hotkeys.js b/popup/hotkeys.js index 157a34bb..492d36b2 100644 --- a/popup/hotkeys.js +++ b/popup/hotkeys.js @@ -9,14 +9,13 @@ const hotkeys = (() => { let enabled = false; let ready = false; - window.addEventListener('showStyles:done', function _() { - window.removeEventListener('showStyles:done', _); + window.addEventListener('showStyles:done', () => { togglablesShown = true; togglables = getTogglables(); ready = true; setState(true); initHotkeyInfo(); - }); + }, {once: true}); window.addEventListener('resize', adjustInfoPosition); @@ -38,40 +37,27 @@ const hotkeys = (() => { return; } let entry; - const {which: k, key, code} = event; + let {key, code, shiftKey} = event; - if (code.startsWith('Digit') || code.startsWith('Numpad') && code.length === 7) { + if (key >= '0' && key <= '9') { + entry = entries[(Number(key) || 10) - 1]; + } else if (code >= 'Digit0' && code <= 'Digit9') { entry = entries[(Number(code.slice(-1)) || 10) - 1]; - - } else if ( - code === 'Backquote' || code === 'NumpadMultiply' || - key && (key === '`' || key === '*') || - k === 192 || k === 106) { + } else if (key === '`' || key === '*' || code === 'Backquote' || code === 'NumpadMultiply') { invertTogglables(); - - } else if ( - code === 'NumpadSubtract' || - key && key === '-' || - k === 109) { + } else if (key === '-' || code === 'NumpadSubtract') { toggleState(entries, 'enabled', false); - - } else if ( - code === 'NumpadAdd' || - key && key === '+' || - k === 107) { + } else if (key === '+' || code === 'NumpadAdd') { toggleState(entries, 'disabled', true); - - } else if ( - // any single character - key && key.length === 1 || - k >= 65 && k <= 90) { - const letter = new RegExp(key ? '^' + key : '^\\x' + k.toString(16), 'i'); - entry = [...entries].find(entry => letter.test(entry.textContent)); + } else if (key.length === 1) { + shiftKey = false; // typing ':' etc. needs Shift so we hide it here to avoid opening editor + key = key.toLocaleLowerCase(); + entry = [...entries].find(e => e.innerText.toLocaleLowerCase().startsWith(key)); } if (!entry) { return; } - const target = $(event.shiftKey ? '.style-edit-link' : '.checker', entry); + const target = $(shiftKey ? '.style-edit-link' : '.checker', entry); target.dispatchEvent(new MouseEvent('click', {cancelable: true})); } diff --git a/popup/popup.css b/popup/popup.css index 5706b877..531d6f77 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -342,11 +342,7 @@ a.configure[target="_blank"] .svg-icon.config { text-overflow: ellipsis; } #confirm button[data-cmd="ok"]:not([data-focused-via-click]):focus { - outline: red auto 1px; -} -/* FF ignores color with 'auto' */ -.firefox #confirm button[data-cmd="ok"]:not([data-focused-via-click]):focus { - outline: red solid 1px; + box-shadow: 0 0 0 1px red; /* Using box-shadow instead of the ugly outline in new Chrome */ } .menu-items-wrapper { width: 80%; diff --git a/popup/popup.js b/popup/popup.js index fb2b10bf..1510ce76 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -507,16 +507,15 @@ Object.assign(handleEvent, { window.onkeydown = event => { const close = $('.menu-close', entry); const checkbox = $('.exclude-by-domain-checkbox', entry); - const keyCode = event.keyCode || event.which; - if (document.activeElement === close && (keyCode === 9) && !event.shiftKey) { + if (document.activeElement === close && (event.key === 'Tab') && !event.shiftKey) { event.preventDefault(); checkbox.focus(); } - if (document.activeElement === checkbox && (keyCode === 9) && event.shiftKey) { + if (document.activeElement === checkbox && (event.key === 'Tab') && event.shiftKey) { event.preventDefault(); close.focus(); } - if (keyCode === 27) { + if (event.key === 'Escape') { event.preventDefault(); close.click(); } @@ -542,20 +541,20 @@ Object.assign(handleEvent, { const close = $('.menu-close', entry); const checkbox = $('.exclude-by-domain-checkbox', entry); const confirmActive = $('#confirm[data-display="true"]'); - const keyCode = event.keyCode || event.which; - if (document.activeElement === cancel && (keyCode === 9)) { + const {key} = event; + if (document.activeElement === cancel && (key === 'Tab')) { event.preventDefault(); affirm.focus(); } - if (document.activeElement === close && (keyCode === 9) && !event.shiftKey) { + if (document.activeElement === close && (key === 'Tab') && !event.shiftKey) { event.preventDefault(); checkbox.focus(); } - if (document.activeElement === checkbox && (keyCode === 9) && event.shiftKey) { + if (document.activeElement === checkbox && (key === 'Tab') && event.shiftKey) { event.preventDefault(); close.focus(); } - if (keyCode === 27) { + if (key === 'Escape') { event.preventDefault(); if (confirmActive) { box.dataset.display = false; diff --git a/popup/search-results.css b/popup/search-results.css index a7eb69cf..ba0bf97c 100755 --- a/popup/search-results.css +++ b/popup/search-results.css @@ -271,9 +271,7 @@ body.search-results-shown { /* spinner: https://github.com/loadingio/css-spinner */ .lds-spinner { - -webkit-user-select: none; -moz-user-select: none; - -ms-user-select: none; user-select: none; pointer-events: none; position: absolute; diff --git a/popup/search-results.js b/popup/search-results.js index 590a2337..f596553e 100755 --- a/popup/search-results.js +++ b/popup/search-results.js @@ -54,7 +54,7 @@ window.addEventListener('showStyles:done', () => { href: URLS.usoArchive, onclick(event) { if (!prefs.get('popup.findStylesInline') || dom.container) { - this.search = `${new URLSearchParams({category, search: $('#search-query').value})}`; + this.search = new URLSearchParams({category, search: $('#search-query').value}); handleEvent.openURLandHide.call(this, event); return; } @@ -83,6 +83,9 @@ window.addEventListener('showStyles:done', () => { const n = Number(m[2]); query.push(n >= 2000 && n <= thisYear ? n : m[1] || m[2]); } + if (category === STYLUS_CATEGORY && !query.includes('stylus')) { + query.push('stylus'); + } ready = ready.then(start); }; $('#search-order').value = order; @@ -464,13 +467,18 @@ window.addEventListener('showStyles:done', () => { } function isResultMatching(res) { + // We're trying to call calcHaystack only when needed, not on all 100K items + const {c} = res; return ( - res.c === category || - searchGlobals && res.c === 'global' && (query.length || calcHaystack(res)._nLC.includes(category)) + c === category || + category !== STYLUS_CATEGORY && ( + searchGlobals && + c === 'global' && + (query.length || calcHaystack(res)._nLC.includes(category)) + ) ) && ( - category === STYLUS_CATEGORY - ? /\bStylus\b/.test(res.n) - : !query.length || query.every(isInHaystack, calcHaystack(res)) + !query.length || // to skip calling calcHaystack + query.every(isInHaystack, calcHaystack(res)) ); } diff --git a/vendor-overwrites/colorpicker/colorpicker.css b/vendor-overwrites/colorpicker/colorpicker.css index 45bdb9f0..fa6d5369 100644 --- a/vendor-overwrites/colorpicker/colorpicker.css +++ b/vendor-overwrites/colorpicker/colorpicker.css @@ -86,10 +86,7 @@ border: 1px solid var(--main-border-color); background-color: var(--main-background-color); box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.12); - -webkit-user-select: none; -moz-user-select: none; - -ms-user-select: none; - -o-user-select: none; user-select: none; } @@ -295,10 +292,7 @@ font-size: 11px; font-weight: bold; box-sizing: border-box; - -webkit-user-select: text; -moz-user-select: text; - -ms-user-select: text; - -o-user-select: text; user-select: text; border: 1px solid var(--input-border-color); background-color: var(--input-background-color); @@ -306,7 +300,6 @@ } .colorpicker-theme-dark .colorpicker-input::-webkit-inner-spin-button { - -webkit-filter: invert(1); filter: invert(1); } diff --git a/vendor-overwrites/colorpicker/colorpicker.js b/vendor-overwrites/colorpicker/colorpicker.js index 2fb84913..c3381495 100644 --- a/vendor-overwrites/colorpicker/colorpicker.js +++ b/vendor-overwrites/colorpicker/colorpicker.js @@ -355,29 +355,29 @@ } function setFromKeyboard(event) { - const {which, ctrlKey: ctrl, altKey: alt, shiftKey: shift, metaKey: meta} = event; - switch (which) { - case 9: // Tab - case 33: // PgUp - case 34: // PgDn + const {key, ctrlKey: ctrl, altKey: alt, shiftKey: shift, metaKey: meta} = event; + switch (key) { + case 'Tab': + case 'PageUp': + case 'PageDown': if (!ctrl && !alt && !meta) { const el = document.activeElement; const inputs = $inputs[currentFormat]; const lastInput = inputs[inputs.length - 1]; - if (which === 9 && shift && el === inputs[0]) { + if (key === 'Tab' && shift && el === inputs[0]) { maybeFocus(lastInput); - } else if (which === 9 && !shift && el === lastInput) { + } else if (key === 'Tab' && !shift && el === lastInput) { maybeFocus(inputs[0]); - } else if (which !== 9 && !shift) { - setFromFormatElement({shift: which === 33 || shift}); + } else if (key !== 'Tab' && !shift) { + setFromFormatElement({shift: key === 'PageUp' || shift}); } else { return; } event.preventDefault(); } return; - case 38: // Up - case 40: // Down + case 'ArrowUp': + case 'ArrowDown': if (!event.metaKey && document.activeElement.localName === 'input' && document.activeElement.checkValidity()) { @@ -389,8 +389,8 @@ function setFromKeyboardIncrement(event) { const el = document.activeElement; - const {which, ctrlKey: ctrl, altKey: alt, shiftKey: shift} = event; - const dir = which === 38 ? 1 : -1; + const {key, ctrlKey: ctrl, altKey: alt, shiftKey: shift} = event; + const dir = key === 'ArrowUp' ? 1 : -1; let value, newValue; if (currentFormat === 'hex') { value = el.value.trim(); @@ -617,9 +617,9 @@ function onKeyDown(e) { if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { - switch (e.which) { - case 13: - case 27: + switch (e.key) { + case 'Enter': + case 'Escape': e.preventDefault(); e.stopPropagation(); hide(); From a71b621bf9e28990c3fdc81af9afbbece18ec942 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 14 Oct 2020 19:48:59 +0300 Subject: [PATCH 37/55] remove -webkit- prefix on standardized features --- edit/codemirror-default.css | 2 +- edit/edit.css | 1 - install-usercss/install-usercss.css | 38 ------------------- manage/manage.css | 1 - vendor-overwrites/colorpicker/colorpicker.css | 13 ------- 5 files changed, 1 insertion(+), 54 deletions(-) diff --git a/edit/codemirror-default.css b/edit/codemirror-default.css index 4ae58f6c..dbd9a72c 100644 --- a/edit/codemirror-default.css +++ b/edit/codemirror-default.css @@ -18,7 +18,7 @@ background: none; } .CodeMirror-dialog { - -webkit-animation: highlight 3s cubic-bezier(.18, .02, 0, .94); + animation: highlight 3s cubic-bezier(.18, .02, 0, .94); } .CodeMirror-bookmark { background: linear-gradient(to right, currentColor, transparent); diff --git a/edit/edit.css b/edit/edit.css index deb7c0ba..54aeed44 100644 --- a/edit/edit.css +++ b/edit/edit.css @@ -50,7 +50,6 @@ label { top: 0; padding: 1rem; border-right: 1px dashed #AAA; - -webkit-box-shadow: 0 0 3rem -1.2rem black; box-shadow: 0 0 3rem -1.2rem black; box-sizing: border-box; z-index: 10; diff --git a/install-usercss/install-usercss.css b/install-usercss/install-usercss.css index 6214629e..e37cd2fb 100644 --- a/install-usercss/install-usercss.css +++ b/install-usercss/install-usercss.css @@ -315,14 +315,6 @@ label { opacity: 0; } } -@-webkit-keyframes lds-spinner { - 0% { - opacity: 1; - } - 100% { - opacity: 0; - } -} .lds-spinner { position: absolute; width: 200px; @@ -338,104 +330,74 @@ label { left: 94px; top: 23px; position: absolute; - -webkit-animation: lds-spinner linear 1s infinite; animation: lds-spinner linear 1s infinite; background: currentColor; width: 12px; height: 34px; border-radius: 20%; - -webkit-transform-origin: 6px 77px; transform-origin: 6px 77px; } .lds-spinner div:nth-child(1) { - -webkit-transform: rotate(0deg); transform: rotate(0deg); - -webkit-animation-delay: -0.916666666666667s; animation-delay: -0.916666666666667s; } .lds-spinner div:nth-child(2) { - -webkit-transform: rotate(30deg); transform: rotate(30deg); - -webkit-animation-delay: -0.833333333333333s; animation-delay: -0.833333333333333s; } .lds-spinner div:nth-child(3) { - -webkit-transform: rotate(60deg); transform: rotate(60deg); - -webkit-animation-delay: -0.75s; animation-delay: -0.75s; } .lds-spinner div:nth-child(4) { - -webkit-transform: rotate(90deg); transform: rotate(90deg); - -webkit-animation-delay: -0.666666666666667s; animation-delay: -0.666666666666667s; } .lds-spinner div:nth-child(5) { - -webkit-transform: rotate(120deg); transform: rotate(120deg); - -webkit-animation-delay: -0.583333333333333s; animation-delay: -0.583333333333333s; } .lds-spinner div:nth-child(6) { - -webkit-transform: rotate(150deg); transform: rotate(150deg); - -webkit-animation-delay: -0.5s; animation-delay: -0.5s; } .lds-spinner div:nth-child(7) { - -webkit-transform: rotate(180deg); transform: rotate(180deg); - -webkit-animation-delay: -0.416666666666667s; animation-delay: -0.416666666666667s; } .lds-spinner div:nth-child(8) { - -webkit-transform: rotate(210deg); transform: rotate(210deg); - -webkit-animation-delay: -0.333333333333333s; animation-delay: -0.333333333333333s; } .lds-spinner div:nth-child(9) { - -webkit-transform: rotate(240deg); transform: rotate(240deg); - -webkit-animation-delay: -0.25s; animation-delay: -0.25s; } .lds-spinner div:nth-child(10) { - -webkit-transform: rotate(270deg); transform: rotate(270deg); - -webkit-animation-delay: -0.166666666666667s; animation-delay: -0.166666666666667s; } .lds-spinner div:nth-child(11) { - -webkit-transform: rotate(300deg); transform: rotate(300deg); - -webkit-animation-delay: -0.083333333333333s; animation-delay: -0.083333333333333s; } .lds-spinner div:nth-child(12) { - -webkit-transform: rotate(330deg); transform: rotate(330deg); - -webkit-animation-delay: 0s; animation-delay: 0s; } @-webkit-keyframes load3 { 0% { - -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { - -webkit-transform: rotate(360deg); transform: rotate(360deg); } } @keyframes load3 { 0% { - -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { - -webkit-transform: rotate(360deg); transform: rotate(360deg); } } diff --git a/manage/manage.css b/manage/manage.css index 5e3f6041..1ed6d70a 100644 --- a/manage/manage.css +++ b/manage/manage.css @@ -54,7 +54,6 @@ a:hover { top: 0; padding: 1rem; border-right: 1px dashed #AAA; - -webkit-box-shadow: 0 0 50px -18px black; box-shadow: 0 0 50px -18px black; overflow: auto; box-sizing: border-box; diff --git a/vendor-overwrites/colorpicker/colorpicker.css b/vendor-overwrites/colorpicker/colorpicker.css index fa6d5369..e02b0d14 100644 --- a/vendor-overwrites/colorpicker/colorpicker.css +++ b/vendor-overwrites/colorpicker/colorpicker.css @@ -136,8 +136,6 @@ position: absolute; width: 10px; height: 10px; - -webkit-border-radius: 50%; - -moz-border-radius: 50%; border-radius: 50%; left: -5px; top: -5px; @@ -165,8 +163,6 @@ -webkit-border-radius: 50%; -moz-border-radius: 50%; border-radius: 50%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; box-sizing: border-box; border: 1px solid var(--input-border-color); } @@ -188,8 +184,6 @@ position: relative; width: 100%; height: 10px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; border-radius: 3px; cursor: pointer; background: linear-gradient(to right, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); @@ -209,8 +203,6 @@ width: 100%; height: 10px; z-index: 2; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; border-radius: 3px; cursor: pointer; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAJElEQVQYV2NctWrVfwYkEBYWxojMZ6SDAmT7QGx0K1EcRBsFAADeG/3M/HteAAAAAElFTkSuQmCC"); @@ -236,9 +228,6 @@ .colorpicker-input-container { position: relative; - -webkit-box-sizing: padding-box; - -moz-box-sizing: padding-box; - box-sizing: padding-box; } .colorpicker-input-group { @@ -260,8 +249,6 @@ position: relative; flex: 1; padding: 5px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; box-sizing: border-box; } From 492b75d84ebfaf8aeca4baad519c114def29daa2 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 14 Oct 2020 20:17:57 +0300 Subject: [PATCH 38/55] parserlib: implement @supports selector() --- vendor-overwrites/csslint/parserlib.js | 48 +++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/vendor-overwrites/csslint/parserlib.js b/vendor-overwrites/csslint/parserlib.js index b9f7dc69..c16c18f5 100644 --- a/vendor-overwrites/csslint/parserlib.js +++ b/vendor-overwrites/csslint/parserlib.js @@ -4020,24 +4020,19 @@ self.parserlib = (() => { _supportsCondition() { const stream = this._tokenStream; - if (stream.match(Tokens.IDENT)) { - const ident = lower(stream._token.value); - if (ident === 'not') { - stream.mustMatch(Tokens.S); - this._supportsConditionInParens(); - } else { - stream.unget(); - } + const next = stream.LT(1); + if (next.type === Tokens.IDENT && lower(next.value) === 'not') { + stream.get(); + stream.mustMatch(Tokens.S); + this._supportsConditionInParens(); } else { this._supportsConditionInParens(); - this._ws(); while (stream.peek() === Tokens.IDENT) { const ident = lower(stream.LT(1).value); if (ident === 'and' || ident === 'or') { - stream.mustMatch(Tokens.IDENT); + stream.get(); this._ws(); this._supportsConditionInParens(); - this._ws(); } } } @@ -4045,36 +4040,41 @@ self.parserlib = (() => { _supportsConditionInParens() { const stream = this._tokenStream; - if (stream.match(Tokens.LPAREN)) { + const next = stream.LT(1); + if (next.type === Tokens.LPAREN) { + stream.get(); this._ws(); - if (stream.match([Tokens.IDENT, Tokens.CUSTOM_PROP])) { - // look ahead for not keyword, - // if not given, continue with declaration condition. - const ident = lower(stream._token.value); - if (ident === 'not') { - this._ws(); + const {type, value} = stream.LT(1); + if (type === Tokens.IDENT || type === Tokens.CUSTOM_PROP) { + if (lower(value) === 'not') { this._supportsCondition(); stream.mustMatch(Tokens.RPAREN); } else { - stream.unget(); - this._supportsDeclarationCondition(false); + this._supportsDecl(false); } } else { this._supportsCondition(); stream.mustMatch(Tokens.RPAREN); } + } else if (next.type === Tokens.FUNCTION && lower(next.value) === 'selector(') { + stream.get(); + this._ws(); + this._selector(); + stream.mustMatch(Tokens.RPAREN); } else { - this._supportsDeclarationCondition(); + this._supportsDecl(); } + this._ws(); } - _supportsDeclarationCondition(requireStartParen = true) { + _supportsDecl(requireStartParen = true) { + const stream = this._tokenStream; if (requireStartParen) { - this._tokenStream.mustMatch(Tokens.LPAREN); + stream.mustMatch(Tokens.LPAREN); } this._ws(); this._declaration(); - this._tokenStream.mustMatch(Tokens.RPAREN); + stream.mustMatch(Tokens.RPAREN); } _media() { From 56a8212fdf95c893d84df7c4137dd5437e40446c Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 15 Oct 2020 22:31:13 +0300 Subject: [PATCH 39/55] parserlib: add `text` to background-clip --- vendor-overwrites/csslint/parserlib.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor-overwrites/csslint/parserlib.js b/vendor-overwrites/csslint/parserlib.js index c16c18f5..7d126f8f 100644 --- a/vendor-overwrites/csslint/parserlib.js +++ b/vendor-overwrites/csslint/parserlib.js @@ -159,7 +159,7 @@ self.parserlib = (() => { 'background': '[ , ]* ', 'background-attachment': '#', 'background-blend-mode': '', - 'background-clip': '#', + 'background-clip': '[ | text ]#', 'background-color': '', 'background-image': '#', 'background-origin': '#', From 3d0b733e9af385bc38fa51dceb327225ced634ca Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 15 Oct 2020 14:36:15 +0300 Subject: [PATCH 40/55] parserlib: skip spaces before "," in @document foo() , bar() --- vendor-overwrites/csslint/parserlib.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/vendor-overwrites/csslint/parserlib.js b/vendor-overwrites/csslint/parserlib.js index 7d126f8f..7ab9b454 100644 --- a/vendor-overwrites/csslint/parserlib.js +++ b/vendor-overwrites/csslint/parserlib.js @@ -4326,13 +4326,8 @@ self.parserlib = (() => { _document() { const stream = this._tokenStream; const functions = []; - let prefix = ''; - const start = stream.mustMatch(Tokens.DOCUMENT_SYM); - if (/^@-([^-]+)-/.test(start.value)) { - prefix = RegExp.$1; - } - + const prefix = start.value.split('-')[1] || ''; do { this._ws(); functions.push(this._documentFunction()); @@ -4373,9 +4368,12 @@ self.parserlib = (() => { _documentFunction() { const stream = this._tokenStream; - return stream.match(Tokens.URI) ? - new PropertyValuePart(stream._token) : - this._function(); + if (stream.match(Tokens.URI)) { + const res = new PropertyValuePart(stream._token); + this._ws(); + return res; + } + return this._function(); } _operator(inFunction) { From 5501efb1beb37d51e296c165e40ea7785dc74343 Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 3 Oct 2020 15:05:40 +0300 Subject: [PATCH 41/55] expose version for styles installed from greasyfork/sleazyfork --- background/content-scripts.js | 15 +++++++++++++++ content/install-hook-greasyfork.js | 21 +++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 content/install-hook-greasyfork.js diff --git a/background/content-scripts.js b/background/content-scripts.js index d3c93b40..3aeecd16 100644 --- a/background/content-scripts.js +++ b/background/content-scripts.js @@ -17,6 +17,21 @@ const contentScripts = (() => { } const busyTabs = new Set(); let busyTabsTimer; + + // expose version on greasyfork/sleazyfork 1) info page and 2) code page + const urlMatches = '/scripts/\\d+[^/]*(/code)?([?#].*)?$'; + chrome.webNavigation.onCommitted.addListener(({tabId}) => { + chrome.tabs.executeScript(tabId, { + file: '/content/install-hook-greasyfork.js', + runAt: 'document_start', + }); + }, { + url: [ + {hostEquals: 'greasyfork.org', urlMatches}, + {hostEquals: 'sleazyfork.org', urlMatches}, + ] + }); + return {injectToTab, injectToAllTabs}; function injectToTab({url, tabId, frameId = null}) { diff --git a/content/install-hook-greasyfork.js b/content/install-hook-greasyfork.js new file mode 100644 index 00000000..9e125530 --- /dev/null +++ b/content/install-hook-greasyfork.js @@ -0,0 +1,21 @@ +/* global API */ +'use strict'; + +// onCommitted may fire twice +// Note, we're checking against a literal `1`, not just `if (truthy)`, +// because is exposed per HTML spec as a global variable and `window.INJECTED`. + +if (window.INJECTED_GREASYFORK !== 1) { + window.INJECTED_GREASYFORK = 1; + addEventListener('message', async function onMessage(e) { + if (e.origin === location.origin && + e.data && + e.data.name && + e.data.type === 'style-version-query') { + removeEventListener('message', onMessage); + const style = await API.findUsercss(e.data) || {}; + const {version} = style.usercssData || {}; + postMessage({type: 'style-version', version}, '*'); + } + }); +} From d405bc64aec4bc0a3f69114e44d6cfe085efb8f9 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 9 Oct 2020 21:05:09 +0300 Subject: [PATCH 42/55] ignore empty documents produced by stylus-lang bug --- background/background-worker.js | 2 +- js/moz-parser.js | 13 +++++++++++-- js/usercss.js | 2 +- vendor-overwrites/csslint/parserlib.js | 6 ++++++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/background/background-worker.js b/background/background-worker.js index ddb33d53..1e6126f6 100644 --- a/background/background-worker.js +++ b/background/background-worker.js @@ -37,7 +37,7 @@ function compileUsercss(preprocessor, code, vars) { const builder = getUsercssCompiler(preprocessor); vars = simpleVars(vars); return Promise.resolve(builder.preprocess ? builder.preprocess(code, vars) : code) - .then(code => parseMozFormat({code})) + .then(code => parseMozFormat({code, emptyDocument: preprocessor === 'stylus'})) .then(({sections, errors}) => { if (builder.postprocess) { builder.postprocess(sections, vars); diff --git a/js/moz-parser.js b/js/moz-parser.js index d79c4789..6ffb3b67 100644 --- a/js/moz-parser.js +++ b/js/moz-parser.js @@ -7,10 +7,12 @@ * Puts the global comments into the following section to minimize the amount of global sections. * Doesn't move the comment with ==UserStyle== inside. * @param {string} code + * @param {boolean} emptyDocument - https://github.com/stylus/stylus/issues/2415, + * TODO: update stylus-lang and remove emptyDocument everywhere * @param {number} styleId - used to preserve parserCache on subsequent runs over the same style * @returns {{sections: Array, errors: Array}} */ -function parseMozFormat({code, styleId}) { +function parseMozFormat({code, emptyDocument, styleId}) { const CssToProperty = { 'url': 'urls', 'url-prefix': 'urlPrefixes', @@ -18,7 +20,7 @@ function parseMozFormat({code, styleId}) { 'regexp': 'regexps', }; const hasSingleEscapes = /([^\\]|^)\\([^\\]|$)/; - const parser = new parserlib.css.Parser({starHack: true}); + const parser = new parserlib.css.Parser({starHack: true, emptyDocument}); const sectionStack = [{code: '', start: 0}]; const errors = []; const sections = []; @@ -70,6 +72,13 @@ function parseMozFormat({code, styleId}) { doAddSection(section); }); + parser.addListener('emptydocument', e => { + const token = parser._tokenStream._token; + const section = sectionStack[sectionStack.length - 1]; + section.code += mozStyle.slice(section.start, e.offset); + section.start = token.offset + token.value.length; + }); + parser.addListener('endstylesheet', () => { // add nonclosed outer sections (either broken or the last global one) const lastSection = sectionStack[sectionStack.length - 1]; diff --git a/js/usercss.js b/js/usercss.js index 85937ec6..b2afea36 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -71,7 +71,7 @@ const usercss = (() => { .then(({sections, errors}) => { if (!errors.length) errors = false; if (!sections.length || errors && !allowErrors) { - throw errors; + throw errors || 'Style does not contain any actual CSS to apply.'; } style.sections = sections; return allowErrors ? {style, errors} : style; diff --git a/vendor-overwrites/csslint/parserlib.js b/vendor-overwrites/csslint/parserlib.js index 7ab9b454..60ffc3a1 100644 --- a/vendor-overwrites/csslint/parserlib.js +++ b/vendor-overwrites/csslint/parserlib.js @@ -3865,6 +3865,7 @@ self.parserlib = (() => { * @param {Boolean} [options.starHack] - allows IE6 star hack * @param {Boolean} [options.underscoreHack] - interprets leading underscores as IE6-7 for known properties * @param {Boolean} [options.ieFilters] - accepts IE < 8 filters instead of throwing syntax errors + * @param {Boolean} [options.emptyDocument] - accepts @document without {} block produced by stylus-lang */ constructor(options) { super(); @@ -4333,6 +4334,11 @@ self.parserlib = (() => { functions.push(this._documentFunction()); } while (stream.match(Tokens.COMMA)); + this._ws(); + if (this.options.emptyDocument && stream.peek() !== Tokens.LBRACE) { + this.fire({type: 'emptydocument', functions, prefix}, start); + return; + } stream.mustMatch(Tokens.LBRACE); this.fire({ From e6d73be04934248906c93d8d6850878a3c121a61 Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 18 Oct 2020 16:37:42 +0300 Subject: [PATCH 43/55] option to open editor in a simple window (no omnibox) (#1067) --- _locales/en/messages.json | 4 ++++ background/background.js | 7 +++--- js/messaging.js | 50 ++++++++++++++++----------------------- js/prefs.js | 1 + manage/manage.js | 5 ++-- options.html | 7 ++++++ 6 files changed, 40 insertions(+), 34 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e5f505f3..39f194f3 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1122,6 +1122,10 @@ "message": "Action menu", "description": "Tooltip for menu button in popup." }, + "popupOpenEditInPopup": { + "message": "Use a simple window (no omnibox)", + "description": "Label for the checkbox controlling 'edit' action behavior in the popup." + }, "popupOpenEditInWindow": { "message": "Open editor in a new window", "description": "Label for the checkbox controlling 'edit' action behavior in the popup." diff --git a/background/background.js b/background/background.js index fa54e208..0c4eaaf7 100644 --- a/background/background.js +++ b/background/background.js @@ -303,9 +303,10 @@ function openEditor(params) { u.search = new URLSearchParams(params); return openURL({ url: `${u}`, - newWindow: prefs.get('openEditInWindow'), - windowPosition: prefs.get('windowPosition'), - currentWindow: null + currentWindow: null, + newWindow: prefs.get('openEditInWindow') && Object.assign({}, + prefs.get('openEditInWindow.popup') && {type: 'popup'}, + prefs.get('windowPosition')), }); } diff --git a/js/messaging.js b/js/messaging.js index ec51267c..632b2116 100644 --- a/js/messaging.js +++ b/js/messaging.js @@ -165,47 +165,39 @@ function findExistingTab({url, currentWindow, ignoreHash = true, ignoreSearch = * @param {number} [_.openerTabId] defaults to the active tab * @param {Boolean} [_.active=true] `true` to activate the tab * @param {Boolean|null} [_.currentWindow=true] `null` to check all windows - * @param {Boolean} [_.newWindow=false] `true` to open a new window - * @param {chrome.windows.CreateData} [_.windowPosition] options for chrome.windows.create + * @param {chrome.windows.CreateData} [_.newWindow] creates a new window with these params if specified * @returns {Promise} Promise -> opened/activated tab */ -function openURL({ +async function openURL({ url, index, openerTabId, active = true, currentWindow = true, - newWindow = false, - windowPosition, + newWindow, }) { if (!url.includes('://')) { url = chrome.runtime.getURL(url); } - return findExistingTab({url, currentWindow}).then(tab => { - if (tab) { - return activateTab(tab, { - index, - openerTabId, - // when hash is different we can only set `url` if it has # otherwise the tab would reload - url: url !== (tab.pendingUrl || tab.url) && url.includes('#') ? url : undefined, - }); - } - if (newWindow && browser.windows) { - return browser.windows.create(Object.assign({url}, windowPosition)) - .then(wnd => wnd.tabs[0]); - } - return getActiveTab().then((activeTab = {url: ''}) => - isTabReplaceable(activeTab, url) ? - activateTab(activeTab, {url, openerTabId}) : // not moving the tab - createTabWithOpener(activeTab, {url, index, active})); - }); - function createTabWithOpener(openerTab, options) { - const id = openerTabId == null ? openerTab.id : openerTabId; - if (id != null && !openerTab.incognito && openerTabIdSupported) { - options.openerTabId = id; - } - return browser.tabs.create(options); + let tab = await findExistingTab({url, currentWindow}); + if (tab) { + return activateTab(tab, { + index, + openerTabId, + // when hash is different we can only set `url` if it has # otherwise the tab would reload + url: url !== (tab.pendingUrl || tab.url) && url.includes('#') ? url : undefined, + }); } + if (newWindow && browser.windows) { + return (await browser.windows.create(Object.assign({url}, newWindow)).tabs)[0]; + } + tab = await getActiveTab() || {url: ''}; + if (isTabReplaceable(tab, url)) { + return activateTab(tab, {url, openerTabId}); + } + const id = openerTabId == null ? tab.id : openerTabId; + const opener = id != null && !tab.incognito && openerTabIdSupported && {openerTabId: id}; + return browser.tabs.create(Object.assign({url, index, active}, opener)); } // replace empty tab (NTP or about:blank) diff --git a/js/prefs.js b/js/prefs.js index 3b39cf85..bb3ba650 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -4,6 +4,7 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => { const defaults = { 'openEditInWindow': false, // new editor opens in a own browser window + 'openEditInWindow.popup': false, // new editor opens in a simplified browser window without omnibox 'windowPosition': {}, // detached window position 'show-badge': true, // display text on popup menu icon 'disableAll': false, // boss key diff --git a/manage/manage.js b/manage/manage.js index 2b856600..5bd0ca55 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -392,10 +392,11 @@ Object.assign(handleEvent, { const openWindow = left && shift && !ctrl; const openBackgroundTab = (middle && !shift) || (left && ctrl && !shift); const openForegroundTab = (middle && shift) || (left && ctrl && shift); - const url = $('[href]', event.target.closest('.entry')).href; + const entry = event.target.closest('.entry'); + const url = $('[href]', entry).href; if (openWindow || openBackgroundTab || openForegroundTab) { if (chrome.windows && openWindow) { - chrome.windows.create(Object.assign(prefs.get('windowPosition'), {url})); + API.openEditor({id: entry.styleId}); } else { getOwnTab().then(({index}) => { openURL({ diff --git a/options.html b/options.html index 718a8d31..ca5e0451 100644 --- a/options.html +++ b/options.html @@ -111,6 +111,13 @@ +