From df570dab9e5d73cc10256de4f00cf7a3fc6fd734 Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 1 Apr 2017 05:50:03 +0300 Subject: [PATCH 001/235] Store SVG icons in a collection +preserve the page colors via fill:currentColor and opacity transition. --- edit.html | 49 +++++++++++++++++++++++++++++------------- edit.js | 4 ++-- manage.html | 62 ++++++++++++++++++++++++++++++++--------------------- 3 files changed, 73 insertions(+), 42 deletions(-) diff --git a/edit.html b/edit.html index e12f34ae..a41664fe 100644 --- a/edit.html +++ b/edit.html @@ -86,6 +86,7 @@ } #url { margin-left: 0.25rem; + color: inherit; } #url:not([href^="http"]) { display: none; @@ -93,7 +94,10 @@ .svg-icon { cursor: pointer; vertical-align: middle; - transition: fill .5s; + transition: opacity .5s; + width: 16px; + height: 16px; + fill: currentColor; } .svg-icon:not(.applies-to-help):not(.dismiss) { margin-left: 0.2rem; @@ -101,12 +105,14 @@ h2 .svg-icon, label .svg-icon { margin-top: -2px; } - .svg-icon.info:hover { - fill: #000000; + .svg-icon:hover, + .svg-icon.info { + opacity: .6; + } + .svg-icon, + .svg-icon.info:hover { + opacity: 1; } - a:hover .svg-icon.installed, .svg-icon.dismiss:hover { - fill: hsl(0, 0%, 40%); - } #enabled { margin-left: 0; vertical-align: middle; @@ -275,10 +281,10 @@ max-height: calc(100vh - 8rem); overflow-y: auto; } - #help-popup .close-icon { + #help-popup .dismiss { position: absolute; right: 4px; - top: 4px; + top: .5em; } .keymap-list { @@ -514,7 +520,7 @@
    @@ -565,7 +571,7 @@
    - +
    @@ -579,7 +585,7 @@
    -

    +

    @@ -605,21 +611,34 @@
    - +
    -

    :

    +

    :

    -

    +

    -
    +
    + + + + + + + + + + + + + diff --git a/edit.js b/edit.js index d07c759f..4ae673d4 100644 --- a/edit.js +++ b/edit.js @@ -1348,7 +1348,7 @@ function fromMozillaFormat() { function doImport() { var replaceOldStyle = this.name == "import-replace"; - popup.querySelector(".close-icon").click(); + popup.querySelector(".dismiss").click(); var mozStyle = trimNewLines(popup.codebox.getValue()); var parser = new parserlib.css.Parser(), lines = mozStyle.split("\n"); var sectionStack = [{code: "", start: {line: 1, col: 1}}]; @@ -1575,7 +1575,7 @@ function showHelp(title, text) { if (getComputedStyle(div).display == "none") { document.addEventListener("keydown", closeHelp); - div.querySelector(".close-icon").onclick = closeHelp; // avoid chaining on multiple showHelp() calls + div.querySelector(".dismiss").onclick = closeHelp; // avoid chaining on multiple showHelp() calls } div.style.display = "block"; diff --git a/manage.html b/manage.html index 798a4eab..6a5e4a15 100644 --- a/manage.html +++ b/manage.html @@ -10,11 +10,16 @@ } a, a:visited { - color: #555; - -webkit-transition: color 0.5s; + color: inherit; + opacity: .75; + -webkit-transition: opacity 0.5s; } - a:hover { - color: #999; + a:hover, + a.homepage:hover { + opacity: .6; + } + a.homepage { + opacity: 1; } #header { height: 100%; @@ -47,20 +52,21 @@ height: 2px; background-color: #fff; } - .svg-icon.installed { - cursor: pointer; - vertical-align: middle; - margin-left: 0.3rem; - margin-top: -4px; - transition: fill .5s; - } - a:hover .svg-icon.installed { - fill: hsl(0, 0%, 40%); - } - .style-name { - margin-top: .25em; - word-break: break-word; - } + .svg-icon { + cursor: pointer; + vertical-align: middle; + margin-left: 0.3rem; + margin-right: 0.3rem; + margin-top: -4px; + transition: opacity .5s; + width: 16px; + height: 16px; + fill: currentColor; + } + .style-name { + margin-top: .25em; + word-break: break-word; + } .applies-to { word-break: break-word; } @@ -83,9 +89,10 @@ .enabled .enable { display: none; } - .style-name a[target="_blank"] { - text-decoration: none; - } + .style-name a[target="_blank"] { + text-decoration: none; + color: inherit; + } /* Default, no update buttons */ .update, @@ -219,10 +226,8 @@ @@ -293,6 +298,13 @@

    + + + + + + + From b2e8bf02a9c242dcf242aa50cfa72c9a380e55fd Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 18 Mar 2017 03:24:59 +0300 Subject: [PATCH 002/235] CodeMirror 5.24 Notable change for css mode: Expose lineComment property for LESS and SCSS dialects. Recognize vendor prefixes on pseudo-elements. --- codemirror/LICENSE | 2 + codemirror/addon/comment/comment.js | 19 ++-- codemirror/addon/lint/lint.js | 6 +- codemirror/addon/scroll/annotatescrollbar.js | 6 +- codemirror/keymap/emacs.js | 4 +- codemirror/keymap/sublime.js | 17 ++-- codemirror/keymap/vim.js | 93 +++++++++++++------- codemirror/lib/codemirror.css | 9 +- codemirror/mode/css/css.js | 8 +- codemirror/mode/css/index.html | 2 +- 10 files changed, 113 insertions(+), 53 deletions(-) diff --git a/codemirror/LICENSE b/codemirror/LICENSE index 1bca6bfe..ff7db4b9 100644 --- a/codemirror/LICENSE +++ b/codemirror/LICENSE @@ -1,3 +1,5 @@ +MIT License + Copyright (C) 2017 by Marijn Haverbeke and others Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/codemirror/addon/comment/comment.js b/codemirror/addon/comment/comment.js index d71cf436..568e639d 100644 --- a/codemirror/addon/comment/comment.js +++ b/codemirror/addon/comment/comment.js @@ -46,12 +46,17 @@ // Rough heuristic to try and detect lines that are part of multi-line string function probablyInsideString(cm, pos, line) { - return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"`]/.test(line) + return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"\`]/.test(line) + } + + function getMode(cm, pos) { + var mode = cm.getMode() + return mode.useInnerComments === false || !mode.innerMode ? mode : cm.getModeAt(pos) } CodeMirror.defineExtension("lineComment", function(from, to, options) { if (!options) options = noOptions; - var self = this, mode = self.getModeAt(from); + var self = this, mode = getMode(self, from); var firstLine = self.getLine(from.line); if (firstLine == null || probablyInsideString(self, from, firstLine)) return; @@ -95,7 +100,7 @@ CodeMirror.defineExtension("blockComment", function(from, to, options) { if (!options) options = noOptions; - var self = this, mode = self.getModeAt(from); + var self = this, mode = getMode(self, from); var startString = options.blockCommentStart || mode.blockCommentStart; var endString = options.blockCommentEnd || mode.blockCommentEnd; if (!startString || !endString) { @@ -129,7 +134,7 @@ CodeMirror.defineExtension("uncomment", function(from, to, options) { if (!options) options = noOptions; - var self = this, mode = self.getModeAt(from); + var self = this, mode = getMode(self, from); var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end); // Try finding line comments @@ -171,9 +176,11 @@ endLine = self.getLine(--end); close = endLine.indexOf(endString); } + var insideStart = Pos(start, open + 1), insideEnd = Pos(end, close + 1) if (close == -1 || - !/comment/.test(self.getTokenTypeAt(Pos(start, open + 1))) || - !/comment/.test(self.getTokenTypeAt(Pos(end, close + 1)))) + !/comment/.test(self.getTokenTypeAt(insideStart)) || + !/comment/.test(self.getTokenTypeAt(insideEnd)) || + self.getRange(insideStart, insideEnd, "\n").indexOf(endString) > -1) return false; // Avoid killing block comments completely outside the selection. diff --git a/codemirror/addon/lint/lint.js b/codemirror/addon/lint/lint.js index c1f1702f..e5ee7477 100644 --- a/codemirror/addon/lint/lint.js +++ b/codemirror/addon/lint/lint.js @@ -140,7 +140,11 @@ if (options.async || getAnnotations.async) { lintAsync(cm, getAnnotations, passOptions) } else { - updateLinting(cm, getAnnotations(cm.getValue(), passOptions, cm)); + var annotations = getAnnotations(cm.getValue(), passOptions, cm); + if (annotations.then) annotations.then(function(issues) { + updateLinting(cm, issues); + }); + else updateLinting(cm, annotations); } } diff --git a/codemirror/addon/scroll/annotatescrollbar.js b/codemirror/addon/scroll/annotatescrollbar.js index 5e748e81..f2276fc7 100644 --- a/codemirror/addon/scroll/annotatescrollbar.js +++ b/codemirror/addon/scroll/annotatescrollbar.js @@ -77,17 +77,21 @@ curLine = pos.line; curLineObj = cm.getLineHandle(curLine); } - if (wrapping && curLineObj.height > singleLineH) + if ((curLineObj.widgets && curLineObj.widgets.length) || + (wrapping && curLineObj.height > singleLineH)) return cm.charCoords(pos, "local")[top ? "top" : "bottom"]; var topY = cm.heightAtLine(curLineObj, "local"); return topY + (top ? 0 : curLineObj.height); } + var lastLine = cm.lastLine() if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) { var ann = anns[i]; + if (ann.to.line > lastLine) continue; var top = nextTop || getY(ann.from, true) * hScale; var bottom = getY(ann.to, false) * hScale; while (i < anns.length - 1) { + if (anns[i + 1].to.line > lastLine) break; nextTop = getY(anns[i + 1].from, true) * hScale; if (nextTop > bottom + .9) break; ann = anns[++i]; diff --git a/codemirror/keymap/emacs.js b/codemirror/keymap/emacs.js index 57cf6e85..2d5fe1b8 100644 --- a/codemirror/keymap/emacs.js +++ b/codemirror/keymap/emacs.js @@ -371,7 +371,9 @@ "Shift-Alt-,": "goDocStart", "Shift-Alt-.": "goDocEnd", "Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": quit, "Shift-Alt-5": "replace", "Alt-/": "autocomplete", - "Ctrl-J": "newlineAndIndent", "Enter": false, "Tab": "indentAuto", + "Enter": "newlineAndIndent", + "Ctrl-J": repeated(function(cm) { cm.replaceSelection("\n", "end"); }), + "Tab": "indentAuto", "Alt-G G": function(cm) { var prefix = getPrefix(cm, true); diff --git a/codemirror/keymap/sublime.js b/codemirror/keymap/sublime.js index 3d112ab9..0ce89558 100644 --- a/codemirror/keymap/sublime.js +++ b/codemirror/keymap/sublime.js @@ -152,18 +152,25 @@ var text = cm.getRange(from, to); var query = fullWord ? new RegExp("\\b" + text + "\\b") : text; var cur = cm.getSearchCursor(query, to); - if (cur.findNext()) { - cm.addSelection(cur.from(), cur.to()); - } else { + var found = cur.findNext(); + if (!found) { cur = cm.getSearchCursor(query, Pos(cm.firstLine(), 0)); - if (cur.findNext()) - cm.addSelection(cur.from(), cur.to()); + found = cur.findNext(); } + if (!found || isSelectedRange(cm.listSelections(), cur.from(), cur.to())) + return CodeMirror.Pass + cm.addSelection(cur.from(), cur.to()); } if (fullWord) cm.state.sublimeFindFullWord = cm.doc.sel; }; + function isSelectedRange(ranges, from, to) { + for (var i = 0; i < ranges.length; i++) + if (ranges[i].from() == from && ranges[i].to() == to) return true + return false + } + var mirror = "(){}[]"; function selectBetweenBrackets(cm) { var ranges = cm.listSelections(), newRanges = [] diff --git a/codemirror/keymap/vim.js b/codemirror/keymap/vim.js index 34570bb8..b2c404d4 100644 --- a/codemirror/keymap/vim.js +++ b/codemirror/keymap/vim.js @@ -142,7 +142,7 @@ { keys: 'X', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }}, { keys: 'D', type: 'operatorMotion', operator: 'delete', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'}, { keys: 'D', type: 'operator', operator: 'delete', operatorArgs: { linewise: true }, context: 'visual'}, - { keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'}, + { keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'expandToLine', motionArgs: { linewise: true }, context: 'normal'}, { keys: 'Y', type: 'operator', operator: 'yank', operatorArgs: { linewise: true }, context: 'visual'}, { keys: 'C', type: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'}, { keys: 'C', type: 'operator', operator: 'change', operatorArgs: { linewise: true }, context: 'visual'}, @@ -1245,11 +1245,13 @@ } } function onPromptKeyUp(e, query, close) { - var keyName = CodeMirror.keyName(e), up; + var keyName = CodeMirror.keyName(e), up, offset; if (keyName == 'Up' || keyName == 'Down') { up = keyName == 'Up' ? true : false; + offset = e.target ? e.target.selectionEnd : 0; query = vimGlobalState.searchHistoryController.nextMatch(query, up) || ''; close(query); + if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length); } else { if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift') vimGlobalState.searchHistoryController.reset(); @@ -1281,6 +1283,8 @@ clearInputState(cm); close(); cm.focus(); + } else if (keyName == 'Up' || keyName == 'Down') { + CodeMirror.e_stop(e); } else if (keyName == 'Ctrl-U') { // Ctrl-U clears input. CodeMirror.e_stop(e); @@ -1344,7 +1348,7 @@ exCommandDispatcher.processCommand(cm, input); } function onPromptKeyDown(e, input, close) { - var keyName = CodeMirror.keyName(e), up; + var keyName = CodeMirror.keyName(e), up, offset; if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' || (keyName == 'Backspace' && input == '')) { vimGlobalState.exCommandHistoryController.pushInput(input); @@ -1355,9 +1359,12 @@ cm.focus(); } if (keyName == 'Up' || keyName == 'Down') { + CodeMirror.e_stop(e); up = keyName == 'Up' ? true : false; + offset = e.target ? e.target.selectionEnd : 0; input = vimGlobalState.exCommandHistoryController.nextMatch(input, up) || ''; close(input); + if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length); } else if (keyName == 'Ctrl-U') { // Ctrl-U clears input. CodeMirror.e_stop(e); @@ -1620,9 +1627,8 @@ return findNext(cm, prev/** prev */, query, motionArgs.repeat); }, goToMark: function(cm, _head, motionArgs, vim) { - var mark = vim.marks[motionArgs.selectedCharacter]; - if (mark) { - var pos = mark.find(); + var pos = getMarkPos(cm, vim, motionArgs.selectedCharacter); + if (pos) { return motionArgs.linewise ? { line: pos.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(pos.line)) } : pos; } return null; @@ -3966,6 +3972,17 @@ return {top: from.line, bottom: to.line}; } + function getMarkPos(cm, vim, markName) { + if (markName == '\'') { + var history = cm.doc.history.done; + var event = history[history.length - 2]; + return event && event.ranges && event.ranges[0].head; + } + + var mark = vim.marks[markName]; + return mark && mark.find(); + } + var ExCommandDispatcher = function() { this.buildCommandMap_(); }; @@ -4074,11 +4091,10 @@ case '$': return cm.lastLine(); case '\'': - var mark = cm.state.vim.marks[inputStream.next()]; - if (mark && mark.find()) { - return mark.find().line; - } - throw new Error('Mark not set'); + var markName = inputStream.next(); + var markPos = getMarkPos(cm, cm.state.vim, markName); + if (!markPos) throw new Error('Mark not set'); + return markPos.line; default: inputStream.backUp(1); return undefined; @@ -4147,8 +4163,8 @@ var mapping = { keys: lhs, type: 'keyToEx', - exArgs: { input: rhs.substring(1) }, - user: true}; + exArgs: { input: rhs.substring(1) } + }; if (ctx) { mapping.context = ctx; } defaultKeymap.unshift(mapping); } else { @@ -4156,8 +4172,7 @@ var mapping = { keys: lhs, type: 'keyToKey', - toKeys: rhs, - user: true + toKeys: rhs }; if (ctx) { mapping.context = ctx; } defaultKeymap.unshift(mapping); @@ -4178,8 +4193,7 @@ var keys = lhs; for (var i = 0; i < defaultKeymap.length; i++) { if (keys == defaultKeymap[i].keys - && defaultKeymap[i].context === ctx - && defaultKeymap[i].user) { + && defaultKeymap[i].context === ctx) { defaultKeymap.splice(i, 1); return; } @@ -4310,25 +4324,27 @@ showConfirm(cm, regInfo); }, sort: function(cm, params) { - var reverse, ignoreCase, unique, number; + var reverse, ignoreCase, unique, number, pattern; function parseArgs() { if (params.argString) { var args = new CodeMirror.StringStream(params.argString); if (args.eat('!')) { reverse = true; } if (args.eol()) { return; } if (!args.eatSpace()) { return 'Invalid arguments'; } - var opts = args.match(/[a-z]+/); - if (opts) { - opts = opts[0]; - ignoreCase = opts.indexOf('i') != -1; - unique = opts.indexOf('u') != -1; - var decimal = opts.indexOf('d') != -1 && 1; - var hex = opts.indexOf('x') != -1 && 1; - var octal = opts.indexOf('o') != -1 && 1; + var opts = args.match(/([dinuox]+)?\s*(\/.+\/)?\s*/); + if (!opts && !args.eol()) { return 'Invalid arguments'; } + if (opts[1]) { + ignoreCase = opts[1].indexOf('i') != -1; + unique = opts[1].indexOf('u') != -1; + var decimal = opts[1].indexOf('d') != -1 || opts[1].indexOf('n') != -1 && 1; + var hex = opts[1].indexOf('x') != -1 && 1; + var octal = opts[1].indexOf('o') != -1 && 1; if (decimal + hex + octal > 1) { return 'Invalid arguments'; } number = decimal && 'decimal' || hex && 'hex' || octal && 'octal'; } - if (args.match(/\/.*\//)) { return 'patterns not supported'; } + if (opts[2]) { + pattern = new RegExp(opts[2].substr(1, opts[2].length - 2), ignoreCase ? 'i' : ''); + } } } var err = parseArgs(); @@ -4342,14 +4358,18 @@ var curStart = Pos(lineStart, 0); var curEnd = Pos(lineEnd, lineLength(cm, lineEnd)); var text = cm.getRange(curStart, curEnd).split('\n'); - var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ : + var numberRegex = pattern ? pattern : + (number == 'decimal') ? /(-?)([\d]+)/ : (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i : (number == 'octal') ? /([0-7]+)/ : null; var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null; var numPart = [], textPart = []; - if (number) { + if (number || pattern) { for (var i = 0; i < text.length; i++) { - if (numberRegex.exec(text[i])) { + var matchPart = pattern ? text[i].match(pattern) : null; + if (matchPart && matchPart[0] != '') { + numPart.push(matchPart); + } else if (!pattern && numberRegex.exec(text[i])) { numPart.push(text[i]); } else { textPart.push(text[i]); @@ -4368,8 +4388,17 @@ bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix); return anum - bnum; } - numPart.sort(compareFn); - textPart.sort(compareFn); + function comparePatternFn(a, b) { + if (reverse) { var tmp; tmp = a; a = b; b = tmp; } + if (ignoreCase) { a[0] = a[0].toLowerCase(); b[0] = b[0].toLowerCase(); } + return (a[0] < b[0]) ? -1 : 1; + } + numPart.sort(pattern ? comparePatternFn : compareFn); + if (pattern) { + for (var i = 0; i < numPart.length; i++) { + numPart[i] = numPart[i].input; + } + } else if (!number) { textPart.sort(compareFn); } text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart); if (unique) { // Remove duplicate lines var textOld = text; diff --git a/codemirror/lib/codemirror.css b/codemirror/lib/codemirror.css index 2a6a2622..b962b383 100644 --- a/codemirror/lib/codemirror.css +++ b/codemirror/lib/codemirror.css @@ -223,11 +223,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} cursor: default; z-index: 4; } -.CodeMirror-gutter-wrapper { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} +.CodeMirror-gutter-wrapper ::selection { background-color: transparent } +.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } .CodeMirror-lines { cursor: text; @@ -272,6 +269,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} .CodeMirror-widget {} +.CodeMirror-rtl pre { direction: rtl; } + .CodeMirror-code { outline: none; } diff --git a/codemirror/mode/css/css.js b/codemirror/mode/css/css.js index 90de4ee7..02cc93d9 100644 --- a/codemirror/mode/css/css.js +++ b/codemirror/mode/css/css.js @@ -28,6 +28,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) { colorKeywords = parserConfig.colorKeywords || {}, valueKeywords = parserConfig.valueKeywords || {}, allowNested = parserConfig.allowNested, + lineComment = parserConfig.lineComment, supportsAtComponent = parserConfig.supportsAtComponent === true; var type, override; @@ -253,6 +254,8 @@ CodeMirror.defineMode("css", function(config, parserConfig) { }; states.pseudo = function(type, stream, state) { + if (type == "meta") return "pseudo"; + if (type == "word") { override = "variable-3"; return state.context.type; @@ -407,6 +410,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) { electricChars: "}", blockCommentStart: "/*", blockCommentEnd: "*/", + lineComment: lineComment, fold: "brace" }; }); @@ -663,7 +667,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) { "small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali", "source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "spell-out", "square", "square-button", "start", "static", "status-bar", "stretch", "stroke", "sub", - "subpixel-antialiased", "super", "sw-resize", "symbolic", "symbols", "table", + "subpixel-antialiased", "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", @@ -730,6 +734,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) { valueKeywords: valueKeywords, fontProperties: fontProperties, allowNested: true, + lineComment: "//", tokenHooks: { "/": function(stream, state) { if (stream.eat("/")) { @@ -772,6 +777,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) { valueKeywords: valueKeywords, fontProperties: fontProperties, allowNested: true, + lineComment: "//", tokenHooks: { "/": function(stream, state) { if (stream.eat("/")) { diff --git a/codemirror/mode/css/index.html b/codemirror/mode/css/index.html index 2d2b9b07..0d85311f 100644 --- a/codemirror/mode/css/index.html +++ b/codemirror/mode/css/index.html @@ -64,7 +64,7 @@ code { From df59fca29caffd5aad2b50c88051d7b20a511f4d Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 19 Mar 2017 06:17:23 +0300 Subject: [PATCH 003/235] Add match-highlighter of the word under cursor --- codemirror/addon/search/match-highlighter.js | 165 +++++++++++++++++++ edit.html | 8 + edit.js | 1 + 3 files changed, 174 insertions(+) create mode 100644 codemirror/addon/search/match-highlighter.js diff --git a/codemirror/addon/search/match-highlighter.js b/codemirror/addon/search/match-highlighter.js new file mode 100644 index 00000000..73ba0e05 --- /dev/null +++ b/codemirror/addon/search/match-highlighter.js @@ -0,0 +1,165 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +// Highlighting text that matches the selection +// +// Defines an option highlightSelectionMatches, which, when enabled, +// will style strings that match the selection throughout the +// document. +// +// The option can be set to true to simply enable it, or to a +// {minChars, style, wordsOnly, showToken, delay} object to explicitly +// configure it. minChars is the minimum amount of characters that should be +// selected for the behavior to occur, and style is the token style to +// apply to the matches. This will be prefixed by "cm-" to create an +// actual CSS class name. If wordsOnly is enabled, the matches will be +// highlighted only if the selected text is a word. showToken, when enabled, +// will cause the current token to be highlighted when nothing is selected. +// delay is used to specify how much time to wait, in milliseconds, before +// highlighting the matches. If annotateScrollbar is enabled, the occurences +// will be highlighted on the scrollbar via the matchesonscrollbar addon. + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("./matchesonscrollbar")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "./matchesonscrollbar"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var defaults = { + style: "matchhighlight", + minChars: 2, + delay: 100, + wordsOnly: false, + annotateScrollbar: false, + showToken: false, + trim: true + } + + function State(options) { + this.options = {} + for (var name in defaults) + this.options[name] = (options && options.hasOwnProperty(name) ? options : defaults)[name] + this.overlay = this.timeout = null; + this.matchesonscroll = null; + this.active = false; + } + + CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + removeOverlay(cm); + clearTimeout(cm.state.matchHighlighter.timeout); + cm.state.matchHighlighter = null; + cm.off("cursorActivity", cursorActivity); + cm.off("focus", onFocus) + } + if (val) { + var state = cm.state.matchHighlighter = new State(val); + if (cm.hasFocus()) { + state.active = true + highlightMatches(cm) + } else { + cm.on("focus", onFocus) + } + cm.on("cursorActivity", cursorActivity); + } + }); + + function cursorActivity(cm) { + var state = cm.state.matchHighlighter; + if (state.active || cm.hasFocus()) scheduleHighlight(cm, state) + } + + function onFocus(cm) { + var state = cm.state.matchHighlighter + if (!state.active) { + state.active = true + scheduleHighlight(cm, state) + } + } + + function scheduleHighlight(cm, state) { + clearTimeout(state.timeout); + state.timeout = setTimeout(function() {highlightMatches(cm);}, state.options.delay); + } + + function addOverlay(cm, query, hasBoundary, style) { + var state = cm.state.matchHighlighter; + cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style)); + if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) { + var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query; + state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false, + {className: "CodeMirror-selection-highlight-scrollbar"}); + } + } + + function removeOverlay(cm) { + var state = cm.state.matchHighlighter; + if (state.overlay) { + cm.removeOverlay(state.overlay); + state.overlay = null; + if (state.matchesonscroll) { + state.matchesonscroll.clear(); + state.matchesonscroll = null; + } + } + } + + function highlightMatches(cm) { + cm.operation(function() { + var state = cm.state.matchHighlighter; + removeOverlay(cm); + if (!cm.somethingSelected() && state.options.showToken) { + var re = state.options.showToken === true ? /[\w$]/ : state.options.showToken; + var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start; + while (start && re.test(line.charAt(start - 1))) --start; + while (end < line.length && re.test(line.charAt(end))) ++end; + if (start < end) + addOverlay(cm, line.slice(start, end), re, state.options.style); + return; + } + var from = cm.getCursor("from"), to = cm.getCursor("to"); + if (from.line != to.line) return; + if (state.options.wordsOnly && !isWord(cm, from, to)) return; + var selection = cm.getRange(from, to) + if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "") + if (selection.length >= state.options.minChars) + addOverlay(cm, selection, false, state.options.style); + }); + } + + function isWord(cm, from, to) { + var str = cm.getRange(from, to); + if (str.match(/^\w+$/) !== null) { + if (from.ch > 0) { + var pos = {line: from.line, ch: from.ch - 1}; + var chr = cm.getRange(pos, from); + if (chr.match(/\W/) === null) return false; + } + if (to.ch < cm.getLine(from.line).length) { + var pos = {line: to.line, ch: to.ch + 1}; + var chr = cm.getRange(to, pos); + if (chr.match(/\W/) === null) return false; + } + return true; + } else return false; + } + + function boundariesAround(stream, re) { + return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) && + (stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos))); + } + + function makeOverlay(query, hasBoundary, style) { + return {token: function(stream) { + if (stream.match(query) && + (!hasBoundary || boundariesAround(stream, hasBoundary))) + return style; + stream.next(); + stream.skipTo(query.charAt(0)) || stream.skipToEnd(); + }}; + } +}); diff --git a/edit.html b/edit.html index a41664fe..f2831947 100644 --- a/edit.html +++ b/edit.html @@ -9,6 +9,7 @@ + @@ -189,6 +190,13 @@ .CodeMirror-search-hint { color: #888; } + .cm-matchhighlight { + text-decoration: underline; + text-decoration-skip: ink; + } + .CodeMirror-selection-highlight-scrollbar { + background-color: rgba(144, 179, 214, 0.3); + } @-webkit-keyframes highlight { from { background-color: #ff9; diff --git a/edit.js b/edit.js index 4ae673d4..8443af7e 100644 --- a/edit.js +++ b/edit.js @@ -138,6 +138,7 @@ function initCodeMirror() { foldGutter: true, gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"], matchBrackets: true, + highlightSelectionMatches: {showToken: /[#.\-\w]/, annotateScrollbar: true}, lint: {getAnnotations: CodeMirror.lint.css, delay: prefs.get("editor.lintDelay")}, lintReportDelay: prefs.get("editor.lintReportDelay"), styleActiveLine: true, From f4e689721ae09fb781577594f3458a4f35434ce2 Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 18 Mar 2017 01:50:35 +0300 Subject: [PATCH 004/235] Improve style caching, cache requests too, add code:false mode Previously, when a cache was invalidated and every tab/iframe issued a getStyles request, we previous needlessly accessed IndexedDB for each of these requests. It happened because 1) the global cachedStyles was created only at the end of the async DB-reading, 2) and each style record is retrieved asynchronously so the single threaded JS engine interleaved all these operations. It could easily span a few seconds when many tabs are open and you have like 100 styles. Now, in getStyles: all requests issued while cachedStyles is being populated are queued and invoked at the end. Now, in filterStyles: all requests are cached using the request's options combined in a string as a key. It also helps on each navigation because we monitor page loading process at different stages: before, when committed, history traversal, requesting applicable styles by a content script. Icon badge update also may issue a copy of the just issued request by one of the navigation listeners. Now, the caches are invalidated smartly: style add/update/delete/toggle only purges filtering cache, and modifies style cache in-place without re-reading the entire IndexedDB. Now, code:false mode for manage page that only needs style meta. It reduces the transferred message size 10-100 times thus reducing the overhead caused by to internal JSON-fication in the extensions API. Also fast&direct getStylesSafe for own pages; code cosmetics --- .eslintrc | 1 + apply.js | 38 +++++-- background.js | 37 ++----- edit.js | 18 +-- manage.js | 6 +- messaging.js | 57 +++++----- popup.js | 3 +- storage.js | 299 ++++++++++++++++++++++++++++++++++++-------------- 8 files changed, 299 insertions(+), 160 deletions(-) diff --git a/.eslintrc b/.eslintrc index f71971cd..479e9a94 100644 --- a/.eslintrc +++ b/.eslintrc @@ -12,6 +12,7 @@ env: globals: CodeMirror: false runTryCatch: true + getStylesSafe: true getStyles: true updateIcon: true saveStyle: true diff --git a/apply.js b/apply.js index e882ed9f..3d95fc4e 100644 --- a/apply.js +++ b/apply.js @@ -9,20 +9,22 @@ var retiredStyleIds = []; initObserver(); requestStyles(); -function requestStyles() { +function requestStyles(options = {}) { // If this is a Stylish page (Edit Style or Manage Styles), // we'll request the styles directly to minimize delay and flicker, // unless Chrome still starts up and the background page isn't fully loaded. // (Note: in this case the function may be invoked again from applyStyles.) - var request = {method: "getStyles", matchUrl: location.href, enabled: true, asHash: true}; - if (location.href.indexOf(chrome.extension.getURL("")) == 0) { - var bg = chrome.extension.getBackgroundPage(); - if (bg && bg.getStyles) { - // apply styles immediately, then proceed with a normal request that will update the icon - bg.getStyles(request, applyStyles); - } + var request = Object.assign({ + method: "getStyles", + matchUrl: location.href, + enabled: true, + asHash: true, + }, options); + if (typeof getStylesSafe !== 'undefined') { + getStylesSafe(request).then(applyStyles); + } else { + chrome.runtime.sendMessage(request, applyStyles); } - chrome.runtime.sendMessage(request, applyStyles); } chrome.runtime.onMessage.addListener(applyOnMessage); @@ -34,6 +36,10 @@ function applyOnMessage(request, sender, sendResponse) { removeStyle(request.id, document); break; case "styleUpdated": + if (request.codeIsUpdated === false) { + applyStyleState(request.style.id, request.style.enabled, document); + break; + } if (request.style.enabled) { retireStyle(request.style.id); // fallthrough to "styleAdded" @@ -92,6 +98,20 @@ function disableAll(disable) { } } +function applyStyleState(id, enabled, doc) { + var e = doc.getElementById("stylus-" + id); + if (!e) { + if (enabled) { + requestStyles({id}); + } + } else { + e.sheet.disabled = !enabled; + getDynamicIFrames(doc).forEach(function(iframe) { + applyStyleState(id, iframe.contentDocument); + }); + } +} + function removeStyle(id, doc) { var e = doc.getElementById("stylus-" + id); delete g_styleElements["stylus-" + id]; diff --git a/background.js b/background.js index bea7653b..b7915c76 100644 --- a/background.js +++ b/background.js @@ -1,36 +1,21 @@ /* globals wildcardAsRegExp, KEEP_CHANNEL_OPEN */ -var frameIdMessageable; -runTryCatch(function() { - chrome.tabs.sendMessage(0, {}, {frameId: 0}, function() { - var clearError = chrome.runtime.lastError; - frameIdMessageable = true; - }); -}); - // This happens right away, sometimes so fast that the content script isn't even ready. That's // why the content script also asks for this stuff. -chrome.webNavigation.onCommitted.addListener(webNavigationListener.bind(this, "styleApply")); -// Not supported in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1239349 -if ("onHistoryStateUpdated" in chrome.webNavigation) { - chrome.webNavigation.onHistoryStateUpdated.addListener(webNavigationListener.bind(this, "styleReplaceAll")); -} +chrome.webNavigation.onCommitted.addListener(webNavigationListener.bind(this, 'styleApply')); +chrome.webNavigation.onHistoryStateUpdated.addListener(webNavigationListener.bind(this, 'styleReplaceAll')); chrome.webNavigation.onBeforeNavigate.addListener(webNavigationListener.bind(this, null)); + function webNavigationListener(method, data) { - // Until Chrome 41, we can't target a frame with a message - // (https://developer.chrome.com/extensions/tabs#method-sendMessage) - // so a style affecting a page with an iframe will affect the main page as well. - // Skip doing this for frames in pre-41 to prevent page flicker. - if (data.frameId != 0 && !frameIdMessageable) { - return; - } - getStyles({matchUrl: data.url, enabled: true, asHash: true}, function(styleHash) { - if (method) { - chrome.tabs.sendMessage(data.tabId, {method: method, styles: styleHash}, - frameIdMessageable ? {frameId: data.frameId} : undefined); + getStyles({matchUrl: data.url, enabled: true, asHash: true}, styles => { + // we can't inject chrome:// and chrome-extension:// pages except our own + // that request the styles on their own, so we'll only update the icon + if (method && !data.url.startsWith('chrome')) { + chrome.tabs.sendMessage(data.tabId, {method, styles}, {frameId: data.frameId}); } + // main page frame id is 0 if (data.frameId == 0) { - updateIcon({id: data.tabId, url: data.url}, styleHash); + updateIcon({id: data.tabId, url: data.url}, styles); } }); } @@ -70,7 +55,7 @@ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { return KEEP_CHANNEL_OPEN; case "invalidateCache": if (typeof invalidateCache != "undefined") { - invalidateCache(false); + invalidateCache(false, request); } break; case "healthCheck": diff --git a/edit.js b/edit.js index 8443af7e..faffc91a 100644 --- a/edit.js +++ b/edit.js @@ -1087,18 +1087,10 @@ function init() { } // This is an edit tE("heading", "editStyleHeading", null, false); - requestStyle(); - function requestStyle() { - chrome.runtime.sendMessage({method: "getStyles", id: params.id}, function callback(styles) { - if (!styles) { // Chrome is starting up and shows edit.html - requestStyle(); - return; - } - var style = styles[0]; - styleId = style.id; - initWithStyle(style); - }); - } + getStylesSafe({id: params.id}).then(styles => { + styleId = styles[0].id; + initWithStyle(styles[0]); + }); } function initWithStyle(style) { @@ -1107,7 +1099,7 @@ function initWithStyle(style) { document.getElementById("url").href = style.url; // if this was done in response to an update, we need to clear existing sections getSections().forEach(function(div) { div.remove(); }); - var queue = style.sections.length ? style.sections : [{code: ""}]; + var queue = style.sections.length ? style.sections.slice() : [{code: ""}]; var queueStart = new Date().getTime(); // after 100ms the sections will be added asynchronously while (new Date().getTime() - queueStart <= 100 && queue.length) { diff --git a/manage.js b/manage.js index 955f9e41..3b779272 100644 --- a/manage.js +++ b/manage.js @@ -6,13 +6,9 @@ var appliesToExtraTemplate = document.createElement("span"); appliesToExtraTemplate.className = "applies-to-extra"; appliesToExtraTemplate.innerHTML = " " + t('appliesDisplayTruncatedSuffix'); -chrome.runtime.sendMessage({method: "getStyles"}, showStyles); +getStylesSafe({code: false}).then(showStyles); function showStyles(styles) { - if (!styles) { // Chrome is starting up - chrome.runtime.sendMessage({method: "getStyles"}, showStyles); - return; - } if (!installed) { // "getStyles" message callback is invoked before document is loaded, // postpone the action until DOMContentLoaded is fired diff --git a/messaging.js b/messaging.js index 379eace0..c9eb1f5f 100644 --- a/messaging.js +++ b/messaging.js @@ -4,12 +4,15 @@ const OWN_ORIGIN = chrome.runtime.getURL(''); function notifyAllTabs(request) { // list all tabs including chrome-extension:// which can be ours + if (request.codeIsUpdated === false && request.style) { + request = Object.assign({}, request, { + style: getStyleWithNoCode(request.style) + }); + } chrome.tabs.query({}, tabs => { for (let tab of tabs) { - if (request.codeIsUpdated !== false || tab.url.startsWith(OWN_ORIGIN)) { - chrome.tabs.sendMessage(tab.id, request); - updateIcon(tab); - } + chrome.tabs.sendMessage(tab.id, request); + updateIcon(tab); } }); // notify all open popups @@ -47,57 +50,59 @@ function refreshAllTabs() { function updateIcon(tab, styles) { // while NTP is still loading only process the request for its main frame with a real url // (but when it's loaded we should process style toggle requests from popups, for example) - if (tab.url == "chrome://newtab/" && tab.status != "complete") { + if (tab.url == 'chrome://newtab/' && tab.status != 'complete') { return; } if (styles) { // check for not-yet-existing tabs e.g. omnibox instant search - chrome.tabs.get(tab.id, function() { + chrome.tabs.get(tab.id, () => { if (!chrome.runtime.lastError) { - // for 'styles' asHash:true fake the length by counting numeric ids manually - if (styles.length === undefined) { - styles.length = 0; - for (var id in styles) { - styles.length += id.match(/^\d+$/) ? 1 : 0; - } - } stylesReceived(styles); } }); return; } - getTabRealURL(tab, function(url) { - // if we have access to this, call directly. a page sending a message to itself doesn't seem to work right. - if (typeof getStyles != "undefined") { - getStyles({matchUrl: url, enabled: true}, stylesReceived); + getTabRealURL(tab, url => { + // if we have access to this, call directly + // (Chrome no longer sends messages to the page itself) + const options = {method: 'getStyles', matchUrl: url, enabled: true, asHash: true}; + if (typeof getStyles != 'undefined') { + getStyles(options, stylesReceived); } else { - chrome.runtime.sendMessage({method: "getStyles", matchUrl: url, enabled: true}, stylesReceived); + chrome.runtime.sendMessage(options, stylesReceived); } }); function stylesReceived(styles) { - var disableAll = "disableAll" in styles ? styles.disableAll : prefs.get("disableAll"); - var postfix = disableAll ? "x" : styles.length == 0 ? "w" : ""; + let numStyles = styles.length; + if (numStyles === undefined) { + // for 'styles' asHash:true fake the length by counting numeric ids manually + numStyles = 0; + for (let id of Object.keys(styles)) { + numStyles += id.match(/^\d+$/) ? 1 : 0; + } + } + const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll'); + const postfix = disableAll ? 'x' : numStyles == 0 ? 'w' : ''; chrome.browserAction.setIcon({ path: { // Material Design 2016 new size is 16px - 16: "16" + postfix + ".png", 32: "32" + postfix + ".png", + 16: '16' + postfix + '.png', 32: '32' + postfix + '.png', // Chromium forks or non-chromium browsers may still use the traditional 19px - 19: "19" + postfix + ".png", 38: "38" + postfix + ".png", + 19: '19' + postfix + '.png', 38: '38' + postfix + '.png', }, tabId: tab.id - }, function() { + }, () => { // if the tab was just closed an error may occur, // e.g. 'windowPosition' pref updated in edit.js::window.onbeforeunload if (!chrome.runtime.lastError) { - var t = prefs.get("show-badge") && styles.length ? ("" + styles.length) : ""; - chrome.browserAction.setBadgeText({text: t, tabId: tab.id}); + const text = prefs.get('show-badge') && numStyles ? String(numStyles) : ''; + chrome.browserAction.setBadgeText({text, tabId: tab.id}); chrome.browserAction.setBadgeBackgroundColor({ color: prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal') }); } }); - //console.log("Tab " + tab.id + " (" + tab.url + ") badge text set to '" + t + "'."); } } diff --git a/popup.js b/popup.js index cfdda45f..85665ce4 100644 --- a/popup.js +++ b/popup.js @@ -19,7 +19,8 @@ function updatePopUp(url) { return; } - chrome.runtime.sendMessage({method: "getStyles", matchUrl: url}, showStyles); + getStylesSafe({matchUrl: url}).then(showStyles); + document.querySelector("#find-styles a").href = "https://userstyles.org/styles/browse/all/" + encodeURIComponent("file" === urlWillWork[1] ? "file:" : url); // Write new style links diff --git a/storage.js b/storage.js index 47c56c9a..8a40ec21 100644 --- a/storage.js +++ b/storage.js @@ -17,103 +17,226 @@ function getDatabase(ready, error) { } }; -var cachedStyles = null; + +// Let manage/popup/edit reuse background page variables +// Note, only "var"-declared variables are visible from another extension page +var cachedStyles = ((bg) => bg && bg.cache || { + bg, + list: null, + noCode: null, + byId: new Map(), + filters: new Map(), + mutex: { + inProgress: false, + onDone: [], + }, +})(chrome.extension.getBackgroundPage()); + + +// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage +function getStylesSafe(options) { + return new Promise(resolve => { + if (cachedStyles.bg) { + getStyles(options, resolve); + return; + } + chrome.runtime.sendMessage(Object.assign({method: 'getStyles'}, options), styles => { + if (!styles) { + resolve(getStylesSafe(options)); + } else { + cachedStyles = chrome.extension.getBackgroundPage().cachedStyles; + resolve(styles); + } + }); + }); +} + + function getStyles(options, callback) { - if (cachedStyles != null) { - callback(filterStyles(cachedStyles, options)); + if (cachedStyles.list) { + callback(filterStyles(options)); return; } - getDatabase(function(db) { - var tx = db.transaction(["styles"], "readonly"); - var os = tx.objectStore("styles"); - var all = []; - os.openCursor().onsuccess = function(event) { - var cursor = event.target.result; + if (cachedStyles.mutex.inProgress) { + cachedStyles.mutex.onDone.push({options, callback}); + return; + } + cachedStyles.mutex.inProgress = true; + + const t0 = performance.now() + getDatabase(db => { + const tx = db.transaction(['styles'], 'readonly'); + const os = tx.objectStore('styles'); + const all = []; + os.openCursor().onsuccess = event => { + const cursor = event.target.result; if (cursor) { - var s = cursor.value; + const s = cursor.value; s.id = cursor.key; all.push(cursor.value); cursor.continue(); } else { - cachedStyles = all; + cachedStyles.list = all; + cachedStyles.noCode = []; + for (let style of all) { + const noCode = getStyleWithNoCode(style); + cachedStyles.noCode.push(noCode); + cachedStyles.byId.set(style.id, {style, noCode}); + } + //console.log('%s getStyles %s, invoking cached callbacks: %o', (performance.now() - t0).toFixed(1), JSON.stringify(options), cache.mutex.onDone.map(e => JSON.stringify(e.options))) try{ - callback(filterStyles(all, options)); + callback(filterStyles(options)); } catch(e){ // no error in console, it works } + + cachedStyles.mutex.inProgress = false; + for (let {options, callback} of cachedStyles.mutex.onDone) { + callback(filterStyles(options)); + } + cachedStyles.mutex.onDone = []; } }; - }, null); + }, null); } -function invalidateCache(andNotify) { - cachedStyles = null; + +function getStyleWithNoCode(style) { + const stripped = Object.assign({}, style, {sections: []}); + for (let section of style.sections) { + stripped.sections.push(Object.assign({}, section, {code: null})); + } + return stripped; +} + + +function invalidateCache(andNotify, {added, updated, deletedId} = {}) { + // prevent double-add on echoed invalidation + const cached = added && cachedStyles.byId.get(added.id); + if (cached) { + return; + } if (andNotify) { - chrome.runtime.sendMessage({method: "invalidateCache"}); + chrome.runtime.sendMessage({method: 'invalidateCache', added, updated, deletedId}); } -} - -function filterStyles(styles, options) { - var enabled = fixBoolean(options.enabled); - var url = "url" in options ? options.url : null; - var id = "id" in options ? Number(options.id) : null; - var matchUrl = "matchUrl" in options ? options.matchUrl : null; - - if (enabled != null) { - styles = styles.filter(function(style) { - return style.enabled == enabled; - }); + if (!cachedStyles.list) { + return; } - if (url != null) { - styles = styles.filter(function(style) { - return style.url == url; - }); - } - if (id != null) { - styles = styles.filter(function(style) { - return style.id == id; - }); - } - if (matchUrl != null) { - // Return as a hash from style to applicable sections? Can only be used with matchUrl. - var asHash = "asHash" in options ? options.asHash : false; - if (asHash) { - var h = {disableAll: prefs.get("disableAll", false)}; - styles.forEach(function(style) { - var applicableSections = getApplicableSections(style, matchUrl); - if (applicableSections.length > 0) { - h[style.id] = applicableSections; - } - }); - return h; + if (updated) { + const cached = cachedStyles.byId.get(updated.id); + if (cached) { + Object.assign(cached.style, updated); + Object.assign(cached.noCode, getStyleWithNoCode(updated)); + //console.log('cache: updated', updated); } - styles = styles.filter(function(style) { - var applicableSections = getApplicableSections(style, matchUrl); - return applicableSections.length > 0; - }); + cachedStyles.filters.clear(); + return; } - return styles; + if (added) { + const noCode = getStyleWithNoCode(added); + cachedStyles.list.push(added); + cachedStyles.noCode.push(noCode); + cachedStyles.byId.set(added.id, {style: added, noCode}); + //console.log('cache: added', added); + cachedStyles.filters.clear(); + return; + } + if (deletedId != undefined) { + const deletedStyle = (cachedStyles.byId.get(deletedId) || {}).style; + if (deletedStyle) { + const cachedIndex = cachedStyles.list.indexOf(deletedStyle); + cachedStyles.list.splice(cachedIndex, 1); + cachedStyles.noCode.splice(cachedIndex, 1); + cachedStyles.byId.delete(deletedId); + //console.log('cache: deleted', deletedStyle); + cachedStyles.filters.clear(); + return; + } + } + cachedStyles.list = null; + cachedStyles.noCode = null; + //console.log('cache cleared'); + cachedStyles.filters.clear(); } + +function filterStyles(options = {}) { + const t0 = performance.now() + const enabled = fixBoolean(options.enabled); + const url = 'url' in options ? options.url : null; + const id = 'id' in options ? Number(options.id) : null; + const matchUrl = 'matchUrl' in options ? options.matchUrl : null; + const code = 'code' in options ? options.code : true; + const asHash = 'asHash' in options ? options.asHash : false; + + if (enabled == null + && url == null + && id == null + && matchUrl == null + && asHash != true) { + //console.log('%c%s filterStyles SKIPPED LOOP %s', 'color:gray', (performance.now() - t0).toFixed(1), JSON.stringify(options)) + return code ? cachedStyles.list : cachedStyles.noCode; + } + + // add \t after url to prevent collisions (not sure it can actually happen though) + const cacheKey = '' + enabled + url + '\t' + id + matchUrl + '\t' + code + asHash; + const cached = cachedStyles.filters.get(cacheKey); + if (cached) { + //console.log('%c%s filterStyles REUSED RESPONSE %s', 'color:gray', (performance.now() - t0).toFixed(1), JSON.stringify(options)) + return asHash + ? Object.assign({disableAll: prefs.get('disableAll', false)}, cached) + : cached; + } + + const styles = id == null + ? (code ? cachedStyles.list : cachedStyles.noCode) + : [code ? cachedStyles.byId.get(id).style : cachedStyles.byId.get(id).noCode]; + const filtered = asHash ? {} : []; + + for (let i = 0, style; (style = styles[i]); i++) { + if ((enabled == null || style.enabled == enabled) + && (url == null || style.url == url) + && (id == null || style.id == id)) { + const sections = (asHash || matchUrl != null) && getApplicableSections(style, matchUrl); + if (asHash) { + if (sections.length) { + filtered[style.id] = sections; + } + } else if (matchUrl == null || sections.length) { + filtered.push(style); + } + } + } + //console.log('%s filterStyles %s', (performance.now() - t0).toFixed(1), JSON.stringify(options)) + cachedStyles.filters.set(cacheKey, filtered); + return asHash + ? Object.assign({disableAll: prefs.get('disableAll', false)}, filtered) + : filtered; +} + + function saveStyle(style, {notify = true} = {}) { return new Promise(resolve => { getDatabase(db => { const tx = db.transaction(['styles'], 'readwrite'); const os = tx.objectStore('styles'); + delete style.method; + // Update if (style.id) { style.id = Number(style.id); os.get(style.id).onsuccess = eventGet => { const oldStyle = Object.assign({}, eventGet.target.result); - const codeIsUpdated = !styleSectionsEqual(style, oldStyle); + const codeIsUpdated = 'sections' in style && !styleSectionsEqual(style, oldStyle); style = Object.assign(oldStyle, style); + addMissingStyleTargets(style); os.put(style).onsuccess = eventPut => { style.id = style.id || eventPut.target.result; + invalidateCache(notify, {updated: style}); if (notify) { notifyAllTabs({method: 'styleUpdated', style, codeIsUpdated}); } - invalidateCache(notify); resolve(style); }; }; @@ -121,6 +244,7 @@ function saveStyle(style, {notify = true} = {}) { } // Create + delete style.id; style = Object.assign({ // Set optional things if they're undefined enabled: true, @@ -128,23 +252,12 @@ function saveStyle(style, {notify = true} = {}) { md5Url: null, url: null, originalMd5: null, - }, style, { - // Set other optional things to empty array if they're undefined - sections: style.sections.map(section => - Object.assign({ - urls: [], - urlPrefixes: [], - domains: [], - regexps: [], - }, section) - ), - }) - // Make sure it's not null - that makes indexeddb sad - delete style.id; + }, style); + addMissingStyleTargets(style); os.add(style).onsuccess = event => { - invalidateCache(true); // Give it the ID that was generated style.id = event.target.result; + invalidateCache(true, {added: style}); notifyAllTabs({method: 'styleAdded', style}); resolve(style); }; @@ -152,13 +265,25 @@ function saveStyle(style, {notify = true} = {}) { }); } -function enableStyle(id, enabled) { - saveStyle({id: id, enabled: enabled}).then(style => { - handleUpdate(style); - notifyAllTabs({method: "styleUpdated", style}); - }); + +function addMissingStyleTargets(style) { + style.sections = (style.sections || []).map(section => + Object.assign({ + urls: [], + urlPrefixes: [], + domains: [], + regexps: [], + }, section) + ); } + +function enableStyle(id, enabled) { + saveStyle({id, enabled}) + .then(handleUpdate); +} + + function deleteStyle(id, callback = function (){}) { getDatabase(function(db) { var tx = db.transaction(["styles"], "readwrite"); @@ -166,13 +291,14 @@ function deleteStyle(id, callback = function (){}) { var request = os.delete(Number(id)); request.onsuccess = function(event) { handleDelete(id); - invalidateCache(true); - notifyAllTabs({method: "styleDeleted", id: id}); + invalidateCache(true, {deletedId: id}); + notifyAllTabs({method: "styleDeleted", id}); callback(); }; }); } + function reportError() { for (i in arguments) { if ("message" in arguments[i]) { @@ -182,6 +308,7 @@ function reportError() { } } + function fixBoolean(b) { if (typeof b != "undefined") { return b != "false"; @@ -189,6 +316,7 @@ function fixBoolean(b) { return null; } + function getDomains(url) { if (url.indexOf("file:") == 0) { return []; @@ -202,6 +330,7 @@ function getDomains(url) { return domains; } + function getType(o) { if (typeof o == "undefined" || typeof o == "string") { return typeof o; @@ -212,7 +341,8 @@ function getType(o) { throw "Not supported - " + o; } -var namespacePattern = /^\s*(@namespace[^;]+;\s*)+$/; +const namespacePattern = /^\s*(@namespace[^;]+;\s*)+$/; + function getApplicableSections(style, url) { var sections = style.sections.filter(function(section) { return sectionAppliesToUrl(section, url); @@ -224,6 +354,7 @@ function getApplicableSections(style, url) { return sections; } + function sectionAppliesToUrl(section, url) { // only http, https, file, and chrome-extension allowed if (url.indexOf("http") != 0 && url.indexOf("file") != 0 && url.indexOf("chrome-extension") != 0 && url.indexOf("ftp") != 0) { @@ -275,6 +406,7 @@ function sectionAppliesToUrl(section, url) { return false; } + function isCheckbox(el) { return el.nodeName.toLowerCase() == "input" && "checkbox" == el.type.toLowerCase(); } @@ -462,6 +594,7 @@ var prefs = chrome.extension.getBackgroundPage().prefs || new function Prefs() { } }; + function getCodeMirrorThemes(callback) { chrome.runtime.getPackageDirectoryEntry(function(rootDir) { rootDir.getDirectory("codemirror/theme", {create: false}, function(themeDir) { @@ -481,6 +614,7 @@ function getCodeMirrorThemes(callback) { }); } + function sessionStorageHash(name) { var hash = { value: {}, @@ -495,6 +629,7 @@ function sessionStorageHash(name) { return hash; } + function deepCopy(obj) { if (!obj || typeof obj != "object") { return obj; @@ -504,6 +639,7 @@ function deepCopy(obj) { } } + function deepMerge(target, obj1 /* plus any number of object arguments */) { for (var i = 1; i < arguments.length; i++) { var obj = arguments[i]; @@ -522,6 +658,7 @@ function deepMerge(target, obj1 /* plus any number of object arguments */) { return target; } + function equal(a, b) { if (!a || !b || typeof a != "object" || typeof b != "object") { return a === b; @@ -537,6 +674,7 @@ function equal(a, b) { return true; } + function defineReadonlyProperty(obj, key, value) { var copy = deepCopy(value); // In ES6, freezing a literal is OK (it returns the same value), but in previous versions it's an exception. @@ -546,7 +684,7 @@ function defineReadonlyProperty(obj, key, value) { Object.defineProperty(obj, key, {value: copy, configurable: true}) } -// Polyfill, can be removed when Firefox gets this - https://bugzilla.mozilla.org/show_bug.cgi?id=1220494 +// Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494 function getSync() { if ("sync" in chrome.storage) { return chrome.storage.sync; @@ -567,6 +705,7 @@ function getSync() { } } + function styleSectionsEqual(styleA, styleB) { if (!styleA.sections || !styleB.sections) { return undefined; From f256f558dc4e4235cd62a527282d09318ddf3d6f Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 19 Mar 2017 22:49:43 +0300 Subject: [PATCH 005/235] IndexedDB getAll to read all styles in one op Available since Chrome 48, FF44 (or FF27+ using dom.indexedDB.experimental flag) --- storage.js | 50 +++++++++++++++++++++----------------------------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/storage.js b/storage.js index 8a40ec21..5c627d18 100644 --- a/storage.js +++ b/storage.js @@ -20,7 +20,7 @@ function getDatabase(ready, error) { // Let manage/popup/edit reuse background page variables // Note, only "var"-declared variables are visible from another extension page -var cachedStyles = ((bg) => bg && bg.cache || { +var cachedStyles = ((bg) => bg && bg.cachedStyles || { bg, list: null, noCode: null, @@ -67,35 +67,27 @@ function getStyles(options, callback) { getDatabase(db => { const tx = db.transaction(['styles'], 'readonly'); const os = tx.objectStore('styles'); - const all = []; - os.openCursor().onsuccess = event => { - const cursor = event.target.result; - if (cursor) { - const s = cursor.value; - s.id = cursor.key; - all.push(cursor.value); - cursor.continue(); - } else { - cachedStyles.list = all; - cachedStyles.noCode = []; - for (let style of all) { - const noCode = getStyleWithNoCode(style); - cachedStyles.noCode.push(noCode); - cachedStyles.byId.set(style.id, {style, noCode}); - } - //console.log('%s getStyles %s, invoking cached callbacks: %o', (performance.now() - t0).toFixed(1), JSON.stringify(options), cache.mutex.onDone.map(e => JSON.stringify(e.options))) - try{ - callback(filterStyles(options)); - } catch(e){ - // no error in console, it works - } - - cachedStyles.mutex.inProgress = false; - for (let {options, callback} of cachedStyles.mutex.onDone) { - callback(filterStyles(options)); - } - cachedStyles.mutex.onDone = []; + os.getAll().onsuccess = event => { + cachedStyles.list = event.target.result || []; + cachedStyles.noCode = []; + cachedStyles.byId.clear(); + for (let style of cachedStyles.list) { + const noCode = getStyleWithNoCode(style); + cachedStyles.noCode.push(noCode); + cachedStyles.byId.set(style.id, {style, noCode}); } + //console.log('%s getStyles %s, invoking cached callbacks: %o', (performance.now() - t0).toFixed(1), JSON.stringify(options), cachedStyles.mutex.onDone.map(e => JSON.stringify(e.options))) + try{ + callback(filterStyles(options)); + } catch(e){ + // no error in console, it works + } + + cachedStyles.mutex.inProgress = false; + for (let {options, callback} of cachedStyles.mutex.onDone) { + callback(filterStyles(options)); + } + cachedStyles.mutex.onDone = []; }; }, null); } From b2e18177c3df05ef49cd37594426d218acaf9ab8 Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 19 Mar 2017 23:30:20 +0300 Subject: [PATCH 006/235] Minimum Chrome version is 49 for ES6 stuff we use Default function parameters were implemented in Chrome 49 and FF 15+ Stable Chrome is v57. One year has passed since v49. --- manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/manifest.json b/manifest.json index 4acfbc88..82530612 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,7 @@ { "name": "Stylus", "version": "1.0.5", + "minimum_chrome_version": "49", "description": "__MSG_description__", "homepage_url": "http://add0n.com/stylus.html", "manifest_version": 2, From ba8301fdcef108069a706b11bc5e845eb216b671 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 20 Mar 2017 00:24:29 +0300 Subject: [PATCH 007/235] Middle-click in popup on a style name to open the editor --- popup.js | 112 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 70 insertions(+), 42 deletions(-) diff --git a/popup.js b/popup.js index 85665ce4..a0d48dae 100644 --- a/popup.js +++ b/popup.js @@ -91,44 +91,67 @@ function showStyles(styles) { } function createStyleElement(style) { - var e = template.style.cloneNode(true); - var checkbox = e.querySelector(".checker"); - checkbox.id = "style-" + style.id; - checkbox.checked = style.enabled; + // reuse event function references + createStyleElement.events = createStyleElement.events || { + checkboxClick() { + enableStyle(getClickedStyleId(), this.checked); + }, + styleNameClick() { + this.checkbox.click(); + window.event.preventDefault(); + }, + toggleClick() { + enableStyle(getClickedStyleId(), this.matches('.enable')); + }, + deleteClick() { + doDelete(); + } + }; + const entry = template.style.cloneNode(true); + entry.setAttribute('style-id', style.id); + Object.assign(entry, { + styleId: style.id, + className: ['entry', style.enabled ? 'enabled' : 'disabled'].join(' '), + onmousedown: openEditorOnMiddleclick, + onauxclick: openEditorOnMiddleclick, + }); - e.setAttribute("class", "entry " + (style.enabled ? "enabled" : "disabled")); - e.setAttribute("style-id", style.id); - var styleName = e.querySelector(".style-name"); - styleName.appendChild(document.createTextNode(style.name)); - styleName.setAttribute("for", "style-" + style.id); + const checkbox = entry.querySelector('.checker'); + Object.assign(checkbox, { + id: 'style-' + style.id, + checked: style.enabled, + onclick: createStyleElement.events.checkboxClick, + }); + + const editLink = entry.querySelector('.style-edit-link'); + Object.assign(editLink, { + href: editLink.getAttribute('href') + style.id, + onclick: openLinkInTabOrWindow, + }); + + const styleName = entry.querySelector('.style-name'); + Object.assign(styleName, { + htmlFor: 'style-' + style.id, + onclick: createStyleElement.events.styleNameClick, + }); styleName.checkbox = checkbox; - var editLink = e.querySelector(".style-edit-link"); - editLink.setAttribute("href", editLink.getAttribute("href") + style.id); - editLink.addEventListener("click", openLinkInTabOrWindow, false); + styleName.appendChild(document.createTextNode(style.name)); - styleName.addEventListener("click", function() { this.checkbox.click(); event.preventDefault(); }); - // clicking the checkbox will toggle it, and this will run after that happens - checkbox.addEventListener("click", function() { enable(event, event.target.checked); }, false); - e.querySelector(".enable").addEventListener("click", function() { enable(event, true); }, false); - e.querySelector(".disable").addEventListener("click", function() { enable(event, false); }, false); + entry.querySelector('.enable').onclick = createStyleElement.events.toggleClick; + entry.querySelector('.disable').onclick = createStyleElement.events.toggleClick; + entry.querySelector('.delete').onclick = createStyleElement.events.deleteClick; - e.querySelector(".delete").addEventListener("click", function() { doDelete(event, false); }, false); - return e; -} - -function enable(event, enabled) { - var id = getId(event); - enableStyle(id, enabled); + return entry; } function doDelete() { document.getElementById('confirm').dataset.display = true; - let id = getId(event); + const id = getClickedStyleId(); document.querySelector('#confirm b').textContent = document.querySelector(`[style-id="${id}"] label`).textContent; document.getElementById('confirm').dataset.id = id; - } + document.getElementById('confirm').addEventListener('click', e => { let cmd = e.target.dataset.cmd; if (cmd === 'ok') { @@ -145,22 +168,9 @@ document.getElementById('confirm').addEventListener('click', e => { } }); -function getBrowser() { - if (navigator.userAgent.indexOf("OPR") > -1) { - return "Opera"; - } - return "Chrome"; -} - -function getId(event) { - var e = event.target; - while (e) { - if (e.hasAttribute("style-id")) { - return e.getAttribute("style-id"); - } - e = e.parentNode; - } - return null; +function getClickedStyleId() { + const entry = window.event.target.closest('.entry'); + return entry ? entry.styleId : null; } function openLinkInTabOrWindow(event) { @@ -176,6 +186,24 @@ function openLinkInTabOrWindow(event) { close(); } +function openEditorOnMiddleclick(event) { + if (event.button != 1) { + return; + } + // open an editor on middleclick + if (event.target.matches('.entry, .style-name, .style-edit-link')) { + this.querySelector('.style-edit-link').click(); + event.preventDefault(); + return; + } + // prevent the popup being opened in a background tab + // when an irrelevant link was accidentally clicked + if (event.target.closest('a')) { + event.preventDefault(); + return; + } +} + function openLink(event) { event.preventDefault(); chrome.runtime.sendMessage({method: "openURL", url: event.target.href}); From 1dde91ea852ec83cb0a6659759479d6303cdf5f1 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 20 Mar 2017 01:58:10 +0300 Subject: [PATCH 008/235] Global 'event' var is non-standard, FF doesn't support it --- manage.js | 2 +- popup.js | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/manage.js b/manage.js index 3b779272..0cc68127 100644 --- a/manage.js +++ b/manage.js @@ -132,7 +132,7 @@ function enable(event, enabled) { enableStyle(id, enabled); } -function doDelete() { +function doDelete(event) { if (!confirm(t('deleteStyleConfirm'))) { return; } diff --git a/popup.js b/popup.js index a0d48dae..75b0ccb8 100644 --- a/popup.js +++ b/popup.js @@ -94,17 +94,17 @@ function createStyleElement(style) { // reuse event function references createStyleElement.events = createStyleElement.events || { checkboxClick() { - enableStyle(getClickedStyleId(), this.checked); + enableStyle(getClickedStyleId(event), this.checked); }, - styleNameClick() { + styleNameClick(event) { this.checkbox.click(); - window.event.preventDefault(); + event.preventDefault(); }, - toggleClick() { - enableStyle(getClickedStyleId(), this.matches('.enable')); + toggleClick(event) { + enableStyle(getClickedStyleId(event), this.matches('.enable')); }, deleteClick() { - doDelete(); + doDelete(event); } }; const entry = template.style.cloneNode(true); @@ -144,9 +144,9 @@ function createStyleElement(style) { return entry; } -function doDelete() { +function doDelete(event) { document.getElementById('confirm').dataset.display = true; - const id = getClickedStyleId(); + const id = getClickedStyleId(event); document.querySelector('#confirm b').textContent = document.querySelector(`[style-id="${id}"] label`).textContent; document.getElementById('confirm').dataset.id = id; @@ -168,8 +168,8 @@ document.getElementById('confirm').addEventListener('click', e => { } }); -function getClickedStyleId() { - const entry = window.event.target.closest('.entry'); +function getClickedStyleId(event) { + const entry = event.target.closest('.entry'); return entry ? entry.styleId : null; } From 8c7f7b81f88e078f0f48ec5602e256cdec84c9c1 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 20 Mar 2017 04:58:55 +0300 Subject: [PATCH 009/235] Don't recreate editors after save --- edit.js | 26 +++++++++++++++++--------- storage.js | 6 ++++-- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/edit.js b/edit.js index faffc91a..fcf4f0cb 100644 --- a/edit.js +++ b/edit.js @@ -1088,15 +1088,23 @@ function init() { // This is an edit tE("heading", "editStyleHeading", null, false); getStylesSafe({id: params.id}).then(styles => { - styleId = styles[0].id; - initWithStyle(styles[0]); + const style = styles[0]; + styleId = style.id; + initWithStyle({style}); }); } -function initWithStyle(style) { +function initWithStyle({style, codeIsUpdated}) { document.getElementById("name").value = style.name; document.getElementById("enabled").checked = style.enabled; document.getElementById("url").href = style.url; + + if (codeIsUpdated === false) { + setCleanGlobal(); + updateTitle(); + return; + } + // if this was done in response to an update, we need to clear existing sections getSections().forEach(function(div) { div.remove(); }); var queue = style.sections.length ? style.sections.slice() : [{code: ""}]; @@ -1247,14 +1255,14 @@ function save() { } var name = document.getElementById("name").value; var enabled = document.getElementById("enabled").checked; - var request = { - method: "saveStyle", + saveStyle({ id: styleId, name: name, enabled: enabled, + reason: 'editSave', sections: getSectionsHashes() - }; - chrome.runtime.sendMessage(request, saveComplete); + }) + .then(saveComplete); } function getSectionsHashes() { @@ -1621,8 +1629,8 @@ function getParams() { chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { switch (request.method) { case "styleUpdated": - if (styleId && styleId == request.id) { - initWithStyle(request.style); + if (styleId && styleId == request.style.id && request.reason != 'editSave') { + initWithStyle(request); } break; case "styleDeleted": diff --git a/storage.js b/storage.js index 5c627d18..1696ad1e 100644 --- a/storage.js +++ b/storage.js @@ -213,7 +213,9 @@ function saveStyle(style, {notify = true} = {}) { const tx = db.transaction(['styles'], 'readwrite'); const os = tx.objectStore('styles'); + const reason = style.reason; delete style.method; + delete style.reason; // Update if (style.id) { @@ -227,7 +229,7 @@ function saveStyle(style, {notify = true} = {}) { style.id = style.id || eventPut.target.result; invalidateCache(notify, {updated: style}); if (notify) { - notifyAllTabs({method: 'styleUpdated', style, codeIsUpdated}); + notifyAllTabs({method: 'styleUpdated', style, codeIsUpdated, reason}); } resolve(style); }; @@ -250,7 +252,7 @@ function saveStyle(style, {notify = true} = {}) { // Give it the ID that was generated style.id = event.target.result; invalidateCache(true, {added: style}); - notifyAllTabs({method: 'styleAdded', style}); + notifyAllTabs({method: 'styleAdded', style, reason}); resolve(style); }; }); From 7a7c6798119fe1b77573bed14c912e4f9fbc2451 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 20 Mar 2017 07:59:51 +0300 Subject: [PATCH 010/235] Avoid flickering of editor header on load, also for manage<=>edit nav --- edit.html | 11 ++++++----- edit.js | 30 ++++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/edit.html b/edit.html index f2831947..7cde2c7b 100644 --- a/edit.html +++ b/edit.html @@ -41,16 +41,16 @@ body { margin: 0; - font: 9pt arial,sans-serif; + font: 12px arial,sans-serif; } /************ header ************/ #header { height: calc(100vh - 30px); overflow: auto; - width: 15rem; + width: 250px; position: fixed; top: 0; - padding: 0.95rem; + padding: 15px; border-right: 1px dashed #AAA; -webkit-box-shadow: 0 0 3rem -1.2rem black; } @@ -58,10 +58,11 @@ margin-top: 0; } #sections { - padding-left: 18rem; + padding-left: 280px; } #sections h2 { - margin-top: 0.5rem; + margin-top: 1rem; + margin-left: 1.7rem; } .aligned { display: table-row; diff --git a/edit.js b/edit.js index fcf4f0cb..ace4d0ab 100644 --- a/edit.js +++ b/edit.js @@ -1066,23 +1066,26 @@ function beautify(event) { } } -window.addEventListener("load", init, false); +document.addEventListener("DOMContentLoaded", init); function init() { var params = getParams(); if (!params.id) { // match should be 2 - one for the whole thing, one for the parentheses // This is an add + tE("heading", "addStyleTitle"); var section = {code: ""} for (var i in CssToProperty) { if (params[i]) { section[CssToProperty[i]] = [params[i]]; } } - addSection(null, section); - // default to enabled - document.getElementById("enabled").checked = true - tE("heading", "addStyleTitle"); - initHooks(); + onload = () => { + onload = null; + addSection(null, section); + // default to enabled + document.getElementById("enabled").checked = true + initHooks(); + }; return; } // This is an edit @@ -1090,14 +1093,25 @@ function init() { getStylesSafe({id: params.id}).then(styles => { const style = styles[0]; styleId = style.id; - initWithStyle({style}); + setStyleMeta(style); + onload = () => { + onload = null; + initWithStyle({style}); + }; + if (document.readyState != 'loading') { + onload(); + } }); } -function initWithStyle({style, codeIsUpdated}) { +function setStyleMeta(style) { document.getElementById("name").value = style.name; document.getElementById("enabled").checked = style.enabled; document.getElementById("url").href = style.url; +} + +function initWithStyle({style, codeIsUpdated}) { + setStyleMeta(style); if (codeIsUpdated === false) { setCleanGlobal(); From f746cb15816c1a81590f14ea30768c5459e1e70c Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 20 Mar 2017 12:30:19 +0300 Subject: [PATCH 011/235] Update lint report immediately on load --- edit.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/edit.js b/edit.js index ace4d0ab..f948ecbb 100644 --- a/edit.js +++ b/edit.js @@ -309,7 +309,7 @@ function acmeEventListener(event) { // replace given textarea with the CodeMirror editor function setupCodeMirror(textarea, index) { - var cm = CodeMirror.fromTextArea(textarea); + var cm = CodeMirror.fromTextArea(textarea, {lint: null}); cm.on("change", indicateCodeChange); cm.on("blur", function(cm) { @@ -858,29 +858,25 @@ function getEditorInSight(nearbyElement) { function updateLintReport(cm, delay) { if (delay == 0) { // immediately show pending csslint messages in onbeforeunload and save - update.call(cm); + update(cm); return; } if (delay > 0) { - // give csslint some time to find the issues, e.g. 500 (1/10 of our default 5s) - // by settings its internal delay to 1ms and restoring it back later - var lintOpt = editors[0].state.lint.options; - setTimeout((function(opt, delay) { - opt.delay = delay == 1 ? opt.delay : delay; // options object is shared between editors - update(this); - }).bind(cm, lintOpt, lintOpt.delay), delay); - lintOpt.delay = 1; + setTimeout(cm => { cm.performLint(); update(cm) }, delay, cm); + return; + } + var state = cm.state.lint; + if (!state) { return; } // user is editing right now: postpone updating the report for the new issues (default: 500ms lint + 4500ms) // or update it as soon as possible (default: 500ms lint + 100ms) in case an existing issue was just fixed - var state = cm.state.lint; clearTimeout(state.reportTimeout); - state.reportTimeout = setTimeout(update.bind(cm), state.options.delay + 100); + state.reportTimeout = setTimeout(update, state.options.delay + 100, cm); state.postponeNewIssues = delay == undefined || delay == null; - function update() { // this == cm - var scope = this ? [this] : editors; + function update(cm) { + var scope = cm ? [cm] : editors; var changed = false; var fixedOldIssues = false; scope.forEach(function(cm) { @@ -939,7 +935,7 @@ function renderLintReport(someBlockChanged) { var newContent = content.cloneNode(false); var issueCount = 0; editors.forEach(function(cm, index) { - if (cm.state.lint.html) { + if (cm.state.lint && cm.state.lint.html) { var newBlock = newContent.appendChild(document.createElement("table")); var html = "" + label + " " + (index+1) + "" + cm.state.lint.html; newBlock.innerHTML = html; @@ -1138,7 +1134,11 @@ function initWithStyle({style, codeIsUpdated}) { function add() { var sectionDiv = addSection(null, queue.shift()); maximizeCodeHeight(sectionDiv, !queue.length); - updateLintReport(sectionDiv.CodeMirror, prefs.get("editor.lintDelay")); + const cm = sectionDiv.CodeMirror; + setTimeout(() => { + cm.setOption('lint', CodeMirror.defaults.lint); + updateLintReport(cm, 0); + }, prefs.get("editor.lintDelay")); } } From 9fd067c6e33e0956d90988e19424b8bdefdcd586 Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 21 Mar 2017 00:35:51 +0300 Subject: [PATCH 012/235] Switch editor to add style mode when id=nonexistentstyle --- edit.js | 6 +++++- storage.js | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/edit.js b/edit.js index f948ecbb..a17fabdb 100644 --- a/edit.js +++ b/edit.js @@ -1087,7 +1087,11 @@ function init() { // This is an edit tE("heading", "editStyleHeading", null, false); getStylesSafe({id: params.id}).then(styles => { - const style = styles[0]; + let style = styles[0]; + if (!style) { + style = {id: null, sections: []}; + history.replaceState({}, document.title, location.pathname); + } styleId = style.id; setStyleMeta(style); onload = () => { diff --git a/storage.js b/storage.js index 1696ad1e..9d7fd5cd 100644 --- a/storage.js +++ b/storage.js @@ -182,9 +182,13 @@ function filterStyles(options = {}) { const styles = id == null ? (code ? cachedStyles.list : cachedStyles.noCode) - : [code ? cachedStyles.byId.get(id).style : cachedStyles.byId.get(id).noCode]; + : [(cachedStyles.byId.get(id) || {})[code ? 'style' : 'noCode']]; const filtered = asHash ? {} : []; - + if (!styles) { + // may happen when users [accidentally] reopen an old URL + // of edit.html with a non-existent style id parameter + return filtered; + } for (let i = 0, style; (style = styles[i]); i++) { if ((enabled == null || style.enabled == enabled) && (url == null || style.url == url) From 2f4da37fdb1fe036a916bf54c6f26612d35858e0 Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 21 Mar 2017 04:32:38 +0300 Subject: [PATCH 013/235] Refactor and speed up popup & manager Popup: * Enforce 200-800px range for the popup width option Manage: * faster search via cachedStyles.byId * faster restoration of search results on history nav * style name is clickable and opens the editor * animated highlight of style element on update/add/save * expandable extra applies-to targets * remember scroll position on normal history navigation * boz-sizing in #header, also in editor * applies-to targets use structured markup * get*Tab*, enableStyle and deleteStyle are promisified --- .eslintrc | 2 + background.js | 17 +- backup/fileSaveLoad.js | 1 + edit.html | 5 +- edit.js | 2 +- health.js | 2 +- manage.css | 284 ++++++++++++++ manage.html | 412 ++++++--------------- manage.js | 819 +++++++++++++++++++++-------------------- messaging.js | 82 ++++- options/index.html | 2 +- options/index.js | 16 +- popup.css | 10 +- popup.html | 8 +- popup.js | 493 +++++++++++++------------ storage.js | 26 +- 16 files changed, 1199 insertions(+), 982 deletions(-) create mode 100644 manage.css diff --git a/.eslintrc b/.eslintrc index 479e9a94..c32b677b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -34,6 +34,8 @@ globals: getType: true importStyles: true getActiveTabRealURL: true + openURL: true + onDOMready: true getDomains: true webSqlStorage: true notifyAllTabs: true diff --git a/background.js b/background.js index b7915c76..79940789 100644 --- a/background.js +++ b/background.js @@ -1,4 +1,4 @@ -/* globals wildcardAsRegExp, KEEP_CHANNEL_OPEN */ +/* globals openURL, wildcardAsRegExp, KEEP_CHANNEL_OPEN */ // This happens right away, sometimes so fast that the content script isn't even ready. That's // why the content script also asks for this stuff. @@ -149,21 +149,6 @@ chrome.tabs.onAttached.addListener(function(tabId, data) { }); }); -function openURL(options) { - chrome.tabs.query({currentWindow: true, url: options.url}, function(tabs) { - // switch to an existing tab with the requested url - if (tabs.length) { - chrome.tabs.highlight({windowId: tabs[0].windowId, tabs: tabs[0].index}, function (window) {}); - } else { - delete options.method; - getActiveTab(function(tab) { - // re-use an active new tab page - chrome.tabs[tab.url == "chrome://newtab/" ? "update" : "create"](options); - }); - } - }); -} - var codeMirrorThemes; getCodeMirrorThemes(function(themes) { codeMirrorThemes = themes; diff --git a/backup/fileSaveLoad.js b/backup/fileSaveLoad.js index 2c4736a7..89db6fc9 100644 --- a/backup/fileSaveLoad.js +++ b/backup/fileSaveLoad.js @@ -64,6 +64,7 @@ function importFromString(jsonString) { }); } else { refreshAllTabs().then(() => { + scrollTo(0, 0); setTimeout(alert, 100, numStyles + ' styles installed/updated'); resolve(numStyles); }); diff --git a/edit.html b/edit.html index 7cde2c7b..2d9e8d06 100644 --- a/edit.html +++ b/edit.html @@ -45,14 +45,15 @@ } /************ header ************/ #header { - height: calc(100vh - 30px); + width: 280px; + height: 100vh; overflow: auto; - width: 250px; position: fixed; top: 0; padding: 15px; border-right: 1px dashed #AAA; -webkit-box-shadow: 0 0 3rem -1.2rem black; + box-sizing: border-box; } #header h1 { margin-top: 0; diff --git a/edit.js b/edit.js index a17fabdb..a2043067 100644 --- a/edit.js +++ b/edit.js @@ -415,7 +415,7 @@ chrome.tabs.query({currentWindow: true}, function(tabs) { }); }); -getActiveTab(function(tab) { +getActiveTab().then(tab => { useHistoryBack = sessionStorageHash("manageStylesHistory").value[tab.id] == location.href; }); diff --git a/health.js b/health.js index 857e0736..727fa4ad 100644 --- a/health.js +++ b/health.js @@ -1,4 +1,4 @@ -healthCheck(); +setTimeout(healthCheck, 0); function healthCheck() { chrome.runtime.sendMessage({method: "healthCheck"}, function(ok) { diff --git a/manage.css b/manage.css new file mode 100644 index 00000000..b83669de --- /dev/null +++ b/manage.css @@ -0,0 +1,284 @@ +body { + margin: 0; + font: 12px arial, sans-serif; +} + +a, +a:visited { + color: inherit; + opacity: .75; + -webkit-transition: opacity 0.5s; +} + +a:hover, +a.homepage:hover { + opacity: .6; +} + +a.homepage { + opacity: 1; +} + +#header { + width: 280px; + height: 100vh; + position: fixed; + top: 0; + padding: 15px; + border-right: 1px dashed #AAA; + -webkit-box-shadow: 0 0 50px -18px black; + overflow: auto; + box-sizing: border-box; +} + +#header h1 { + margin-top: 0; +} + +#installed { + position: relative; + margin-left: 280px; +} + +.entry { + margin: 0; + padding: 1.25em 2em 1.5em; + border-top: 1px solid #ddd; +} + +.entry:first-child { + border-top: none; +} + +.svg-icon { + cursor: pointer; + vertical-align: middle; + margin-left: 0.3rem; + margin-right: 0.3rem; + margin-top: -4px; + transition: opacity .5s; + width: 16px; + height: 16px; + fill: currentColor; +} + +.style-name { + margin-top: .25em; +} + +.style-name a, .style-edit-link { + text-decoration: none; + color: inherit; +} + +.applies-to { + word-break: break-word; +} + +.applies-to, +.actions { + padding-left: 15px; + margin-bottom: 0; +} + +.applies-to > :first-child { + margin-right: .5ex; +} + +.applies-to .target:hover { + background-color: rgba(128, 128, 128, .15); +} + +.applies-to-extra { + display: inline; +} + +.applies-to-extra summary { + font-weight: bold; + cursor: pointer; + list-style-type: none; /* for FF, allegedly */ +} + +.applies-to-extra summary::-webkit-details-marker { + display: none; +} + +.disabled h2::after { + content: " (Disabled)"; +} + +.disabled { + opacity: 0.5; +} + +.disabled .disable { + display: none; +} + +.enabled .enable { + display: none; +} + +/* Default, no update buttons */ + +.update, +.check-update { + display: none; +} + +/* Check update button for things that can*/ + +*[style-update-url] .check-update { + display: inline; +} + +/* Update check in progress */ + +.checking-update .check-update { + display: none; +} + +/* Updates available */ + +.can-update .update { + display: inline; +} + +.can-update .check-update { + display: none; +} + +/* Updates not available */ + +.no-update .check-update { + display: none; +} + +/* Updates done */ + +.update-done .check-update { + display: none; +} + +.hidden { + display: none +} + +fieldset { + border-width: 1px; + border-radius: 6px; + margin: 1em 0; +} + +.enabled-only > .disabled, +.edited-only > [style-update-url] { + display: none; +} + +#search { + width: calc(100% - 4px); + margin: 0.25rem 4px 0; + border-radius: 0.25rem; + padding-left: 0.25rem; + border-width: 1px; +} + +#import ul { + margin-left: 0; + padding-left: 0; + list-style: none; +} + +#import li { + margin-bottom: .5em; +} + +#import pre { + background: #eee; + overflow: auto; + margin: 0 0 .5em 0; +} + +/* drag-n-drop on import button */ +.dropzone:after { + background-color: rgba(0, 0, 0, 0.7); + color: white; + left: 0; + top: 0; + right: 0; + bottom: 0; + z-index: 1000; + position: fixed; + padding: calc(50vh - 3em) calc(50vw - 5em); + content: attr(dragndrop-hint); + text-shadow: 1px 1px 10px black; + font-size: xx-large; + text-align: center; + animation: fadein 1s cubic-bezier(.03, .67, .08, .94); + animation-fill-mode: both; +} + +.fadeout.dropzone:after { + animation: fadeout .25s ease-in-out; + animation-fill-mode: both; +} + +@keyframes fadein { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fadeout { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +@media (max-width: 675px) { + #header { + height: auto; + position: static; + width: auto; + border-right: none; + border-bottom: 1px dashed #AAA; + } + + #installed { + position: static; + margin-left: 0; + overflow: visible; + } + + #header h1, + #header h2, + #header h3, + #backup-message { + display: none; + } + + #header p, + #header fieldset div, + #backup { + display: inline-block; + } + + #backup { + margin-right: 1em; + } + + #backup p, + #header fieldset { + margin: 0; + } + + .entry { + margin: 0; + } +} diff --git a/manage.html b/manage.html index 6a5e4a15..ea2629d6 100644 --- a/manage.html +++ b/manage.html @@ -1,312 +1,128 @@ - + - - - + + + - - - - - - - - - + + + + + + + + + + + + + + + - + + + @@ -75,11 +80,10 @@ - diff --git a/popup.js b/popup.js index 75b0ccb8..3fe9fbf3 100644 --- a/popup.js +++ b/popup.js @@ -1,275 +1,298 @@ -/* globals configureCommands */ +/* globals configureCommands, openURL */ -var writeStyleTemplate = document.createElement("a"); -writeStyleTemplate.className = "write-style-link"; +const RX_SUPPORTED_URLS = new RegExp( + `^(file|https?|ftps?):|^${OWN_ORIGIN}`); +let installed; -var installed = document.getElementById("installed"); -if (!prefs.get("popup.stylesFirst")) { - document.body.insertBefore(document.querySelector("body > .actions"), installed); +getActiveTabRealURL().then(url => { + const isUrlSupported = RX_SUPPORTED_URLS.test(url); + Promise.all([ + isUrlSupported ? getStylesSafe({matchUrl: url}) : null, + onDOMready().then(() => initPopup(isUrlSupported ? url : '')), + ]) + .then(([styles]) => styles && showStyles(styles)); +}); + + +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + if (msg.method == 'updatePopup') { + switch (msg.reason) { + case 'styleAdded': + case 'styleUpdated': + handleUpdate(msg.style); + break; + case 'styleDeleted': + handleDelete(msg.id); + break; + } + } +}); + + +function initPopup(url) { + installed = $('#installed'); + + // popup width + document.body.style.width = + Math.max(200, Math.min(800, Number(localStorage.popupWidth) || 246)) + 'px'; + + // confirm dialog + $('#confirm').onclick = e => { + const cmd = e.target.dataset.cmd; + if (cmd === 'ok') { + deleteStyle($('#confirm').dataset.id).then(() => { + // update view with 'No styles installed for this site' message + if ($('#installed').children.length === 0) { + showStyles([]); + } + }); + } + // + if (cmd) { + $('#confirm').dataset.display = false; + } + }; + + // action buttons + $('#disableAll').onchange = () => + installed.classList.toggle('disabled', prefs.get('disableAll')); + setupLivePrefs(['disableAll']); + $('#find-styles-link').onclick = openURLandHide; + $('#popup-manage-button').href = 'manage.html'; + $('#popup-manage-button').onclick = openURLandHide; + $('#popup-options-button').onclick = () => chrome.runtime.openOptionsPage(); + $('#popup-shortcuts-button').onclick = configureCommands.open; + + // styles first? + if (!prefs.get('popup.stylesFirst')) { + document.body.insertBefore( + $('body > .actions'), + installed); + } + + // find styles link + $('#find-styles a').href = + 'https://userstyles.org/styles/browse/all/' + + encodeURIComponent(url.startsWith('file:') ? 'file:' : url); + + if (!url) { + document.body.classList.add('blocked'); + return; + } + + // Write new style links + const writeStyle = $('#write-style'); + const matchTargets = document.createElement('span'); + matchTargets.id = 'match'; + + // For this URL + const urlLink = template.writeStyle.cloneNode(true); + Object.assign(urlLink, { + href: 'edit.html?url-prefix=' + encodeURIComponent(url), + title: `url-prefix("${url}")`, + textContent: prefs.get('popup.breadcrumbs.usePath') + ? new URL(url).pathname.slice(1) + : t('writeStyleForURL').replace(/ /g, '\u00a0'), // this URL + onclick: openLinkInTabOrWindow, + }); + if (prefs.get('popup.breadcrumbs')) { + urlLink.onmouseenter = + urlLink.onfocus = () => urlLink.parentNode.classList.add('url()'); + urlLink.onmouseleave = + urlLink.onblur = () => urlLink.parentNode.classList.remove('url()'); + } + matchTargets.appendChild(urlLink); + + // For domain + const domains = getDomains(url); + for (let domain of domains) { + // Don't include TLD + if (domains.length > 1 && !domain.includes('.')) { + continue; + } + const domainLink = template.writeStyle.cloneNode(true); + Object.assign(domainLink, { + href: 'edit.html?domain=' + encodeURIComponent(domain), + textContent: domain, + title: `domain("${domain}")`, + onclick: openLinkInTabOrWindow, + }); + domainLink.setAttribute('subdomain', domain.substring(0, domain.indexOf('.'))); + matchTargets.appendChild(domainLink); + } + + if (prefs.get('popup.breadcrumbs')) { + matchTargets.classList.add('breadcrumbs'); + matchTargets.appendChild(matchTargets.removeChild(matchTargets.firstElementChild)); + } + writeStyle.appendChild(matchTargets); } -getActiveTabRealURL(updatePopUp); - -function updatePopUp(url) { - var urlWillWork = /^(file|http|https|ftps?|chrome\-extension):/.exec(url); - if (!urlWillWork) { - document.body.classList.add("blocked"); - document.getElementById("unavailable").style.display = "flex"; - return; - } - - getStylesSafe({matchUrl: url}).then(showStyles); - - document.querySelector("#find-styles a").href = "https://userstyles.org/styles/browse/all/" + encodeURIComponent("file" === urlWillWork[1] ? "file:" : url); - - // Write new style links - var writeStyleLinks = [], - container = document.createElement('span'); - container.id = "match"; - - // For this URL - var urlLink = writeStyleTemplate.cloneNode(true); - urlLink.href = "edit.html?url-prefix=" + encodeURIComponent(url); - urlLink.appendChild(document.createTextNode( // switchable; default="this URL" - !prefs.get("popup.breadcrumbs.usePath") - ? t("writeStyleForURL").replace(/ /g, "\u00a0") - : /\/\/[^/]+\/(.*)/.exec(url)[1] - )); - urlLink.title = "url-prefix(\"$\")".replace("$", url); - writeStyleLinks.push(urlLink); - document.querySelector("#write-style").appendChild(urlLink) - if (prefs.get("popup.breadcrumbs")) { // switchable; default=enabled - urlLink.addEventListener("mouseenter", function(event) { this.parentNode.classList.add("url()") }, false); - urlLink.addEventListener("focus", function(event) { this.parentNode.classList.add("url()") }, false); - urlLink.addEventListener("mouseleave", function(event) { this.parentNode.classList.remove("url()") }, false); - urlLink.addEventListener("blur", function(event) { this.parentNode.classList.remove("url()") }, false); - } - - // For domain - var domains = getDomains(url) - domains.forEach(function(domain) { - // Don't include TLD - if (domains.length > 1 && domain.indexOf(".") == -1) { - return; - } - var domainLink = writeStyleTemplate.cloneNode(true); - domainLink.href = "edit.html?domain=" + encodeURIComponent(domain); - domainLink.appendChild(document.createTextNode(domain)); - domainLink.title = "domain(\"$\")".replace("$", domain); - domainLink.setAttribute("subdomain", domain.substring(0, domain.indexOf("."))); - writeStyleLinks.push(domainLink); - }); - - var writeStyle = document.querySelector("#write-style"); - writeStyleLinks.forEach(function(link, index) { - link.addEventListener("click", openLinkInTabOrWindow, false); - container.appendChild(link); - }); - if (prefs.get("popup.breadcrumbs")) { - container.classList.add("breadcrumbs"); - container.appendChild(container.removeChild(container.firstChild)); - } - writeStyle.appendChild(container); -} function showStyles(styles) { - var enabledFirst = prefs.get("popup.enabledFirst"); - styles.sort(function(a, b) { - if (enabledFirst && a.enabled !== b.enabled) return !(a.enabled < b.enabled) ? -1 : 1; - return a.name.localeCompare(b.name); - }); - if (styles.length == 0) { - installed.innerHTML = "
    " + t('noStylesForSite') + "
    "; - } - styles.map(createStyleElement).forEach(function(e) { - installed.appendChild(e); - }); - // force Chrome to resize the popup - document.body.style.height = '10px'; - document.documentElement.style.height = '10px'; + if (!styles.length) { + installed.innerHTML = + `
    ${t('noStylesForSite')}
    `; + } else { + const enabledFirst = prefs.get('popup.enabledFirst'); + styles.sort((a, b) => + enabledFirst && a.enabled !== b.enabled + ? !(a.enabled < b.enabled) ? -1 : 1 + : a.name.localeCompare(b.name)); + const fragment = document.createDocumentFragment(); + for (let style of styles) { + fragment.appendChild(createStyleElement(style)); + } + installed.appendChild(fragment); + } + // force Chrome to resize the popup + document.body.style.height = '10px'; + document.documentElement.style.height = '10px'; } + function createStyleElement(style) { - // reuse event function references - createStyleElement.events = createStyleElement.events || { - checkboxClick() { - enableStyle(getClickedStyleId(event), this.checked); - }, - styleNameClick(event) { - this.checkbox.click(); - event.preventDefault(); - }, - toggleClick(event) { - enableStyle(getClickedStyleId(event), this.matches('.enable')); - }, - deleteClick() { - doDelete(event); + // reuse event listener function references + const listeners = createStyleElement.listeners = createStyleElement.listeners || { + checkboxClick() { + enableStyle(getClickedStyleId(event), this.checked) + .then(handleUpdate); + }, + styleNameClick(event) { + this.checkbox.click(); + event.preventDefault(); + }, + toggleClick(event) { + enableStyle(getClickedStyleId(event), this.matches('.enable')) + .then(handleUpdate); + }, + deleteClick(event) { + doDelete(event); } - }; - const entry = template.style.cloneNode(true); - entry.setAttribute('style-id', style.id); - Object.assign(entry, { - styleId: style.id, - className: ['entry', style.enabled ? 'enabled' : 'disabled'].join(' '), - onmousedown: openEditorOnMiddleclick, - onauxclick: openEditorOnMiddleclick, - }); + }; + const entry = template.style.cloneNode(true); + entry.setAttribute('style-id', style.id); + Object.assign(entry, { + styleId: style.id, + className: ['entry', style.enabled ? 'enabled' : 'disabled'].join(' '), + onmousedown: openEditorOnMiddleclick, + onauxclick: openEditorOnMiddleclick, + }); - const checkbox = entry.querySelector('.checker'); - Object.assign(checkbox, { - id: 'style-' + style.id, - checked: style.enabled, - onclick: createStyleElement.events.checkboxClick, - }); + const checkbox = $('.checker', entry); + Object.assign(checkbox, { + id: 'style-' + style.id, + checked: style.enabled, + onclick: listeners.checkboxClick, + }); - const editLink = entry.querySelector('.style-edit-link'); - Object.assign(editLink, { - href: editLink.getAttribute('href') + style.id, - onclick: openLinkInTabOrWindow, - }); + const editLink = $('.style-edit-link', entry); + Object.assign(editLink, { + href: editLink.getAttribute('href') + style.id, + onclick: openLinkInTabOrWindow, + }); - const styleName = entry.querySelector('.style-name'); - Object.assign(styleName, { - htmlFor: 'style-' + style.id, - onclick: createStyleElement.events.styleNameClick, - }); - styleName.checkbox = checkbox; - styleName.appendChild(document.createTextNode(style.name)); + const styleName = $('.style-name', entry); + Object.assign(styleName, { + htmlFor: 'style-' + style.id, + onclick: listeners.styleNameClick, + }); + styleName.checkbox = checkbox; + styleName.appendChild(document.createTextNode(style.name)); - entry.querySelector('.enable').onclick = createStyleElement.events.toggleClick; - entry.querySelector('.disable').onclick = createStyleElement.events.toggleClick; - entry.querySelector('.delete').onclick = createStyleElement.events.deleteClick; + $('.enable', entry).onclick = listeners.toggleClick; + $('.disable', entry).onclick = listeners.toggleClick; + $('.delete', entry).onclick = listeners.deleteClick; - return entry; + return entry; } + function doDelete(event) { - document.getElementById('confirm').dataset.display = true; - const id = getClickedStyleId(event); - document.querySelector('#confirm b').textContent = - document.querySelector(`[style-id="${id}"] label`).textContent; - document.getElementById('confirm').dataset.id = id; + $('#confirm').dataset.display = true; + const id = getClickedStyleId(event); + $('#confirm b').textContent = + $(`[style-id="${id}"] label`).textContent; + $('#confirm').dataset.id = id; } -document.getElementById('confirm').addEventListener('click', e => { - let cmd = e.target.dataset.cmd; - if (cmd === 'ok') { - deleteStyle(document.getElementById('confirm').dataset.id, () => { - // update view with 'No styles installed for this site' message - if (document.getElementById('installed').children.length === 0) { - showStyles([]); - } - }); - } - // - if (cmd) { - document.getElementById('confirm').dataset.display = false; - } -}); function getClickedStyleId(event) { - const entry = event.target.closest('.entry'); - return entry ? entry.styleId : null; + const entry = event.target.closest('.entry'); + return entry ? entry.styleId : null; } + function openLinkInTabOrWindow(event) { - event.preventDefault(); - if (prefs.get("openEditInWindow", false)) { - var options = {url: event.target.href} - var wp = prefs.get("windowPosition", {}); - for (var k in wp) options[k] = wp[k]; - chrome.windows.create(options); - } else { - openLink(event); - } - close(); + if (!prefs.get('openEditInWindow', false)) { + openURLandHide(event); + return; + } + event.preventDefault(); + chrome.windows.create( + Object.assign({ + url: event.target.href + }, prefs.get('windowPosition', {})) + ); + close(); } + function openEditorOnMiddleclick(event) { - if (event.button != 1) { - return; - } - // open an editor on middleclick - if (event.target.matches('.entry, .style-name, .style-edit-link')) { - this.querySelector('.style-edit-link').click(); - event.preventDefault(); - return; - } - // prevent the popup being opened in a background tab - // when an irrelevant link was accidentally clicked - if (event.target.closest('a')) { - event.preventDefault(); - return; - } + if (event.button != 1) { + return; + } + // open an editor on middleclick + if (event.target.matches('.entry, .style-name, .style-edit-link')) { + $('.style-edit-link', this).click(); + event.preventDefault(); + return; + } + // prevent the popup being opened in a background tab + // when an irrelevant link was accidentally clicked + if (event.target.closest('a')) { + event.preventDefault(); + return; + } } -function openLink(event) { - event.preventDefault(); - chrome.runtime.sendMessage({method: "openURL", url: event.target.href}); - close(); + +function openURLandHide(event) { + event.preventDefault(); + openURL({url: event.target.href}) + .then(close); } + function handleUpdate(style) { - var styleElement = installed.querySelector("[style-id='" + style.id + "']"); - if (styleElement) { - installed.replaceChild(createStyleElement(style), styleElement); - } else { - getActiveTabRealURL(function(url) { - if (chrome.extension.getBackgroundPage().getApplicableSections(style, url).length) { - // a new style for the current url is installed - document.getElementById("unavailable").style.display = "none"; - installed.appendChild(createStyleElement(style)); - } - }); - } + const styleElement = $(`[style-id="${style.id}"]`, installed); + if (styleElement) { + installed.replaceChild(createStyleElement(style), styleElement); + } else { + getActiveTabRealURL().then(url => { + if (getApplicableSections(style, url).length) { + // a new style for the current url is installed + $('#unavailable').style.display = 'none'; + installed.appendChild(createStyleElement(style)); + } + }); + } } + function handleDelete(id) { - var styleElement = installed.querySelector("[style-id='" + id + "']"); - if (styleElement) { - installed.removeChild(styleElement); - } + var styleElement = $(`[style-id="${id}"]`, installed); + if (styleElement) { + installed.removeChild(styleElement); + } } -chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { - if (request.method == "updatePopup") { - switch (request.reason) { - case "styleAdded": - case "styleUpdated": - handleUpdate(request.style); - break; - case "styleDeleted": - handleDelete(request.id); - break; - } - } -}); -["find-styles-link"].forEach(function(id) { - document.getElementById(id).addEventListener("click", openLink, false); -}); - -document.getElementById("disableAll").addEventListener("change", function(event) { - installed.classList.toggle("disabled", prefs.get("disableAll")); -}); -setupLivePrefs(["disableAll"]); - -document.querySelector('#popup-manage-button').addEventListener("click", function() { - window.open(chrome.runtime.getURL('manage.html')); -}); - -document.querySelector('#popup-options-button').addEventListener("click", function() { - if (chrome.runtime.openOptionsPage) { - // Supported (Chrome 42+) - chrome.runtime.openOptionsPage(); - } else { - // Fallback - window.open(chrome.runtime.getURL('options/index.html')); - } -}); - -document.querySelector('#popup-shortcuts-button').addEventListener("click", configureCommands.open); - -// popup width -document.body.style.width = (localStorage.getItem('popupWidth') || '246') + 'px'; +function $(selector, base = document) { + if (selector.startsWith('#') && /^#[^,\s]+$/.test(selector)) { + return document.getElementById(selector.slice(1)); + } else { + return base.querySelector(selector); + } +} diff --git a/storage.js b/storage.js index 9d7fd5cd..4908aec7 100644 --- a/storage.js +++ b/storage.js @@ -277,23 +277,21 @@ function addMissingStyleTargets(style) { function enableStyle(id, enabled) { - saveStyle({id, enabled}) - .then(handleUpdate); + return saveStyle({id, enabled}); } -function deleteStyle(id, callback = function (){}) { - getDatabase(function(db) { - var tx = db.transaction(["styles"], "readwrite"); - var os = tx.objectStore("styles"); - var request = os.delete(Number(id)); - request.onsuccess = function(event) { - handleDelete(id); - invalidateCache(true, {deletedId: id}); - notifyAllTabs({method: "styleDeleted", id}); - callback(); - }; - }); +function deleteStyle(id) { + return new Promise(resolve => + getDatabase(db => { + const tx = db.transaction(['styles'], 'readwrite'); + const os = tx.objectStore('styles'); + os.delete(Number(id)).onsuccess = event => { + invalidateCache(true, {deletedId: id}); + notifyAllTabs({method: 'styleDeleted', id}); + resolve(id); + }; + })); } From c54e22ad644935b738207b7c99d94b77c4748d43 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 23 Mar 2017 07:51:09 +0300 Subject: [PATCH 014/235] Don't enforce non-native end_of_line in editorconfig --- .editorconfig | 1 - 1 file changed, 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index aaaa7a4b..690f0125 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,7 +4,6 @@ root = true indent_style = space indent_size = 2 tab_width = 2 -end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true From 95e2263c10ac4a0a08a398d03846a5da41323a0d Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 22 Mar 2017 08:12:11 +0300 Subject: [PATCH 015/235] Autocleanup cached filters when over 10k items (a few MB) Precaution for the [rare but possible] case of users running Stylus for a very long time without restarting the browser. --- storage.js | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/storage.js b/storage.js index 4908aec7..a93d7978 100644 --- a/storage.js +++ b/storage.js @@ -175,9 +175,11 @@ function filterStyles(options = {}) { const cached = cachedStyles.filters.get(cacheKey); if (cached) { //console.log('%c%s filterStyles REUSED RESPONSE %s', 'color:gray', (performance.now() - t0).toFixed(1), JSON.stringify(options)) + cached.hits++; + cached.lastHit = Date.now(); return asHash - ? Object.assign({disableAll: prefs.get('disableAll', false)}, cached) - : cached; + ? Object.assign({disableAll: prefs.get('disableAll', false)}, cached.styles) + : cached.styles; } const styles = id == null @@ -204,13 +206,50 @@ function filterStyles(options = {}) { } } //console.log('%s filterStyles %s', (performance.now() - t0).toFixed(1), JSON.stringify(options)) - cachedStyles.filters.set(cacheKey, filtered); + cachedStyles.filters.set(cacheKey, { + styles: filtered, + lastHit: Date.now(), + hits: 1, + }); + if (cachedStyles.filters.size > 10000) { + cleanupCachedFilters(); + } return asHash ? Object.assign({disableAll: prefs.get('disableAll', false)}, filtered) : filtered; } +function cleanupCachedFilters({force = false} = {}) { + if (!force) { + // sliding timer for 1 second + clearTimeout(cleanupCachedFilters.timeout); + cleanupCachedFilters.timeout = setTimeout(cleanupCachedFilters, 1000, {force: true}); + return; + } + const size = cachedStyles.filters.size; + const oldestHit = cachedStyles.filters.values().next().value.lastHit; + const now = Date.now(); + const timeSpan = now - oldestHit; + const recencyWeight = 5 / size; + const hitWeight = 1 / 4; // we make ~4 hits per URL + const lastHitWeight = 10; + // delete the oldest 10% + const sorted = [...cachedStyles.filters.entries()] + .map(([id, v], index) => ({ + id, + weight: + index * recencyWeight + + v.hits * hitWeight + + (v.lastHit - oldestHit) / timeSpan * lastHitWeight, + })) + .sort((a, b) => a.weight - b.weight) + .slice(0, size / 10 + 1) + .forEach(({id}) => cachedStyles.filters.delete(id)); + cleanupCachedFilters.timeout = 0; +} + + function saveStyle(style, {notify = true} = {}) { return new Promise(resolve => { getDatabase(db => { From 07bee69359ef0f253a60b5704e03303edfaf44d8 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 22 Mar 2017 10:07:34 +0300 Subject: [PATCH 016/235] Fix deoptimization triggers --- edit.js | 4 ++-- messaging.js | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/edit.js b/edit.js index a2043067..7fc06efd 100644 --- a/edit.js +++ b/edit.js @@ -1551,8 +1551,8 @@ function showKeyMapHelp() { cell.innerHTML = cell.textContent; }); } - function mergeKeyMaps(merged) { - [].slice.call(arguments, 1).forEach(function(keyMap) { + function mergeKeyMaps(merged, ...more) { + more.forEach(keyMap => { if (typeof keyMap == "string") { keyMap = CodeMirror.keyMap[keyMap]; } diff --git a/messaging.js b/messaging.js index ba3aba52..f8173814 100644 --- a/messaging.js +++ b/messaging.js @@ -183,7 +183,10 @@ function wildcardAsRegExp(s, flags) { } -var configureCommands = { +// isolate deoptimization trigger: +// https://github.com/petkaantonov/bluebird/wiki/Optimization-killers +// * Functions that contain object literals that contain __proto__, or get or set declarations. +const configureCommands = (() => ({ get url () { return navigator.userAgent.indexOf('OPR') > -1 ? 'opera://settings/configureCommands' : @@ -194,4 +197,4 @@ var configureCommands = { 'url': configureCommands.url }); } -}; +}))(); From c1338e63d167e6355f0bc7c25cb2ce9351582b69 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 23 Mar 2017 07:47:30 +0300 Subject: [PATCH 017/235] Highlight updated/saved style in manage page --- backup/fileSaveLoad.js | 2 +- manage.css | 34 +++++++++++++++++++++++++++++++++- manage.html | 2 +- manage.js | 12 ++++++++++-- 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/backup/fileSaveLoad.js b/backup/fileSaveLoad.js index 89db6fc9..0cfc22e3 100644 --- a/backup/fileSaveLoad.js +++ b/backup/fileSaveLoad.js @@ -59,7 +59,7 @@ function importFromString(jsonString) { const nextStyle = json.shift(); if (nextStyle) { saveStyle(nextStyle, {notify: false}).then(style => { - handleUpdate(style); + handleUpdate(style, {reason: 'import'}); setTimeout(proceed, 0); }); } else { diff --git a/manage.css b/manage.css index b83669de..9afe0ff0 100644 --- a/manage.css +++ b/manage.css @@ -42,7 +42,7 @@ a.homepage { .entry { margin: 0; - padding: 1.25em 2em 1.5em; + padding: 1.25em 2em; border-top: 1px solid #ddd; } @@ -71,6 +71,11 @@ a.homepage { color: inherit; } +.style-name-link:hover { + text-decoration: underline; + color: black; +} + .applies-to { word-break: break-word; } @@ -81,6 +86,19 @@ a.homepage { margin-bottom: 0; } +.actions { + display: flex; + flex-wrap: wrap; +} + +.actions > * { + margin-bottom: .25rem; +} + +.actions > *:not(:last-child) { + margin-right: .25rem; +} + .applies-to > :first-child { margin-right: .5ex; } @@ -160,6 +178,20 @@ a.homepage { display: none; } +/* highlight updated/added styles */ +.highlight { + animation: highlight 10s cubic-bezier(0,.82,.47,.98); +} + +@keyframes highlight { + from { + background-color: rgba(128, 128, 128, .5); + } + to { + background-color: none; + } +} + .hidden { display: none } diff --git a/manage.html b/manage.html index ea2629d6..e561dc06 100644 --- a/manage.html +++ b/manage.html @@ -7,7 +7,7 @@ - - diff --git a/manage.js b/manage.js index 6af717d6..c9bd0b6f 100644 --- a/manage.js +++ b/manage.js @@ -95,9 +95,8 @@ function showStyles(styles = []) { .sort((a, b) => (a.name < b.name ? -1 : a.name == b.name ? 0 : 1)); const shouldRenderAll = (history.state || {}).scrollY > window.innerHeight; const renderBin = document.createDocumentFragment(); - tDocLoader.stop(); renderStyles(0); - // TODO: remember how many styles fit one page to display just that portion first next time + function renderStyles(index) { const t0 = performance.now(); while (index < sorted.length) { @@ -116,40 +115,57 @@ function showStyles(styles = []) { } else if (shouldRenderAll && 'scrollY' in (history.state || {})) { setTimeout(() => scrollTo(0, history.state.scrollY)); } + if (newUI.enabled && newUI.favicons) { + debounce(handleEvent.loadFavicons, 16); + } } } function createStyleElement({style, name}) { - const entry = template[`style${newUI.enabled ? 'Compact' : ''}`].cloneNode(true); - entry.className += ' ' + - (style.enabled ? 'enabled' : 'disabled') + - (style.updateUrl ? ' updatable' : ''); - entry.id = 'style-' + style.id; - entry.styleId = style.id; - entry.styleNameLowerCase = name || style.name.toLocaleLowerCase(); - - const editLink = $('.style-name-link', entry); - editLink.appendChild(document.createTextNode(style.name)); - editLink.href = editLink.getAttribute('href') + style.id; - - const homepage = $('.homepage', entry); - if (style.url) { - homepage.href = homepage.title = style.url; - } else { - homepage.remove(); + // query the sub-elements just once, then reuse the references + if ((createStyleElement.parts || {}).newUI !== newUI.enabled) { + const entry = template[`style${newUI.enabled ? 'Compact' : ''}`].cloneNode(true); + createStyleElement.parts = { + newUI: newUI.enabled, + entry, + entryClassBase: entry.className, + checker: $('.checker', entry), + nameLink: $('.style-name-link', entry), + editLink: $('.style-edit-link', entry) || {}, + editHrefBase: $('.style-name-link, .style-edit-link', entry).getAttribute('href'), + homepage: $('.homepage', entry), + appliesTo: $('.applies-to', entry), + targets: $('.targets', entry), + expander: $('.expander', entry), + decorations: { + urlPrefixesAfter: '*', + regexpsBefore: '/', + regexpsAfter: '/', + }, + }; } + const parts = createStyleElement.parts; + Object.assign(parts.entry, { + className: parts.entryClassBase + ' ' + + (style.enabled ? 'enabled' : 'disabled') + + (style.updateUrl ? ' updatable' : ''), + id: 'style-' + style.id, + styleId: style.id, + styleNameLowerCase: name || style.name.toLocaleLowerCase(), + }); - const appliesTo = $('.applies-to', entry); - const decorations = { - urlPrefixesAfter: '*', - regexpsBefore: '/', - regexpsAfter: '/', - }; - const displayed = new Set(); - let container = newUI.enabled ? $('.targets', appliesTo) : appliesTo; + parts.nameLink.textContent = style.name; + parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id; + parts.homepage.href = parts.homepage.title = style.url || ''; + + // .targets may be a large list so we clone it separately + // and paste into the cloned entry in the end + const targets = parts.targets.cloneNode(true); + let container = targets; let numTargets = 0; let numIcons = 0; + const displayed = new Set(); for (const type of TARGET_TYPES) { for (const section of style.sections) { for (const targetValue of section[type] || []) { @@ -160,7 +176,7 @@ function createStyleElement({style, name}) { const element = template.appliesToTarget.cloneNode(true); if (!newUI.enabled) { if (numTargets == 10) { - container = appliesTo.appendChild(template.extraAppliesTo.cloneNode(true)); + container = container.appendChild(template.extraAppliesTo.cloneNode(true)); } else if (numTargets > 1) { container.appendChild(template.appliesToSeparator.cloneNode(true)); } @@ -181,33 +197,33 @@ function createStyleElement({style, name}) { } element.appendChild( document.createTextNode( - (decorations[type + 'Before'] || '') + + (parts.decorations[type + 'Before'] || '') + targetValue + - (decorations[type + 'After'] || ''))); + (parts.decorations[type + 'After'] || ''))); container.appendChild(element); numTargets++; } } } - if (!numTargets) { - appliesTo.appendChild(template.appliesToEverything.cloneNode(true)); - entry.classList.add('global'); - } if (newUI.enabled) { - $('.checker', entry).checked = style.enabled; - if (numTargets > newUI.targets) { - appliesTo.appendChild(template.expandAppliesTo.cloneNode(true)); - } - if (numIcons) { + parts.checker.checked = style.enabled; + parts.appliesTo.classList.toggle('has-more', numTargets > newUI.targets); + // name is supplied by showStyles so we let it decide when to load the icons + if (numIcons && !name) { debounce(handleEvent.loadFavicons); } - } else { - const editLink = $('.style-edit-link', entry); - editLink.href = editLink.getAttribute('href') + style.id; } - return entry; + const newEntry = parts.entry.cloneNode(true); + const newTargets = $('.targets', newEntry); + if (numTargets) { + newTargets.parentElement.replaceChild(targets, newTargets); + } else { + newTargets.appendChild(template.appliesToEverything.cloneNode(true)); + newEntry.classList.add('global'); + } + return newEntry; } @@ -378,6 +394,11 @@ function switchUI({styleOnly} = {}) { if (!styleOnly && (stateToggled || missingFavicons)) { installed.innerHTML = ''; getStylesSafe().then(showStyles); + } else if (targetsChanged) { + for (const targets of $$('.entry .targets')) { + const hasMore = targets.children.length > newUI.targets; + targets.parentElement.classList.toggle('has-more', hasMore); + } } } From 5a61ac2f1831f5619d4fd362e3f6488e90a4905d Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 10 Apr 2017 14:13:51 +0300 Subject: [PATCH 113/235] fixup for CodeMirror failing on keyMap="" This should not happen *normally* but a user may edit chrome.storage manually etc. --- edit.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/edit.js b/edit.js index 9587df87..447fe943 100644 --- a/edit.js +++ b/edit.js @@ -131,6 +131,11 @@ function initCodeMirror() { var CM = CodeMirror; var isWindowsOS = navigator.appVersion.indexOf("Windows") > 0; + // CodeMirror miserably fails on keyMap="" so let's ensure it's not + if (!prefs.get('editor.keyMap')) { + prefs.reset('editor.keyMap'); + } + // default option values Object.assign(CM.defaults, { mode: 'css', From 257fda4d1d85b7b446c0bed984b008daf3e90b82 Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 11 Apr 2017 06:51:32 +0300 Subject: [PATCH 114/235] updateIcon: use the old flow to avoid "no tab" errors --- messaging.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/messaging.js b/messaging.js index 0de622f5..b62c9673 100644 --- a/messaging.js +++ b/messaging.js @@ -97,13 +97,12 @@ function updateIcon(tab, styles) { }); return; } - (isNTP ? getTabRealURL(tab) : Promise.resolve(tab.url)) - .then(url => getStylesSafe({ - matchUrl: url, - enabled: true, - asHash: true, - })) - .then(stylesReceived); + if (isNTP) { + getTabRealURL(tab).then(url => + getStyles({matchUrl: url, enabled: true, asHash: true}, stylesReceived)); + } else { + getStyles({matchUrl: tab.url, enabled: true, asHash: true}, stylesReceived); + } function stylesReceived(styles) { let numStyles = styles.length; @@ -129,10 +128,13 @@ function updateIcon(tab, styles) { 38: `images/icon/38${postfix}.png`, // TODO: add Edge preferred sizes: 20, 25, 30, 40 }, - }, ignoreChromeError); - // Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor - chrome.browserAction.setBadgeBackgroundColor({color}); - chrome.browserAction.setBadgeText({text, tabId: tab.id}); + }, () => { + if (!chrome.runtime.lastError) { + // Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor + chrome.browserAction.setBadgeBackgroundColor({color}); + chrome.browserAction.setBadgeText({text, tabId: tab.id}); + } + }); } } From b61dc4184b2ec954c61fad09f5ccea1f7ab97053 Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 11 Apr 2017 08:16:54 +0300 Subject: [PATCH 115/235] fixup: import report onclick --- backup/fileSaveLoad.js | 45 ++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/backup/fileSaveLoad.js b/backup/fileSaveLoad.js index 114d8763..6bfdd049 100644 --- a/backup/fileSaveLoad.js +++ b/backup/fileSaveLoad.js @@ -1,3 +1,4 @@ +/* global messageBox */ 'use strict'; const STYLISH_DUMP_FILE_EXT = '.txt'; @@ -24,7 +25,7 @@ function importFromFile({fileTypeFilter, file} = {}) { function readFile() { if (file || fileInput.value !== fileInput.initialValue) { file = file || fileInput.files[0]; - if (file.size > 100*1000*1000) { + if (file.size > 100e6) { console.warn("100MB backup? I don't believe you."); importFromString('').then(resolve); return; @@ -94,19 +95,20 @@ function importFromString(jsonString) { if (!oldStyle) { stats.added.names.push(style.name); stats.added.ids.push(style.id); + return; } - else if (!metaEqual && !codeEqual) { + if (!metaEqual && !codeEqual) { stats.metaAndCode.names.push(reportNameChange(oldStyle, style)); stats.metaAndCode.ids.push(style.id); + return; } - else if (!codeEqual) { + if (!codeEqual) { stats.codeOnly.names.push(style.name); stats.codeOnly.ids.push(style.id); + return; } - else { - stats.metaOnly.names.push(reportNameChange(oldStyle, style)); - stats.metaOnly.ids.push(style.id); - } + stats.metaOnly.names.push(reportNameChange(oldStyle, style)); + stats.metaOnly.ids.push(style.id); }); return; } @@ -135,10 +137,10 @@ function importFromString(jsonString) { buttons: [t('confirmOK'), numChanged && t('undo')], onshow: bindClick, }).then(({button, enter, esc}) => { - if (button == 1) { - undo(); - } - }); + if (button == 1) { + undo(); + } + }); resolve(numChanged); }); } @@ -181,16 +183,17 @@ function importFromString(jsonString) { } function bindClick(box) { - for (let block of $$('details')) { + const highlightElement = event => { + const styleElement = $('#style-' + event.target.dataset.id); + if (styleElement) { + scrollElementIntoView(styleElement); + animateElement(styleElement, {className: 'highlight'}); + } + }; + for (const block of $$('details')) { if (block.dataset.id != 'invalid') { block.style.cursor = 'pointer'; - block.onclick = event => { - const styleElement = $(`[style-id="${event.target.dataset.id}"]`); - if (styleElement) { - scrollElementIntoView(styleElement); - animateElement(styleElement, {className: 'highlight'}); - } - }; + block.onclick = highlightElement; } } } @@ -202,7 +205,7 @@ function importFromString(jsonString) { style.sections = style.sections.slice(); for (let i = 0, section; (section = style.sections[i]); i++) { const copy = style.sections[i] = Object.assign({}, section); - for (let propName in copy) { + for (const propName in copy) { const prop = copy[propName]; if (prop instanceof Array) { copy[propName] = prop.slice(); @@ -236,7 +239,7 @@ $('#file-all-styles').onclick = () => { fetch(url) .then(res => res.blob()) .then(blob => { - let a = document.createElement('a'); + const a = document.createElement('a'); a.setAttribute('download', fileName); a.setAttribute('href', URL.createObjectURL(blob)); a.dispatchEvent(new MouseEvent('click')); From 7f6d3e241a2c10364e55fbc2b8eb578af1ccd83c Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 11 Apr 2017 08:17:37 +0300 Subject: [PATCH 116/235] speedup: don't animate elements during import --- manage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manage.js b/manage.js index c9bd0b6f..fc44e139 100644 --- a/manage.js +++ b/manage.js @@ -352,10 +352,10 @@ function handleUpdate(style, {reason, quiet} = {}) { } } installed.insertBefore(element, findNextElement(style)); - if (!quiet) { + if (reason != 'import') { animateElement(element, {className: 'highlight'}); - scrollElementIntoView(element); } + scrollElementIntoView(element); } From 5c8d1950a7a185e7cc402fb40579e2d77ab499d9 Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 11 Apr 2017 13:51:40 +0300 Subject: [PATCH 117/235] Isolate storage.js in background context To prevent cross-page leaks we need to create/copy prefs and cachedStyles inside the background page context. * storage.js is now used only in the background page * messaging.js now contains less bg-specific methods and more common methods. Added saveStyleSafe, deleteStyleSafe which automatically invoke onRuntimeMessage of the current page or just handleUpdate/handleDelete when notify:false * prefs.js with 'prefs' for background and UI pages: separate objects because a UI page may load before the background page and it can read prefs from localStorage/sync/defaults --- .eslintrc | 36 +- background.js | 175 +++++++++- backup/fileSaveLoad.js | 76 ++--- dom.js | 15 + edit.html | 2 +- edit.js | 11 +- manage.html | 3 +- manage.js | 31 +- manifest.json | 2 +- messaging.js | 294 +++++++++------- options/index.html | 4 +- options/index.js | 4 +- popup.html | 3 +- popup.js | 28 +- prefs.js | 265 ++++++++++++++ storage.js | 758 +++++++++-------------------------------- update.js | 3 +- 17 files changed, 885 insertions(+), 825 deletions(-) create mode 100644 prefs.js diff --git a/.eslintrc b/.eslintrc index e81cbc5d..cee33bfa 100644 --- a/.eslintrc +++ b/.eslintrc @@ -15,17 +15,25 @@ globals: FIREFOX: false OPERA: false URLS: false + BG: false notifyAllTabs: false - refreshAllTabs: false - updateIcon: false getActiveTab: false getActiveTabRealURL: false getTabRealURL: false openURL: false activateTab: false stringAsRegExp: false - wildcardAsRegExp: false ignoreChromeError: false + tryCatch: false + tryRegExp: false + tryJSONparse: false + debounce: false + deepCopy: false + onBackgroundReady: false + deleteStyleSafe: false + getStylesSafe: false + saveStyleSafe: false + sessionStorageHash: false # localization.js template: false t: false @@ -37,31 +45,13 @@ globals: # dom.js onDOMready: false scrollElementIntoView: false + enforceInputRange: false animateElement: false $: false $$: false - # storage.js + # prefs.js prefs: false - cachedStyles: false - sessionStorageHash: false - getStylesSafe: false - invalidateCache: false - saveStyle: false - enableStyle: false - deleteStyle: false - fixBoolean: false - getDomains: false - getType: false - getApplicableSections: false - isCheckbox: false - runTryCatch: false - tryRegExp: false - tryJSONparse: false - debounce: false setupLivePrefs: false - enforceInputRange: false - getCodeMirrorThemes: false - styleSectionsEqual: false rules: accessor-pairs: [2] diff --git a/background.js b/background.js index 415f1b3b..9b24a668 100644 --- a/background.js +++ b/background.js @@ -1,4 +1,4 @@ -/* global getDatabase, getStyles, reportError */ +/* global getDatabase, getStyles, saveStyle, reportError, invalidateCache */ 'use strict'; chrome.webNavigation.onBeforeNavigate.addListener(data => { @@ -39,9 +39,9 @@ function webNavigationListener(method, data) { // messaging -chrome.runtime.onMessage.addListener(onBackgroundMessage); +chrome.runtime.onMessage.addListener(onRuntimeMessage); -function onBackgroundMessage(request, sender, sendResponse) { +function onRuntimeMessage(request, sender, sendResponse) { switch (request.method) { case 'getStyles': @@ -61,9 +61,7 @@ function onBackgroundMessage(request, sender, sendResponse) { return KEEP_CHANNEL_OPEN; case 'invalidateCache': - if (typeof invalidateCache != 'undefined') { - invalidateCache(false, request); - } + invalidateCache(false, request); break; case 'healthCheck': @@ -101,8 +99,8 @@ if ('commands' in chrome) { } // context menus - -const contextMenus = { +// eslint-disable-next-line no-var +var contextMenus = { 'show-badge': { title: 'menuShowBadge', click: info => prefs.set(info.menuItemId, info.checked), @@ -123,7 +121,7 @@ const contextMenus = { // Vivaldi: Vivaldi/# if (/Vivaldi\/[\d.]+$/.test(navigator.userAgent) || /Safari\/[\d.]+$/.test(navigator.userAgent) - && ![...navigator.plugins].some(p => p.name == 'Shockwave Flash')) { + && !Array.from(navigator.plugins).some(p => p.name == 'Shockwave Flash')) { contextMenus.editDeleteText = { title: 'editDeleteText', contexts: ['editable'], @@ -172,8 +170,11 @@ chrome.tabs.onAttached.addListener((tabId, data) => { }); }); -var codeMirrorThemes; // eslint-disable-line no-var -getCodeMirrorThemes(themes => (codeMirrorThemes = themes)); +// eslint-disable-next-line no-var +var codeMirrorThemes; +getCodeMirrorThemes().then(themes => { + codeMirrorThemes = themes; +}); // do not use prefs.get('version', null) as it might not yet be available chrome.storage.local.get('version', prefs => { @@ -198,6 +199,9 @@ chrome.storage.local.get('version', prefs => { injectContentScripts(); function injectContentScripts() { + // expand * as .*? + const wildcardAsRegExp = (s, flags) => + new RegExp(s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&').replace(/\*/g, '.*?'), flags); const contentScripts = chrome.runtime.getManifest().content_scripts; for (const cs of contentScripts) { cs.matches = cs.matches.map(m => ( @@ -227,3 +231,152 @@ function injectContentScripts() { } }); } + + +function refreshAllTabs() { + return new Promise(resolve => { + // list all tabs including chrome-extension:// which can be ours + chrome.tabs.query({}, tabs => { + const lastTab = tabs[tabs.length - 1]; + for (const tab of tabs) { + getStyles({matchUrl: tab.url, enabled: true, asHash: true}, styles => { + const message = {method: 'styleReplaceAll', styles}; + chrome.tabs.sendMessage(tab.id, message); + updateIcon(tab, styles); + if (tab == lastTab) { + resolve(); + } + }); + } + }); + }); +} + + +function updateIcon(tab, styles) { + // while NTP is still loading only process the request for its main frame with a real url + // (but when it's loaded we should process style toggle requests from popups, for example) + const isNTP = tab.url == 'chrome://newtab/'; + if (isNTP && tab.status != 'complete' || tab.id < 0) { + return; + } + if (styles) { + // check for not-yet-existing tabs e.g. omnibox instant search + chrome.tabs.get(tab.id, () => { + if (!chrome.runtime.lastError) { + stylesReceived(styles); + } + }); + return; + } + if (isNTP) { + getTabRealURL(tab).then(url => + getStyles({matchUrl: url, enabled: true, asHash: true}, stylesReceived)); + } else { + getStyles({matchUrl: tab.url, enabled: true, asHash: true}, stylesReceived); + } + + function stylesReceived(styles) { + let numStyles = styles.length; + if (numStyles === undefined) { + // for 'styles' asHash:true fake the length by counting numeric ids manually + numStyles = 0; + for (const id of Object.keys(styles)) { + numStyles += id.match(/^\d+$/) ? 1 : 0; + } + } + const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll'); + const postfix = disableAll ? 'x' : numStyles == 0 ? 'w' : ''; + const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal'); + const text = prefs.get('show-badge') && numStyles ? String(numStyles) : ''; + chrome.browserAction.setIcon({ + tabId: tab.id, + path: { + // Material Design 2016 new size is 16px + 16: `images/icon/16${postfix}.png`, + 32: `images/icon/32${postfix}.png`, + // Chromium forks or non-chromium browsers may still use the traditional 19px + 19: `images/icon/19${postfix}.png`, + 38: `images/icon/38${postfix}.png`, + // TODO: add Edge preferred sizes: 20, 25, 30, 40 + }, + }, () => { + if (!chrome.runtime.lastError) { + // Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor + chrome.browserAction.setBadgeBackgroundColor({color}); + chrome.browserAction.setBadgeText({text, tabId: tab.id}); + } + }); + } +} + + +function getCodeMirrorThemes() { + if (!chrome.runtime.getPackageDirectoryEntry) { + return Promise.resolve([ + '3024-day', + '3024-night', + 'abcdef', + 'ambiance', + 'ambiance-mobile', + 'base16-dark', + 'base16-light', + 'bespin', + 'blackboard', + 'cobalt', + 'colorforth', + 'dracula', + 'duotone-dark', + 'duotone-light', + 'eclipse', + 'elegant', + 'erlang-dark', + 'hopscotch', + 'icecoder', + 'isotope', + 'lesser-dark', + 'liquibyte', + 'material', + 'mbo', + 'mdn-like', + 'midnight', + 'monokai', + 'neat', + 'neo', + 'night', + 'panda-syntax', + 'paraiso-dark', + 'paraiso-light', + 'pastel-on-dark', + 'railscasts', + 'rubyblue', + 'seti', + 'solarized', + 'the-matrix', + 'tomorrow-night-bright', + 'tomorrow-night-eighties', + 'ttcn', + 'twilight', + 'vibrant-ink', + 'xq-dark', + 'xq-light', + 'yeti', + 'zenburn', + ]); + } + return new Promise(resolve => { + chrome.runtime.getPackageDirectoryEntry(rootDir => { + rootDir.getDirectory('codemirror/theme', {create: false}, themeDir => { + themeDir.createReader().readEntries(entries => { + resolve([ + chrome.i18n.getMessage('defaultTheme') + ].concat( + entries.filter(entry => entry.isFile) + .sort((a, b) => (a.name < b.name ? -1 : 1)) + .map(entry => entry.name.replace(/\.css$/, '')) + )); + }); + }); + }); + }); +} diff --git a/backup/fileSaveLoad.js b/backup/fileSaveLoad.js index 6bfdd049..a052516c 100644 --- a/backup/fileSaveLoad.js +++ b/backup/fileSaveLoad.js @@ -1,4 +1,4 @@ -/* global messageBox */ +/* global messageBox, handleUpdate */ 'use strict'; const STYLISH_DUMP_FILE_EXT = '.txt'; @@ -47,8 +47,15 @@ function importFromFile({fileTypeFilter, file} = {}) { function importFromString(jsonString) { - const json = runTryCatch(() => Array.from(JSON.parse(jsonString))) || []; - const oldStyles = json.length && deepCopyStyles(); + if (!BG) { + onBackgroundReady().then(() => importFromString(jsonString)); + return; + } + const json = BG.tryJSONparse(jsonString) || []; // create object in background context + if (typeof json.slice != 'function') { + json.length = 0; + } + const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []); const oldStylesByName = json.length && new Map( oldStyles.map(style => [style.name.trim(), style])); const stats = { @@ -60,18 +67,19 @@ function importFromString(jsonString) { invalid: {names: [], legend: 'invalid skipped'}, }; let index = 0; + let lastRepaint = performance.now(); return new Promise(proceed); function proceed(resolve) { while (index < json.length) { const item = json[index++]; if (!item || !item.name || !item.name.trim() || typeof item != 'object' - || (item.sections && !(item.sections instanceof Array))) { + || (item.sections && typeof item.sections.slice != 'function')) { stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`); continue; } item.name = item.name.trim(); - const byId = cachedStyles.byId.get(item.id); + const byId = BG.cachedStyles.byId.get(item.id); const byName = oldStylesByName.get(item.name); const oldStyle = byId && byId.name.trim() == item.name || !byName ? byId : byName; if (oldStyle == byName && byName) { @@ -81,16 +89,22 @@ function importFromString(jsonString) { const metaEqual = oldStyleKeys && oldStyleKeys.length == Object.keys(item).length && oldStyleKeys.every(k => k == 'sections' || oldStyle[k] === item[k]); - const codeEqual = oldStyle && styleSectionsEqual(oldStyle, item); + const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item); if (metaEqual && codeEqual) { stats.unchanged.names.push(oldStyle.name); stats.unchanged.ids.push(oldStyle.id); continue; } - saveStyle(Object.assign(item, { + // using saveStyle directly since json was parsed in background page context + BG.saveStyle(Object.assign(item, { reason: 'import', notify: false, })).then(style => { + handleUpdate(style, {reason: 'import'}); + if (performance.now() - lastRepaint > 1000) { + scrollElementIntoView($('#style-' + style.id)); + lastRepaint = performance.now(); + } setTimeout(proceed, 0, resolve); if (!oldStyle) { stats.added.names.push(style.name); @@ -120,17 +134,22 @@ function importFromString(jsonString) { stats.metaOnly.names.length + stats.codeOnly.names.length + stats.added.names.length; - Promise.resolve(numChanged && refreshAllTabs()).then(() => { - scrollTo(0, 0); + Promise.resolve(numChanged && BG.refreshAllTabs()).then(() => { + const listNames = kind => { + const {ids, names} = stats[kind]; + return ids + ? names.map((name, i) => `
    ${name}
    `) + : names.map(name => `
    ${name}
    `); + }; const report = Object.keys(stats) .filter(kind => stats[kind].names.length) - .map(kind => `
    + .map(kind => + `
    ${stats[kind].names.length} ${stats[kind].legend} - ` + stats[kind].names.map((name, i) => - `
    ${name}
    `).join('') + ` -
    + ${listNames(kind).join('')}
    `) .join(''); + scrollTo(0, 0); messageBox({ title: 'Finished importing styles', contents: report || 'Nothing was changed.', @@ -155,7 +174,7 @@ function importFromString(jsonString) { ]; index = 0; return new Promise(undoNextId) - .then(refreshAllTabs) + .then(BG.refreshAllTabs) .then(() => messageBox({ title: 'Import has been undone', contents: newIds.length + ' styles were reverted.', @@ -167,14 +186,14 @@ function importFromString(jsonString) { return; } const id = newIds[index++]; - deleteStyle(id, {notify: false}).then(id => { + deleteStyleSafe({id, notify: false}).then(id => { const oldStyle = oldStylesById.get(id); if (oldStyle) { - saveStyle(Object.assign(oldStyle, { - reason: 'undoImport', + saveStyleSafe(Object.assign(oldStyle, { + reason: 'import', notify: false, - })) - .then(() => setTimeout(undoNextId, 0, resolve)); + })).then(() => + setTimeout(undoNextId, 0, resolve)); } else { setTimeout(undoNextId, 0, resolve); } @@ -198,25 +217,6 @@ function importFromString(jsonString) { } } - function deepCopyStyles() { - const clonedStyles = []; - for (let style of cachedStyles.list || []) { - style = Object.assign({}, style); - style.sections = style.sections.slice(); - for (let i = 0, section; (section = style.sections[i]); i++) { - const copy = style.sections[i] = Object.assign({}, section); - for (const propName in copy) { - const prop = copy[propName]; - if (prop instanceof Array) { - copy[propName] = prop.slice(); - } - } - } - clonedStyles.push(style); - } - return clonedStyles; - } - function limitString(s, limit = 100) { return s.length <= limit ? s : s.substr(0, limit) + '...'; } diff --git a/dom.js b/dom.js index 2214fa82..c41f3bcc 100644 --- a/dom.js +++ b/dom.js @@ -50,6 +50,21 @@ function animateElement(element, {className, remove = false}) { } +function enforceInputRange(element) { + const min = Number(element.min); + const max = Number(element.max); + const onChange = () => { + const value = Number(element.value); + if (value < min || value > max) { + element.value = Math.max(min, Math.min(max, value)); + } + }; + onChange(); + element.addEventListener('change', onChange); + element.addEventListener('input', onChange); +} + + function $(selector, base = document) { // we have ids with . like #manage.onlyEdited which look like #id.class // so since getElementById is superfast we'll try it anyway diff --git a/edit.html b/edit.html index 9deb6bf2..6a3597df 100644 --- a/edit.html +++ b/edit.html @@ -645,8 +645,8 @@ - + diff --git a/edit.js b/edit.js index 447fe943..fe3644fd 100644 --- a/edit.js +++ b/edit.js @@ -252,7 +252,8 @@ function initCodeMirror() { } else { // Chrome is starting up and shows our edit.html, but the background page isn't loaded yet themeControl.innerHTML = optionsHtmlFromArray([theme == "default" ? t("defaultTheme") : theme]); - getCodeMirrorThemes(function(themes) { + BG.getCodeMirrorThemes().then(themes => { + BG.codeMirrorThemes = themes; themeControl.innerHTML = optionsHtmlFromArray(themes); themeControl.selectedIndex = Math.max(0, themes.indexOf(theme)); }); @@ -1333,7 +1334,7 @@ function save() { } var name = document.getElementById("name").value; var enabled = document.getElementById("enabled").checked; - saveStyle({ + saveStyleSafe({ id: styleId, name: name, enabled: enabled, @@ -1815,7 +1816,9 @@ function getParams() { return params; } -chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { +chrome.runtime.onMessage.addListener(onRuntimeMessage); + +function onRuntimeMessage(request) { switch (request.method) { case "styleUpdated": if (styleId && styleId == request.style.id && request.reason != 'editSave') { @@ -1838,7 +1841,7 @@ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { document.execCommand('delete'); break; } -}); +} function getComputedHeight(el) { var compStyle = getComputedStyle(el); diff --git a/manage.html b/manage.html index a887aa9e..32b8b92a 100644 --- a/manage.html +++ b/manage.html @@ -116,9 +116,8 @@ - - + diff --git a/manage.js b/manage.js index fc44e139..657cfa4a 100644 --- a/manage.js +++ b/manage.js @@ -27,7 +27,9 @@ Promise.all([ }); -chrome.runtime.onMessage.addListener(msg => { +chrome.runtime.onMessage.addListener(onRuntimeMessage); + +function onRuntimeMessage(msg) { switch (msg.method) { case 'styleUpdated': case 'styleAdded': @@ -37,7 +39,7 @@ chrome.runtime.onMessage.addListener(msg => { handleDelete(msg.id); break; } -}); +} function initGlobalEvents() { @@ -151,8 +153,6 @@ function createStyleElement({style, name}) { (style.enabled ? 'enabled' : 'disabled') + (style.updateUrl ? ' updatable' : ''), id: 'style-' + style.id, - styleId: style.id, - styleNameLowerCase: name || style.name.toLocaleLowerCase(), }); parts.nameLink.textContent = style.name; @@ -216,6 +216,8 @@ function createStyleElement({style, name}) { } const newEntry = parts.entry.cloneNode(true); + newEntry.styleId = style.id; + newEntry.styleNameLowerCase = name || style.name.toLocaleLowerCase(); const newTargets = $('.targets', newEntry); if (numTargets) { newTargets.parentElement.replaceChild(targets, newTargets); @@ -282,7 +284,10 @@ Object.assign(handleEvent, { }, toggle(event, entry) { - enableStyle(entry.styleId, this.matches('.enable') || this.checked); + saveStyleSafe({ + id: entry.styleId, + enabled: this.matches('.enable') || this.checked, + }); }, check(event, entry) { @@ -291,7 +296,7 @@ Object.assign(handleEvent, { update(event, entry) { // update everything but name - saveStyle(Object.assign(entry.updatedCode, { + saveStyleSafe(Object.assign(entry.updatedCode, { id: entry.styleId, name: null, reason: 'update', @@ -300,7 +305,7 @@ Object.assign(handleEvent, { delete(event, entry) { const id = entry.styleId; - const {name} = cachedStyles.byId.get(id) || {}; + const {name} = BG.cachedStyles.byId.get(id) || {}; animateElement(entry, {className: 'highlight'}); messageBox({ title: t('deleteStyleConfirm'), @@ -310,7 +315,7 @@ Object.assign(handleEvent, { }) .then(({button, enter, esc}) => { if (button == 0 || enter) { - deleteStyle(id); + deleteStyleSafe({id}); } }); }, @@ -335,7 +340,7 @@ Object.assign(handleEvent, { }); -function handleUpdate(style, {reason, quiet} = {}) { +function handleUpdate(style, {reason} = {}) { const element = createStyleElement({style}); const oldElement = $('#style-' + style.id, installed); if (oldElement) { @@ -354,8 +359,8 @@ function handleUpdate(style, {reason, quiet} = {}) { installed.insertBefore(element, findNextElement(style)); if (reason != 'import') { animateElement(element, {className: 'highlight'}); + scrollElementIntoView(element); } - scrollElementIntoView(element); } @@ -465,7 +470,7 @@ function checkUpdate(element) { class Updater { constructor(element) { - const style = cachedStyles.byId.get(element.styleId); + const style = BG.cachedStyles.byId.get(element.styleId); Object.assign(this, { element, id: style.id, @@ -504,7 +509,7 @@ class Updater { handleJson(forceUpdate, json) { return getStylesSafe({id: this.id}).then(([style]) => { - const needsUpdate = forceUpdate || !styleSectionsEqual(style, json); + const needsUpdate = forceUpdate || !BG.styleSectionsEqual(style, json); this.display({json: needsUpdate && json}); return needsUpdate; }); @@ -598,7 +603,7 @@ function searchStyles({immediately, container}) { } for (const element of (container || installed).children) { - const style = cachedStyles.byId.get(element.styleId) || {}; + const style = BG.cachedStyles.byId.get(element.styleId) || {}; if (style) { const isMatching = !query || isMatchingText(style.name) diff --git a/manifest.json b/manifest.json index 929b70dd..0628b51a 100644 --- a/manifest.json +++ b/manifest.json @@ -19,7 +19,7 @@ "*://*/*" ], "background": { - "scripts": ["messaging.js", "storage.js", "background.js", "update.js"] + "scripts": ["messaging.js", "storage.js", "prefs.js", "background.js", "update.js"] }, "commands": { "openManage": { diff --git a/messaging.js b/messaging.js index b62c9673..3975b777 100644 --- a/messaging.js +++ b/messaging.js @@ -1,4 +1,4 @@ -/* global getStyleWithNoCode, applyOnMessage, onBackgroundMessage, getStyles */ +/* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */ 'use strict'; // keep message channel open for sendResponse in chrome.runtime.onMessage listener @@ -7,134 +7,59 @@ const FIREFOX = /Firefox/.test(navigator.userAgent); const OPERA = /OPR/.test(navigator.userAgent); const URLS = { ownOrigin: chrome.runtime.getURL(''), - optionsUI: new Set([ + optionsUI: [ chrome.runtime.getURL('options/index.html'), 'chrome://extensions/?options=' + chrome.runtime.id, - ]), - configureCommands: OPERA ? 'opera://settings/configureCommands' - : 'chrome://extensions/configureCommands', + ], + configureCommands: + OPERA ? 'opera://settings/configureCommands' + : 'chrome://extensions/configureCommands', }; const RX_SUPPORTED_URLS = new RegExp(`^(file|https?|ftps?):|^${URLS.ownOrigin}`); -document.documentElement.classList.toggle('firefox', FIREFOX); -document.documentElement.classList.toggle('opera', OPERA); +let BG = chrome.extension.getBackgroundPage(); +if (!BG || BG != window) { + document.documentElement.classList.toggle('firefox', FIREFOX); + document.documentElement.classList.toggle('opera', OPERA); +} -function notifyAllTabs(request) { - // list all tabs including chrome-extension:// which can be ours - if (request.codeIsUpdated === false && request.style) { - request = Object.assign({}, request, { - style: getStyleWithNoCode(request.style) +function notifyAllTabs(msg) { + const originalMessage = msg; + if (msg.codeIsUpdated === false && msg.style) { + msg = Object.assign({}, msg, { + style: getStyleWithNoCode(msg.style) }); } - const affectsAll = !request.affects || request.affects.all; - const affectsOwnOrigin = !affectsAll && (request.affects.editor || request.affects.manager); + const affectsAll = !msg.affects || msg.affects.all; + const affectsOwnOrigin = !affectsAll && (msg.affects.editor || msg.affects.manager); const affectsTabs = affectsAll || affectsOwnOrigin; - const affectsIcon = affectsAll || request.affects.icon; - const affectsPopup = affectsAll || request.affects.popup; + const affectsIcon = affectsAll || msg.affects.icon; + const affectsPopup = affectsAll || msg.affects.popup; if (affectsTabs || affectsIcon) { + // list all tabs including chrome-extension:// which can be ours chrome.tabs.query(affectsOwnOrigin ? {url: URLS.ownOrigin + '*'} : {}, tabs => { for (const tab of tabs) { - if (affectsTabs || URLS.optionsUI.has(tab.url)) { - chrome.tabs.sendMessage(tab.id, request); + if (affectsTabs || URLS.optionsUI.includes(tab.url)) { + chrome.tabs.sendMessage(tab.id, msg); } - if (affectsIcon) { - updateIcon(tab); + if (affectsIcon && BG) { + BG.updateIcon(tab); } } }); } // notify self: the message no longer is sent to the origin in new Chrome - if (window.applyOnMessage) { - applyOnMessage(request); - } else if (window.onBackgroundMessage) { - onBackgroundMessage(request); + if (typeof onRuntimeMessage != 'undefined') { + onRuntimeMessage(originalMessage); + } + // notify apply.js on own pages + if (typeof applyOnMessage != 'undefined') { + applyOnMessage(originalMessage); } // notify background page and all open popups - if (affectsPopup || request.prefs) { - chrome.runtime.sendMessage(request); - } -} - - -function refreshAllTabs() { - return new Promise(resolve => { - // list all tabs including chrome-extension:// which can be ours - chrome.tabs.query({}, tabs => { - const lastTab = tabs[tabs.length - 1]; - for (const tab of tabs) { - getStyles({matchUrl: tab.url, enabled: true, asHash: true}, styles => { - const message = {method: 'styleReplaceAll', styles}; - if (tab.url == location.href && typeof applyOnMessage !== 'undefined') { - applyOnMessage(message); - } else { - chrome.tabs.sendMessage(tab.id, message); - } - updateIcon(tab, styles); - if (tab == lastTab) { - resolve(); - } - }); - } - }); - }); -} - - -function updateIcon(tab, styles) { - // while NTP is still loading only process the request for its main frame with a real url - // (but when it's loaded we should process style toggle requests from popups, for example) - const isNTP = tab.url == 'chrome://newtab/'; - if (isNTP && tab.status != 'complete' || tab.id < 0) { - return; - } - if (styles) { - // check for not-yet-existing tabs e.g. omnibox instant search - chrome.tabs.get(tab.id, () => { - if (!chrome.runtime.lastError) { - stylesReceived(styles); - } - }); - return; - } - if (isNTP) { - getTabRealURL(tab).then(url => - getStyles({matchUrl: url, enabled: true, asHash: true}, stylesReceived)); - } else { - getStyles({matchUrl: tab.url, enabled: true, asHash: true}, stylesReceived); - } - - function stylesReceived(styles) { - let numStyles = styles.length; - if (numStyles === undefined) { - // for 'styles' asHash:true fake the length by counting numeric ids manually - numStyles = 0; - for (const id of Object.keys(styles)) { - numStyles += id.match(/^\d+$/) ? 1 : 0; - } - } - const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll'); - const postfix = disableAll ? 'x' : numStyles == 0 ? 'w' : ''; - const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal'); - const text = prefs.get('show-badge') && numStyles ? String(numStyles) : ''; - chrome.browserAction.setIcon({ - tabId: tab.id, - path: { - // Material Design 2016 new size is 16px - 16: `images/icon/16${postfix}.png`, - 32: `images/icon/32${postfix}.png`, - // Chromium forks or non-chromium browsers may still use the traditional 19px - 19: `images/icon/19${postfix}.png`, - 38: `images/icon/38${postfix}.png`, - // TODO: add Edge preferred sizes: 20, 25, 30, 40 - }, - }, () => { - if (!chrome.runtime.lastError) { - // Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor - chrome.browserAction.setBadgeBackgroundColor({color}); - chrome.browserAction.setBadgeText({text, tabId: tab.id}); - } - }); + if (affectsPopup || msg.prefs) { + chrome.runtime.sendMessage(msg); } } @@ -211,12 +136,153 @@ function stringAsRegExp(s, flags) { } -// expands * as .*? -function wildcardAsRegExp(s, flags) { - return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&').replace(/\*/g, '.*?'), flags); -} - - function ignoreChromeError() { chrome.runtime.lastError; // eslint-disable-line no-unused-expressions } + + +function getStyleWithNoCode(style) { + const stripped = Object.assign({}, style, {sections: []}); + for (const section of style.sections) { + stripped.sections.push(Object.assign({}, section, {code: null})); + } + return stripped; +} + + +// js engine can't optimize the entire function if it contains try-catch +// so we should keep it isolated from normal code in a minimal wrapper +// Update: might get fixed in V8 TurboFan in the future +function tryCatch(func, ...args) { + try { + return func(...args); + } catch (e) {} +} + + +function tryRegExp(regexp) { + try { + return new RegExp(regexp); + } catch (e) {} +} + + +function tryJSONparse(jsonString) { + try { + return JSON.parse(jsonString); + } catch (e) {} +} + + +function debounce(fn, delay, ...args) { + const timers = debounce.timers = debounce.timers || new Map(); + debounce.run = debounce.run || ((fn, ...args) => { + timers.delete(fn); + fn(...args); + }); + clearTimeout(timers.get(fn)); + timers.set(fn, setTimeout(debounce.run, delay, fn, ...args)); +} + + +function deepCopy(obj) { + if (!obj || typeof obj != 'object') { + return obj; + } else { + const emptyCopy = Object.create(Object.getPrototypeOf(obj)); + return deepMerge(emptyCopy, obj); + } +} + + +function deepMerge(target, ...args) { + for (const obj of args) { + for (const k in obj) { + const value = obj[k]; + if (!value || typeof value != 'object') { + target[k] = value; + } else if (typeof value.slice == 'function') { + const arrayCopy = target[k] = target[k] || []; + for (const element of value) { + arrayCopy.push(deepCopy(element)); + } + } else if (k in target) { + deepMerge(target[k], value); + } else { + target[k] = deepCopy(value); + } + } + } + return target; +} + + +function sessionStorageHash(name) { + return { + name, + value: tryCatch(JSON.parse, sessionStorage[name]) || {}, + set(k, v) { + this.value[k] = v; + this.updateStorage(); + }, + unset(k) { + delete this.value[k]; + this.updateStorage(); + }, + updateStorage() { + sessionStorage[this.name] = JSON.stringify(this.value); + } + }; +} + + +function onBackgroundReady() { + return BG ? Promise.resolve() : new Promise(ping); + function ping(resolve) { + chrome.runtime.sendMessage({method: 'healthCheck'}, health => { + if (health !== undefined) { + BG = chrome.extension.getBackgroundPage(); + resolve(); + } else { + ping(resolve); + } + }); + } +} + + +// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage +function getStylesSafe(options) { + return new Promise(resolve => { + if (BG) { + BG.getStyles(options, resolve); + } else { + onBackgroundReady().then(() => + BG.getStyles(options, resolve)); + } + }); +} + + +function saveStyleSafe(style) { + return onBackgroundReady() + .then(() => BG.saveStyle(BG.deepCopy(style))) + .then(savedStyle => { + if (style.notify === false) { + handleUpdate(savedStyle, style); + } + return savedStyle; + }); +} + + +function deleteStyleSafe({id, notify = true} = {}) { + return onBackgroundReady() + .then(() => BG.deleteStyle({id, notify})) + .then(() => { + if (!notify) { + handleDelete(id); + } + return id; + }); +} diff --git a/options/index.html b/options/index.html index 98978a87..b98c447b 100644 --- a/options/index.html +++ b/options/index.html @@ -5,9 +5,9 @@ - - + + diff --git a/options/index.js b/options/index.js index f3e52cfa..19ecf930 100644 --- a/options/index.js +++ b/options/index.js @@ -1,7 +1,5 @@ -/* global update */ 'use strict'; - setupLivePrefs([ 'show-badge', 'popup.stylesFirst', @@ -33,7 +31,7 @@ document.onclick = e => { } function check() { - chrome.extension.getBackgroundPage().update.perform((cmd, value) => { + BG.update.perform((cmd, value) => { switch (cmd) { case 'count': total = value; diff --git a/popup.html b/popup.html index ecb152f5..1c026e9e 100644 --- a/popup.html +++ b/popup.html @@ -57,9 +57,8 @@ - - + diff --git a/popup.js b/popup.js index bacb1316..aed550b7 100644 --- a/popup.js +++ b/popup.js @@ -1,4 +1,3 @@ -/* global SLOPPY_REGEXP_PREFIX, compileStyleRegExps */ 'use strict'; let installed; @@ -17,8 +16,9 @@ getActiveTabRealURL().then(url => { }); }); +chrome.runtime.onMessage.addListener(onRuntimeMessage); -chrome.runtime.onMessage.addListener(msg => { +function onRuntimeMessage(msg) { switch (msg.method) { case 'styleAdded': case 'styleUpdated': @@ -38,7 +38,7 @@ chrome.runtime.onMessage.addListener(msg => { } break; } -}); +} function setPopupWidth(width = prefs.get('popupWidth')) { @@ -117,7 +117,7 @@ function initPopup(url) { matchTargets.appendChild(urlLink); // For domain - const domains = getDomains(url); + const domains = BG.getDomains(url); for (const domain of domains) { // Don't include TLD if (domains.length > 1 && !domain.includes('.')) { @@ -252,7 +252,7 @@ Object.assign(handleEvent, { }, toggle(event) { - saveStyle({ + saveStyleSafe({ id: handleEvent.getClickedStyleId(event), enabled: this.type == 'checkbox' ? this.checked : this.matches('.enable'), }); @@ -263,7 +263,7 @@ Object.assign(handleEvent, { const box = $('#confirm'); box.dataset.display = true; box.style.cssText = ''; - $('b', box).textContent = (cachedStyles.byId.get(id) || {}).name; + $('b', box).textContent = (BG.cachedStyles.byId.get(id) || {}).name; $('[data-cmd="ok"]', box).onclick = () => confirm(true); $('[data-cmd="cancel"]', box).onclick = () => confirm(false); window.onkeydown = event => { @@ -278,7 +278,7 @@ Object.assign(handleEvent, { animateElement(box, {className: 'lights-on'}) .then(() => (box.dataset.display = false)); if (ok) { - deleteStyle(id).then(() => { + deleteStyleSafe({id}).then(() => { // update view with 'No styles installed for this site' message if (!installed.children.length) { showStyles([]); @@ -297,7 +297,7 @@ Object.assign(handleEvent, { entry.appendChild(info); }, - closeExplanation(event) { + closeExplanation() { $('#regexp-explanation').remove(); }, @@ -347,7 +347,7 @@ function handleUpdate(style) { return; } // Add an entry when a new style for the current url is installed - if (tabURL && getApplicableSections({style, matchUrl: tabURL, stopOnFirst: true}).length) { + if (tabURL && BG.getApplicableSections({style, matchUrl: tabURL, stopOnFirst: true}).length) { $('#unavailable').style.display = 'none'; createStyleElement({style}); } @@ -368,13 +368,15 @@ function handleDelete(id) { */ function detectSloppyRegexps({entry, style}) { const { - appliedSections = getApplicableSections({style, matchUrl: tabURL}), - wannabeSections = getApplicableSections({style, matchUrl: tabURL, strictRegexp: false}), + appliedSections = + BG.getApplicableSections({style, matchUrl: tabURL}), + wannabeSections = + BG.getApplicableSections({style, matchUrl: tabURL, strictRegexp: false}), } = style; - compileStyleRegExps({style, compileAll: true}); + BG.compileStyleRegExps({style, compileAll: true}); entry.hasInvalidRegexps = wannabeSections.some(section => - section.regexps.some(rx => !cachedStyles.regexps.has(rx))); + section.regexps.some(rx => !BG.cachedStyles.regexps.has(rx))); entry.sectionsSkipped = wannabeSections.length - appliedSections.length; if (!appliedSections.length) { diff --git a/prefs.js b/prefs.js new file mode 100644 index 00000000..a04e1bee --- /dev/null +++ b/prefs.js @@ -0,0 +1,265 @@ +/* global prefs: true, contextMenus */ +'use strict'; + +// eslint-disable-next-line no-var +var prefs = new function Prefs() { + const defaults = { + 'openEditInWindow': false, // new editor opens in a own browser window + 'windowPosition': {}, // detached window position + 'show-badge': true, // display text on popup menu icon + 'disableAll': false, // boss key + + 'popup.breadcrumbs': true, // display 'New style' links as URL breadcrumbs + 'popup.breadcrumbs.usePath': false, // use URL path for 'this URL' + 'popup.enabledFirst': true, // display enabled styles before disabled styles + 'popup.stylesFirst': true, // display enabled styles before disabled styles + + 'manage.onlyEnabled': false, // display only enabled styles + 'manage.onlyEdited': false, // display only styles created locally + 'manage.newUI': true, // use the new compact layout + 'manage.newUI.favicons': true, // show favicons for the sites in applies-to + 'manage.newUI.targets': 3, // max number of applies-to targets visible: 0 = none + + 'editor.options': {}, // CodeMirror.defaults.* + 'editor.lineWrapping': true, // word wrap + 'editor.smartIndent': true, // 'smart' indent + 'editor.indentWithTabs': false, // smart indent with tabs + 'editor.tabSize': 4, // tab width, in spaces + 'editor.keyMap': navigator.appVersion.indexOf('Windows') > 0 ? 'sublime' : 'default', + 'editor.theme': 'default', // CSS theme + 'editor.beautify': { // CSS beautifier + selector_separator_newline: true, + newline_before_open_brace: false, + newline_after_open_brace: true, + newline_between_properties: true, + newline_before_close_brace: true, + newline_between_rules: false, + end_with_newline: false, + space_around_selector_separator: true, + }, + 'editor.lintDelay': 500, // lint gutter marker update delay, ms + 'editor.lintReportDelay': 4500, // lint report update delay, ms + 'editor.matchHighlight': 'token', // token = token/word under cursor even if nothing is selected + // selection = only when something is selected + // '' (empty string) = disabled + + 'badgeDisabled': '#8B0000', // badge background color when disabled + 'badgeNormal': '#006666', // badge background color + + 'popupWidth': 246, // popup width in pixels + + 'updateInterval': 0 // user-style automatic update interval, hour + }; + const values = deepCopy(defaults); + + const affectsIcon = [ + 'show-badge', + 'disableAll', + 'badgeDisabled', + 'badgeNormal', + ]; + + // coalesce multiple pref changes in broadcast + let broadcastPrefs = {}; + + Object.defineProperty(this, 'readOnlyValues', {value: {}}); + + Object.assign(Prefs.prototype, { + + get(key, defaultValue) { + if (key in values) { + return values[key]; + } + if (defaultValue !== undefined) { + return defaultValue; + } + if (key in defaults) { + return defaults[key]; + } + console.warn("No default preference for '%s'", key); + }, + + getAll() { + return deepCopy(values); + }, + + set(key, value, {noBroadcast, noSync} = {}) { + const oldValue = deepCopy(values[key]); + values[key] = value; + defineReadonlyProperty(this.readOnlyValues, key, value); + if (!noBroadcast && !equal(value, oldValue)) { + this.broadcast(key, value, {noSync}); + } + localStorage[key] = typeof defaults[key] == 'object' + ? JSON.stringify(value) + : value; + }, + + remove: key => this.set(key, undefined), + + reset: key => this.set(key, deepCopy(defaults[key])), + + broadcast(key, value, {noSync} = {}) { + broadcastPrefs[key] = value; + debounce(doBroadcast); + if (!noSync) { + debounce(doSyncSet); + } + }, + }); + + // Unlike sync, HTML5 localStorage is ready at browser startup + // so we'll mirror the prefs to avoid using the wrong defaults + // during the startup phase + for (const key in defaults) { + const defaultValue = defaults[key]; + let value = localStorage[key]; + if (typeof value == 'string') { + switch (typeof defaultValue) { + case 'boolean': + value = value.toLowerCase() === 'true'; + break; + case 'number': + value |= 0; + break; + case 'object': + value = tryJSONparse(value) || defaultValue; + break; + } + } else { + value = defaultValue; + } + this.set(key, value, {noBroadcast: true}); + } + + getSync().get('settings', ({settings: synced} = {}) => { + if (synced) { + for (const key in defaults) { + if (key == 'popupWidth' && synced[key] != values.popupWidth) { + // this is a fix for the period when popupWidth wasn't synced + // TODO: remove it in a couple of months + continue; + } + if (key in synced) { + this.set(key, synced[key], {noSync: true}); + } + } + } + if (typeof contextMenus !== 'undefined') { + for (const id in contextMenus) { + if (typeof values[id] == 'boolean') { + this.broadcast(id, values[id], {noSync: true}); + } + } + } + }); + + chrome.storage.onChanged.addListener((changes, area) => { + if (area == 'sync' && 'settings' in changes) { + const synced = changes.settings.newValue; + if (synced) { + for (const key in defaults) { + if (key in synced) { + this.set(key, synced[key], {noSync: true}); + } + } + } else { + // user manually deleted our settings, we'll recreate them + getSync().set({'settings': values}); + } + } + }); + + function doBroadcast() { + const affects = {all: 'disableAll' in broadcastPrefs}; + if (!affects.all) { + for (const key in broadcastPrefs) { + affects.icon = affects.icon || affectsIcon.includes(key); + affects.popup = affects.popup || key.startsWith('popup'); + affects.editor = affects.editor || key.startsWith('editor'); + affects.manager = affects.manager || key.startsWith('manage'); + } + } + notifyAllTabs({method: 'prefChanged', prefs: broadcastPrefs, affects}); + broadcastPrefs = {}; + } + + function doSyncSet() { + getSync().set({'settings': values}); + } + + // Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494 + function getSync() { + if ('sync' in chrome.storage) { + return chrome.storage.sync; + } + const crappyStorage = {}; + return { + get(key, callback) { + callback(crappyStorage[key] || {}); + }, + set(source, callback) { + for (const property in source) { + if (source.hasOwnProperty(property)) { + crappyStorage[property] = source[property]; + } + } + callback(); + } + }; + } + + function defineReadonlyProperty(obj, key, value) { + const copy = deepCopy(value); + if (typeof copy == 'object') { + Object.freeze(copy); + } + Object.defineProperty(obj, key, {value: copy, configurable: true}); + } + + function equal(a, b) { + if (!a || !b || typeof a != 'object' || typeof b != 'object') { + return a === b; + } + if (Object.keys(a).length != Object.keys(b).length) { + return false; + } + for (const k in a) { + if (a[k] !== b[k]) { + return false; + } + } + return true; + } +}(); + + +// Accepts an array of pref names (values are fetched via prefs.get) +// and establishes a two-way connection between the document elements and the actual prefs +function setupLivePrefs(IDs) { + const localIDs = {}; + IDs.forEach(function(id) { + localIDs[id] = true; + updateElement(id).addEventListener('change', function() { + prefs.set(this.id, isCheckbox(this) ? this.checked : this.value); + }); + }); + chrome.runtime.onMessage.addListener(msg => { + if (msg.prefs) { + for (const prefName in msg.prefs) { + if (prefName in localIDs) { + updateElement(prefName, msg.prefs[prefName]); + } + } + } + }); + function updateElement(id, value) { + const el = document.getElementById(id); + el[isCheckbox(el) ? 'checked' : 'value'] = value || prefs.get(id); + el.dispatchEvent(new Event('change', {bubbles: true, cancelable: true})); + return el; + } + function isCheckbox(el) { + return el.localName == 'input' && el.type == 'checkbox'; + } +} diff --git a/storage.js b/storage.js index f1846fa0..b4bc03bb 100644 --- a/storage.js +++ b/storage.js @@ -1,7 +1,28 @@ -/* global cachedStyles: true, prefs: true, contextMenus: false */ -/* global handleUpdate, handleDelete */ +/* global cachedStyles: true */ 'use strict'; +const RX_NAMESPACE = new RegExp([/[\s\r\n]*/, + /(@namespace[\s\r\n]+(?:[^\s\r\n]+[\s\r\n]+)?url\(http:\/\/.*?\);)/, + /[\s\r\n]*/].map(rx => rx.source).join(''), 'g'); +const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g; +const SLOPPY_REGEXP_PREFIX = '\0'; + +// Note, only 'var'-declared variables are visible from another extension page +// eslint-disable-next-line no-var +var cachedStyles = { + list: null, + byId: new Map(), + filters: new Map(), + regexps: new Map(), + urlDomains: new Map(), + emptyCode: new Map(), // entire code is comments/whitespace/@namespace + mutex: { + inProgress: false, + onDone: [], + }, +}; + + function getDatabase(ready, error) { const dbOpenRequest = window.indexedDB.open('stylish', 2); dbOpenRequest.onsuccess = event => { @@ -24,54 +45,6 @@ function getDatabase(ready, error) { } -const RX_NAMESPACE = new RegExp([/[\s\r\n]*/, - /(@namespace[\s\r\n]+(?:[^\s\r\n]+[\s\r\n]+)?url\(http:\/\/.*?\);)/, - /[\s\r\n]*/].map(rx => rx.source).join(''), 'g'); -const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g; -const SLOPPY_REGEXP_PREFIX = '\0'; - -// Let manage/popup/edit reuse background page variables -// Note, only 'var'-declared variables are visible from another extension page -// eslint-disable-next-line no-var -var cachedStyles, prefs; -(() => { - const bg = chrome.extension.getBackgroundPage(); - cachedStyles = bg && bg.cachedStyles || { - bg, - list: null, - byId: new Map(), - filters: new Map(), - regexps: new Map(), - urlDomains: new Map(), - emptyCode: new Map(), // entire code is comments/whitespace/@namespace - mutex: { - inProgress: false, - onDone: [], - }, - }; - prefs = bg && bg.prefs; -})(); - - -// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage -function getStylesSafe(options) { - return new Promise(resolve => { - if (cachedStyles.bg) { - getStyles(options, resolve); - return; - } - chrome.runtime.sendMessage(Object.assign({method: 'getStyles'}, options), styles => { - if (!styles) { - resolve(getStylesSafe(options)); - } else { - cachedStyles = chrome.extension.getBackgroundPage().cachedStyles; - resolve(styles); - } - }); - }); -} - - function getStyles(options, callback) { if (cachedStyles.list) { callback(filterStyles(options)); @@ -107,60 +80,6 @@ function getStyles(options, callback) { } -function getStyleWithNoCode(style) { - const stripped = Object.assign({}, style, {sections: []}); - for (const section of style.sections) { - stripped.sections.push(Object.assign({}, section, {code: null})); - } - return stripped; -} - - -function invalidateCache(andNotify, {added, updated, deletedId} = {}) { - // prevent double-add on echoed invalidation - const cached = added && cachedStyles.byId.get(added.id); - if (cached) { - return; - } - if (andNotify) { - chrome.runtime.sendMessage({method: 'invalidateCache', added, updated, deletedId}); - } - if (!cachedStyles.list) { - return; - } - if (updated) { - const cached = cachedStyles.byId.get(updated.id); - if (cached) { - Object.assign(cached, updated); - //console.debug('cache: updated', updated); - } - cachedStyles.filters.clear(); - return; - } - if (added) { - cachedStyles.list.push(added); - cachedStyles.byId.set(added.id, added); - //console.debug('cache: added', added); - cachedStyles.filters.clear(); - return; - } - if (deletedId != undefined) { - const deletedStyle = (cachedStyles.byId.get(deletedId) || {}).style; - if (deletedStyle) { - const cachedIndex = cachedStyles.list.indexOf(deletedStyle); - cachedStyles.list.splice(cachedIndex, 1); - cachedStyles.byId.delete(deletedId); - //console.debug('cache: deleted', deletedStyle); - cachedStyles.filters.clear(); - return; - } - } - cachedStyles.list = null; - //console.debug('cache cleared'); - cachedStyles.filters.clear(); -} - - function filterStyles({ enabled, url = null, @@ -174,10 +93,10 @@ function filterStyles({ id = id === null ? null : Number(id); if (enabled === null - && url === null - && id === null - && matchUrl === null - && asHash != true) { + && url === null + && id === null + && matchUrl === null + && asHash != true) { //console.debug('%c%s filterStyles SKIPPED LOOP %s', 'color:gray', (performance.now() - t0).toFixed(1), enabled, id, asHash, strictRegexp, matchUrl); // eslint-disable-line max-len return cachedStyles.list; } @@ -247,36 +166,6 @@ function filterStyles({ } -function cleanupCachedFilters({force = false} = {}) { - if (!force) { - // sliding timer for 1 second - clearTimeout(cleanupCachedFilters.timeout); - cleanupCachedFilters.timeout = setTimeout(cleanupCachedFilters, 1000, {force: true}); - return; - } - const size = cachedStyles.filters.size; - const oldestHit = cachedStyles.filters.values().next().value.lastHit; - const now = Date.now(); - const timeSpan = now - oldestHit; - const recencyWeight = 5 / size; - const hitWeight = 1 / 4; // we make ~4 hits per URL - const lastHitWeight = 10; - // delete the oldest 10% - [...cachedStyles.filters.entries()] - .map(([id, v], index) => ({ - id, - weight: - index * recencyWeight + - v.hits * hitWeight + - (v.lastHit - oldestHit) / timeSpan * lastHitWeight, - })) - .sort((a, b) => a.weight - b.weight) - .slice(0, size / 10 + 1) - .forEach(({id}) => cachedStyles.filters.delete(id)); - cleanupCachedFilters.timeout = 0; -} - - function saveStyle(style) { return new Promise(resolve => { getDatabase(db => { @@ -312,9 +201,6 @@ function saveStyle(style) { style, codeIsUpdated, reason, }); } - if (typeof handleUpdate != 'undefined') { - handleUpdate(style, {reason}); - } resolve(style); }; }; @@ -340,9 +226,6 @@ function saveStyle(style) { if (notify) { notifyAllTabs({method: 'styleAdded', style, reason}); } - if (typeof handleUpdate != 'undefined') { - handleUpdate(style, {reason}); - } resolve(style); }; }); @@ -350,24 +233,7 @@ function saveStyle(style) { } -function addMissingStyleTargets(style) { - style.sections = (style.sections || []).map(section => - Object.assign({ - urls: [], - urlPrefixes: [], - domains: [], - regexps: [], - }, section) - ); -} - - -function enableStyle(id, enabled) { - return saveStyle({id, enabled}); -} - - -function deleteStyle(id, {notify = true} = {}) { +function deleteStyle({id, notify = true}) { return new Promise(resolve => getDatabase(db => { const tx = db.transaction(['styles'], 'readwrite'); @@ -377,61 +243,12 @@ function deleteStyle(id, {notify = true} = {}) { if (notify) { notifyAllTabs({method: 'styleDeleted', id}); } - if (typeof handleDelete != 'undefined') { - handleDelete(id); - } resolve(id); }; })); } -function reportError(...args) { - for (const arg of args) { - if ('message' in arg) { - console.log(arg.message); - } - } -} - - -function fixBoolean(b) { - if (typeof b != 'undefined') { - return b != 'false'; - } - return null; -} - - -function getDomains(url) { - if (url.indexOf('file:') == 0) { - return []; - } - let d = /.*?:\/*([^/:]+)/.exec(url)[1]; - const domains = [d]; - while (d.indexOf('.') != -1) { - d = d.substring(d.indexOf('.') + 1); - domains.push(d); - } - return domains; -} - - -function getType(o) { - if (typeof o == 'undefined' || typeof o == 'string') { - return typeof o; - } - // with the persistent cachedStyles the Array reference is usually different - // so let's check for e.g. type of 'every' which is only present on arrays - // (in the context of our extension) - if (o instanceof Array || typeof o.every == 'function') { - return 'array'; - } - console.warn('Unsupported type:', o); - return 'undefined'; -} - - function getApplicableSections({style, matchUrl, strictRegexp = true, stopOnFirst}) { //let t0 = 0; const sections = []; @@ -518,392 +335,6 @@ function getApplicableSections({style, matchUrl, strictRegexp = true, stopOnFirs } -function isCheckbox(el) { - return el.localName == 'input' && el.type == 'checkbox'; -} - - -// js engine can't optimize the entire function if it contains try-catch -// so we should keep it isolated from normal code in a minimal wrapper -// Update: might get fixed in V8 TurboFan in the future -function runTryCatch(func, ...args) { - try { - return func(...args); - } catch (e) {} -} - - -function tryRegExp(regexp) { - try { - return new RegExp(regexp); - } catch (e) {} -} - - -function tryJSONparse(jsonString) { - try { - return JSON.parse(jsonString); - } catch (e) {} -} - - -function debounce(fn, delay, ...args) { - const timers = debounce.timers = debounce.timers || new Map(); - debounce.run = debounce.run || ((fn, ...args) => { - timers.delete(fn); - fn(...args); - }); - clearTimeout(timers.get(fn)); - timers.set(fn, setTimeout(debounce.run, delay, fn, ...args)); -} - - -prefs = prefs || new function Prefs() { - const defaults = { - 'openEditInWindow': false, // new editor opens in a own browser window - 'windowPosition': {}, // detached window position - 'show-badge': true, // display text on popup menu icon - 'disableAll': false, // boss key - - 'popup.breadcrumbs': true, // display 'New style' links as URL breadcrumbs - 'popup.breadcrumbs.usePath': false, // use URL path for 'this URL' - 'popup.enabledFirst': true, // display enabled styles before disabled styles - 'popup.stylesFirst': true, // display enabled styles before disabled styles - - 'manage.onlyEnabled': false, // display only enabled styles - 'manage.onlyEdited': false, // display only styles created locally - 'manage.newUI': true, // use the new compact layout - 'manage.newUI.favicons': true, // show favicons for the sites in applies-to - 'manage.newUI.targets': 3, // max number of applies-to targets visible: 0 = none - - 'editor.options': {}, // CodeMirror.defaults.* - 'editor.lineWrapping': true, // word wrap - 'editor.smartIndent': true, // 'smart' indent - 'editor.indentWithTabs': false, // smart indent with tabs - 'editor.tabSize': 4, // tab width, in spaces - 'editor.keyMap': navigator.appVersion.indexOf('Windows') > 0 ? 'sublime' : 'default', - 'editor.theme': 'default', // CSS theme - 'editor.beautify': { // CSS beautifier - selector_separator_newline: true, - newline_before_open_brace: false, - newline_after_open_brace: true, - newline_between_properties: true, - newline_before_close_brace: true, - newline_between_rules: false, - end_with_newline: false, - space_around_selector_separator: true, - }, - 'editor.lintDelay': 500, // lint gutter marker update delay, ms - 'editor.lintReportDelay': 4500, // lint report update delay, ms - 'editor.matchHighlight': 'token', // token = token/word under cursor even if nothing is selected - // selection = only when something is selected - // '' (empty string) = disabled - - 'badgeDisabled': '#8B0000', // badge background color when disabled - 'badgeNormal': '#006666', // badge background color - - 'popupWidth': 246, // popup width in pixels - - 'updateInterval': 0 // user-style automatic update interval, hour - }; - const values = deepCopy(defaults); - - const affectsIcon = [ - 'show-badge', - 'disableAll', - 'badgeDisabled', - 'badgeNormal', - ]; - - // coalesce multiple pref changes in broadcast - let broadcastPrefs = {}; - - function doBroadcast() { - const affects = {all: 'disableAll' in broadcastPrefs}; - if (!affects.all) { - for (const key in broadcastPrefs) { - affects.icon = affects.icon || affectsIcon.includes(key); - affects.popup = affects.popup || key.startsWith('popup'); - affects.editor = affects.editor || key.startsWith('editor'); - affects.manager = affects.manager || key.startsWith('manage'); - } - } - notifyAllTabs({method: 'prefChanged', prefs: broadcastPrefs, affects}); - broadcastPrefs = {}; - } - - function doSyncSet() { - getSync().set({'settings': values}); - } - - Object.defineProperty(this, 'readOnlyValues', {value: {}}); - - Object.assign(Prefs.prototype, { - - get(key, defaultValue) { - if (key in values) { - return values[key]; - } - if (defaultValue !== undefined) { - return defaultValue; - } - if (key in defaults) { - return defaults[key]; - } - console.warn("No default preference for '%s'", key); - }, - - getAll() { - return deepCopy(values); - }, - - set(key, value, {noBroadcast, noSync} = {}) { - const oldValue = deepCopy(values[key]); - values[key] = value; - defineReadonlyProperty(this.readOnlyValues, key, value); - if (!noBroadcast && !equal(value, oldValue)) { - this.broadcast(key, value, {noSync}); - } - localStorage[key] = typeof defaults[key] == 'object' - ? JSON.stringify(value) - : value; - }, - - remove: key => this.set(key, undefined), - - reset: key => this.set(key, deepCopy(defaults[key])), - - broadcast(key, value, {noSync} = {}) { - broadcastPrefs[key] = value; - debounce(doBroadcast); - if (!noSync) { - debounce(doSyncSet); - } - }, - }); - - // Unlike sync, HTML5 localStorage is ready at browser startup - // so we'll mirror the prefs to avoid using the wrong defaults - // during the startup phase - for (const key in defaults) { - const defaultValue = defaults[key]; - let value = localStorage[key]; - if (typeof value == 'string') { - switch (typeof defaultValue) { - case 'boolean': - value = value.toLowerCase() === 'true'; - break; - case 'number': - value |= 0; - break; - case 'object': - value = tryJSONparse(value) || defaultValue; - break; - } - } else { - value = defaultValue; - } - this.set(key, value, {noBroadcast: true}); - } - - getSync().get('settings', ({settings: synced}) => { - if (synced) { - for (const key in defaults) { - if (key == 'popupWidth' && synced[key] != values.popupWidth) { - // this is a fix for the period when popupWidth wasn't synced - // TODO: remove it in a couple of months before the summer 2017 - continue; - } - if (key in synced) { - this.set(key, synced[key], {noSync: true}); - } - } - } - if (typeof contextMenus !== 'undefined') { - for (const id in contextMenus) { - if (typeof values[id] == 'boolean') { - this.broadcast(id, values[id], {noSync: true}); - } - } - } - }); - - chrome.storage.onChanged.addListener((changes, area) => { - if (area == 'sync' && 'settings' in changes) { - const synced = changes.settings.newValue; - if (synced) { - for (const key in defaults) { - if (key in synced) { - this.set(key, synced[key], {noSync: true}); - } - } - } else { - // user manually deleted our settings, we'll recreate them - getSync().set({'settings': values}); - } - } - }); -}(); - - -// Accepts an array of pref names (values are fetched via prefs.get) -// and establishes a two-way connection between the document elements and the actual prefs -function setupLivePrefs(IDs) { - const localIDs = {}; - IDs.forEach(function(id) { - localIDs[id] = true; - updateElement(id).addEventListener('change', function() { - prefs.set(this.id, isCheckbox(this) ? this.checked : this.value); - }); - }); - chrome.runtime.onMessage.addListener(msg => { - if (msg.prefs) { - for (const prefName in msg.prefs) { - if (prefName in localIDs) { - updateElement(prefName, msg.prefs[prefName]); - } - } - } - }); - function updateElement(id, value) { - const el = document.getElementById(id); - el[isCheckbox(el) ? 'checked' : 'value'] = value || prefs.get(id); - el.dispatchEvent(new Event('change', {bubbles: true, cancelable: true})); - return el; - } -} - - -function enforceInputRange(element) { - const min = Number(element.min); - const max = Number(element.max); - const onChange = () => { - const value = Number(element.value); - if (value < min || value > max) { - element.value = Math.max(min, Math.min(max, value)); - } - }; - onChange(); - element.addEventListener('change', onChange); - element.addEventListener('input', onChange); -} - - -function getCodeMirrorThemes(callback) { - chrome.runtime.getPackageDirectoryEntry(function(rootDir) { - rootDir.getDirectory('codemirror/theme', {create: false}, function(themeDir) { - themeDir.createReader().readEntries(function(entries) { - const themes = [chrome.i18n.getMessage('defaultTheme')]; - entries - .filter(entry => entry.isFile) - .sort((a, b) => (a.name < b.name ? -1 : 1)) - .forEach(function(entry) { - themes.push(entry.name.replace(/\.css$/, '')); - }); - if (callback) { - callback(themes); - } - }); - }); - }); -} - - -function sessionStorageHash(name) { - return { - name, - value: runTryCatch(JSON.parse, sessionStorage[name]) || {}, - set(k, v) { - this.value[k] = v; - this.updateStorage(); - }, - unset(k) { - delete this.value[k]; - this.updateStorage(); - }, - updateStorage() { - sessionStorage[this.name] = JSON.stringify(this.value); - } - }; -} - - -function deepCopy(obj) { - if (!obj || typeof obj != 'object') { - return obj; - } else { - const emptyCopy = Object.create(Object.getPrototypeOf(obj)); - return deepMerge(emptyCopy, obj); - } -} - - -function deepMerge(target, ...args) { - for (const obj of args) { - for (const k in obj) { - const value = obj[k]; - if (!value || typeof value != 'object') { - target[k] = value; - } else if (k in target) { - deepMerge(target[k], value); - } else if (typeof value.slice == 'function') { - target[k] = value.slice(); - } else { - target[k] = deepCopy(value); - } - } - } - return target; -} - - -function equal(a, b) { - if (!a || !b || typeof a != 'object' || typeof b != 'object') { - return a === b; - } - if (Object.keys(a).length != Object.keys(b).length) { - return false; - } - for (const k in a) { - if (a[k] !== b[k]) { - return false; - } - } - return true; -} - - -function defineReadonlyProperty(obj, key, value) { - const copy = deepCopy(value); - if (typeof copy == 'object') { - Object.freeze(copy); - } - Object.defineProperty(obj, key, {value: copy, configurable: true}); -} - - -// Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494 -function getSync() { - if ('sync' in chrome.storage) { - return chrome.storage.sync; - } - const crappyStorage = {}; - return { - get(key, callback) { - callback(crappyStorage[key] || {}); - }, - set(source, callback) { - for (const property in source) { - if (source.hasOwnProperty(property)) { - crappyStorage[property] = source[property]; - } - } - callback(); - } - }; -} - - function styleSectionsEqual(styleA, styleB) { if (!styleA.sections || !styleB.sections) { return undefined; @@ -990,3 +421,136 @@ function compileStyleRegExps({style, compileAll}) { } } } + + +function invalidateCache(andNotify, {added, updated, deletedId} = {}) { + // prevent double-add on echoed invalidation + const cached = added && cachedStyles.byId.get(added.id); + if (cached) { + return; + } + if (andNotify) { + chrome.runtime.sendMessage({method: 'invalidateCache', added, updated, deletedId}); + } + if (!cachedStyles.list) { + return; + } + if (updated) { + const cached = cachedStyles.byId.get(updated.id); + if (cached) { + Object.assign(cached, updated); + //console.debug('cache: updated', updated); + } + cachedStyles.filters.clear(); + return; + } + if (added) { + cachedStyles.list.push(added); + cachedStyles.byId.set(added.id, added); + //console.debug('cache: added', added); + cachedStyles.filters.clear(); + return; + } + if (deletedId != undefined) { + const deletedStyle = (cachedStyles.byId.get(deletedId) || {}).style; + if (deletedStyle) { + const cachedIndex = cachedStyles.list.indexOf(deletedStyle); + cachedStyles.list.splice(cachedIndex, 1); + cachedStyles.byId.delete(deletedId); + //console.debug('cache: deleted', deletedStyle); + cachedStyles.filters.clear(); + return; + } + } + cachedStyles.list = null; + //console.debug('cache cleared'); + cachedStyles.filters.clear(); +} + + +function cleanupCachedFilters({force = false} = {}) { + if (!force) { + // sliding timer for 1 second + clearTimeout(cleanupCachedFilters.timeout); + cleanupCachedFilters.timeout = setTimeout(cleanupCachedFilters, 1000, {force: true}); + return; + } + const size = cachedStyles.filters.size; + const oldestHit = cachedStyles.filters.values().next().value.lastHit; + const now = Date.now(); + const timeSpan = now - oldestHit; + const recencyWeight = 5 / size; + const hitWeight = 1 / 4; // we make ~4 hits per URL + const lastHitWeight = 10; + // delete the oldest 10% + [...cachedStyles.filters.entries()] + .map(([id, v], index) => ({ + id, + weight: + index * recencyWeight + + v.hits * hitWeight + + (v.lastHit - oldestHit) / timeSpan * lastHitWeight, + })) + .sort((a, b) => a.weight - b.weight) + .slice(0, size / 10 + 1) + .forEach(({id}) => cachedStyles.filters.delete(id)); + cleanupCachedFilters.timeout = 0; +} + + +function addMissingStyleTargets(style) { + style.sections = (style.sections || []).map(section => + Object.assign({ + urls: [], + urlPrefixes: [], + domains: [], + regexps: [], + }, section) + ); +} + + +function reportError(...args) { + for (const arg of args) { + if ('message' in arg) { + console.log(arg.message); + } + } +} + + +function fixBoolean(b) { + if (typeof b != 'undefined') { + return b != 'false'; + } + return null; +} + + +function getDomains(url) { + if (url.indexOf('file:') == 0) { + return []; + } + let d = /.*?:\/*([^/:]+)/.exec(url)[1]; + const domains = [d]; + while (d.indexOf('.') != -1) { + d = d.substring(d.indexOf('.') + 1); + domains.push(d); + } + return domains; +} + + +function getType(o) { + if (typeof o == 'undefined' || typeof o == 'string') { + return typeof o; + } + // with the persistent cachedStyles the Array reference is usually different + // so let's check for e.g. type of 'every' which is only present on arrays + // (in the context of our extension) + if (o instanceof Array || typeof o.every == 'function') { + return 'array'; + } + console.warn('Unsupported type:', o); + return 'undefined'; +} diff --git a/update.js b/update.js index bc85d283..3ff46ebf 100644 --- a/update.js +++ b/update.js @@ -1,4 +1,5 @@ -/* globals getStyles */ +/* eslint brace-style: 1, arrow-parens: 1, space-before-function-paren: 1, arrow-body-style: 1 */ +/* globals getStyles, saveStyle */ 'use strict'; // TODO: refactor to make usable in manage::Updater From 97c5972348d3eb03c6fc23f0b801c35692281c30 Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 11 Apr 2017 14:22:00 +0300 Subject: [PATCH 118/235] prefs: keep up-to-date using prefChanged event --- popup.js | 5 +++-- prefs.js | 22 +++++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/popup.js b/popup.js index aed550b7..f0e47287 100644 --- a/popup.js +++ b/popup.js @@ -59,8 +59,9 @@ function initPopup(url) { } // action buttons - $('#disableAll').onchange = () => - installed.classList.toggle('disabled', prefs.get('disableAll')); + $('#disableAll').onchange = function() { + installed.classList.toggle('disabled', this.checked); + }; setupLivePrefs(['disableAll']); $('#find-styles-link').onclick = handleEvent.openURLandHide; diff --git a/prefs.js b/prefs.js index a04e1bee..1d80488a 100644 --- a/prefs.js +++ b/prefs.js @@ -87,12 +87,16 @@ var prefs = new function Prefs() { const oldValue = deepCopy(values[key]); values[key] = value; defineReadonlyProperty(this.readOnlyValues, key, value); - if (!noBroadcast && !equal(value, oldValue)) { - this.broadcast(key, value, {noSync}); + if (BG && BG != window) { + BG.prefs.set(key, BG.deepCopy(value), {noBroadcast, noSync}); + } else { + localStorage[key] = typeof defaults[key] == 'object' + ? JSON.stringify(value) + : value; + if (!noBroadcast && !equal(value, oldValue)) { + this.broadcast(key, value, {noSync}); + } } - localStorage[key] = typeof defaults[key] == 'object' - ? JSON.stringify(value) - : value; }, remove: key => this.set(key, undefined), @@ -170,6 +174,14 @@ var prefs = new function Prefs() { } }); + chrome.runtime.onMessage.addListener(msg => { + if (msg.prefs) { + for (const id in msg.prefs) { + this.set(id, msg.prefs[id], {noBroadcast: true, noSync: true}); + } + } + }); + function doBroadcast() { const affects = {all: 'disableAll' in broadcastPrefs}; if (!affects.all) { From 8c539dabd6c6975deab2f8d8075d5744960106c4 Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 11 Apr 2017 16:12:18 +0300 Subject: [PATCH 119/235] prefs.set: enforce value type based on defaults --- prefs.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/prefs.js b/prefs.js index 1d80488a..854c06d6 100644 --- a/prefs.js +++ b/prefs.js @@ -85,6 +85,19 @@ var prefs = new function Prefs() { set(key, value, {noBroadcast, noSync} = {}) { const oldValue = deepCopy(values[key]); + switch (typeof defaults[key]) { + case typeof value: + break; + case 'string': + value = String(value); + break; + case 'number': + value |= 0; + break; + case 'boolean': + value = value === true || value === 'true'; + break; + } values[key] = value; defineReadonlyProperty(this.readOnlyValues, key, value); if (BG && BG != window) { From 2086f10af455ea5216c959a5e690a081a8dafb96 Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 11 Apr 2017 16:12:40 +0300 Subject: [PATCH 120/235] prefs.set: deep compare --- prefs.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/prefs.js b/prefs.js index 854c06d6..a47e9b6c 100644 --- a/prefs.js +++ b/prefs.js @@ -84,7 +84,7 @@ var prefs = new function Prefs() { }, set(key, value, {noBroadcast, noSync} = {}) { - const oldValue = deepCopy(values[key]); + const oldValue = values[key]; switch (typeof defaults[key]) { case typeof value: break; @@ -250,7 +250,11 @@ var prefs = new function Prefs() { return false; } for (const k in a) { - if (a[k] !== b[k]) { + if (typeof a[k] == 'object') { + if (!equal(a[k], b[k])) { + return false; + } + } else if (a[k] !== b[k]) { return false; } } From 279149b8b80a4333a2848c259fbb6269be4d2660 Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 11 Apr 2017 16:13:56 +0300 Subject: [PATCH 121/235] refactor deepCopy & deepMerge --- messaging.js | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/messaging.js b/messaging.js index 3975b777..d53b89f5 100644 --- a/messaging.js +++ b/messaging.js @@ -186,27 +186,24 @@ function debounce(fn, delay, ...args) { function deepCopy(obj) { - if (!obj || typeof obj != 'object') { - return obj; - } else { - const emptyCopy = Object.create(Object.getPrototypeOf(obj)); - return deepMerge(emptyCopy, obj); - } + return obj !== null && obj !== undefined && typeof obj == 'object' + ? deepMerge(typeof obj.slice == 'function' ? [] : {}, obj) + : obj; } function deepMerge(target, ...args) { + const isArray = typeof target.slice == 'function'; for (const obj of args) { + if (isArray && obj !== null && obj !== undefined) { + for (const element of obj) { + target.push(deepCopy(element)); + } + continue; + } for (const k in obj) { const value = obj[k]; - if (!value || typeof value != 'object') { - target[k] = value; - } else if (typeof value.slice == 'function') { - const arrayCopy = target[k] = target[k] || []; - for (const element of value) { - arrayCopy.push(deepCopy(element)); - } - } else if (k in target) { + if (k in target && typeof value == 'object' && value !== null) { deepMerge(target[k], value); } else { target[k] = deepCopy(value); From 2468784eb34a7b9f7c5960f8c044c94a72157f3c Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 12 Apr 2017 06:14:59 +0300 Subject: [PATCH 122/235] csslint: fix crashing on unclosed calc() at eof --- csslint/WARNING.txt | 12 ++++++++++++ csslint/csslint-worker.js | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 csslint/WARNING.txt diff --git a/csslint/WARNING.txt b/csslint/WARNING.txt new file mode 100644 index 00000000..bf059aff --- /dev/null +++ b/csslint/WARNING.txt @@ -0,0 +1,12 @@ +1. Until https://github.com/CSSLint/parser-lib/issues/229 is fixed, manually replace: + + while (lt !== Tokens.COMMA && lt !== Tokens.S && lt !== Tokens.RPAREN) { + + in "_function: function()" with + + while (lt !== Tokens.COMMA && lt !== Tokens.S && lt !== Tokens.RPAREN && lt !== Tokens.EOF) { + +2. Apply our hacks unless supported natively: + + * Support :any(), :-webkit-any(), :-moz-any() + * Support @supports inside @-moz-document diff --git a/csslint/csslint-worker.js b/csslint/csslint-worker.js index e55704cc..bb6be9df 100644 --- a/csslint/csslint-worker.js +++ b/csslint/csslint-worker.js @@ -2781,7 +2781,7 @@ Parser.prototype = function() { //functionText += this._term(); lt = tokenStream.peek(); - while (lt !== Tokens.COMMA && lt !== Tokens.S && lt !== Tokens.RPAREN) { + while (lt !== Tokens.COMMA && lt !== Tokens.S && lt !== Tokens.RPAREN && lt !== Tokens.EOF) { tokenStream.get(); functionText += tokenStream.token().value; lt = tokenStream.peek(); From 7ec41bcea150b2e42e68affc834ab7977e1a23de Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 12 Apr 2017 09:15:57 +0300 Subject: [PATCH 123/235] apply: refactor observers --- apply.js | 275 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 156 insertions(+), 119 deletions(-) diff --git a/apply.js b/apply.js index 73a4e3a9..91d3d240 100644 --- a/apply.js +++ b/apply.js @@ -8,10 +8,7 @@ var disableAll = false; var styleElements = new Map(); var disabledElements = new Map(); var retiredStyleIds = []; -var iframeObserver; -var docRewriteObserver; -initIFrameObserver(); requestStyles(); chrome.runtime.onMessage.addListener(applyOnMessage); @@ -71,7 +68,7 @@ function applyOnMessage(request, sender, sendResponse) { case 'styleAdded': if (request.style.enabled) { - requestStyles({id: request.style.id}, applyStyles); + requestStyles({id: request.style.id}); } break; @@ -96,35 +93,29 @@ function applyOnMessage(request, sender, sendResponse) { } -function doDisableAll(disable) { - if (!disable === !disableAll) { +function doDisableAll(disable, doc = document) { + if (doc == document && !disable === !disableAll) { return; } disableAll = disable; - if (disableAll) { - iframeObserver.disconnect(); + if (disable && doc.iframeObserver) { + doc.iframeObserver.stop(); } - - disableSheets(disableAll, document); - - if (!disableAll && document.readyState != 'loading') { - iframeObserver.start(); - } - - function disableSheets(disable, doc) { - Array.prototype.forEach.call(doc.styleSheets, stylesheet => { - if (stylesheet.ownerNode.classList.contains('stylus') - && stylesheet.disabled != disable) { - stylesheet.disabled = disable; - } - }); - for (const iframe of getDynamicIFrames(doc)) { - if (!disable) { - // update the IFRAME if it was created while the observer was disconnected - addDocumentStylesToIFrame(iframe); - } - disableSheets(disable, iframe.contentDocument); + Array.prototype.forEach.call(doc.styleSheets, stylesheet => { + if (stylesheet.ownerNode.matches('stylus[id^="stylus-"]') + && stylesheet.disabled != disable) { + stylesheet.disabled = disable; } + }); + for (const iframe of getDynamicIFrames(doc)) { + if (!disable) { + // update the IFRAME if it was created while the observer was disconnected + addDocumentStylesToIFrame(iframe); + } + doDisableAll(disable, iframe.contentDocument); + } + if (!disable && doc.readyState != 'loading' && doc.iframeObserver) { + doc.iframeObserver.start(); } } @@ -141,15 +132,23 @@ function applyStyleState(id, enabled, doc) { } if (enabled && inCache) { const el = inCache.cloneNode(true); - document.documentElement.appendChild(el); + doc.documentElement.appendChild(el); el.sheet.disabled = disableAll; processDynamicIFrames(doc, applyStyleState, id, enabled); disabledElements.delete(id); return; } if (!enabled && inDoc) { - disabledElements.set(id, inDoc); + if (!inCache) { + disabledElements.set(id, inDoc); + } inDoc.remove(); + if (doc.location.href == 'about:srcdoc') { + const original = doc.getElementById('stylus-' + id); + if (original) { + original.remove(); + } + } processDynamicIFrames(doc, applyStyleState, id, enabled); return; } @@ -162,7 +161,7 @@ function removeStyle(id, doc) { styleElements.delete('stylus-' + id); disabledElements.delete(id); if (!styleElements.size) { - iframeObserver.disconnect(); + doc.iframeObserver.disconnect(); } } processDynamicIFrames(doc, removeStyle, id); @@ -213,12 +212,7 @@ function applyStyles(styleHash) { document.head.appendChild(document.getElementById(id)); } } - if (document.readyState != 'loading') { - onDOMContentLoaded(); - } else { - document.addEventListener('DOMContentLoaded', onDOMContentLoaded); - } - initDocRewriteObserver(); + initObservers(); } if (retiredStyleIds.length) { @@ -231,12 +225,6 @@ function applyStyles(styleHash) { } -function onDOMContentLoaded() { - addDocumentStylesToAllIFrames(); - iframeObserver.start(); -} - - function applySections(styleId, sections) { let el = document.getElementById('stylus-' + styleId); // Already there. @@ -288,18 +276,18 @@ function addDocumentStylesToIFrame(iframe) { addStyleElement(el, doc); } } - initDocRewriteObserver(doc); + initObservers(doc); } -function addDocumentStylesToAllIFrames() { - getDynamicIFrames(document).forEach(addDocumentStylesToIFrame); +function addDocumentStylesToAllIFrames(doc = document) { + getDynamicIFrames(doc).forEach(addDocumentStylesToIFrame); } // Only dynamic iframes get the parent document's styles. Other ones should get styles based on their own URLs. function getDynamicIFrames(doc) { - return [...doc.getElementsByTagName('iframe')].filter(iframeIsDynamic); + return Array.prototype.filter.call(doc.getElementsByTagName('iframe'), iframeIsDynamic); } @@ -319,7 +307,9 @@ function iframeIsDynamic(f) { function processDynamicIFrames(doc, fn, ...args) { - for (const iframe of [...doc.getElementsByTagName('iframe')]) { + var iframes = doc.getElementsByTagName('iframe'); + for (var i = 0, il = iframes.length; i < il; i++) { + var iframe = iframes[i]; if (iframeIsDynamic(iframe)) { fn(...args, iframe.contentDocument); } @@ -344,8 +334,8 @@ function addStyleToIFrameSrcDoc(iframe, el) { function replaceAll(newStyles, doc) { - const oldStyles = [...doc.querySelectorAll('STYLE.stylus')]; - oldStyles.forEach(style => (style.id += '-ghost')); + Array.prototype.forEach.call(doc.querySelectorAll('STYLE.stylus[id^="stylus-"]'), + e => (e.id += '-ghost')); processDynamicIFrames(doc, replaceAll, newStyles); if (doc == document) { styleElements.clear(); @@ -357,80 +347,123 @@ function replaceAll(newStyles, doc) { function replaceAllpass2(newStyles, doc) { - const oldStyles = [...doc.querySelectorAll('STYLE.stylus[id$="-ghost"]')]; + const oldStyles = doc.querySelectorAll('STYLE.stylus[id$="-ghost"]'); processDynamicIFrames(doc, replaceAllpass2, newStyles); - oldStyles.forEach(e => e.remove()); + Array.prototype.forEach.call(oldStyles, + e => e.remove()); } -function initIFrameObserver() { - iframeObserver = Object.assign(new MutationObserver(observer), { - start() { - this.observe(document, {childList: true, subtree: true}); - } - }); - const iframesCollection = document.getElementsByTagName('iframe'); - - function observer(mutations) { - // autoupdated HTMLCollection is superfast - if (!iframesCollection[0]) { - return; - } - // use a much faster method for very complex pages with lots of mutations - // (observer usually receives 1k-10k mutations per call) - if (mutations.length > 1000) { - addDocumentStylesToAllIFrames(); - return; - } - // move the check out of current execution context - // because some same-domain (!) iframes fail to load when their 'contentDocument' is accessed (!) - // namely gmail's old chat iframe talkgadget.google.com - setTimeout(process, 0, mutations); - } - - function process(mutations) { - for (var m = 0, mutation; (mutation = mutations[m++]);) { - var added = mutation.addedNodes; - for (var n = 0, node; (node = added[n++]);) { - // process only ELEMENT_NODE - if (node.nodeType != 1) { - continue; - } - var iframes = node.localName === 'iframe' ? [node] : - node.children.length && node.getElementsByTagName('iframe'); - for (var i = 0, iframe; (iframe = iframes[i++]);) { - if (iframeIsDynamic(iframe)) { - addDocumentStylesToIFrame(iframe); - } - } - } - } +function onDOMContentLoaded({target = document} = {}) { + addDocumentStylesToAllIFrames(target); + if (target.iframeObserver) { + target.iframeObserver.start(); } } -function initDocRewriteObserver() { - if (isOwnPage) { +function initObservers(doc = document) { + if (isOwnPage || doc.rewriteObserver) { return; } - // re-add styles if we detect documentElement being recreated - docRewriteObserver = new MutationObserver(observer); - docRewriteObserver.observe(document, {childList: true}); + initIFrameObserver(doc); + initDocRewriteObserver(doc); + if (doc.readyState != 'loading') { + onDOMContentLoaded({target: doc}); + } else { + doc.addEventListener('DOMContentLoaded', onDOMContentLoaded); + } +} - function observer(mutations) { - for (const mutation of mutations) { - for (const node of mutation.addedNodes) { - if (node.localName != 'html') { - continue; - } - for (const [id, el] of styleElements.entries()) { - if (!document.getElementById(id)) { - document.documentElement.appendChild(el); + +function initIFrameObserver(doc = document) { + if (!initIFrameObserver.methods) { + initIFrameObserver.methods = { + start() { + this.observe(this.doc, {childList: true, subtree: true}); + }, + stop() { + this.disconnect(); + getDynamicIFrames(this.doc).forEach(iframe => { + const observer = iframe.contentDocument.iframeObserver; + if (observer) { + observer.stop(); } - } - document.addEventListener('DOMContentLoaded', onDOMContentLoaded); - return; + }); + }, + }; + } + doc.iframeObserver = Object.assign( + new MutationObserver(iframeObserver), + initIFrameObserver.methods, { + iframes: doc.getElementsByTagName('iframe'), + doc, + }); +} + + +function iframeObserver(mutations, observer) { + // autoupdated HTMLCollection is superfast + if (!observer.iframes[0]) { + return; + } + // use a much faster method for very complex pages with lots of mutations + // (observer usually receives 1k-10k mutations per call) + if (mutations.length > 1000) { + addDocumentStylesToAllIFrames(observer.doc); + return; + } + for (var m = 0, ml = mutations.length; m < ml; m++) { + var added = mutations[m].addedNodes; + for (var n = 0, nl = added.length; n < nl; n++) { + var node = added[n]; + // process only ELEMENT_NODE + if (node.nodeType != 1) { + continue; } + var iframes = node.localName === 'iframe' ? [node] : + node.children.length && node.getElementsByTagName('iframe'); + if (iframes.length) { + // move the check out of current execution context + // because some same-domain (!) iframes fail to load when their 'contentDocument' is accessed (!) + // namely gmail's old chat iframe talkgadget.google.com + setTimeout(testIFrames, 0, iframes); + } + } + } +} + + +function testIFrames(iframes) { + for (const iframe of iframes) { + if (iframeIsDynamic(iframe)) { + addDocumentStylesToIFrame(iframe); + } + } +} + + +function initDocRewriteObserver(doc = document) { + // re-add styles if we detect documentElement being recreated + doc.rewriteObserver = new MutationObserver(docRewriteObserver); + doc.rewriteObserver.observe(doc, {childList: true}); +} + + +function docRewriteObserver(mutations) { + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (node.localName != 'html') { + continue; + } + const doc = node.ownerDocument; + for (const [id, el] of styleElements.entries()) { + if (!doc.getElementById(id)) { + doc.documentElement.appendChild(el); + } + } + initObservers(doc); + return; } } } @@ -447,15 +480,19 @@ function orphanCheck() { // we're orphaned due to an extension update // we can detach the mutation observer - iframeObserver.takeRecords(); - iframeObserver.disconnect(); - iframeObserver = null; - if (docRewriteObserver) { - docRewriteObserver.disconnect(); - docRewriteObserver = null; - } // we can detach event listeners - document.removeEventListener('DOMContentLoaded', onDOMContentLoaded); + (function unbind(doc) { + if (doc.iframeObserver) { + doc.iframeObserver.disconnect(); + delete doc.iframeObserver; + } + if (doc.rewriteObserver) { + doc.rewriteObserver.disconnect(); + delete doc.rewriteObserver; + } + doc.removeEventListener('DOMContentLoaded', onDOMContentLoaded); + getDynamicIFrames(doc).forEach(iframe => unbind(iframe.contentDocument)); + })(document); window.removeEventListener(chrome.runtime.id, orphanCheck, true); // we can't detach chrome.runtime.onMessage because it's no longer connected internally // we can destroy our globals in this context to free up memory From a93354de8c88847f592659a7609659cbc6c0b30b Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 12 Apr 2017 12:54:55 +0300 Subject: [PATCH 124/235] remove iframe observer * documentElement may be overwritten right after iframe was initialized with contentDocument.write() and due to this change being external it's not reported in our existing rewrite observer so we enqueue an additional check using setTimeout(0). * match_about_blank in manifest.json is back * iframes with src = about: or javascript: don't have a proper URL when our content script runs so we get the real URL from the parent window * minor refactoring --- apply.js | 464 ++++++++++++++------------------------------------ background.js | 7 +- manifest.json | 1 + 3 files changed, 130 insertions(+), 342 deletions(-) diff --git a/apply.js b/apply.js index 91d3d240..5737ba78 100644 --- a/apply.js +++ b/apply.js @@ -3,11 +3,14 @@ /* eslint no-var: 0 */ 'use strict'; +var ID_PREFIX = 'stylus-'; +var ROOT = document.documentElement; var isOwnPage = location.href.startsWith('chrome-extension:'); var disableAll = false; var styleElements = new Map(); var disabledElements = new Map(); -var retiredStyleIds = []; +var retiredStyleTimers = new Map(); +var docRewriteObserver; requestStyles(); chrome.runtime.onMessage.addListener(applyOnMessage); @@ -18,16 +21,24 @@ if (!isOwnPage) { } function requestStyles(options) { + var matchUrl = location.href; + try { + // 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 + if (!matchUrl.match(/^(http|file|chrome|ftp)/) && window != parent) { + matchUrl = parent.location.href; + } + } catch (e) {} + const request = Object.assign({ + method: 'getStyles', + matchUrl, + enabled: true, + asHash: true, + }, options); // If this is a Stylish page (Edit Style or Manage Styles), // we'll request the styles directly to minimize delay and flicker, // unless Chrome is still starting up and the background page isn't fully loaded. // (Note: in this case the function may be invoked again from applyStyles.) - const request = Object.assign({ - method: 'getStyles', - matchUrl: location.href, - enabled: true, - asHash: true, - }, options); if (typeof getStylesSafe !== 'undefined') { getStylesSafe(request).then(applyStyles); } else { @@ -51,19 +62,19 @@ function applyOnMessage(request, sender, sendResponse) { switch (request.method) { case 'styleDeleted': - removeStyle(request.id, document); + removeStyle(request); break; case 'styleUpdated': if (request.codeIsUpdated === false) { - applyStyleState(request.style.id, request.style.enabled, document); + applyStyleState(request.style); break; } if (!request.style.enabled) { - removeStyle(request.style.id, document); + removeStyle(request.style); break; } - retireStyle(request.style.id); + removeStyle({id: request.style.id, retire: true}); // fallthrough to 'styleAdded' case 'styleAdded': @@ -77,7 +88,7 @@ function applyOnMessage(request, sender, sendResponse) { break; case 'styleReplaceAll': - replaceAll(request.styles, document); + replaceAll(request.styles); break; case 'prefChanged': @@ -93,36 +104,23 @@ function applyOnMessage(request, sender, sendResponse) { } -function doDisableAll(disable, doc = document) { - if (doc == document && !disable === !disableAll) { +function doDisableAll(disable) { + if (!disable === !disableAll) { return; } disableAll = disable; - if (disable && doc.iframeObserver) { - doc.iframeObserver.stop(); - } - Array.prototype.forEach.call(doc.styleSheets, stylesheet => { - if (stylesheet.ownerNode.matches('stylus[id^="stylus-"]') + Array.prototype.forEach.call(document.styleSheets, stylesheet => { + if (stylesheet.ownerNode.matches(`STYLE.stylus[id^="${ID_PREFIX}"]`) && stylesheet.disabled != disable) { stylesheet.disabled = disable; } }); - for (const iframe of getDynamicIFrames(doc)) { - if (!disable) { - // update the IFRAME if it was created while the observer was disconnected - addDocumentStylesToIFrame(iframe); - } - doDisableAll(disable, iframe.contentDocument); - } - if (!disable && doc.readyState != 'loading' && doc.iframeObserver) { - doc.iframeObserver.start(); - } } -function applyStyleState(id, enabled, doc) { - const inCache = disabledElements.get(id); - const inDoc = doc.getElementById('stylus-' + id); +function applyStyleState({id, enabled}) { + const inCache = disabledElements.get(id) || styleElements.get(id); + const inDoc = document.getElementById(ID_PREFIX + id); if (enabled && inDoc || !enabled && !inDoc) { return; } @@ -131,103 +129,80 @@ function applyStyleState(id, enabled, doc) { return; } if (enabled && inCache) { - const el = inCache.cloneNode(true); - doc.documentElement.appendChild(el); - el.sheet.disabled = disableAll; - processDynamicIFrames(doc, applyStyleState, id, enabled); + addStyleElement(inCache); disabledElements.delete(id); return; } if (!enabled && inDoc) { - if (!inCache) { - disabledElements.set(id, inDoc); - } + disabledElements.set(id, inDoc); inDoc.remove(); - if (doc.location.href == 'about:srcdoc') { - const original = doc.getElementById('stylus-' + id); + if (document.location.href == 'about:srcdoc') { + const original = document.getElementById(ID_PREFIX + id); if (original) { original.remove(); } } - processDynamicIFrames(doc, applyStyleState, id, enabled); return; } } -function removeStyle(id, doc) { - [doc.getElementById('stylus-' + id)].forEach(e => e && e.remove()); - if (doc == document) { - styleElements.delete('stylus-' + id); - disabledElements.delete(id); - if (!styleElements.size) { - doc.iframeObserver.disconnect(); +function removeStyle({id, retire = false}) { + const el = document.getElementById(ID_PREFIX + id); + if (el) { + if (retire) { + // to avoid page flicker when the style is updated + // instead of removing it immediately we rename its ID and queue it + // to be deleted in applyStyles after a new version is fetched and applied + const deadID = 'ghost-' + id; + el.id = ID_PREFIX + deadID; + // in case something went wrong and new style was never applied + retiredStyleTimers.set(deadID, setTimeout(removeStyle, 1000, {id: deadID})); + } else { + el.remove(); } } - processDynamicIFrames(doc, removeStyle, id); + styleElements.delete(ID_PREFIX + id); + disabledElements.delete(id); + retiredStyleTimers.delete(id); } -// to avoid page flicker when the style is updated -// instead of removing it immediately we rename its ID and queue it -// to be deleted in applyStyles after a new version is fetched and applied -function retireStyle(id, doc) { - const deadID = 'ghost-' + id; - if (!doc) { - doc = document; - retiredStyleIds.push(deadID); - styleElements.delete('stylus-' + id); - disabledElements.delete(id); - // in case something went wrong and new style was never applied - setTimeout(removeStyle, 1000, deadID, doc); - } - const el = doc.getElementById('stylus-' + id); - if (el) { - el.id = 'stylus-' + deadID; - } - processDynamicIFrames(doc, retireStyle, id); -} - - -function applyStyles(styleHash) { - if (!styleHash) { // Chrome is starting up +function applyStyles(styles) { + if (!styles) { + // Chrome is starting up requestStyles(); return; } - if ('disableAll' in styleHash) { - doDisableAll(styleHash.disableAll); - delete styleHash.disableAll; + if ('disableAll' in styles) { + doDisableAll(styles.disableAll); + delete styles.disableAll; } - - for (const styleId in styleHash) { - applySections(styleId, styleHash[styleId]); - } - - if (styleElements.size) { + if (document.head + && document.head.firstChild + && document.head.firstChild.id == 'xml-viewer-style') { // when site response is application/xml Chrome displays our style elements // under document.documentElement as plain text so we need to move them into HEAD // which is already autogenerated at this moment - if (document.head && document.head.firstChild && document.head.firstChild.id == 'xml-viewer-style') { - for (const id of styleElements.keys()) { - document.head.appendChild(document.getElementById(id)); - } - } - initObservers(); + ROOT = document.head; } - - if (retiredStyleIds.length) { - setTimeout(function() { - while (retiredStyleIds.length) { - removeStyle(retiredStyleIds.shift(), document); + for (const id in styles) { + applySections(id, styles[id]); + } + initDocRewriteObserver(); + if (retiredStyleTimers.size) { + setTimeout(() => { + for (const [id, timer] of retiredStyleTimers.entries()) { + removeStyle({id}); + clearTimeout(timer); } - }, 0); + }); } } function applySections(styleId, sections) { - let el = document.getElementById('stylus-' + styleId); - // Already there. + let el = document.getElementById(ID_PREFIX + styleId); if (el) { return; } @@ -240,232 +215,63 @@ function applySections(styleId, sections) { // This will make an HTML style element. If there's SVG embedded in an HTML document, this works on the SVG too. el = document.createElement('style'); } - el.setAttribute('id', 'stylus-' + styleId); - el.setAttribute('class', 'stylus'); - el.setAttribute('type', 'text/css'); - el.appendChild(document.createTextNode(sections.map(section => section.code).join('\n'))); - addStyleElement(el, document); + Object.assign(el, { + id: ID_PREFIX + styleId, + className: 'stylus', + type: 'text/css', + textContent: sections.map(section => section.code).join('\n'), + }); + addStyleElement(el); styleElements.set(el.id, el); disabledElements.delete(styleId); } -function addStyleElement(el, doc) { - if (!doc.documentElement || doc.getElementById(el.id)) { +function addStyleElement(el) { + if (ROOT && !document.getElementById(el.id)) { + ROOT.appendChild(el); + el.disabled = disableAll; + } +} + + +function replaceAll(newStyles) { + const oldStyles = Array.prototype.slice.call( + document.querySelectorAll(`STYLE.stylus[id^="${ID_PREFIX}"]`)); + oldStyles.forEach(el => (el.id += '-ghost')); + styleElements.clear(); + disabledElements.clear(); + retiredStyleTimers.clear(); + applyStyles(newStyles); + oldStyles.forEach(el => el.remove()); +} + + +function initDocRewriteObserver() { + if (isOwnPage || docRewriteObserver || !styleElements.size) { return; } - doc.documentElement.appendChild(doc.importNode(el, true)) - .disabled = disableAll; - for (const iframe of getDynamicIFrames(doc)) { - if (iframeIsLoadingSrcDoc(iframe)) { - addStyleToIFrameSrcDoc(iframe, el); - } else { - addStyleElement(el, iframe.contentDocument); - } - } -} - - -function addDocumentStylesToIFrame(iframe) { - const doc = iframe.contentDocument; - const srcDocIsLoading = iframeIsLoadingSrcDoc(iframe); - for (const el of styleElements.values()) { - if (srcDocIsLoading) { - addStyleToIFrameSrcDoc(iframe, el); - } else { - addStyleElement(el, doc); - } - } - initObservers(doc); -} - - -function addDocumentStylesToAllIFrames(doc = document) { - getDynamicIFrames(doc).forEach(addDocumentStylesToIFrame); -} - - -// Only dynamic iframes get the parent document's styles. Other ones should get styles based on their own URLs. -function getDynamicIFrames(doc) { - return Array.prototype.filter.call(doc.getElementsByTagName('iframe'), iframeIsDynamic); -} - - -function iframeIsDynamic(f) { - let href; - if (f.src && f.src.startsWith('http') && new URL(f.src).origin != location.origin) { - return false; - } - try { - href = f.contentDocument.location.href; - } catch (ex) { - // Cross-origin, so it's not a dynamic iframe - return false; - } - return href == document.location.href || href.startsWith('about:'); -} - - -function processDynamicIFrames(doc, fn, ...args) { - var iframes = doc.getElementsByTagName('iframe'); - for (var i = 0, il = iframes.length; i < il; i++) { - var iframe = iframes[i]; - if (iframeIsDynamic(iframe)) { - fn(...args, iframe.contentDocument); - } - } -} - - -function iframeIsLoadingSrcDoc(f) { - return f.srcdoc && f.contentDocument.all.length <= 3; - // 3 nodes or less in total (html, head, body) == new empty iframe about to be overwritten by its 'srcdoc' -} - - -function addStyleToIFrameSrcDoc(iframe, el) { - if (disableAll) { - return; - } - iframe.srcdoc += el.outerHTML; - // make sure the style is added in case srcdoc was malformed - setTimeout(addStyleElement, 100, el, iframe.contentDocument); -} - - -function replaceAll(newStyles, doc) { - Array.prototype.forEach.call(doc.querySelectorAll('STYLE.stylus[id^="stylus-"]'), - e => (e.id += '-ghost')); - processDynamicIFrames(doc, replaceAll, newStyles); - if (doc == document) { - styleElements.clear(); - disabledElements.clear(); - applyStyles(newStyles); - replaceAllpass2(newStyles, doc); - } -} - - -function replaceAllpass2(newStyles, doc) { - const oldStyles = doc.querySelectorAll('STYLE.stylus[id$="-ghost"]'); - processDynamicIFrames(doc, replaceAllpass2, newStyles); - Array.prototype.forEach.call(oldStyles, - e => e.remove()); -} - - -function onDOMContentLoaded({target = document} = {}) { - addDocumentStylesToAllIFrames(target); - if (target.iframeObserver) { - target.iframeObserver.start(); - } -} - - -function initObservers(doc = document) { - if (isOwnPage || doc.rewriteObserver) { - return; - } - initIFrameObserver(doc); - initDocRewriteObserver(doc); - if (doc.readyState != 'loading') { - onDOMContentLoaded({target: doc}); - } else { - doc.addEventListener('DOMContentLoaded', onDOMContentLoaded); - } -} - - -function initIFrameObserver(doc = document) { - if (!initIFrameObserver.methods) { - initIFrameObserver.methods = { - start() { - this.observe(this.doc, {childList: true, subtree: true}); - }, - stop() { - this.disconnect(); - getDynamicIFrames(this.doc).forEach(iframe => { - const observer = iframe.contentDocument.iframeObserver; - if (observer) { - observer.stop(); - } - }); - }, - }; - } - doc.iframeObserver = Object.assign( - new MutationObserver(iframeObserver), - initIFrameObserver.methods, { - iframes: doc.getElementsByTagName('iframe'), - doc, - }); -} - - -function iframeObserver(mutations, observer) { - // autoupdated HTMLCollection is superfast - if (!observer.iframes[0]) { - return; - } - // use a much faster method for very complex pages with lots of mutations - // (observer usually receives 1k-10k mutations per call) - if (mutations.length > 1000) { - addDocumentStylesToAllIFrames(observer.doc); - return; - } - for (var m = 0, ml = mutations.length; m < ml; m++) { - var added = mutations[m].addedNodes; - for (var n = 0, nl = added.length; n < nl; n++) { - var node = added[n]; - // process only ELEMENT_NODE - if (node.nodeType != 1) { - continue; - } - var iframes = node.localName === 'iframe' ? [node] : - node.children.length && node.getElementsByTagName('iframe'); - if (iframes.length) { - // move the check out of current execution context - // because some same-domain (!) iframes fail to load when their 'contentDocument' is accessed (!) - // namely gmail's old chat iframe talkgadget.google.com - setTimeout(testIFrames, 0, iframes); - } - } - } -} - - -function testIFrames(iframes) { - for (const iframe of iframes) { - if (iframeIsDynamic(iframe)) { - addDocumentStylesToIFrame(iframe); - } - } -} - - -function initDocRewriteObserver(doc = document) { // re-add styles if we detect documentElement being recreated - doc.rewriteObserver = new MutationObserver(docRewriteObserver); - doc.rewriteObserver.observe(doc, {childList: true}); -} - - -function docRewriteObserver(mutations) { - for (const mutation of mutations) { - for (const node of mutation.addedNodes) { - if (node.localName != 'html') { - continue; - } - const doc = node.ownerDocument; - for (const [id, el] of styleElements.entries()) { - if (!doc.getElementById(id)) { - doc.documentElement.appendChild(el); + const reinjectStyles = () => { + ROOT = document.documentElement; + for (const el of styleElements.values()) { + addStyleElement(document.importNode(el, true)); + } + }; + // detect documentElement being rewritten from inside the script + docRewriteObserver = new MutationObserver(mutations => { + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (node.localName == 'html') { + reinjectStyles(); + return; } } - initObservers(doc); - return; } - } + }); + docRewriteObserver.observe(document, {childList: true}); + // detect dynamic iframes rewritten after creation by the embedder i.e. externally + setTimeout(() => document.documentElement != ROOT && reinjectStyles()); } @@ -473,55 +279,35 @@ function orphanCheck() { const port = chrome.runtime.connect(); if (port) { port.disconnect(); - //console.debug('orphanCheck: still connected'); return; } - //console.debug('orphanCheck: disconnected'); // we're orphaned due to an extension update // we can detach the mutation observer + if (docRewriteObserver) { + docRewriteObserver.disconnect(); + } // we can detach event listeners - (function unbind(doc) { - if (doc.iframeObserver) { - doc.iframeObserver.disconnect(); - delete doc.iframeObserver; - } - if (doc.rewriteObserver) { - doc.rewriteObserver.disconnect(); - delete doc.rewriteObserver; - } - doc.removeEventListener('DOMContentLoaded', onDOMContentLoaded); - getDynamicIFrames(doc).forEach(iframe => unbind(iframe.contentDocument)); - })(document); window.removeEventListener(chrome.runtime.id, orphanCheck, true); // we can't detach chrome.runtime.onMessage because it's no longer connected internally // we can destroy our globals in this context to free up memory [ // functions - 'addDocumentStylesToAllIFrames', - 'addDocumentStylesToIFrame', 'addStyleElement', - 'addStyleToIFrameSrcDoc', 'applyOnMessage', 'applySections', 'applyStyles', + 'applyStyleState', 'doDisableAll', - 'getDynamicIFrames', - 'iframeIsDynamic', - 'iframeIsLoadingSrcDoc', 'initDocRewriteObserver', - 'initIFrameObserver', 'orphanCheck', - 'processDynamicIFrames', 'removeStyle', 'replaceAll', - 'replaceAllpass2', 'requestStyles', - 'retireStyle', - 'styleObserver', // variables - 'docRewriteObserver', - 'iframeObserver', - 'retiredStyleIds', + 'ROOT', + 'disabledElements', + 'retiredStyleTimers', 'styleElements', + 'docRewriteObserver', ].forEach(fn => (window[fn] = null)); } diff --git a/background.js b/background.js index 9b24a668..781818ff 100644 --- a/background.js +++ b/background.js @@ -208,18 +208,19 @@ function injectContentScripts() { m == '' ? m : wildcardAsRegExp(m) )); } - // also inject in chrome://newtab/ page - chrome.tabs.query({url: '*://*/*'}, tabs => { + chrome.tabs.query({}, tabs => { for (const tab of tabs) { for (const cs of contentScripts) { for (const m of cs.matches) { - if (m == '' || tab.url.match(m)) { + if ((m == '' || tab.url.match(m)) + && (!tab.url.startsWith('chrome') || tab.url == 'chrome://newtab/')) { chrome.tabs.sendMessage(tab.id, {method: 'ping'}, pong => { if (!pong) { chrome.tabs.executeScript(tab.id, { file: cs.js[0], runAt: cs.run_at, allFrames: cs.all_frames, + matchAboutBlank: cs.match_about_blank, }, ignoreChromeError); } }); diff --git a/manifest.json b/manifest.json index 0628b51a..eb283982 100644 --- a/manifest.json +++ b/manifest.json @@ -34,6 +34,7 @@ "matches": [""], "run_at": "document_start", "all_frames": true, + "match_about_blank": true, "js": ["apply.js"] }, { From 80538a17f5e2e13e956645c5baaeeebf7540990c Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 12 Apr 2017 12:57:40 +0300 Subject: [PATCH 125/235] Report unreachable content script in popup Chrome can't executeScript on file:// URLs even though we have in manifest.json so on such pages we'll display a warning in the popup. This should only happen when Stylus is [re]enabled/reloaded. --- _locales/en/messages.json | 8 ++++++++ popup.css | 20 ++++++++++++++++++++ popup.js | 8 ++++++++ 3 files changed, 36 insertions(+) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 9df5f5e6..f70b8092 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -510,6 +510,14 @@ "message": "Undo (global)", "description": "CSS-beautify global Undo button label" }, + "unreachableContentScript": { + "message": "Could not communicate with the page. Try reloading the tab.", + "description": "Note in the toolbar popup usually on file:// URLs after [re]loading Stylus" + }, + "unreachableFileHint": { + "message": "To allow Stylus access file:// URLs enable the checkbox on chrome://extensions page.", + "description": "Note in the toolbar popup for file:// URLs" + }, "updateCheckFailBadResponseCode": { "message": "Update failed - server responded with code $code$.", "description": "Text that displays when an update check failed because the response code indicates an error", diff --git a/popup.css b/popup.css index 8c15be9d..897bd92f 100644 --- a/popup.css +++ b/popup.css @@ -433,6 +433,26 @@ body.blocked #unavailable { text-align: right; } +.unreachable .entry { + opacity: .25; +} + +.unreachable:before { + content: "__MSG_unreachableContentScript__"; + padding: 5px 0.75em; + display: block; + font-weight: bold; +} + +.unreachable #installed:before { + content: "__MSG_unreachableFileHint__"; + padding: 1px 0.75em 9px; + display: block; + font-size: 90%; + border-bottom: 1px solid black; + margin-bottom: 5px; +} + @keyframes lights-off { from { background-color: transparent; diff --git a/popup.js b/popup.js index f0e47287..7c091869 100644 --- a/popup.js +++ b/popup.js @@ -92,6 +92,14 @@ function initPopup(url) { return; } + getActiveTab().then(tab => { + chrome.tabs.sendMessage(tab.id, {method: 'ping'}, {frameId: 0}, pong => { + if (pong === undefined) { + document.body.classList.add('unreachable'); + } + }); + }); + // Write new style links const writeStyle = $('#write-style'); const matchTargets = document.createElement('span'); From dad1d1fe5f97542111f5b9c9d7f808aa444cf3bb Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 12 Apr 2017 15:31:05 +0300 Subject: [PATCH 126/235] get rid of switch-fallthrough; reuse requestStyles --- apply.js | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/apply.js b/apply.js index 5737ba78..6dd2c67d 100644 --- a/apply.js +++ b/apply.js @@ -20,15 +20,17 @@ if (!isOwnPage) { window.addEventListener(chrome.runtime.id, orphanCheck, true); } -function requestStyles(options) { +function requestStyles(options, callback = applyStyles) { var matchUrl = location.href; - try { + if (!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 - if (!matchUrl.match(/^(http|file|chrome|ftp)/) && window != parent) { - matchUrl = parent.location.href; - } - } catch (e) {} + try { + if (window != parent) { + matchUrl = parent.location.href; + } + } catch (e) {} + } const request = Object.assign({ method: 'getStyles', matchUrl, @@ -40,23 +42,21 @@ function requestStyles(options) { // unless Chrome is still starting up and the background page isn't fully loaded. // (Note: in this case the function may be invoked again from applyStyles.) if (typeof getStylesSafe !== 'undefined') { - getStylesSafe(request).then(applyStyles); + getStylesSafe(request).then(callback); } else { - chrome.runtime.sendMessage(request, applyStyles); + chrome.runtime.sendMessage(request, callback); } } function applyOnMessage(request, sender, sendResponse) { - // Do-It-Yourself tells our built-in pages to fetch the styles directly - // which is faster because IPC messaging JSON-ifies everything internally if (request.styles == 'DIY') { - getStylesSafe({ - matchUrl: location.href, - enabled: true, - asHash: true, - }).then(styles => - applyOnMessage(Object.assign(request, {styles}))); + // Do-It-Yourself tells our built-in pages to fetch the styles directly + // which is faster because IPC messaging JSON-ifies everything internally + requestStyles({}, styles => { + request.styles = styles; + applyOnMessage(request); + }); return; } switch (request.method) { @@ -70,12 +70,13 @@ function applyOnMessage(request, sender, sendResponse) { applyStyleState(request.style); break; } - if (!request.style.enabled) { + if (request.style.enabled) { + removeStyle({id: request.style.id, retire: true}); + requestStyles({id: request.style.id}); + } else { removeStyle(request.style); - break; } - removeStyle({id: request.style.id, retire: true}); - // fallthrough to 'styleAdded' + break; case 'styleAdded': if (request.style.enabled) { From 1749057b919e21ea4d4a61a1ea0a91a186c17bd3 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 12 Apr 2017 16:56:41 +0300 Subject: [PATCH 127/235] Explainer for stylusUnavailableForURL message We don't mention the G+ iframe on CWS stylable only when "Out of process iframes" feature is enabled which can be set manually via chrome://flags/#enable-site-per-process. It's still in development and is known to break some sites, which is why it's not enabled by default. --- _locales/ar/messages.json | 4 - _locales/cs/messages.json | 4 +- _locales/de/messages.json | 4 +- _locales/el/messages.json | 4 +- _locales/en/messages.json | 10 +- _locales/es/messages.json | 4 +- _locales/fi/messages.json | 4 - _locales/fr/messages.json | 4 +- _locales/it/messages.json | 4 - _locales/ja/messages.json | 4 - _locales/nl/messages.json | 4 +- _locales/pt_BR/messages.json | 4 - _locales/ru/messages.json | 4 +- _locales/sr/messages.json | 849 +++++++++++++++++------------------ _locales/sv/messages.json | 4 +- _locales/sv_SE/messages.json | 4 +- _locales/te/messages.json | 4 - _locales/tr/messages.json | 4 - _locales/zh/messages.json | 4 - _locales/zh_CN/messages.json | 4 +- _locales/zh_TW/messages.json | 4 +- messaging.js | 8 +- popup.css | 47 +- popup.html | 6 - popup.js | 2 +- storage.js | 6 + 26 files changed, 490 insertions(+), 514 deletions(-) diff --git a/_locales/ar/messages.json b/_locales/ar/messages.json index f151170a..a808bf3c 100644 --- a/_locales/ar/messages.json +++ b/_locales/ar/messages.json @@ -294,10 +294,6 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus does not work on pages like this.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, "sectionRemove": { "message": "إزالة القسم", "description": "Label for the button to remove a section" diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index 2e822c29..d2ab037c 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -294,8 +294,8 @@ "message": "Mozilla Formát", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus nefunguje na těchto stránkách.)", + "stylusUnavailableForURL": { + "message": "Stylus nefunguje na těchto stránkách.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 8b13f9be..11c75031 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -356,8 +356,8 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus funktioniert nicht auf Seiten wie diesen.)", + "stylusUnavailableForURL": { + "message": "Stylus funktioniert nicht auf Seiten wie diesen.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/el/messages.json b/_locales/el/messages.json index e0ad203d..1ce194ef 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -294,8 +294,8 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(To Stylus δεν λειτουργεί σε σελίδες όπως αυτή.)", + "stylusUnavailableForURL": { + "message": "To Stylus δεν λειτουργεί σε σελίδες όπως αυτή.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f70b8092..4d2e9786 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -494,10 +494,14 @@ } } }, - "stylishUnavailableForURL": { - "message": "(Stylus can't affect this page.)", + "stylusUnavailableForURL": { + "message": "Stylus doesn't work on pages like this.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, + "stylusUnavailableForURLdetails": { + "message": "As a security precaution, the browser prohibits extensions from affecting its built-in pages (like chrome://version or about:addons) as well as other extensions' pages. Chrome/Chromium forks also restrict the Chrome Web Store.", + "description": "Sub-note in the toolbar pop-up when on a URL Stylus can't affect" + }, "toggleStyle": { "message": "Toggle style", "description": "Label for the checkbox to enable/disable a style" @@ -515,7 +519,7 @@ "description": "Note in the toolbar popup usually on file:// URLs after [re]loading Stylus" }, "unreachableFileHint": { - "message": "To allow Stylus access file:// URLs enable the checkbox on chrome://extensions page.", + "message": "Stylus can access file:// URLs only if you enable the corresponding checkbox for Stylus extension on chrome://extensions page.", "description": "Note in the toolbar popup for file:// URLs" }, "updateCheckFailBadResponseCode": { diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 57b69ab4..8eb8fb4e 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -350,8 +350,8 @@ "message": "Formato Mozilla", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus no funciona en páginas como esta)", + "stylusUnavailableForURL": { + "message": "Stylus no funciona en páginas como esta", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/fi/messages.json b/_locales/fi/messages.json index c567ec52..05f23581 100644 --- a/_locales/fi/messages.json +++ b/_locales/fi/messages.json @@ -294,10 +294,6 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus does not work on pages like this.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, "sectionRemove": { "message": "Poista osio", "description": "Label for the button to remove a section" diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index dbc35928..bcefde28 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -294,8 +294,8 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus ne fonctionne pas sur les pages de ce genre)", + "stylusUnavailableForURL": { + "message": "Stylus ne fonctionne pas sur les pages de ce genre", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 6ffd1349..0c0e3c96 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -294,10 +294,6 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus does not work on pages like this.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, "sectionRemove": { "message": "Rimuovi sezione", "description": "Label for the button to remove a section" diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index dcd249cd..0bff027c 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -294,10 +294,6 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus does not work on pages like this.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, "sectionRemove": { "message": "セクションを削除", "description": "Label for the button to remove a section" diff --git a/_locales/nl/messages.json b/_locales/nl/messages.json index b9c7709a..6361e18d 100644 --- a/_locales/nl/messages.json +++ b/_locales/nl/messages.json @@ -294,8 +294,8 @@ "message": "Mozilla-opmaak", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus werkt niet op pagina's als deze.)", + "stylusUnavailableForURL": { + "message": "Stylus werkt niet op pagina's als deze.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/pt_BR/messages.json b/_locales/pt_BR/messages.json index 1c45919b..1781676d 100644 --- a/_locales/pt_BR/messages.json +++ b/_locales/pt_BR/messages.json @@ -294,10 +294,6 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus does not work on pages like this.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, "sectionRemove": { "message": "Remover seção", "description": "Label for the button to remove a section" diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 585f30dc..1de473da 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -294,8 +294,8 @@ "message": "Формат Mozilla", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus не работает на таких страницах)", + "stylusUnavailableForURL": { + "message": "Stylus не работает на таких страницах.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/sr/messages.json b/_locales/sr/messages.json index 56723fd0..34e16b6d 100644 --- a/_locales/sr/messages.json +++ b/_locales/sr/messages.json @@ -1,425 +1,424 @@ -{ - "appliesToEverything": { - "message": "Све", - "description": "Text displayed for styles that apply to all sites" - }, - "defaultTheme": { - "message": "подразумевано", - "description": "Default CodeMirror CSS theme option on the edit style page" - }, - "manageOnlyEdited": { - "message": "Само уређени стилови", - "description": "Checkbox to show only locally edited styles" - }, - "exportLabel": { - "message": "Извези", - "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" - }, - "issues": { - "message": "Проблеми", - "description": "Label for the CSSLint issues block on the style edit page" - }, - "cm_tabSize": { - "message": "Величина картице", - "description": "Label for the text box controlling tab size option for the style editor." - }, - "enableStyleLabel": { - "message": "Омогући", - "description": "Label for the button to enable a style" - }, - "styleMissingName": { - "message": "Унесите назив", - "description": "Error displayed when user saves without providing a name" - }, - "appliesDomainOption": { - "message": "УРЛ адресе на домену", - "description": "Option to make the style apply to the entered string as a domain" - }, - "checkForUpdate": { - "message": "Проверите ажурирање", - "description": "Label for the button to check a single style for an update" - }, - "importAppendLabel": { - "message": "Додај стилу", - "description": "Label for the button to import a style and append to the existing sections" - }, - "updateAllCheckSucceededNoUpdate": { - "message": "Сви стилови су ажурирани.", - "description": "Text that displays when an update all check completed and no updates are available" - }, - "styleFromMozillaFormatPrompt": { - "message": "Налепи код у Mozilla формату", - "description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button" - }, - "helpAlt": { - "message": "Помоћ", - "description": "Alternate text for help buttons" - }, - "search": { - "message": "Претражи", - "description": "Label before the search input field in the editor shown on Ctrl-F" - }, - "confirmYes": { - "message": "Да", - "description": "'Yes' button in a confirm dialog" - }, - "findStylesForSite": { - "message": "Пронађи још стилова за овај сајт.", - "description": "Text for a link that gets a list of styles for the current site" - }, - "manageHeading": { - "message": "Инсталирани стилови", - "description": "Heading for the manage page" - }, - "styleBeautify": { - "message": " Улепшај", - "description": "Label for the CSS-beautifier button on the edit style page" - }, - "styleEnabledLabel": { - "message": "Омогућено", - "description": "Label for the enabled state of styles" - }, - "styleToMozillaFormatHelp": { - "message": "Mozilla формат кода се може користити у Stylish за Firefox и може се послати на userstyles.org.", - "description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format" - }, - "sectionAdd": { - "message": "Додај нови одељак", - "description": "Label for the button to add a section" - }, - "styleSaveLabel": { - "message": "Сачувај", - "description": "Label for save button for style editing" - }, - "confirmStop": { - "message": "Заустави", - "description": "'Stop' button in a confirm dialog" - }, - "writeStyleForURL": { - "message": "ову УРЛ адресу", - "description": "Text for link in toolbar pop-up to write a new style for the current URL" - }, - "appliesAdd": { - "message": "Додај", - "description": "Label for the button to add an 'applies' entry" - }, - "appliesRegexpOption": { - "message": "УРЛ адресе које одговарају регуларном изразу", - "description": "Option to make the style apply to the entered string as a regular expression" - }, - "styleInstall": { - "message": "Инсталирати '$stylename$' у Stylus?", - "description": "Confirmation when installing a style", - "placeholders": { - "stylename": { - "content": "$1" - } - } - }, - "manageText": { - "message": "Преузмите стилове са userstyles.org | Помоћ", - "description": "Help text on the manage page" - }, - "searchStyles": { - "message": "Претражи садржај", - "description": "Label for the search filter textbox on the Manage styles page" - }, - "disableStyleLabel": { - "message": "Онемогући", - "description": "Label for the button to disable a style" - }, - "prefShowBadge": { - "message": "Прикажи број активних стилова за тренутни сајт на дугмету на алатној траци", - "description": "Label for the checkbox controlling toolbar badge text." - }, - "menuShowBadge": { - "message": "Прикажи број активних стилова", - "description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text." - }, - "cm_lineWrapping": { - "message": "Преламање текста", - "description": "Label for the checkbox controlling word wrap option for the style editor." - }, - "styleCancelEditLabel": { - "message": "Назад на управљање", - "description": "Label for cancel button for style editing" - }, - "styleChangesNotSaved": { - "message": "Направили сте измене овог стила које нисте сачували.", - "description": "Text for the prompt when changes are made to a style and the user tries to leave without saving" - }, - "importLabel": { - "message": "Увези", - "description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)" - }, - "updateCheckFailServerUnreachable": { - "message": "Ажурирање није успело - сервер није доступан.", - "description": "Text that displays when an update check failed because the update server is unreachable" - }, - "manageFilters": { - "message": "Филтери", - "description": "Label for filters container" - }, - "applyAllUpdates": { - "message": "Примени сва ажурирања", - "description": "Label for the button to apply all detected updates" - }, - "deleteStyleConfirm": { - "message": "Да ли сте сигурни да желите да избришете овај стил?", - "description": "Confirmation before deleting a style" - }, - "confirmDelete": { - "message": "Delete" - }, - "confirmCancel": { - "message": "Cancel" - }, - "styleBadRegexp": { - "message": "Регуларни израз је неисправан.", - "description": "Validation message for a bad regexp in a style" - }, - "optionsHeading": { - "message": "Опције", - "description": "Heading for options section on manage page." - }, - "appliesDisplay": { - "message": "Примењује се на: $applies$", - "description": "Text on the manage screen to describe what the style applies to", - "placeholders": { - "applies": { - "content": "$1" - } - } - }, - "styleUpdate": { - "message": "Да ли сте сигурни да желите да ажурирате '$stylename$'?", - "description": "Confirmation when updating a style", - "placeholders": { - "stylename": { - "content": "$1" - } - } - }, - "styleSectionsTitle": { - "message": "Одељци", - "description": "Title for the style sections section" - }, - "editStyleTitle": { - "message": "Уреди стил $stylename$", - "description": "Title of the page for editing styles", - "placeholders": { - "stylename": { - "content": "$1" - } - } - }, - "updateCheckSucceededNoUpdate": { - "message": "Стил је ажуриран.", - "description": "Text that displays when an update check completed and no update is available" - }, - "appliesUrlPrefixOption": { - "message": "УРЛ адресе које почињу са", - "description": "Option to make the style apply to the entered string as a URL prefix" - }, - "searchRegexp": { - "message": "Користи /re/ синтаксу за претрагу регуларним изразом", - "description": "Label after the search input field in the editor shown on Ctrl-F" - }, - "importReplaceTooltip": { - "message": "Одбаци садржај тренутног стила и упиши преко њега увезени стил", - "description": "Label for the button to import and overwrite current style" - }, - "popupStylesFirst": { - "message": "Излистај стилове пре команди у менију дугмета на алатној траци", - "description": "Label for the checkbox controlling section order in the toolbar button menu." - }, - "sectionHelp": { - "message": "Одељци вам омогућавају да дефинишете различите делове кода који се примењују на раличите скупове УРЛ-ова у истом стилу. На пример, један исти стил може променити почетну страницу једног сајта на један начин а остатак сајта на други начин.", - "description": "Help text for sections" - }, - "noStylesForSite": { - "message": "Нема инсталираних стилова за овај сајт.", - "description": "Text displayed when no styles are installed for the current site" - }, - "appliesDisplayTruncatedSuffix": { - "message": "и још", - "description": "Text added to appliesDisplay when there are more sites for the style than are displayed" - }, - "appliesRemove": { - "message": "Уклони", - "description": "Label for the button to remove an 'applies' entry" - }, - "styleToMozillaFormatTitle": { - "message": "Стил у Mozilla формату", - "description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page" - }, - "manageTitle": { - "message": "Stylus", - "description": "Title for the manage page" - }, - "writeStyleFor": { - "message": "Упиши стил за:", - "description": "Label for toolbar pop-up that precedes the links to write a new style" - }, - "replace": { - "message": "Замени", - "description": "Label before the replace input field in the editor shown on Ctrl-H" - }, - "appliesLabel": { - "message": "Примењује се на", - "description": "Label for 'applies to' fields on the edit/add screen" - }, - "openManage": { - "message": "Управљај инсталираним стиловима", - "description": "Link to open the manage page." - }, - "updateCheckFailBadResponseCode": { - "message": "Ажурирање није успело - сервер је одговорио кодом $code$.", - "description": "Text that displays when an update check failed because the response code indicates an error", - "placeholders": { - "code": { - "content": "$1" - } - } - }, - "appliesSpecify": { - "message": "Детаљније", - "description": "Label for the button to make a style apply only to specific sites" - }, - "installUpdate": { - "message": "Инсталирај ажурирање", - "description": "Label for the button to install an update for a single style" - }, - "styleMozillaFormatHeading": { - "message": "Mozilla формат", - "description": "Heading for the section with buttons to import/export Mozilla format of the style" - }, - "stylishUnavailableForURL": { - "message": "(Stylus не ради на страницама као што је ова.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, - "sectionRemove": { - "message": "Уклони одељак", - "description": "Label for the button to remove a section" - }, - "disableAllStyles": { - "message": "Искључи све стилове", - "description": "Label for the checkbox that turns all enabled styles off." - }, - "undoGlobal": { - "message": "Опозови (свеобухватно)", - "description": "CSS-beautify global Undo button label" - }, - "updateCompleted": { - "message": "Ажурирање је комплетирано.", - "description": "Text that displays when an update completed" - }, - "checkingForUpdate": { - "message": "Проверавање...", - "description": "Text to display when checking a style for an update" - }, - "sectionCode": { - "message": "Код", - "description": "Label for the code for a section" - }, - "cm_smartIndent": { - "message": "Користи паметно увлачење редова", - "description": "Label for the checkbox controlling smart indentation option for the style editor." - }, - "appliesHelp": { - "message": "Употреба 'Примењује се на' одређује опсег УРЛ адреса на које се код у овом одељку примењује.", - "description": "Help text for 'applies to' section" - }, - "editStyleHeading": { - "message": "Уреди стил", - "description": "Title of the page for editing styles" - }, - "appliesUrlOption": { - "message": "УРЛ", - "description": "Option to make the style apply to the entered string as a URL" - }, - "addStyleTitle": { - "message": "Додај стил", - "description": "Title of the page for adding styles" - }, - "importReplaceLabel": { - "message": "Упиши преко стила", - "description": "Label for the button to import and overwrite current style" - }, - "dbError": { - "message": "Дошло је до грешке користећи Stylus базу података. Да ли желите да посетите веб страницу са могућим решењима?", - "description": "Prompt when a DB error is encountered" - }, - "importAppendTooltip": { - "message": "Додај увезени стил тренутном стилу", - "description": "Tooltip for the button to import a style and append to the existing sections" - }, - "helpKeyMapHotkey": { - "message": "Притисни пречицу", - "description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short" - }, - "replaceAll": { - "message": "Замени све", - "description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey" - }, - "editGotoLine": { - "message": "Иди на ред (или line:col)", - "description": "Go to line or line:column on Ctrl-G in style code editor" - }, - "checkAllUpdates": { - "message": "Проверите ажурирања за све стилове", - "description": "Label for the button to check all styles for updates" - }, - "issuesHelp": { - "message": "Проблем пронађен од стране CSSLint са овим омогућеним правилима:", - "description": "Help popup message for the CSSLint issues block on the style edit page" - }, - "confirmNo": { - "message": "Не", - "description": "'No' button in a confirm dialog" - }, - "undo": { - "message": "Опозови", - "description": "Button label" - }, - "cm_keyMap": { - "message": "Мапа тастера", - "description": "Label for the drop-down list controlling the keymap for the style editor." - }, - "cm_indentWithTabs": { - "message": "Користи картице са паметним увлачењем редова", - "description": "Label for the checkbox controlling tabs with smart indentation option for the style editor." - }, - "replaceWith": { - "message": "Замени са", - "description": "Label before the replace-with input field in the editor shown on Ctrl-H etc." - }, - "deleteStyleLabel": { - "message": "Избриши", - "description": "Label for the button to delete a style" - }, - "addStyleLabel": { - "message": "Упиши нови стил", - "description": "Label for the button to go to the add style page" - }, - "manageOnlyEnabled": { - "message": "Само омогућени стилови", - "description": "Checkbox to show only enabled styles" - }, - "editStyleLabel": { - "message": "Уреди", - "description": "Label for the button to go to the edit style page" - }, - "cm_theme": { - "message": "Тема", - "description": "Label for the style editor's CSS theme." - }, - "helpKeyMapCommand": { - "message": "Укуцај име команде", - "description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short" - }, - "description": { - "message": "Измените стил интернет мреже управљачем корисничких стилова. Stylus вам омогућава да лако инсталирате теме и скинове за многе популарне сајтове.", - "description": "Extension description" - } -} +{ + "appliesToEverything": { + "message": "Све", + "description": "Text displayed for styles that apply to all sites" + }, + "defaultTheme": { + "message": "подразумевано", + "description": "Default CodeMirror CSS theme option on the edit style page" + }, + "manageOnlyEdited": { + "message": "Само уређени стилови", + "description": "Checkbox to show only locally edited styles" + }, + "exportLabel": { + "message": "Извези", + "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" + }, + "issues": { + "message": "Проблеми", + "description": "Label for the CSSLint issues block on the style edit page" + }, + "cm_tabSize": { + "message": "Величина картице", + "description": "Label for the text box controlling tab size option for the style editor." + }, + "enableStyleLabel": { + "message": "Омогући", + "description": "Label for the button to enable a style" + }, + "styleMissingName": { + "message": "Унесите назив", + "description": "Error displayed when user saves without providing a name" + }, + "appliesDomainOption": { + "message": "УРЛ адресе на домену", + "description": "Option to make the style apply to the entered string as a domain" + }, + "checkForUpdate": { + "message": "Проверите ажурирање", + "description": "Label for the button to check a single style for an update" + }, + "importAppendLabel": { + "message": "Додај стилу", + "description": "Label for the button to import a style and append to the existing sections" + }, + "updateAllCheckSucceededNoUpdate": { + "message": "Сви стилови су ажурирани.", + "description": "Text that displays when an update all check completed and no updates are available" + }, + "styleFromMozillaFormatPrompt": { + "message": "Налепи код у Mozilla формату", + "description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button" + }, + "helpAlt": { + "message": "Помоћ", + "description": "Alternate text for help buttons" + }, + "search": { + "message": "Претражи", + "description": "Label before the search input field in the editor shown on Ctrl-F" + }, + "confirmYes": { + "message": "Да", + "description": "'Yes' button in a confirm dialog" + }, + "findStylesForSite": { + "message": "Пронађи још стилова за овај сајт.", + "description": "Text for a link that gets a list of styles for the current site" + }, + "manageHeading": { + "message": "Инсталирани стилови", + "description": "Heading for the manage page" + }, + "styleBeautify": { + "message": " Улепшај", + "description": "Label for the CSS-beautifier button on the edit style page" + }, + "styleEnabledLabel": { + "message": "Омогућено", + "description": "Label for the enabled state of styles" + }, + "styleToMozillaFormatHelp": { + "message": "Mozilla формат кода се може користити у Stylish за Firefox и може се послати на userstyles.org.", + "description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format" + }, + "sectionAdd": { + "message": "Додај нови одељак", + "description": "Label for the button to add a section" + }, + "styleSaveLabel": { + "message": "Сачувај", + "description": "Label for save button for style editing" + }, + "confirmStop": { + "message": "Заустави", + "description": "'Stop' button in a confirm dialog" + }, + "writeStyleForURL": { + "message": "ову УРЛ адресу", + "description": "Text for link in toolbar pop-up to write a new style for the current URL" + }, + "appliesAdd": { + "message": "Додај", + "description": "Label for the button to add an 'applies' entry" + }, + "appliesRegexpOption": { + "message": "УРЛ адресе које одговарају регуларном изразу", + "description": "Option to make the style apply to the entered string as a regular expression" + }, + "styleInstall": { + "message": "Инсталирати '$stylename$' у Stylus?", + "description": "Confirmation when installing a style", + "placeholders": { + "stylename": { + "content": "$1" + } + } + }, + "manageText": { + "message": "Преузмите стилове са userstyles.org | Помоћ", + "description": "Help text on the manage page" + }, + "searchStyles": { + "message": "Претражи садржај", + "description": "Label for the search filter textbox on the Manage styles page" + }, + "disableStyleLabel": { + "message": "Онемогући", + "description": "Label for the button to disable a style" + }, + "prefShowBadge": { + "message": "Прикажи број активних стилова за тренутни сајт на дугмету на алатној траци", + "description": "Label for the checkbox controlling toolbar badge text." + }, + "menuShowBadge": { + "message": "Прикажи број активних стилова", + "description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text." + }, + "cm_lineWrapping": { + "message": "Преламање текста", + "description": "Label for the checkbox controlling word wrap option for the style editor." + }, + "styleCancelEditLabel": { + "message": "Назад на управљање", + "description": "Label for cancel button for style editing" + }, + "styleChangesNotSaved": { + "message": "Направили сте измене овог стила које нисте сачували.", + "description": "Text for the prompt when changes are made to a style and the user tries to leave without saving" + }, + "importLabel": { + "message": "Увези", + "description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)" + }, + "updateCheckFailServerUnreachable": { + "message": "Ажурирање није успело - сервер није доступан.", + "description": "Text that displays when an update check failed because the update server is unreachable" + }, + "manageFilters": { + "message": "Филтери", + "description": "Label for filters container" + }, + "applyAllUpdates": { + "message": "Примени сва ажурирања", + "description": "Label for the button to apply all detected updates" + }, + "deleteStyleConfirm": { + "message": "Да ли сте сигурни да желите да избришете овај стил?", + "description": "Confirmation before deleting a style" + }, + "confirmDelete": { + "message": "Delete" + }, + "confirmCancel": { + "message": "Cancel" + }, + "styleBadRegexp": { + "message": "Регуларни израз је неисправан.", + "description": "Validation message for a bad regexp in a style" + }, + "optionsHeading": { + "message": "Опције", + "description": "Heading for options section on manage page." + }, + "appliesDisplay": { + "message": "Примењује се на: $applies$", + "description": "Text on the manage screen to describe what the style applies to", + "placeholders": { + "applies": { + "content": "$1" + } + } + }, + "styleUpdate": { + "message": "Да ли сте сигурни да желите да ажурирате '$stylename$'?", + "description": "Confirmation when updating a style", + "placeholders": { + "stylename": { + "content": "$1" + } + } + }, + "styleSectionsTitle": { + "message": "Одељци", + "description": "Title for the style sections section" + }, + "editStyleTitle": { + "message": "Уреди стил $stylename$", + "description": "Title of the page for editing styles", + "placeholders": { + "stylename": { + "content": "$1" + } + } + }, + "updateCheckSucceededNoUpdate": { + "message": "Стил је ажуриран.", + "description": "Text that displays when an update check completed and no update is available" + }, + "appliesUrlPrefixOption": { + "message": "УРЛ адресе које почињу са", + "description": "Option to make the style apply to the entered string as a URL prefix" + }, + "searchRegexp": { + "message": "Користи /re/ синтаксу за претрагу регуларним изразом", + "description": "Label after the search input field in the editor shown on Ctrl-F" + }, + "importReplaceTooltip": { + "message": "Одбаци садржај тренутног стила и упиши преко њега увезени стил", + "description": "Label for the button to import and overwrite current style" + }, + "popupStylesFirst": { + "message": "Излистај стилове пре команди у менију дугмета на алатној траци", + "description": "Label for the checkbox controlling section order in the toolbar button menu." + }, + "sectionHelp": { + "message": "Одељци вам омогућавају да дефинишете различите делове кода који се примењују на раличите скупове УРЛ-ова у истом стилу. На пример, један исти стил може променити почетну страницу једног сајта на један начин а остатак сајта на други начин.", + "description": "Help text for sections" + }, + "noStylesForSite": { + "message": "Нема инсталираних стилова за овај сајт.", + "description": "Text displayed when no styles are installed for the current site" + }, + "appliesDisplayTruncatedSuffix": { + "message": "и још", + "description": "Text added to appliesDisplay when there are more sites for the style than are displayed" + }, + "appliesRemove": { + "message": "Уклони", + "description": "Label for the button to remove an 'applies' entry" + }, + "styleToMozillaFormatTitle": { + "message": "Стил у Mozilla формату", + "description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page" + }, + "manageTitle": { + "message": "Stylus", + "description": "Title for the manage page" + }, + "writeStyleFor": { + "message": "Упиши стил за:", + "description": "Label for toolbar pop-up that precedes the links to write a new style" + }, + "replace": { + "message": "Замени", + "description": "Label before the replace input field in the editor shown on Ctrl-H" + }, + "appliesLabel": { + "message": "Примењује се на", + "description": "Label for 'applies to' fields on the edit/add screen" + }, + "openManage": { + "message": "Управљај инсталираним стиловима", + "description": "Link to open the manage page." + }, + "updateCheckFailBadResponseCode": { + "message": "Ажурирање није успело - сервер је одговорио кодом $code$.", + "description": "Text that displays when an update check failed because the response code indicates an error", + "placeholders": { + "code": { + "content": "$1" + } + } + }, + "appliesSpecify": { + "message": "Детаљније", + "description": "Label for the button to make a style apply only to specific sites" + }, + "installUpdate": { + "message": "Инсталирај ажурирање", + "description": "Label for the button to install an update for a single style" + }, + "styleMozillaFormatHeading": { + "message": "Mozilla формат", + "description": "Heading for the section with buttons to import/export Mozilla format of the style" + }, + "stylusUnavailableForURL": { + "message": "Stylus не ради на страницама као што је ова.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" + }, + "sectionRemove": { + "message": "Уклони одељак", + "description": "Label for the button to remove a section" + }, + "disableAllStyles": { + "message": "Искључи све стилове", + "description": "Label for the checkbox that turns all enabled styles off." + }, + "undoGlobal": { + "message": "Опозови (свеобухватно)", + "description": "CSS-beautify global Undo button label" + }, + "updateCompleted": { + "message": "Ажурирање је комплетирано.", + "description": "Text that displays when an update completed" + }, + "checkingForUpdate": { + "message": "Проверавање...", + "description": "Text to display when checking a style for an update" + }, + "sectionCode": { + "message": "Код", + "description": "Label for the code for a section" + }, + "cm_smartIndent": { + "message": "Користи паметно увлачење редова", + "description": "Label for the checkbox controlling smart indentation option for the style editor." + }, + "appliesHelp": { + "message": "Употреба 'Примењује се на' одређује опсег УРЛ адреса на које се код у овом одељку примењује.", + "description": "Help text for 'applies to' section" + }, + "editStyleHeading": { + "message": "Уреди стил", + "description": "Title of the page for editing styles" + }, + "appliesUrlOption": { + "message": "УРЛ", + "description": "Option to make the style apply to the entered string as a URL" + }, + "addStyleTitle": { + "message": "Додај стил", + "description": "Title of the page for adding styles" + }, + "importReplaceLabel": { + "message": "Упиши преко стила", + "description": "Label for the button to import and overwrite current style" + }, + "dbError": { + "message": "Дошло је до грешке користећи Stylus базу података. Да ли желите да посетите веб страницу са могућим решењима?", + "description": "Prompt when a DB error is encountered" + }, + "importAppendTooltip": { + "message": "Додај увезени стил тренутном стилу", + "description": "Tooltip for the button to import a style and append to the existing sections" + }, + "helpKeyMapHotkey": { + "message": "Притисни пречицу", + "description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short" + }, + "replaceAll": { + "message": "Замени све", + "description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey" + }, + "editGotoLine": { + "message": "Иди на ред (или line:col)", + "description": "Go to line or line:column on Ctrl-G in style code editor" + }, + "checkAllUpdates": { + "message": "Проверите ажурирања за све стилове", + "description": "Label for the button to check all styles for updates" + }, + "issuesHelp": { + "message": "Проблем пронађен од стране CSSLint са овим омогућеним правилима:", + "description": "Help popup message for the CSSLint issues block on the style edit page" + }, + "confirmNo": { + "message": "Не", + "description": "'No' button in a confirm dialog" + }, + "undo": { + "message": "Опозови", + "description": "Button label" + }, + "cm_keyMap": { + "message": "Мапа тастера", + "description": "Label for the drop-down list controlling the keymap for the style editor." + }, + "cm_indentWithTabs": { + "message": "Користи картице са паметним увлачењем редова", + "description": "Label for the checkbox controlling tabs with smart indentation option for the style editor." + }, + "replaceWith": { + "message": "Замени са", + "description": "Label before the replace-with input field in the editor shown on Ctrl-H etc." + }, + "deleteStyleLabel": { + "message": "Избриши", + "description": "Label for the button to delete a style" + }, + "addStyleLabel": { + "message": "Упиши нови стил", + "description": "Label for the button to go to the add style page" + }, + "manageOnlyEnabled": { + "message": "Само омогућени стилови", + "description": "Checkbox to show only enabled styles" + }, + "editStyleLabel": { + "message": "Уреди", + "description": "Label for the button to go to the edit style page" + }, + "cm_theme": { + "message": "Тема", + "description": "Label for the style editor's CSS theme." + }, + "helpKeyMapCommand": { + "message": "Укуцај име команде", + "description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short" + }, + "description": { + "message": "Измените стил интернет мреже управљачем корисничких стилова. Stylus вам омогућава да лако инсталирате теме и скинове за многе популарне сајтове.", + "description": "Extension description" + } +} diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index 292b3372..14ad05cb 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -294,8 +294,8 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus fungerar inte på sidor som denna.)", + "stylusUnavailableForURL": { + "message": "Stylus fungerar inte på sidor som denna.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/sv_SE/messages.json b/_locales/sv_SE/messages.json index 1fef387a..913f6b8c 100644 --- a/_locales/sv_SE/messages.json +++ b/_locales/sv_SE/messages.json @@ -294,8 +294,8 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus fungerar inte på sidor som dessa.)", + "stylusUnavailableForURL": { + "message": "Stylus fungerar inte på sidor som dessa.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/te/messages.json b/_locales/te/messages.json index a6f9ef12..82268516 100644 --- a/_locales/te/messages.json +++ b/_locales/te/messages.json @@ -294,10 +294,6 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus does not work on pages like this.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, "sectionRemove": { "message": "Remove section", "description": "Label for the button to remove a section" diff --git a/_locales/tr/messages.json b/_locales/tr/messages.json index 3006bcb0..7a15d65c 100644 --- a/_locales/tr/messages.json +++ b/_locales/tr/messages.json @@ -294,10 +294,6 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus does not work on pages like this.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, "sectionRemove": { "message": "Bölümü kaldır", "description": "Label for the button to remove a section" diff --git a/_locales/zh/messages.json b/_locales/zh/messages.json index 3727b871..fbc30b4c 100644 --- a/_locales/zh/messages.json +++ b/_locales/zh/messages.json @@ -294,10 +294,6 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus does not work on pages like this.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, "sectionRemove": { "message": "移除节", "description": "Label for the button to remove a section" diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index b2e72bd9..99b9dc9c 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -352,8 +352,8 @@ "message": "Mozilla 格式", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus在这样的页面上不工作)", + "stylusUnavailableForURL": { + "message": "Stylus在这样的页面上不工作", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json index b5deba4a..07e51fad 100644 --- a/_locales/zh_TW/messages.json +++ b/_locales/zh_TW/messages.json @@ -294,8 +294,8 @@ "message": "Mozilla格式", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "( Stylus 不能在諸如此類的網頁上生效。)", + "stylusUnavailableForURL": { + "message": "Stylus 不能在諸如此類的網頁上生效。", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/messaging.js b/messaging.js index d53b89f5..44705306 100644 --- a/messaging.js +++ b/messaging.js @@ -14,8 +14,14 @@ const URLS = { configureCommands: OPERA ? 'opera://settings/configureCommands' : 'chrome://extensions/configureCommands', + // CWS cannot be scripted in chromium, see ChromeExtensionsClient::IsScriptableURL + // https://cs.chromium.org/chromium/src/chrome/common/extensions/chrome_extensions_client.cc + chromeWebStore: FIREFOX ? 'N/A' : 'https://chrome.google.com/webstore/', }; -const RX_SUPPORTED_URLS = new RegExp(`^(file|https?|ftps?):|^${URLS.ownOrigin}`); +const RX_SUPPORTED_URLS = new RegExp( + '^(file|ftps?|http)://|' + + `^https://${FIREFOX ? '' : '(?!chrome\\.google\\.com/webstore)'}|` + + '^' + URLS.ownOrigin); let BG = chrome.extension.getBackgroundPage(); diff --git a/popup.css b/popup.css index 897bd92f..529a544f 100644 --- a/popup.css +++ b/popup.css @@ -66,7 +66,6 @@ a:hover { display: table-cell; } -#unavailable, #installed { border-bottom: 1px solid black; padding-bottom: 2px; @@ -219,9 +218,7 @@ body > .actions { } .actions > div:not(:last-child):not(#disable-all-wrapper), -.actions > .main-controls > div:not(:last-child), -#unavailable:not(:last-child), -#unavailable + .actions { +.actions > .main-controls > div:not(:last-child) { margin-bottom: 0.75em; } @@ -230,24 +227,10 @@ body > .actions { vertical-align: middle; } -#unavailable { - border: none; +body.blocked #installed > *, +body.blocked .actions > .main-controls, +body.blocked .actions > .left-gutter { display: none; - margin-top: 0.75em; - align-items: center; - justify-content: center; - font-size: 14px; -} - -body.blocked #installed, -body.blocked #find-styles, -body.blocked #write-style, -body:not(.blocked) #unavailable { - display: none; -} - -body.blocked #unavailable { - display: flex; } /* Never shown, but can be enabled with a style */ @@ -437,22 +420,38 @@ body.blocked #unavailable { opacity: .25; } +.blocked:before, .unreachable:before { - content: "__MSG_unreachableContentScript__"; padding: 5px 0.75em; display: block; font-weight: bold; } +.blocked #installed:before, .unreachable #installed:before { - content: "__MSG_unreachableFileHint__"; padding: 1px 0.75em 9px; display: block; font-size: 90%; - border-bottom: 1px solid black; margin-bottom: 5px; } +.blocked:before { + content: "__MSG_stylusUnavailableForURL__"; +} + +.blocked #installed:before { + content: "__MSG_stylusUnavailableForURLdetails__"; +} + +.unreachable:before { + content: "__MSG_unreachableContentScript__"; +} + +.unreachable #installed:before { + content: "__MSG_unreachableFileHint__"; + border-bottom: 1px solid black; +} + @keyframes lights-off { from { background-color: transparent; diff --git a/popup.html b/popup.html index 1c026e9e..4b56fcc0 100644 --- a/popup.html +++ b/popup.html @@ -76,12 +76,6 @@ -
    -
    - -
    -
    -
    diff --git a/popup.js b/popup.js index 7c091869..241860a9 100644 --- a/popup.js +++ b/popup.js @@ -357,7 +357,7 @@ function handleUpdate(style) { } // Add an entry when a new style for the current url is installed if (tabURL && BG.getApplicableSections({style, matchUrl: tabURL, stopOnFirst: true}).length) { - $('#unavailable').style.display = 'none'; + document.body.classList.remove('blocked'); createStyleElement({style}); } } diff --git a/storage.js b/storage.js index b4bc03bb..4bd345e7 100644 --- a/storage.js +++ b/storage.js @@ -104,6 +104,12 @@ function filterStyles({ // eslint-disable-next-line no-use-before-define const disableAll = asHash && prefs.get('disableAll', false); + if (matchUrl && matchUrl.startsWith(URLS.chromeWebStore)) { + // CWS cannot be scripted in chromium, see ChromeExtensionsClient::IsScriptableURL + // https://cs.chromium.org/chromium/src/chrome/common/extensions/chrome_extensions_client.cc + return asHash ? {} : []; + } + // add \t after url to prevent collisions (not sure it can actually happen though) const cacheKey = ' ' + enabled + url + '\t' + id + matchUrl + '\t' + asHash + strictRegexp; const cached = cachedStyles.filters.get(cacheKey); From 4f2ccbe6cb326cfc4e4b0eb7f04f239d50c14cf1 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 12 Apr 2017 18:31:52 +0300 Subject: [PATCH 128/235] speedup import by bulk-updating DOM every 50 styles or 1 sec --- backup/fileSaveLoad.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/backup/fileSaveLoad.js b/backup/fileSaveLoad.js index a052516c..f451c674 100644 --- a/backup/fileSaveLoad.js +++ b/backup/fileSaveLoad.js @@ -67,7 +67,10 @@ function importFromString(jsonString) { invalid: {names: [], legend: 'invalid skipped'}, }; let index = 0; - let lastRepaint = performance.now(); + let lastRenderTime = performance.now(); + const renderQueue = []; + const RENDER_NAP_TIME_MAX = 1000; // ms + const RENDER_QUEUE_MAX = 50; // number of styles return new Promise(proceed); function proceed(resolve) { @@ -100,10 +103,13 @@ function importFromString(jsonString) { reason: 'import', notify: false, })).then(style => { - handleUpdate(style, {reason: 'import'}); - if (performance.now() - lastRepaint > 1000) { - scrollElementIntoView($('#style-' + style.id)); - lastRepaint = performance.now(); + renderQueue.push(style); + if (performance.now() - lastRenderTime > RENDER_NAP_TIME_MAX + || renderQueue.length > RENDER_QUEUE_MAX) { + renderQueue.forEach(style => handleUpdate(style, {reason: 'import'})); + setTimeout(scrollElementIntoView, 0, $('#style-' + renderQueue.pop().id)); + renderQueue.length = 0; + lastRenderTime = performance.now(); } setTimeout(proceed, 0, resolve); if (!oldStyle) { @@ -126,6 +132,8 @@ function importFromString(jsonString) { }); return; } + renderQueue.forEach(style => handleUpdate(style, {reason: 'import'})); + renderQueue.length = 0; done(resolve); } From ce2492c3050cce192c9396272068f9ecbe3bcf0a Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 12 Apr 2017 20:22:58 +0300 Subject: [PATCH 129/235] manage: apply search filter to edited/updated styles --- manage.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/manage.js b/manage.js index 657cfa4a..79039377 100644 --- a/manage.js +++ b/manage.js @@ -107,7 +107,7 @@ function showStyles(styles = []) { break; } } - if ($('#search').value) { + if ($('#search').value.trim()) { // re-apply filtering on history Back searchStyles({immediately: true, container: renderBin}); } @@ -342,6 +342,11 @@ Object.assign(handleEvent, { function handleUpdate(style, {reason} = {}) { const element = createStyleElement({style}); + if ($('#search').value.trim()) { + const renderBin = document.createDocumentFragment(); + renderBin.appendChild(element); + searchStyles({immediately: true, container: renderBin}); + } const oldElement = $('#style-' + style.id, installed); if (oldElement) { if (oldElement.styleNameLowerCase == element.styleNameLowerCase) { From 42f7b11bacaac0d4322b91bd227132ad8a28dc78 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 12 Apr 2017 20:24:05 +0300 Subject: [PATCH 130/235] broadcast only meta for styleUpdated/styleAdded apply/popup/manage use only meta for these two methods, editor may need the full code but can fetch it directly, so we send just the meta to avoid spamming lots of tabs with huge styles --- edit.js | 18 ++++++++++++++---- messaging.js | 11 +++++++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/edit.js b/edit.js index fe3644fd..638f9d4f 100644 --- a/edit.js +++ b/edit.js @@ -12,6 +12,9 @@ var useHistoryBack; // use browser history back when "back to manage" is click var propertyToCss = {urls: "url", urlPrefixes: "url-prefix", domains: "domain", regexps: "regexp"}; var CssToProperty = {"url": "urls", "url-prefix": "urlPrefixes", "domain": "domains", "regexp": "regexps"}; +// if background page hasn't been loaded yet, increase the chances it has before DOMContentLoaded +onBackgroundReady(); + // make querySelectorAll enumeration code readable ["forEach", "some", "indexOf", "map"].forEach(function(method) { NodeList.prototype[method]= Array.prototype[method]; @@ -246,9 +249,8 @@ function initCodeMirror() { return options.map(function(opt) { return ""; }).join(""); } var themeControl = document.getElementById("editor.theme"); - var bg = chrome.extension.getBackgroundPage(); - if (bg && bg.codeMirrorThemes) { - themeControl.innerHTML = optionsHtmlFromArray(bg.codeMirrorThemes); + if (BG && BG.codeMirrorThemes) { + themeControl.innerHTML = optionsHtmlFromArray(BG.codeMirrorThemes); } else { // Chrome is starting up and shows our edit.html, but the background page isn't loaded yet themeControl.innerHTML = optionsHtmlFromArray([theme == "default" ? t("defaultTheme") : theme]); @@ -1822,7 +1824,15 @@ function onRuntimeMessage(request) { switch (request.method) { case "styleUpdated": if (styleId && styleId == request.style.id && request.reason != 'editSave') { - initWithStyle(request); + if ((request.style.sections[0] || {}).code === null) { + // the code-less style came from notifyAllTabs + onBackgroundReady().then(() => { + request.style = BG.cachedStyles.byId.get(request.style.id); + initWithStyle(request); + }); + } else { + initWithStyle(request); + } } break; case "styleDeleted": diff --git a/messaging.js b/messaging.js index 44705306..91be8798 100644 --- a/messaging.js +++ b/messaging.js @@ -32,19 +32,22 @@ if (!BG || BG != window) { function notifyAllTabs(msg) { const originalMessage = msg; - if (msg.codeIsUpdated === false && msg.style) { + if (msg.method == 'styleUpdated' || msg.method == 'styleAdded') { + // apply/popup/manage use only meta for these two methods, + // editor may need the full code but can fetch it directly, + // so we send just the meta to avoid spamming lots of tabs with huge styles msg = Object.assign({}, msg, { style: getStyleWithNoCode(msg.style) }); } const affectsAll = !msg.affects || msg.affects.all; - const affectsOwnOrigin = !affectsAll && (msg.affects.editor || msg.affects.manager); - const affectsTabs = affectsAll || affectsOwnOrigin; + const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager); + const affectsTabs = affectsAll || affectsOwnOriginOnly; const affectsIcon = affectsAll || msg.affects.icon; const affectsPopup = affectsAll || msg.affects.popup; if (affectsTabs || affectsIcon) { // list all tabs including chrome-extension:// which can be ours - chrome.tabs.query(affectsOwnOrigin ? {url: URLS.ownOrigin + '*'} : {}, tabs => { + chrome.tabs.query(affectsOwnOriginOnly ? {url: URLS.ownOrigin + '*'} : {}, tabs => { for (const tab of tabs) { if (affectsTabs || URLS.optionsUI.includes(tab.url)) { chrome.tabs.sendMessage(tab.id, msg); From 3389812766fec2a8857ef2fc741808f20e35382e Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 13 Apr 2017 08:32:41 +0300 Subject: [PATCH 131/235] remove focus ring from --- edit.html | 1 + manage.css | 1 + 2 files changed, 2 insertions(+) diff --git a/edit.html b/edit.html index 6a3597df..3402392f 100644 --- a/edit.html +++ b/edit.html @@ -285,6 +285,7 @@ } .regexp-report summary, .regexp-report div { cursor: pointer; + outline: none; } .regexp-report mark { background-color: rgba(255, 255, 0, .5); diff --git a/manage.css b/manage.css index b0ff7315..dbab0ef9 100644 --- a/manage.css +++ b/manage.css @@ -141,6 +141,7 @@ a:hover { summary { font-weight: bold; cursor: pointer; + outline: none; } .applies-to-extra summary { From c09ee38c9e00693086f208e6354426664b1ebc33 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 13 Apr 2017 09:12:40 +0300 Subject: [PATCH 132/235] rework enforceInputRange * enforce only in onchange * notify on valid input immediately * highlight invalid values --- dom.js | 13 ++++++++----- options/index.css | 5 +++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/dom.js b/dom.js index c41f3bcc..e6b161ff 100644 --- a/dom.js +++ b/dom.js @@ -53,13 +53,16 @@ function animateElement(element, {className, remove = false}) { function enforceInputRange(element) { const min = Number(element.min); const max = Number(element.max); - const onChange = () => { - const value = Number(element.value); - if (value < min || value > max) { - element.value = Math.max(min, Math.min(max, value)); + const doNotify = () => element.dispatchEvent(new Event('change', {bubbles: true})); + const onChange = ({type}) => { + if (type == 'input' && element.checkValidity()) { + doNotify(); + } else if (type == 'change' && !element.checkValidity()) { + element.value = Math.max(min, Math.min(max, Number(element.value))); + doNotify(); } }; - onChange(); + onChange({}); element.addEventListener('change', onChange); element.addEventListener('input', onChange); } diff --git a/options/index.css b/options/index.css index 7ab692c2..6c8c7365 100644 --- a/options/index.css +++ b/options/index.css @@ -58,6 +58,11 @@ input[type=number] { text-align: right; } +input[type=number]:invalid { + background-color: rgba(255, 0, 0, 0.1); + color: darkred; +} + #actions { margin-top: -2em; } From 05c05ec6b9fcca0f1c4938c492c6592c42e09226 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 13 Apr 2017 10:54:56 +0300 Subject: [PATCH 133/235] manage: use chrome://favicon --- _locales/en/messages.json | 4 +++ manage.css | 23 ++++++++++++-- manage.html | 9 +++++- manage.js | 65 +++++++++++++++++++++++++++++++++------ manifest.json | 6 ++-- prefs.js | 2 +- 6 files changed, 93 insertions(+), 16 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 4d2e9786..dfee7509 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -294,6 +294,10 @@ "message": "Favicons in applies-to column", "description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page" }, + "manageFaviconsHelp": { + "message": "Stylus asks for chrome://favicon permission to retrieve the icons from browser cache. For non-cached icons Stylus uses external service https://www.google.com/s2/favicons", + "description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page" + }, "manageMaxTargets": { "message": "Number of applies-to items", "description": "Label for the numeric input box to limit max number of applies-to targets in the new UI on manage page" diff --git a/manage.css b/manage.css index dbab0ef9..a71894e3 100644 --- a/manage.css +++ b/manage.css @@ -61,10 +61,15 @@ a:hover { transition: fill .5s; width: 20px; height: 20px; +} + +.svg-icon, +.svg-icon.info:hover { fill: #000; } -.svg-icon:hover { +.svg-icon:hover, +.svg-icon.info { fill: #666; } @@ -73,6 +78,12 @@ a:hover { height: 16px; } +.svg-icon.info { + width: 14px; + height: 16px; + margin-left: .5ex; +} + .homepage { margin-left: 0.1em; margin-right: 0.1em; @@ -373,10 +384,11 @@ summary { display: initial; } -#newUIoptions label { +#newUIoptions > * { display: flex; align-items: center; margin-bottom: auto; + flex-wrap: wrap; } #newUIoptions input[type="number"] { @@ -388,6 +400,13 @@ input[id^="manage.newUI"] { margin-left: 0; } +#faviconsHelp { + overflow-y: auto; + font-size: 90%; + padding: 1ex 0 2ex 16px; +} + + /* Default, no update buttons */ .update, .check-update { diff --git a/manage.html b/manage.html index 32b8b92a..ba05f28e 100644 --- a/manage.html +++ b/manage.html @@ -149,7 +149,14 @@

    - +
    + + + + + + +

    diff --git a/manage.js b/manage.js index 79039377..500b342f 100644 --- a/manage.js +++ b/manage.js @@ -14,7 +14,10 @@ const newUI = { newUI.renderClass(); const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps']; -const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain='; +const GET_FAVICON_URL = { + builtin: 'chrome://favicon/size/16@2x/', + external: 'https://www.google.com/s2/favicons?domain=', +}; const OWN_ICON = chrome.runtime.getManifest().icons['16']; const handleEvent = {}; @@ -74,7 +77,7 @@ function initGlobalEvents() { checkbox.onchange = () => installed.classList.toggle(className, checkbox.checked); } - enforceInputRange($('#manage.newUI.favicons')); + enforceInputRange($('#manage.newUI.targets')); setupLivePrefs([ 'manage.onlyEnabled', @@ -84,8 +87,28 @@ function initGlobalEvents() { 'manage.newUI.targets', ]); - $$('[id^="manage.newUI"]') - .forEach(el => (el.oninput = (el.onchange = switchUI))); + $('#manage.newUI').onchange = switchUI; + $('#manage.newUI.targets').oninput = switchUI; + $('#manage.newUI.targets').onchange = switchUI; + $('#manage.newUI.favicons').onchange = function() { + if (!this.checked) { + switchUI(); + return; + } + if (this.disabled) { + return; + } + this.disabled = true; + onPermissionsGranted({origins: ['chrome://favicon/']}).then( + switchUI, + () => (this.checked = false) + ).then( + () => (this.disabled = false) + ); + }; + $$('[data-toggle-on-click]').forEach(el => { + el.onclick = () => $(el.dataset.toggleOnClick).classList.toggle('hidden'); + }); switchUI({styleOnly: true}); } @@ -183,12 +206,12 @@ function createStyleElement({style, name}) { } else if (newUI.favicons) { let favicon = ''; if (type == 'domains') { - favicon = GET_FAVICON_URL + targetValue; + favicon = 'http://' + targetValue; } else if (targetValue.startsWith('chrome-extension:')) { favicon = OWN_ICON; } else if (type != 'regexps') { - favicon = targetValue.includes('://') && targetValue.match(/^.*?:\/\/([^/]+)/); - favicon = favicon ? GET_FAVICON_URL + favicon[1] : ''; + favicon = targetValue.includes('://') && targetValue.match(/^.*?:\/\/[^/]+/); + favicon = favicon ? favicon[0] : ''; } if (favicon) { element.appendChild(document.createElement('img')).dataset.src = favicon; @@ -331,10 +354,13 @@ Object.assign(handleEvent, { loadFavicons(container = installed) { for (const img of container.getElementsByTagName('img')) { - if (img.dataset.src) { - img.src = img.dataset.src; - delete img.dataset.src; + const src = img.dataset.src; + if (!src) { + continue; } + img.src = src == OWN_ICON ? src + : GET_FAVICON_URL.builtin + (src.includes('://') ? src : 'http://' + src); + delete img.dataset.src; } } }); @@ -682,3 +708,22 @@ function findNextElement(style) { } return elements[elements[a].styleNameLowerCase <= nameLLC ? a + 1 : a]; } + + +function onPermissionsGranted(permissions) { + return new Promise((resolve, reject) => { + chrome.permissions.contains(permissions, alreadyGranted => { + if (alreadyGranted) { + resolve(); + } else { + chrome.permissions.request(permissions, granted => { + if (granted) { + resolve(); + } else { + reject(); + } + }); + } + }); + }); +} diff --git a/manifest.json b/manifest.json index eb283982..f8301da2 100644 --- a/manifest.json +++ b/manifest.json @@ -15,8 +15,10 @@ "tabs", "webNavigation", "contextMenus", - "storage", - "*://*/*" + "storage" + ], + "optional_permissions": [ + "chrome://favicon/" ], "background": { "scripts": ["messaging.js", "storage.js", "prefs.js", "background.js", "update.js"] diff --git a/prefs.js b/prefs.js index a47e9b6c..f3e817d6 100644 --- a/prefs.js +++ b/prefs.js @@ -17,7 +17,7 @@ var prefs = new function Prefs() { 'manage.onlyEnabled': false, // display only enabled styles 'manage.onlyEdited': false, // display only styles created locally 'manage.newUI': true, // use the new compact layout - 'manage.newUI.favicons': true, // show favicons for the sites in applies-to + 'manage.newUI.favicons': false, // show favicons for the sites in applies-to 'manage.newUI.targets': 3, // max number of applies-to targets visible: 0 = none 'editor.options': {}, // CodeMirror.defaults.* From ca911396a133f69c9e45ddde58a5625e8dff300e Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 13 Apr 2017 11:03:45 +0300 Subject: [PATCH 134/235] Don't use chrome://favicon chrome://favicon doesn't indicate an icon is missing in any way, it simply shows a placeholder instead. It also doesn't extrapolate from sub-pages so `example.com` won't have a favicon even if `example.com/subpage` has one. --- _locales/en/messages.json | 2 +- manage.js | 67 ++++++++------------------------------- manifest.json | 3 -- 3 files changed, 14 insertions(+), 58 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index dfee7509..cb690d9e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -295,7 +295,7 @@ "description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page" }, "manageFaviconsHelp": { - "message": "Stylus asks for chrome://favicon permission to retrieve the icons from browser cache. For non-cached icons Stylus uses external service https://www.google.com/s2/favicons", + "message": "Stylus uses an external service https://www.google.com/s2/favicons", "description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page" }, "manageMaxTargets": { diff --git a/manage.js b/manage.js index 500b342f..ad50c44f 100644 --- a/manage.js +++ b/manage.js @@ -14,10 +14,7 @@ const newUI = { newUI.renderClass(); const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps']; -const GET_FAVICON_URL = { - builtin: 'chrome://favicon/size/16@2x/', - external: 'https://www.google.com/s2/favicons?domain=', -}; +const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain='; const OWN_ICON = chrome.runtime.getManifest().icons['16']; const handleEvent = {}; @@ -77,6 +74,10 @@ function initGlobalEvents() { checkbox.onchange = () => installed.classList.toggle(className, checkbox.checked); } + $$('[data-toggle-on-click]').forEach(el => { + el.onclick = () => $(el.dataset.toggleOnClick).classList.toggle('hidden'); + }); + enforceInputRange($('#manage.newUI.targets')); setupLivePrefs([ @@ -87,28 +88,8 @@ function initGlobalEvents() { 'manage.newUI.targets', ]); - $('#manage.newUI').onchange = switchUI; - $('#manage.newUI.targets').oninput = switchUI; - $('#manage.newUI.targets').onchange = switchUI; - $('#manage.newUI.favicons').onchange = function() { - if (!this.checked) { - switchUI(); - return; - } - if (this.disabled) { - return; - } - this.disabled = true; - onPermissionsGranted({origins: ['chrome://favicon/']}).then( - switchUI, - () => (this.checked = false) - ).then( - () => (this.disabled = false) - ); - }; - $$('[data-toggle-on-click]').forEach(el => { - el.onclick = () => $(el.dataset.toggleOnClick).classList.toggle('hidden'); - }); + $$('[id^="manage.newUI"]') + .forEach(el => (el.oninput = (el.onchange = switchUI))); switchUI({styleOnly: true}); } @@ -206,12 +187,12 @@ function createStyleElement({style, name}) { } else if (newUI.favicons) { let favicon = ''; if (type == 'domains') { - favicon = 'http://' + targetValue; + favicon = GET_FAVICON_URL + targetValue; } else if (targetValue.startsWith('chrome-extension:')) { favicon = OWN_ICON; } else if (type != 'regexps') { - favicon = targetValue.includes('://') && targetValue.match(/^.*?:\/\/[^/]+/); - favicon = favicon ? favicon[0] : ''; + favicon = targetValue.includes('://') && targetValue.match(/^.*?:\/\/([^/]+)/); + favicon = favicon ? GET_FAVICON_URL + favicon[1] : ''; } if (favicon) { element.appendChild(document.createElement('img')).dataset.src = favicon; @@ -354,13 +335,10 @@ Object.assign(handleEvent, { loadFavicons(container = installed) { for (const img of container.getElementsByTagName('img')) { - const src = img.dataset.src; - if (!src) { - continue; + if (img.dataset.src) { + img.src = img.dataset.src; + delete img.dataset.src; } - img.src = src == OWN_ICON ? src - : GET_FAVICON_URL.builtin + (src.includes('://') ? src : 'http://' + src); - delete img.dataset.src; } } }); @@ -708,22 +686,3 @@ function findNextElement(style) { } return elements[elements[a].styleNameLowerCase <= nameLLC ? a + 1 : a]; } - - -function onPermissionsGranted(permissions) { - return new Promise((resolve, reject) => { - chrome.permissions.contains(permissions, alreadyGranted => { - if (alreadyGranted) { - resolve(); - } else { - chrome.permissions.request(permissions, granted => { - if (granted) { - resolve(); - } else { - reject(); - } - }); - } - }); - }); -} diff --git a/manifest.json b/manifest.json index f8301da2..d8d14f28 100644 --- a/manifest.json +++ b/manifest.json @@ -17,9 +17,6 @@ "contextMenus", "storage" ], - "optional_permissions": [ - "chrome://favicon/" - ], "background": { "scripts": ["messaging.js", "storage.js", "prefs.js", "background.js", "update.js"] }, From 470bc92da56bccbd6958f6e63442bf127ee609ab Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 13 Apr 2017 12:59:55 +0300 Subject: [PATCH 135/235] Add in permissions We already have full access to all sites via our content script so this permission doesn't add anything new but we need it to be able to establish page connection via tabs.executeScript when the extension is installed, reloaded, re-enabled. also allows file:// URLs unlike *://*/* used previously. Of course it requires the corresponding checkbox being enabled on chrome://extensions page. --- manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index d8d14f28..22425c24 100644 --- a/manifest.json +++ b/manifest.json @@ -15,7 +15,8 @@ "tabs", "webNavigation", "contextMenus", - "storage" + "storage", + "" ], "background": { "scripts": ["messaging.js", "storage.js", "prefs.js", "background.js", "update.js"] From e6f2034e644313ee088d821c72b0b4f0407039a6 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 13 Apr 2017 14:51:43 +0300 Subject: [PATCH 136/235] make import report messages localizable --- _locales/en/messages.json | 40 +++++++++++++++++++++++++++++++++++++++ backup/fileSaveLoad.js | 22 ++++++++++----------- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index cb690d9e..9354a899 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -234,6 +234,46 @@ "message": "Type a command name", "description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short" }, + "importReportLegendAdded": { + "message": "added", + "description": "Text after the number of styles added in the report shown after importing styles" + }, + "importReportLegendIdentical": { + "message": "identical skipped", + "description": "Text after the number of styles skipped due to being identical to the already installed ones in the report shown after importing styles" + }, + "importReportLegendInvalid": { + "message": "invalid skipped", + "description": "Text after the number of styles skipped due to being invalid (not a Stylus/Stylish backup file probably) in the report shown after importing styles" + }, + "importReportLegendUpdatedBoth": { + "message": "updated both meta info and code", + "description": "Text after the number of styles updated entirely in the report shown after importing styles" + }, + "importReportLegendUpdatedCode": { + "message": "updated code", + "description": "Text after the number of styles with updated code (meta info is unchanged) in the report shown after importing styles" + }, + "importReportLegendUpdatedMeta": { + "message": "updated meta info", + "description": "Text after the number of styles with updated meta info like name/url in the report shown after importing styles" + }, + "importReportTitle": { + "message": "Finished importing styles", + "description": "Title of the report shown after importing styles" + }, + "importReportUnchanged": { + "message": "Nothing was changed.", + "description": "Message in the report shown after importing styles" + }, + "importReportUndoneTitle": { + "message": "Import has been undone", + "description": "Title of the message box shown after undoing the import of styles" + }, + "importReportUndone": { + "message": "styles were reverted", + "description": "Text after the number of styles reverted in the message box shown after undoing the import of styles" + }, "importLabel": { "message": "Import", "description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)" diff --git a/backup/fileSaveLoad.js b/backup/fileSaveLoad.js index f451c674..466c5aa9 100644 --- a/backup/fileSaveLoad.js +++ b/backup/fileSaveLoad.js @@ -59,12 +59,12 @@ function importFromString(jsonString) { const oldStylesByName = json.length && new Map( oldStyles.map(style => [style.name.trim(), style])); const stats = { - added: {names: [], ids: [], legend: 'added'}, - unchanged: {names: [], ids: [], legend: 'identical skipped'}, - metaAndCode: {names: [], ids: [], legend: 'updated both meta info and code'}, - metaOnly: {names: [], ids: [], legend: 'updated meta info'}, - codeOnly: {names: [], ids: [], legend: 'updated code'}, - invalid: {names: [], legend: 'invalid skipped'}, + added: {names: [], ids: [], legend: 'importReportLegendAdded'}, + unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'}, + metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth'}, + metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta'}, + codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode'}, + invalid: {names: [], legend: 'importReportLegendInvalid'}, }; let index = 0; let lastRenderTime = performance.now(); @@ -153,14 +153,14 @@ function importFromString(jsonString) { .filter(kind => stats[kind].names.length) .map(kind => `

    - ${stats[kind].names.length} ${stats[kind].legend} + ${stats[kind].names.length} ${t(stats[kind].legend)} ${listNames(kind).join('')}
    `) .join(''); scrollTo(0, 0); messageBox({ - title: 'Finished importing styles', - contents: report || 'Nothing was changed.', + title: t('importReportTitle'), + contents: report || t('importReportUnchanged'), buttons: [t('confirmOK'), numChanged && t('undo')], onshow: bindClick, }).then(({button, enter, esc}) => { @@ -184,8 +184,8 @@ function importFromString(jsonString) { return new Promise(undoNextId) .then(BG.refreshAllTabs) .then(() => messageBox({ - title: 'Import has been undone', - contents: newIds.length + ' styles were reverted.', + title: t('importReportUndoneTitle'), + contents: newIds.length + ' ' + t('importReportUndone'), buttons: [t('confirmOK')], })); function undoNextId(resolve) { From 4bc27db3fc4baeaebbc0a875121973a1825f9632 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 13 Apr 2017 15:06:43 +0300 Subject: [PATCH 137/235] "backface-visibility: hidden" workaround for a browser bug --- manage.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manage.css b/manage.css index a71894e3..5faa7ec9 100644 --- a/manage.css +++ b/manage.css @@ -357,6 +357,8 @@ summary { /* 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; opacity: .25; display: none; } From 8f784a19d44ce8f6396e2b3024b49763b9126761 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 13 Apr 2017 19:44:43 +0300 Subject: [PATCH 138/235] simplify saveStyle, invalidateCache --- background.js | 6 +-- storage.js | 128 +++++++++++++++++++------------------------------- 2 files changed, 49 insertions(+), 85 deletions(-) diff --git a/background.js b/background.js index 781818ff..4c2bea8d 100644 --- a/background.js +++ b/background.js @@ -1,4 +1,4 @@ -/* global getDatabase, getStyles, saveStyle, reportError, invalidateCache */ +/* global getDatabase, getStyles, saveStyle, reportError */ 'use strict'; chrome.webNavigation.onBeforeNavigate.addListener(data => { @@ -60,10 +60,6 @@ function onRuntimeMessage(request, sender, sendResponse) { saveStyle(request).then(sendResponse); return KEEP_CHANNEL_OPEN; - case 'invalidateCache': - invalidateCache(false, request); - break; - case 'healthCheck': getDatabase( () => sendResponse(true), diff --git a/storage.js b/storage.js index 4bd345e7..a0baf42b 100644 --- a/storage.js +++ b/storage.js @@ -10,15 +10,15 @@ const SLOPPY_REGEXP_PREFIX = '\0'; // Note, only 'var'-declared variables are visible from another extension page // eslint-disable-next-line no-var var cachedStyles = { - list: null, - byId: new Map(), - filters: new Map(), - regexps: new Map(), - urlDomains: new Map(), - emptyCode: new Map(), // entire code is comments/whitespace/@namespace + list: null, // array of all styles + byId: new Map(), // all styles indexed by id + filters: new Map(), // filterStyles() parameters mapped to the returned results, 10k max + regexps: new Map(), // compiled style regexps + urlDomains: new Map(), // getDomain() results for 100 last checked urls + emptyCode: new Map(), // entire code is comments/whitespace/@namespace mutex: { - inProgress: false, - onDone: [], + inProgress: false, // while getStyles() is reading IndexedDB all subsequent calls + onDone: [], // to getStyles() are queued and resolved when the first one finishes }, }; @@ -56,7 +56,6 @@ function getStyles(options, callback) { } cachedStyles.mutex.inProgress = true; - //const t0 = performance.now(); getDatabase(db => { const tx = db.transaction(['styles'], 'readonly'); const os = tx.objectStore('styles'); @@ -67,7 +66,6 @@ function getStyles(options, callback) { cachedStyles.byId.set(style.id, style); compileStyleRegExps({style}); } - //console.debug('%s getStyles %s, invoking cached callbacks: %o', (performance.now() - t0).toFixed(1), JSON.stringify(options), cachedStyles.mutex.onDone.map(e => JSON.stringify(e.options))); // eslint-disable-line max-len callback(filterStyles(options)); cachedStyles.mutex.inProgress = false; @@ -88,7 +86,6 @@ function filterStyles({ asHash = null, strictRegexp = true, // used by the popup to detect bad regexps } = {}) { - //const t0 = performance.now(); enabled = fixBoolean(enabled); id = id === null ? null : Number(id); @@ -97,11 +94,8 @@ function filterStyles({ && id === null && matchUrl === null && asHash != true) { - //console.debug('%c%s filterStyles SKIPPED LOOP %s', 'color:gray', (performance.now() - t0).toFixed(1), enabled, id, asHash, strictRegexp, matchUrl); // eslint-disable-line max-len return cachedStyles.list; } - // silence the inapplicable warning for async code - // eslint-disable-next-line no-use-before-define const disableAll = asHash && prefs.get('disableAll', false); if (matchUrl && matchUrl.startsWith(URLS.chromeWebStore)) { @@ -114,7 +108,6 @@ function filterStyles({ const cacheKey = ' ' + enabled + url + '\t' + id + matchUrl + '\t' + asHash + strictRegexp; const cached = cachedStyles.filters.get(cacheKey); if (cached) { - //console.debug('%c%s filterStyles REUSED RESPONSE %s', 'color:gray', (performance.now() - t0).toFixed(1), enabled, id, asHash, strictRegexp, matchUrl); // eslint-disable-line max-len cached.hits++; cached.lastHit = Date.now(); @@ -144,8 +137,8 @@ function filterStyles({ for (let i = 0, style; (style = styles[i]); i++) { if ((enabled === null || style.enabled == enabled) - && (url === null || style.url == url) - && (id === null || style.id == id)) { + && (url === null || style.url == url) + && (id === null || style.id == id)) { const sections = needSections && getApplicableSections({style, matchUrl, strictRegexp, stopOnFirst: !asHash}); if (asHash) { @@ -157,7 +150,6 @@ function filterStyles({ } } } - //console.debug('%s filterStyles %s', (performance.now() - t0).toFixed(1), enabled, id, asHash, strictRegexp, matchUrl); // eslint-disable-line max-len cachedStyles.filters.set(cacheKey, { styles: filtered, lastHit: Date.now(), @@ -188,52 +180,50 @@ function saveStyle(style) { delete style.name; } - // Update if (id !== null) { + // Update or create style.id = id; os.get(id).onsuccess = eventGet => { const existed = Boolean(eventGet.target.result); const oldStyle = Object.assign({}, eventGet.target.result); const codeIsUpdated = 'sections' in style && !styleSectionsEqual(style, oldStyle); - style = Object.assign(oldStyle, style); - addMissingStyleTargets(style); - os.put(style).onsuccess = eventPut => { - style.id = style.id || eventPut.target.result; - invalidateCache(notify, existed ? {updated: style} : {added: style}); - compileStyleRegExps({style}); - if (notify) { - notifyAllTabs({ - method: existed ? 'styleUpdated' : 'styleAdded', - style, codeIsUpdated, reason, - }); - } - resolve(style); - }; + write(Object.assign(oldStyle, style), {existed, codeIsUpdated}); }; - return; + } else { + // Create + delete style.id; + write(Object.assign({ + // Set optional things if they're undefined + enabled: true, + updateUrl: null, + md5Url: null, + url: null, + originalMd5: null, + }, style)); } - // Create - delete style.id; - style = Object.assign({ - // Set optional things if they're undefined - enabled: true, - updateUrl: null, - md5Url: null, - url: null, - originalMd5: null, - }, style); - addMissingStyleTargets(style); - os.add(style).onsuccess = event => { - // Give it the ID that was generated - style.id = event.target.result; - invalidateCache(notify, {added: style}); - compileStyleRegExps({style}); - if (notify) { - notifyAllTabs({method: 'styleAdded', style, reason}); - } - resolve(style); - }; + function write(style, {existed, codeIsUpdated} = {}) { + style.sections = (style.sections || []).map(section => + Object.assign({ + urls: [], + urlPrefixes: [], + domains: [], + regexps: [], + }, section) + ); + os.put(style).onsuccess = event => { + style.id = style.id || event.target.result; + invalidateCache(existed ? {updated: style} : {added: style}); + compileStyleRegExps({style}); + if (notify) { + notifyAllTabs({ + method: existed ? 'styleUpdated' : 'styleAdded', + style, codeIsUpdated, reason, + }); + } + resolve(style); + }; + } }); }); } @@ -245,7 +235,7 @@ function deleteStyle({id, notify = true}) { const tx = db.transaction(['styles'], 'readwrite'); const os = tx.objectStore('styles'); os.delete(Number(id)).onsuccess = () => { - invalidateCache(notify, {deletedId: id}); + invalidateCache({deletedId: id}); if (notify) { notifyAllTabs({method: 'styleDeleted', id}); } @@ -429,15 +419,12 @@ function compileStyleRegExps({style, compileAll}) { } -function invalidateCache(andNotify, {added, updated, deletedId} = {}) { +function invalidateCache({added, updated, deletedId} = {}) { // prevent double-add on echoed invalidation const cached = added && cachedStyles.byId.get(added.id); if (cached) { return; } - if (andNotify) { - chrome.runtime.sendMessage({method: 'invalidateCache', added, updated, deletedId}); - } if (!cachedStyles.list) { return; } @@ -445,7 +432,6 @@ function invalidateCache(andNotify, {added, updated, deletedId} = {}) { const cached = cachedStyles.byId.get(updated.id); if (cached) { Object.assign(cached, updated); - //console.debug('cache: updated', updated); } cachedStyles.filters.clear(); return; @@ -453,7 +439,6 @@ function invalidateCache(andNotify, {added, updated, deletedId} = {}) { if (added) { cachedStyles.list.push(added); cachedStyles.byId.set(added.id, added); - //console.debug('cache: added', added); cachedStyles.filters.clear(); return; } @@ -463,22 +448,18 @@ function invalidateCache(andNotify, {added, updated, deletedId} = {}) { const cachedIndex = cachedStyles.list.indexOf(deletedStyle); cachedStyles.list.splice(cachedIndex, 1); cachedStyles.byId.delete(deletedId); - //console.debug('cache: deleted', deletedStyle); cachedStyles.filters.clear(); return; } } cachedStyles.list = null; - //console.debug('cache cleared'); cachedStyles.filters.clear(); } function cleanupCachedFilters({force = false} = {}) { if (!force) { - // sliding timer for 1 second - clearTimeout(cleanupCachedFilters.timeout); - cleanupCachedFilters.timeout = setTimeout(cleanupCachedFilters, 1000, {force: true}); + debounce(cleanupCachedFilters, 1000, {force: true}); return; } const size = cachedStyles.filters.size; @@ -500,19 +481,6 @@ function cleanupCachedFilters({force = false} = {}) { .sort((a, b) => a.weight - b.weight) .slice(0, size / 10 + 1) .forEach(({id}) => cachedStyles.filters.delete(id)); - cleanupCachedFilters.timeout = 0; -} - - -function addMissingStyleTargets(style) { - style.sections = (style.sections || []).map(section => - Object.assign({ - urls: [], - urlPrefixes: [], - domains: [], - regexps: [], - }, section) - ); } From db1dc0df7bcfe029f02250c4f4237c50d5a4e4e2 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 14 Apr 2017 00:44:23 +0300 Subject: [PATCH 139/235] simplify/modularize getApplicableSections --- storage.js | 150 +++++++++++++++++++++++++++-------------------------- 1 file changed, 76 insertions(+), 74 deletions(-) diff --git a/storage.js b/storage.js index a0baf42b..833f85b5 100644 --- a/storage.js +++ b/storage.js @@ -246,88 +246,90 @@ function deleteStyle({id, notify = true}) { function getApplicableSections({style, matchUrl, strictRegexp = true, stopOnFirst}) { - //let t0 = 0; + if (!matchUrl.startsWith('http') + && !matchUrl.startsWith('ftp') + && !matchUrl.startsWith('file') + && !matchUrl.startsWith(URLS.ownOrigin)) { + return []; + } const sections = []; - checkingSections: for (const section of style.sections) { - andCollect: - do { - // only http, https, file, ftp, and chrome-extension://OWN_EXTENSION_ID allowed - if (!matchUrl.startsWith('http') - && !matchUrl.startsWith('ftp') - && !matchUrl.startsWith('file') - && !matchUrl.startsWith(URLS.ownOrigin)) { - continue checkingSections; - } - if (section.urls.length == 0 - && section.domains.length == 0 - && section.urlPrefixes.length == 0 - && section.regexps.length == 0) { - break andCollect; - } - if (section.urls.indexOf(matchUrl) != -1) { - break andCollect; - } - for (const urlPrefix of section.urlPrefixes) { - if (matchUrl.startsWith(urlPrefix)) { - break andCollect; - } - } - if (section.domains.length) { - const urlDomains = cachedStyles.urlDomains.get(matchUrl) || getDomains(matchUrl); - for (const domain of urlDomains) { - if (section.domains.indexOf(domain) != -1) { - break andCollect; - } - } - } - for (const regexp of section.regexps) { - for (let pass = 1; pass <= (strictRegexp ? 1 : 2); pass++) { - const cacheKey = pass == 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp; - let rx = cachedStyles.regexps.get(cacheKey); - if (rx == false) { - // invalid regexp - break; - } - if (!rx) { - const anchored = pass == 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$'; - rx = tryRegExp(anchored); - cachedStyles.regexps.set(cacheKey, rx || false); - if (!rx) { - // invalid regexp - break; - } - } - if (rx.test(matchUrl)) { - break andCollect; - } - } - } - continue checkingSections; - } while (0); - // Collect the section if not empty or namespace-only. - // We don't check long code as it's slow both for emptyCode declared as Object - // and as Map in case the string is not the same reference used to add the item - //const t0start = performance.now(); - const code = section.code; - let isEmpty = code !== null && code.length < 1000 && cachedStyles.emptyCode.get(code); - if (isEmpty === undefined) { - isEmpty = !code || !code.trim() - || code.indexOf('@namespace') >= 0 - && code.replace(RX_CSS_COMMENTS, '').replace(RX_NAMESPACE, '').trim() == ''; - cachedStyles.emptyCode.set(code, isEmpty); - } - //t0 += performance.now() - t0start; - if (!isEmpty) { + const {urls, domains, urlPrefixes, regexps, code} = section; + if ((!urls.length && !urlPrefixes.length && !domains.length && !regexps.length + || urls.length && urls.indexOf(matchUrl) >= 0 + || urlPrefixes.length && arraySomeIsPrefix(urlPrefixes, matchUrl) + || domains.length && arraySomeIn(cachedStyles.urlDomains.get(matchUrl) || getDomains(matchUrl), domains) + || regexps.length && arraySomeMatches(regexps, matchUrl, strictRegexp) + ) && !styleCodeEmpty(code)) { sections.push(section); if (stopOnFirst) { - //t0 >= 0.1 && console.debug('%s emptyCode', t0.toFixed(1)); // eslint-disable-line no-unused-expressions - return sections; + break; } } } - //t0 >= 0.1 && console.debug('%s emptyCode', t0.toFixed(1)); // eslint-disable-line no-unused-expressions return sections; + + function arraySomeIsPrefix(array, string) { + for (const prefix of array) { + if (string.startsWith(prefix)) { + return true; + } + } + return false; + } + + function arraySomeIn(array, haystack) { + for (const el of array) { + if (haystack.indexOf(el) >= 0) { + return true; + } + } + return false; + } + + function arraySomeMatches(array, matchUrl, strictRegexp) { + for (const regexp of array) { + for (let pass = 1; pass <= (strictRegexp ? 1 : 2); pass++) { + const cacheKey = pass == 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp; + let rx = cachedStyles.regexps.get(cacheKey); + if (rx == false) { + // invalid regexp + break; + } + if (!rx) { + const anchored = pass == 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$'; + rx = tryRegExp(anchored); + cachedStyles.regexps.set(cacheKey, rx || false); + if (!rx) { + // invalid regexp + break; + } + } + if (rx.test(matchUrl)) { + return true; + } + } + } + return false; + } +} + + +function styleCodeEmpty(code) { + // Collect the section if not empty or namespace-only. + // We don't check long code as it's slow both for emptyCode declared as Object + // and as Map in case the string is not the same reference used to add the item + let isEmpty = code !== null && + code.length < 1000 && + cachedStyles.emptyCode.get(code); + if (isEmpty !== undefined) { + return isEmpty; + } + isEmpty = !code || !code.trim() + || code.indexOf('@namespace') >= 0 + && code.replace(RX_CSS_COMMENTS, '').replace(RX_NAMESPACE, '').trim() == ''; + cachedStyles.emptyCode.set(code, isEmpty); + return isEmpty; } From dcfb8ad3567fc5c58ed2f2649b10f63c0a1fdc77 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 14 Apr 2017 00:49:18 +0300 Subject: [PATCH 140/235] simplify/modularize styleSectionsEqual + 1.5x speedup thanks to checkedInB memoization + more strict type comparison + two-way array comparison (more correct, even if there's no practical difference) --- storage.js | 108 +++++++++++++++++++---------------------------------- 1 file changed, 38 insertions(+), 70 deletions(-) diff --git a/storage.js b/storage.js index 833f85b5..454f0e6a 100644 --- a/storage.js +++ b/storage.js @@ -333,69 +333,52 @@ function styleCodeEmpty(code) { } -function styleSectionsEqual(styleA, styleB) { - if (!styleA.sections || !styleB.sections) { +function styleSectionsEqual({sections: a}, {sections: b}) { + if (!a || !b) { return undefined; } - if (styleA.sections.length != styleB.sections.length) { + if (a.length != b.length) { return false; } - const propNames = ['code', 'urlPrefixes', 'urls', 'domains', 'regexps']; - const typeBcaches = []; - checkingEveryInA: - for (const sectionA of styleA.sections) { - const typeAcache = new Map(); - for (const name of propNames) { - typeAcache.set(name, getType(sectionA[name])); + const checkedInB = []; + return a.every(sectionA => b.some(sectionB => { + if (!checkedInB.includes(sectionB) && propertiesEqual(sectionA, sectionB)) { + checkedInB.push(sectionB); + return true; } - lookingForDupeInB: - for (let i = 0, sectionB; (sectionB = styleB.sections[i]); i++) { - const typeBcache = typeBcaches[i] = typeBcaches[i] || new Map(); - comparingProps: - for (const name of propNames) { - const propA = sectionA[name]; - const typeA = typeAcache.get(name); - const propB = sectionB[name]; - let typeB = typeBcache.get(name); - if (!typeB) { - typeB = getType(propB); - typeBcache.set(name, typeB); - } - if (typeA != typeB) { - const bothEmptyOrUndefined = - (typeA == 'undefined' || (typeA == 'array' && propA.length == 0)) && - (typeB == 'undefined' || (typeB == 'array' && propB.length == 0)); - if (bothEmptyOrUndefined) { - continue comparingProps; - } else { - continue lookingForDupeInB; - } - } - if (typeA == 'undefined') { - continue comparingProps; - } - if (typeA == 'array') { - if (propA.length != propB.length) { - continue lookingForDupeInB; - } - for (const item of propA) { - if (propB.indexOf(item) < 0) { - continue lookingForDupeInB; - } - } - continue comparingProps; - } - if (typeA == 'string' && propA != propB) { - continue lookingForDupeInB; - } + })); + + function propertiesEqual(secA, secB) { + for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) { + if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) { + return false; } - // dupe found - continue checkingEveryInA; } - // dupe not found - return false; + return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a == b); + } + + function equalOrEmpty(a, b, telltale, comparator) { + const typeA = a && typeof a[telltale] == 'function'; + const typeB = b && typeof b[telltale] == 'function'; + return ( + (a === null || a === undefined || (typeA && !a.length)) && + (b === null || b === undefined || (typeB && !b.length)) + ) || typeA && typeB && a.length == b.length && comparator(a, b); + } + + function arrayMirrors(array1, array2) { + for (const el of array1) { + if (array2.indexOf(el) < 0) { + return false; + } + } + for (const el of array2) { + if (array1.indexOf(el) < 0) { + return false; + } + } + return true; } - return true; } @@ -515,18 +498,3 @@ function getDomains(url) { } return domains; } - - -function getType(o) { - if (typeof o == 'undefined' || typeof o == 'string') { - return typeof o; - } - // with the persistent cachedStyles the Array reference is usually different - // so let's check for e.g. type of 'every' which is only present on arrays - // (in the context of our extension) - if (o instanceof Array || typeof o.every == 'function') { - return 'array'; - } - console.warn('Unsupported type:', o); - return 'undefined'; -} From fa46a2c336c6da8b16ca7cab0e8e4c11d32b8ecf Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 13 Apr 2017 21:03:25 +0300 Subject: [PATCH 141/235] split filterStyles() js engines don't like big functions (V8 often deoptimized the original filterStyles), it also makes sense to extract the less frequently executed code --- storage.js | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/storage.js b/storage.js index 454f0e6a..4fc861cc 100644 --- a/storage.js +++ b/storage.js @@ -110,12 +110,36 @@ function filterStyles({ if (cached) { cached.hits++; cached.lastHit = Date.now(); - return asHash ? Object.assign({disableAll}, cached.styles) : cached.styles; } + return filterStylesInternal({ + enabled, + url, + id, + matchUrl, + asHash, + strictRegexp, + disableAll, + cacheKey, + }); +} + + +function filterStylesInternal({ + // js engines don't like big functions (V8 often deoptimized the original filterStyles) + // it also makes sense to extract the less frequently executed code + enabled, + url, + id, + matchUrl, + asHash, + strictRegexp, + disableAll, + cacheKey, +}) { if (matchUrl && !cachedStyles.urlDomains.has(matchUrl)) { cachedStyles.urlDomains.set(matchUrl, getDomains(matchUrl)); for (let i = cachedStyles.urlDomains.size - 100; i > 0; i--) { @@ -133,6 +157,7 @@ function filterStyles({ // of edit.html with a non-existent style id parameter return filtered; } + const needSections = asHash || matchUrl !== null; for (let i = 0, style; (style = styles[i]); i++) { @@ -150,6 +175,7 @@ function filterStyles({ } } } + cachedStyles.filters.set(cacheKey, { styles: filtered, lastHit: Date.now(), @@ -158,6 +184,7 @@ function filterStyles({ if (cachedStyles.filters.size > 10000) { cleanupCachedFilters(); } + return asHash ? Object.assign({disableAll}, filtered) : filtered; From bec60f54abd7ecdb7959658b51c0ce1fd6ac0ca3 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 13 Apr 2017 21:30:24 +0300 Subject: [PATCH 142/235] fixup ccbccae2: add default CM theme entry for firefox --- background.js | 1 + 1 file changed, 1 insertion(+) diff --git a/background.js b/background.js index 4c2bea8d..4eee2c94 100644 --- a/background.js +++ b/background.js @@ -311,6 +311,7 @@ function updateIcon(tab, styles) { function getCodeMirrorThemes() { if (!chrome.runtime.getPackageDirectoryEntry) { return Promise.resolve([ + chrome.i18n.getMessage('defaultTheme'), '3024-day', '3024-night', 'abcdef', From bdcac21d7e1c83ef12b6946499f32702dab46908 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 14 Apr 2017 10:50:52 +0300 Subject: [PATCH 143/235] optionsUI: group options and shorten labels --- _locales/en/messages.json | 25 ++++++--- options/index.css | 80 ++++++++++++++------------ options/index.html | 115 ++++++++++++++++++++------------------ options/index.js | 29 +++++----- 4 files changed, 136 insertions(+), 113 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 9354a899..ec51af06 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -387,11 +387,11 @@ "description": "Subheading for options section on manage page." }, "popupStylesFirst": { - "message": "List styles before commands in the toolbar button menu", - "description": "Label for the checkbox controlling section order in the toolbar button menu." + "message": "Styles before commands", + "description": "Label for the checkbox controlling section order in the popup." }, "prefShowBadge": { - "message": "Show number of styles active for the current site on the toolbar button", + "message": "Number of styles active for the current site", "description": "Label for the checkbox controlling toolbar badge text." }, "replace": { @@ -606,13 +606,13 @@ "message": "Import styles" }, "optionsBadgeNormal": { - "message": "Badge background color" + "message": "Background color" }, "optionsBadgeDisabled": { - "message": "Badge background color (when disabled)" + "message": "Background color when disabled" }, "optionsPopupWidth": { - "message": "Popup width (in pixels)" + "message": "Width (in pixels)" }, "optionsUpdateInterval": { "message": "Automatically check for and install all available userstyle updates (in hrs)" @@ -620,13 +620,22 @@ "optionsUpdateIntervalNote": { "message": "To disable the automatic userstyle update checks, set interval to 0" }, - "optionsCustomize": { - "message": "UI Customizations" + "optionsCustomizeBadge": { + "message": "Badge on the toolbar icon" + }, + "optionsCustomizePopup": { + "message": "Popup" + }, + "optionsCustomizeUpdate": { + "message": "Updates" }, "optionsActions": { "message": "Actions" }, "optionsReset": { + "message": "Reset the options to default values" + }, + "optionsResetButton": { "message": "Reset" }, "optionsOpenManager": { diff --git a/options/index.css b/options/index.css index 6c8c7365..7b6185e3 100644 --- a/options/index.css +++ b/options/index.css @@ -7,41 +7,63 @@ body { margin: 0; font-family: "Helvetica Neue", Helvetica, sans-serif; font-size: 12px; - display: flex; - flex-direction: column; -} - -body > * { - padding: .5rem 1.5rem 1em 48px; -} - -body > *:first-child { - padding-top: .75rem; + width: calc(16px + 100px + 8px + 240px + 8px + 80px + 4px + 16px); } .firefox .chromium-only { display: none; } -table { - width: 100%; - border-collapse: collapse; +.block { + display: flex; + align-items: center; + margin: 1em 0; + border-bottom: 1px dotted #ccc; + padding: 0 0 1em 16px; } -td { - padding: 2px 0; +.block:last-child { + border-bottom: none; + padding-bottom: 4px; } -td:last-child { - text-align: right; +h1 { + width: 100px; + margin: 0; + font-size: 120%; + font-weight: bold; + padding-right: 8px; +} + +label { + display: block; + white-space: nowrap; + margin: .25ex 0; +} + +label > * { + display: inline-block; + vertical-align: middle; +} + +label > :first-child { + width: 240px; + white-space: normal; + margin-right: 8px; +} + +label:not([disabled]) > :first-child { + cursor: default; +} + +label:not([disabled]):hover > :first-child { + text-shadow: 0 0 0.01px rgba(0, 0, 0, .25); } button, -td:last-child, input[type=number], input[type="color"], -.onoffswitch, -#update-counter { +.onoffswitch { width: 80px; box-sizing: border-box; } @@ -63,18 +85,9 @@ input[type=number]:invalid { color: darkred; } -#actions { - margin-top: -2em; -} - -#reset { - text-align: right; - position: relative; -} - #notes { background-color: #f4f4f4; - margin-top: .75rem; + padding: 1.5ex 16px 1ex calc(16px + 2ex); font-size: 90%; color: #999; } @@ -101,12 +114,6 @@ input[type="color"] { color: black; } -#update-counter { - margin-top: .5ex; - position: absolute; - text-align: center; -} - @keyframes fadeinout { 0% { opacity: 0 } 10% { opacity: 1 } @@ -118,6 +125,7 @@ input[type="color"] { .onoffswitch { position: relative; + margin: 1ex 0; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; diff --git a/options/index.html b/options/index.html index b98c447b..df23c4b1 100644 --- a/options/index.html +++ b/options/index.html @@ -11,64 +11,69 @@ -
    -

    - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    +
    +
    +

    +
    + + +
    -
    -
    - - -
    -
    1
    -
    - + + + +
    -
    - -
    -

    - - - - - - - - - -
    2
    +
    +

    +
    + + +
    +
    +
    +

    +
    + +
    +
    +
    +

    +
    + + +
    + +
    +
    diff --git a/options/index.js b/options/index.js index 19ecf930..09eac5a6 100644 --- a/options/index.js +++ b/options/index.js @@ -19,31 +19,33 @@ document.onclick = e => { let total = 0; let updated = 0; - function showProgress() { - $('#update-counter').textContent = `${updated} / ${total}`; - } - - function done(target) { - target.disabled = false; - window.setTimeout(() => { - $('#update-counter').textContent = ''; - }, 750); - } - function check() { + const originalLabel = e.target.textContent; + e.target.disabled = true; + e.target.parentElement.setAttribute('disabled', ''); + function showProgress() { + e.target.textContent = `${updated} / ${total}`; + } + function done() { + setTimeout(() => { + e.target.disabled = false; + e.target.textContent = originalLabel; + e.target.parentElement.removeAttribute('disabled'); + }, 750); + } BG.update.perform((cmd, value) => { switch (cmd) { case 'count': total = value; if (!total) { - done(e.target); + done(); } break; case 'single-updated': case 'single-skipped': updated++; if (total && updated === total) { - done(e.target); + done(); } break; } @@ -61,7 +63,6 @@ document.onclick = e => { break; case 'check-updates': - e.target.disabled = true; check(); break; From eb37b3e4adf0a9553d291337e1f94379a1111f4e Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 14 Apr 2017 14:08:31 +0300 Subject: [PATCH 144/235] optionsUI: center in opera correctly --- options/index.css | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/options/index.css b/options/index.css index 7b6185e3..96c93fa1 100644 --- a/options/index.css +++ b/options/index.css @@ -1,6 +1,10 @@ -html { - max-width: 40em; - margin: auto; +html.opera { + text-align: center; +} + +html.opera body { + display: inline-block; + text-align: initial; } body { From ba02bc52a1f21c8d3cd7fdacbbef415a39e3263f Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 14 Apr 2017 14:13:02 +0300 Subject: [PATCH 145/235] optionsUI: left padding in firefox is 6px --- options/index.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/options/index.css b/options/index.css index 96c93fa1..bad4ca01 100644 --- a/options/index.css +++ b/options/index.css @@ -7,6 +7,14 @@ html.opera body { text-align: initial; } +html.firefox .block { + padding-left: 6px; +} + +html.firefox #notes { + padding-left: calc(6px + 2ex); +} + body { margin: 0; font-family: "Helvetica Neue", Helvetica, sans-serif; From 142666ac0f259273634498609ca1e55c2157bdbd Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 14 Apr 2017 18:30:09 +0300 Subject: [PATCH 146/235] optionsUI: show progress bar and # of installed updates --- _locales/en/messages.json | 4 ++++ options/index.css | 38 ++++++++++++++++++++++++++++++++++++++ options/index.html | 7 ++++--- options/index.js | 36 +++++++++++++++++++++--------------- 4 files changed, 67 insertions(+), 18 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index ec51af06..6944f367 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -591,6 +591,10 @@ "message": "Update completed.", "description": "Text that displays when an update completed" }, + "updatesCurrentlyInstalled": { + "message": "Updates installed:", + "description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates." + }, "writeStyleFor": { "message": "Write style for: ", "description": "Label for toolbar pop-up that precedes the links to write a new style" diff --git a/options/index.css b/options/index.css index bad4ca01..7d20ed45 100644 --- a/options/index.css +++ b/options/index.css @@ -45,6 +45,7 @@ h1 { font-size: 120%; font-weight: bold; padding-right: 8px; + word-wrap: break-word; } label { @@ -97,6 +98,43 @@ input[type=number]:invalid { color: darkred; } +[data-cmd="check-updates"] button { + position: relative; +} + +.update-in-progress [data-cmd="check-updates"] { + opacity: .5; + pointer-events: none; +} + +.update-in-progress #update-progress { + position: absolute; + top: 0; + left: 0; + bottom: 0; + background-color: currentColor; + content: ""; + opacity: .35; +} + +#updates-installed { + position: absolute; + font-size: 85%; + right: 16px; + margin-top: 1px; +} + +#updates-installed:after { + content: attr(data-value); + margin-left: .5ex; + font-weight: bold; +} + +#updates-installed:not([data-value]), +#updates-installed[data-value=""] { + display: none; +} + #notes { background-color: #f4f4f4; padding: 1.5ex 16px 1ex calc(16px + 2ex); diff --git a/options/index.html b/options/index.html index df23c4b1..527f2c17 100644 --- a/options/index.html +++ b/options/index.html @@ -66,12 +66,13 @@ -
    diff --git a/options/index.js b/options/index.js index 09eac5a6..6da43d07 100644 --- a/options/index.js +++ b/options/index.js @@ -15,23 +15,27 @@ $('[data-cmd="open-keyboard"]').href = URLS.configureCommands; // actions document.onclick = e => { - const cmd = e.target.dataset.cmd; - let total = 0; - let updated = 0; + const target = e.target.closest('[data-cmd]'); + if (!target) { + return; + } + // prevent double-triggering in case a sub-element was clicked + e.stopPropagation(); function check() { - const originalLabel = e.target.textContent; - e.target.disabled = true; - e.target.parentElement.setAttribute('disabled', ''); + let total = 0; + let checked = 0; + let updated = 0; + $('#update-progress').style.width = 0; + $('#updates-installed').dataset.value = ''; + document.body.classList.add('update-in-progress'); + const maxWidth = $('#update-progress').parentElement.clientWidth; function showProgress() { - e.target.textContent = `${updated} / ${total}`; + $('#update-progress').style.width = Math.round(checked / total * maxWidth) + 'px'; + $('#updates-installed').dataset.value = updated || ''; } function done() { - setTimeout(() => { - e.target.disabled = false; - e.target.textContent = originalLabel; - e.target.parentElement.removeAttribute('disabled'); - }, 750); + document.body.classList.remove('update-in-progress'); } BG.update.perform((cmd, value) => { switch (cmd) { @@ -42,9 +46,11 @@ document.onclick = e => { } break; case 'single-updated': - case 'single-skipped': updated++; - if (total && updated === total) { + // fallthrough + case 'single-skipped': + checked++; + if (total && checked === total) { done(); } break; @@ -57,7 +63,7 @@ document.onclick = e => { }); } - switch (cmd) { + switch (target.dataset.cmd) { case 'open-manage': openURL({url: '/manage.html'}); break; From d8adb582c6963efa781c9b0f810613a78088fdcd Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 14 Apr 2017 21:30:55 +0300 Subject: [PATCH 147/235] add missing favicons in-place instead of full rerendering --- manage.js | 75 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/manage.js b/manage.js index ad50c44f..dcc3be73 100644 --- a/manage.js +++ b/manage.js @@ -136,7 +136,7 @@ function createStyleElement({style, name}) { newUI: newUI.enabled, entry, entryClassBase: entry.className, - checker: $('.checker', entry), + checker: $('.checker', entry) || {}, nameLink: $('.style-name-link', entry), editLink: $('.style-edit-link', entry) || {}, editHrefBase: $('.style-name-link, .style-edit-link', entry).getAttribute('href'), @@ -152,19 +152,29 @@ function createStyleElement({style, name}) { }; } const parts = createStyleElement.parts; - Object.assign(parts.entry, { - className: parts.entryClassBase + ' ' + - (style.enabled ? 'enabled' : 'disabled') + - (style.updateUrl ? ' updatable' : ''), - id: 'style-' + style.id, - }); - + parts.checker.checked = style.enabled; parts.nameLink.textContent = style.name; parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id; parts.homepage.href = parts.homepage.title = style.url || ''; - // .targets may be a large list so we clone it separately - // and paste into the cloned entry in the end + const entry = parts.entry.cloneNode(true); + entry.id = 'style-' + style.id; + entry.styleId = style.id; + entry.styleNameLowerCase = name || style.name.toLocaleLowerCase(); + entry.className = parts.entryClassBase + ' ' + + (style.enabled ? 'enabled' : 'disabled') + + (style.updateUrl ? ' updatable' : ''); + + // name being supplied signifies we're invoked by showStyles() + // which debounces its main loop thus loading the postponed favicons + createStyleTargetsElement({entry, style, postponeFavicons: name}); + + return entry; +} + + +function createStyleTargetsElement({entry, style, postponeFavicons}) { + const parts = createStyleElement.parts; const targets = parts.targets.cloneNode(true); let container = targets; let numTargets = 0; @@ -209,27 +219,21 @@ function createStyleElement({style, name}) { } } } - if (newUI.enabled) { - parts.checker.checked = style.enabled; - parts.appliesTo.classList.toggle('has-more', numTargets > newUI.targets); - // name is supplied by showStyles so we let it decide when to load the icons - if (numIcons && !name) { + if (numTargets > newUI.targets) { + $('.applies-to', entry).classList.add('has-more'); + } + if (numIcons && !postponeFavicons) { debounce(handleEvent.loadFavicons); } } - - const newEntry = parts.entry.cloneNode(true); - newEntry.styleId = style.id; - newEntry.styleNameLowerCase = name || style.name.toLocaleLowerCase(); - const newTargets = $('.targets', newEntry); + const entryTargets = $('.targets', entry); if (numTargets) { - newTargets.parentElement.replaceChild(targets, newTargets); + entryTargets.parentElement.replaceChild(targets, entryTargets); } else { - newTargets.appendChild(template.appliesToEverything.cloneNode(true)); - newEntry.classList.add('global'); + entryTargets.appendChild(template.appliesToEverything.cloneNode(true)); } - return newEntry; + entry.classList.toggle('global', !numTargets); } @@ -405,14 +409,33 @@ function switchUI({styleOnly} = {}) { } `; - if (!styleOnly && (stateToggled || missingFavicons)) { + if (styleOnly) { + return; + } + + if (stateToggled || missingFavicons && !createStyleElement.parts) { installed.innerHTML = ''; getStylesSafe().then(showStyles); - } else if (targetsChanged) { + return; + } + if (targetsChanged) { for (const targets of $$('.entry .targets')) { const hasMore = targets.children.length > newUI.targets; targets.parentElement.classList.toggle('has-more', hasMore); } + return; + } + if (missingFavicons) { + getStylesSafe().then(styles => { + for (const style of styles) { + const entry = $('#style-' + style.id); + if (entry) { + createStyleTargetsElement({entry, style, postponeFavicons: true}); + } + } + debounce(handleEvent.loadFavicons); + }); + return; } } From e21d65217a0f882803455b7eea4e9a56e12f421d Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 14 Apr 2017 23:25:54 +0300 Subject: [PATCH 148/235] manage: show progress bar on update check --- manage.css | 18 +++++++++++++++ manage.html | 2 +- manage.js | 63 ++++++++++++++++++++++++++++++++--------------------- 3 files changed, 57 insertions(+), 26 deletions(-) diff --git a/manage.css b/manage.css index 5faa7ec9..e78a6e5c 100644 --- a/manage.css +++ b/manage.css @@ -444,6 +444,24 @@ input[id^="manage.newUI"] { display: none; } +#apply-all-updates:after { + content: " (" attr(data-value) ")"; +} + +#check-all-updates[disabled] { + position: relative; +} + +#check-all-updates[disabled] #update-progress { + position: absolute; + top: 0; + left: 0; + bottom: 0; + background-color: currentColor; + content: ""; + opacity: .35; +} + /* highlight updated/added styles */ .highlight { animation: highlight 10s cubic-bezier(0,.82,.47,.98); diff --git a/manage.html b/manage.html index ba05f28e..ff787a50 100644 --- a/manage.html +++ b/manage.html @@ -134,7 +134,7 @@

    - +

    diff --git a/manage.js b/manage.js index dcc3be73..c514d8ee 100644 --- a/manage.js +++ b/manage.js @@ -365,7 +365,7 @@ function handleUpdate(style, {reason} = {}) { if (reason == 'update') { element.classList.add('update-done'); element.classList.remove('can-update', 'updatable'); - $('.update-note', element).innerHTML = t('updateCompleted'); + $('.update-note', element).textContent = t('updateCompleted'); renderUpdatesOnlyFilter(); } } @@ -444,13 +444,12 @@ function applyUpdateAll() { const btnApply = $('#apply-all-updates'); btnApply.disabled = true; setTimeout(() => { - btnApply.style.display = 'none'; + btnApply.classList.add('hidden'); btnApply.disabled = false; }, 1000); $$('.can-update .update').forEach(button => { - // align to the bottom of the visible area if wasn't visible - button.scrollIntoView(false); + scrollElementIntoView(button); button.click(); }); @@ -462,37 +461,51 @@ function checkUpdateAll() { const btnCheck = $('#check-all-updates'); const btnApply = $('#apply-all-updates'); const noUpdates = $('#update-all-no-updates'); + const progress = $('#update-progress'); btnCheck.disabled = true; btnApply.classList.add('hidden'); noUpdates.classList.add('hidden'); + const maxWidth = progress.parentElement.clientWidth; - Promise.all($$('.updatable:not(.can-update)').map(checkUpdate)) - .then(updatables => { - btnCheck.disabled = false; - const numUpdatable = updatables.filter(u => u).length; - if (numUpdatable) { - btnApply.classList.remove('hidden'); - btnApply.originalLabel = btnApply.originalLabel || btnApply.textContent; - btnApply.textContent = btnApply.originalLabel + ` (${numUpdatable})`; - renderUpdatesOnlyFilter({check: true}); - } else { - noUpdates.classList.remove('hidden'); - setTimeout(() => { - noUpdates.classList.add('hidden'); - }, 10e3); - } - }); - + const queue = $$('.updatable:not(.can-update)').map(checkUpdate); + const total = queue.length; + let updatesFound = false; + let checked = 0; + processQueue(); // notify the automatic updater to reset the next automatic update accordingly chrome.runtime.sendMessage({ method: 'resetInterval' }); + + function processQueue(status) { + if (status === true) { + updatesFound = true; + btnApply.disabled = true; + btnApply.classList.remove('hidden'); + renderUpdatesOnlyFilter({check: true}); + } + if (checked < total) { + queue[checked++].then(status => { + progress.style.width = Math.round(checked / total * maxWidth) + 'px'; + setTimeout(processQueue, 0, status); + }); + return; + } + btnCheck.disabled = false; + btnApply.disabled = false; + if (!updatesFound) { + noUpdates.classList.remove('hidden'); + setTimeout(() => { + noUpdates.classList.add('hidden'); + }, 10e3); + } + } } function checkUpdate(element) { - $('.update-note', element).innerHTML = t('checkingForUpdate'); + $('.update-note', element).textContent = t('checkingForUpdate'); $('.check-update', element).title = ''; element.classList.remove('checking-update', 'no-update', 'update-problem'); element.classList.add('checking-update'); @@ -563,12 +576,12 @@ class Updater { if (json) { this.element.classList.add('can-update'); this.element.updatedCode = json; - $('.update-note', this.element).innerHTML = ''; + $('.update-note', this.element).textContent = ''; $('#onlyUpdates').classList.remove('hidden'); } else { this.element.classList.add('no-update'); this.element.classList.toggle('update-problem', Boolean(message)); - $('.update-note', this.element).innerHTML = message || t('updateCheckSucceededNoUpdate'); + $('.update-note', this.element).textContent = message || t('updateCheckSucceededNoUpdate'); if (newUI.enabled) { $('.check-update', this.element).title = message; } @@ -614,7 +627,7 @@ function renderUpdatesOnlyFilter({show, check} = {}) { const btnApply = $('#apply-all-updates'); if (!btnApply.matches('.hidden')) { if (canUpdate) { - btnApply.textContent = btnApply.originalLabel + ` (${numUpdatable})`; + btnApply.dataset.value = numUpdatable; } else { btnApply.classList.add('hidden'); } From 3961224f8041c131fe4c4614270de1ce1572718e Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 14 Apr 2017 23:38:10 +0300 Subject: [PATCH 149/235] differentiate .update-done by doubling the checkmark --- manage.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/manage.css b/manage.css index e78a6e5c..bd39055f 100644 --- a/manage.css +++ b/manage.css @@ -264,6 +264,14 @@ summary { display: inline; } +.newUI .update-done .updated svg { + top: -2px; + position: relative; + /* unprefixed since Chrome 53 */ + -webkit-filter: drop-shadow(0 4px 0 currentColor); + filter: drop-shadow(0 4px 0 currentColor); +} + .newUI .can-update .update, .newUI .no-update.update-problem .check-update { cursor: pointer; From e61a24b4e409e8baae9cd7f6e137087afc67ba29 Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 15 Apr 2017 12:24:14 +0300 Subject: [PATCH 150/235] refactor install.js * run_at: document_start * MutationObserver to rebrand without flickering the original text * reuse styleSectionsEqual * don't proceed when orphaned --- install.js | 385 +++++++++++++++++++++++++++++--------------------- manifest.json | 2 +- 2 files changed, 222 insertions(+), 165 deletions(-) diff --git a/install.js b/install.js index f8a74fb4..d374a810 100644 --- a/install.js +++ b/install.js @@ -1,188 +1,245 @@ -/* eslint-disable no-tabs, indent, quotes, no-var */ 'use strict'; -chrome.runtime.sendMessage({method: "getStyles", url: getMeta("stylish-id-url") || location.href}, function(response) { - if (response.length == 0) { - sendEvent("styleCanBeInstalledChrome"); - } else { - var installedStyle = response[0]; - // maybe an update is needed - // use the md5 if available - var md5Url = getMeta("stylish-md5-url"); - if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) { - getResource(md5Url, function(md5) { - if (md5 == installedStyle.originalMd5) { - sendEvent("styleAlreadyInstalledChrome", {updateUrl: installedStyle.updateUrl}); - } else { - sendEvent("styleCanBeUpdatedChrome", {updateUrl: installedStyle.updateUrl}); - } - }); - } else { - getResource(getMeta("stylish-code-chrome"), function(code) { - // this would indicate a failure (a style with settings?). - if (code === null) { - sendEvent("styleCanBeUpdatedChrome", {updateUrl: installedStyle.updateUrl}); - } - var json = JSON.parse(code); - if (json.sections.length == installedStyle.sections.length) { - if (json.sections.every(function(section) { - return installedStyle.sections.some(function(installedSection) { - return sectionsAreEqual(section, installedSection); - }); - })) { - // everything's the same - sendEvent("styleAlreadyInstalledChrome", {updateUrl: installedStyle.updateUrl}); - return; - } - } - sendEvent("styleCanBeUpdatedChrome", {updateUrl: installedStyle.updateUrl}); - }); - } - } +document.addEventListener('stylishUpdateChrome', onUpdateClicked); +document.addEventListener('stylishInstallChrome', onInstallClicked); + +new MutationObserver(waitForBody) + .observe(document.documentElement, {childList: true}); + +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + // orphaned content script check + if (msg.method == 'ping') { + sendResponse(true); + } }); -function sectionsAreEqual(a, b) { - if (a.code != b.code) { - return false; - } - return ["urls", "urlPrefixes", "domains", "regexps"].every(function(attribute) { - return arraysAreEqual(a[attribute], b[attribute]); - }); + +function waitForBody() { + if (!document.body) { + return; + } + + this.disconnect(); + rebrand([{addedNodes: [document.body]}]); + const rebrandObserver = new MutationObserver(rebrand); + rebrandObserver.observe(document.body, {childList: true, subtree: true}); + + document.addEventListener('DOMContentLoaded', function _() { + document.removeEventListener('DOMContentLoaded', _); + rebrandObserver.disconnect(); + chrome.runtime.sendMessage({ + method: 'getStyles', + url: getMeta('stylish-id-url') || location.href + }, checkUpdatability); + }); } -function arraysAreEqual(a, b) { - // treat empty array and undefined as equivalent - if (typeof a == "undefined") { - return (typeof b == "undefined") || (b.length == 0); - } - if (typeof b == "undefined") { - return (typeof a == "undefined") || (a.length == 0); - } - if (a.length != b.length) { - return false; - } - return a.every(function(entry) { - return b.indexOf(entry) != -1; - }); + +function checkUpdatability([installedStyle]) { + if (!installedStyle) { + sendEvent('styleCanBeInstalledChrome'); + return; + } + const md5Url = getMeta('stylish-md5-url'); + if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) { + getResource(md5Url).then(md5 => { + reportUpdatable(md5 != installedStyle.originalMd5); + }); + } else { + getResource(getMeta('stylish-code-chrome')).then(code => { + reportUpdatable(code === null || + !styleSectionsEqual(JSON.parse(code), installedStyle)); + }); + } + + function reportUpdatable(isUpdatable) { + sendEvent( + isUpdatable + ? 'styleCanBeUpdatedChrome' + : 'styleAlreadyInstalledChrome', + { + updateUrl: installedStyle.updateUrl + } + ); + } } -function sendEvent(type, data) { - if (typeof data == "undefined") { - data = null; - } - var stylishEvent = new CustomEvent(type, {detail: data}); - document.dispatchEvent(stylishEvent); + +function sendEvent(type, detail = null) { + detail = {detail}; + if (typeof cloneInto != 'undefined') { + // Firefox requires explicit cloning, however USO can't process our messages anyway + // because USO tries to use a global "event" variable deprecated in Firefox + detail = cloneInto(detail, document); // eslint-disable-line no-undef + } + document.dispatchEvent(new CustomEvent(type, detail)); } -document.addEventListener("stylishUpdateChrome", stylishUpdateChrome); -function stylishInstallChrome() { - orphanCheck(); - getResource(getMeta("stylish-description"), function(name) { - if (confirm(chrome.i18n.getMessage('styleInstall', [name]))) { - getResource(getMeta("stylish-code-chrome"), function(code) { - // check for old style json - var json = JSON.parse(code); - json.method = "saveStyle"; - chrome.runtime.sendMessage(json, function() { - sendEvent("styleInstalledChrome"); - }); - }); - getResource(getMeta("stylish-install-ping-url-chrome")); - } - }); + +function onInstallClicked() { + if (!orphanCheck()) { + return; + } + getResource(getMeta('stylish-description')) + .then(name => saveStyleCode('styleInstall', name)) + .then(() => getResource(getMeta('stylish-install-ping-url-chrome'))); } -document.addEventListener("stylishInstallChrome", stylishInstallChrome); -function stylishUpdateChrome() { - orphanCheck(); - chrome.runtime.sendMessage({ - method: "getStyles", - url: getMeta("stylish-id-url") || location.href, - }, function(response) { - var style = response[0]; - if (confirm(chrome.i18n.getMessage('styleUpdate', [style.name]))) { - getResource(getMeta("stylish-code-chrome"), function(code) { - var json = JSON.parse(code); - json.method = "saveStyle"; - json.id = style.id; - chrome.runtime.sendMessage(json, function() { - sendEvent("styleInstalledChrome"); - }); - }); - } - }); + +function onUpdateClicked() { + if (!orphanCheck()) { + return; + } + chrome.runtime.sendMessage({ + method: 'getStyles', + url: getMeta('stylish-id-url') || location.href, + }, ([style]) => { + saveStyleCode('styleUpdate', style.name, {id: style.id}); + }); } + +function saveStyleCode(message, name, addProps) { + return new Promise(resolve => { + if (!confirm(chrome.i18n.getMessage(message, [name]))) { + return; + } + getResource(getMeta('stylish-code-chrome')).then(code => { + chrome.runtime.sendMessage( + Object.assign(JSON.parse(code), addProps, {method: 'saveStyle'}), + () => sendEvent('styleInstalledChrome') + ); + resolve(); + }); + }); +} + + function getMeta(name) { - var e = document.querySelector("link[rel='" + name + "']"); - return e ? e.getAttribute("href") : null; + const e = document.querySelector(`link[rel="${name}"]`); + return e ? e.getAttribute('href') : null; } -function getResource(url, callback) { - if (url.indexOf("#") == 0) { - if (callback) { - callback(document.getElementById(url.substring(1)).innerText); - } - return; - } - var xhr = new XMLHttpRequest(); - xhr.onreadystatechange = function() { - if (xhr.readyState == 4 && callback) { - if (xhr.status >= 400) { - callback(null); - } else { - callback(xhr.responseText); - } - } - }; - if (url.length > 2000) { - var parts = url.split("?"); - xhr.open("POST", parts[0], true); - xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); - xhr.send(parts[1]); - } else { - xhr.open("GET", url, true); - xhr.send(); - } + +function getResource(url) { + if (url.startsWith('#')) { + return Promise.resolve(document.getElementById(url.slice(1)).textContent); + } + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.onloadend = () => resolve(xhr.status < 400 ? xhr.responseText : null); + if (url.length > 2000) { + const [mainUrl, query] = url.split('?'); + xhr.open('POST', mainUrl, true); + xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + xhr.send(query); + } else { + xhr.open('GET', url); + xhr.send(); + } + }); } -/* stylish to stylus; https://github.com/schomery/stylish-chrome/issues/12 */ -(function(es) { - es.forEach(e => { - [...e.childNodes].filter(n => n.nodeType == 3).forEach(n => { - n.nodeValue = n.nodeValue.replace('Stylish', 'Stylus'); - }); - }); -})([ - ...document.querySelectorAll('div[id^="stylish-installed-style-not-installed-"]'), - ...document.querySelectorAll('div[id^="stylish-installed-style-needs-update-"]') -]); -// orphaned content script check +function rebrand(mutations) { + /* stylish to stylus; https://github.com/schomery/stylish-chrome/issues/12 */ + for (const mutation of mutations) { + for (const addedNode of mutation.addedNodes) { + if (addedNode.nodeType != Node.ELEMENT_NODE) { + continue; + } + const elementsToCheck = addedNode.matches('.install-status') ? [addedNode] + : Array.prototype.slice.call(addedNode.getElementsByClassName('install-status')); + for (const el of elementsToCheck) { + if (!el.textContent.includes('Stylish')) { + continue; + } + const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); + while (walker.nextNode()) { + const node = walker.currentNode; + const text = node.nodeValue; + if (text.includes('Stylish') && node.parentNode.localName != 'a') { + node.nodeValue = text.replace(/Stylish/g, 'Stylus'); + } + } + } + } + } +} + + +function styleSectionsEqual({sections: a}, {sections: b}) { + if (!a || !b) { + return undefined; + } + if (a.length != b.length) { + return false; + } + const checkedInB = []; + return a.every(sectionA => b.some(sectionB => { + if (!checkedInB.includes(sectionB) && propertiesEqual(sectionA, sectionB)) { + checkedInB.push(sectionB); + return true; + } + })); + + function propertiesEqual(secA, secB) { + for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) { + if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) { + return false; + } + } + return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a == b); + } + + function equalOrEmpty(a, b, telltale, comparator) { + const typeA = a && typeof a[telltale] == 'function'; + const typeB = b && typeof b[telltale] == 'function'; + return ( + (a === null || a === undefined || (typeA && !a.length)) && + (b === null || b === undefined || (typeB && !b.length)) + ) || typeA && typeB && a.length == b.length && comparator(a, b); + } + + function arrayMirrors(array1, array2) { + for (const el of array1) { + if (array2.indexOf(el) < 0) { + return false; + } + } + for (const el of array2) { + if (array1.indexOf(el) < 0) { + return false; + } + } + return true; + } +} -chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => - msg.method == 'ping' && sendResponse(true)); function orphanCheck() { - var port = chrome.runtime.connect(); - if (port) { - port.disconnect(); - return; - } - // we're orphaned due to an extension update - // we can detach event listeners - document.removeEventListener('stylishUpdateChrome', stylishUpdateChrome); - document.removeEventListener('stylishInstallChrome', stylishInstallChrome); - // we can't detach chrome.runtime.onMessage because it's no longer connected internally - // we can destroy global functions in this context to free up memory - [ - 'arraysAreEqual', - 'getMeta', - 'getResource', - 'orphanCheck', - 'sectionsAreEqual', - 'sendEvent', - 'stylishUpdateChrome', - 'stylishInstallChrome' - ].forEach(fn => (window[fn] = null)); + const port = chrome.runtime.connect(); + if (port) { + port.disconnect(); + return true; + } + // we're orphaned due to an extension update + // we can detach event listeners + document.removeEventListener('stylishUpdateChrome', onUpdateClicked); + document.removeEventListener('stylishInstallChrome', onInstallClicked); + // we can't detach chrome.runtime.onMessage because it's no longer connected internally + // we can destroy global functions in this context to free up memory + [ + 'checkUpdatability', + 'getMeta', + 'getResource', + 'onInstallClicked', + 'onUpdateClicked', + 'orphanCheck', + 'rebrand', + 'saveStyleCode', + 'sendEvent', + 'styleSectionsEqual', + 'waitForBody', + ].forEach(fn => (window[fn] = null)); } diff --git a/manifest.json b/manifest.json index 22425c24..f737f6d3 100644 --- a/manifest.json +++ b/manifest.json @@ -39,7 +39,7 @@ }, { "matches": ["http://userstyles.org/*", "https://userstyles.org/*"], - "run_at": "document_end", + "run_at": "document_start", "all_frames": false, "js": ["install.js"] } From aaa50d6b7be0aa3ad14d21fbe46ec0525c519f33 Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 15 Apr 2017 13:25:48 +0300 Subject: [PATCH 151/235] fixup for Chrome 49 without Symbol.iterator --- apply.js | 7 ++++--- install.js | 11 +++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/apply.js b/apply.js index 6dd2c67d..143bba3f 100644 --- a/apply.js +++ b/apply.js @@ -261,9 +261,10 @@ function initDocRewriteObserver() { }; // detect documentElement being rewritten from inside the script docRewriteObserver = new MutationObserver(mutations => { - for (const mutation of mutations) { - for (const node of mutation.addedNodes) { - if (node.localName == 'html') { + for (let m = mutations.length; --m >= 0;) { + const added = mutations[m].addedNodes; + for (let n = added.length; --n >= 0;) { + if (added[n].localName == 'html') { reinjectStyles(); return; } diff --git a/install.js b/install.js index d374a810..0b9e7c91 100644 --- a/install.js +++ b/install.js @@ -143,14 +143,17 @@ function getResource(url) { function rebrand(mutations) { /* stylish to stylus; https://github.com/schomery/stylish-chrome/issues/12 */ - for (const mutation of mutations) { - for (const addedNode of mutation.addedNodes) { + for (let m = mutations.length; --m >= 0;) { + const added = mutations[m].addedNodes; + for (let n = added.length; --n >= 0;) { + const addedNode = added[n]; if (addedNode.nodeType != Node.ELEMENT_NODE) { continue; } const elementsToCheck = addedNode.matches('.install-status') ? [addedNode] - : Array.prototype.slice.call(addedNode.getElementsByClassName('install-status')); - for (const el of elementsToCheck) { + : addedNode.getElementsByClassName('install-status'); + for (let i = elementsToCheck.length; --i >= 0;) { + const el = elementsToCheck[i]; if (!el.textContent.includes('Stylish')) { continue; } From e8ec224dac41375712c19236e318af3717dd1d4e Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 16 Apr 2017 10:19:12 +0300 Subject: [PATCH 152/235] manage: make #installed fill 100% width --- manage.css | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/manage.css b/manage.css index bd39055f..29a8f9d9 100644 --- a/manage.css +++ b/manage.css @@ -26,6 +26,7 @@ a:hover { -webkit-box-shadow: 0 0 50px -18px black; overflow: auto; box-sizing: border-box; + z-index: 9; } #header h1 { @@ -42,7 +43,9 @@ a:hover { #installed { position: relative; - margin-left: 280px; + padding-left: 280px; + box-sizing: border-box; + width: 100%; } .entry { @@ -603,7 +606,7 @@ fieldset > * { #installed { position: static; - margin-left: 0; + padding-left: 0; overflow: visible; } @@ -653,7 +656,7 @@ fieldset > * { } .newUI #installed { - margin-left: 0; + padding-left: 0; } .newUI #header h1, @@ -708,10 +711,6 @@ fieldset > * { width: auto; } - .newUI #installed { - width: 100%; - } - .newUI .entry { margin: 0; } From 1649a262cdaeae36b69ecd14b77d1a562a4abc97 Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 16 Apr 2017 13:20:37 +0300 Subject: [PATCH 153/235] Don't double-notify own pages --- messaging.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/messaging.js b/messaging.js index 91be8798..a06d154c 100644 --- a/messaging.js +++ b/messaging.js @@ -45,11 +45,14 @@ function notifyAllTabs(msg) { const affectsTabs = affectsAll || affectsOwnOriginOnly; const affectsIcon = affectsAll || msg.affects.icon; const affectsPopup = affectsAll || msg.affects.popup; + const affectsSelf = affectsPopup || msg.prefs; if (affectsTabs || affectsIcon) { // list all tabs including chrome-extension:// which can be ours chrome.tabs.query(affectsOwnOriginOnly ? {url: URLS.ownOrigin + '*'} : {}, tabs => { for (const tab of tabs) { - if (affectsTabs || URLS.optionsUI.includes(tab.url)) { + // own pages will be notified via runtime.sendMessage later + if ((affectsTabs || URLS.optionsUI.includes(tab.url)) + && !(affectsSelf && tab.url.startsWith(URLS.ownOrigin))) { chrome.tabs.sendMessage(tab.id, msg); } if (affectsIcon && BG) { @@ -67,7 +70,7 @@ function notifyAllTabs(msg) { applyOnMessage(originalMessage); } // notify background page and all open popups - if (affectsPopup || msg.prefs) { + if (affectsSelf) { chrome.runtime.sendMessage(msg); } } From eccabb8f2742e493fdcf6ad490a23ae3a4f42cf7 Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 16 Apr 2017 14:24:49 +0300 Subject: [PATCH 154/235] Fix even-odd rules on entries * Now filtering is done in js * Visible entries are always at the beginning of #installed * Hidden entries are always at the end of #installed * The code tries to minimize DOM reordering operations: * First pass only moves one hidden entry in hidden groups with odd number of items. * Second [full] pass runs after repaint. --- manage.css | 11 +- manage.html | 17 ++- manage.js | 308 ++++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 245 insertions(+), 91 deletions(-) diff --git a/manage.css b/manage.css index 29a8f9d9..2893a4b5 100644 --- a/manage.css +++ b/manage.css @@ -488,7 +488,7 @@ input[id^="manage.newUI"] { } .hidden { - display: none + display: none !important; } fieldset { @@ -498,13 +498,8 @@ fieldset { } fieldset > * { - display: block; -} - -.enabled-only > .disabled, -.edited-only > .updatable, -.updates-only > .entry:not(.can-update) { - display: none; + display: flex; + align-items: center; } #search { diff --git a/manage.html b/manage.html index ff787a50..34fe0f5a 100644 --- a/manage.html +++ b/manage.html @@ -128,10 +128,19 @@

    - - - - + + + +

    diff --git a/manage.js b/manage.js index c514d8ee..a1948b66 100644 --- a/manage.js +++ b/manage.js @@ -2,6 +2,10 @@ 'use strict'; let installed; +const filtersSelector = { + hide: '', + unhide: '', +}; const newUI = { enabled: prefs.get('manage.newUI'), @@ -65,15 +69,6 @@ function initGlobalEvents() { // remember scroll position on normal history navigation window.onbeforeunload = rememberScrollPosition; - for (const [className, checkbox] of [ - ['enabled-only', $('#manage.onlyEnabled')], - ['edited-only', $('#manage.onlyEdited')], - ['updates-only', $('#onlyUpdates input')], - ]) { - // will be triggered by setupLivePrefs immediately - checkbox.onchange = () => installed.classList.toggle(className, checkbox.checked); - } - $$('[data-toggle-on-click]').forEach(el => { el.onclick = () => $(el.dataset.toggleOnClick).classList.toggle('hidden'); }); @@ -88,6 +83,11 @@ function initGlobalEvents() { 'manage.newUI.targets', ]); + $$('[data-filter]').forEach(el => { + el.onchange = handleEvent.filterOnChange; + }); + handleEvent.filterOnChange({forceRefilter: true}); + $$('[id^="manage.newUI"]') .forEach(el => (el.oninput = (el.onchange = switchUI))); @@ -111,11 +111,7 @@ function showStyles(styles = []) { break; } } - if ($('#search').value.trim()) { - // re-apply filtering on history Back - searchStyles({immediately: true, container: renderBin}); - } - installed.appendChild(renderBin); + filterAndAppend({container: renderBin}); if (index < sorted.length) { setTimeout(renderStyles, 0, index); } else if (shouldRenderAll && 'scrollY' in (history.state || {})) { @@ -337,50 +333,67 @@ Object.assign(handleEvent, { this.closest('.applies-to').classList.toggle('expanded'); }, - loadFavicons(container = installed) { + loadFavicons(container = document.body) { for (const img of container.getElementsByTagName('img')) { if (img.dataset.src) { img.src = img.dataset.src; delete img.dataset.src; } } - } + }, + + filterOnChange({target: el, forceRefilter}) { + const getValue = el => (el.type == 'checkbox' ? el.checked : el.value.trim()); + if (!forceRefilter) { + const value = getValue(el); + if (value == el.lastValue) { + return; + } + el.lastValue = value; + } + const enabledFilters = $$('#header [data-filter]').filter(el => getValue(el)); + Object.assign(filtersSelector, { + hide: enabledFilters.map(el => '.entry:not(.hidden)' + el.dataset.filter).join(','), + unhide: '.entry.hidden' + enabledFilters.map(el => + (':not(' + el.dataset.filter + ')').replace(/^:not\(:not\((.+?)\)\)$/, '$1')).join(''), + }); + reapplyFilter(); + }, }); function handleUpdate(style, {reason} = {}) { - const element = createStyleElement({style}); - if ($('#search').value.trim()) { - const renderBin = document.createDocumentFragment(); - renderBin.appendChild(element); - searchStyles({immediately: true, container: renderBin}); - } - const oldElement = $('#style-' + style.id, installed); - if (oldElement) { - if (oldElement.styleNameLowerCase == element.styleNameLowerCase) { - installed.replaceChild(element, oldElement); + const entry = createStyleElement({style}); + const oldEntry = $('#style-' + style.id); + if (oldEntry) { + if (oldEntry.styleNameLowerCase == entry.styleNameLowerCase) { + installed.replaceChild(entry, oldEntry); } else { - oldElement.remove(); + oldEntry.remove(); } if (reason == 'update') { - element.classList.add('update-done'); - element.classList.remove('can-update', 'updatable'); - $('.update-note', element).textContent = t('updateCompleted'); + entry.classList.add('update-done'); + entry.classList.remove('can-update', 'updatable'); + $('.update-note', entry).textContent = t('updateCompleted'); renderUpdatesOnlyFilter(); } } - installed.insertBefore(element, findNextElement(style)); - if (reason != 'import') { - animateElement(element, {className: 'highlight'}); - scrollElementIntoView(element); + filterAndAppend({entry}); + if (!entry.classList.contains('hidden') && reason != 'import') { + animateElement(entry, {className: 'highlight'}); + scrollElementIntoView(entry); } } function handleDelete(id) { - const node = $('#style-' + id, installed); + const node = $('#style-' + id); if (node) { node.remove(); + if (node.matches('.can-update')) { + const btnApply = $('#apply-all-updates'); + btnApply.dataset.value = Number(btnApply.dataset.value) - 1; + } } } @@ -590,6 +603,9 @@ class Updater { $('#onlyUpdates').classList.toggle('hidden', !$('.can-update')); } } + if (filtersSelector.hide) { + filterAndAppend({entry: this.element}); + } } static download(url) { @@ -636,27 +652,41 @@ function renderUpdatesOnlyFilter({show, check} = {}) { function searchStyles({immediately, container}) { - const query = $('#search').value.toLocaleLowerCase(); - if (query == (searchStyles.lastQuery || '') && !immediately && !container) { + const searchElement = $('#search'); + const query = searchElement.value.toLocaleLowerCase(); + const queryPrev = searchElement.lastValue || ''; + if (query == queryPrev && !immediately && !container) { return; } - searchStyles.lastQuery = query; if (!immediately) { clearTimeout(searchStyles.timeout); searchStyles.timeout = setTimeout(searchStyles, 150, {immediately: true}); return; } + searchElement.lastValue = query; - for (const element of (container || installed).children) { - const style = BG.cachedStyles.byId.get(element.styleId) || {}; - if (style) { - const isMatching = !query - || isMatchingText(style.name) - || style.url && isMatchingText(style.url) - || isMatchingStyle(style); - element.style.display = isMatching ? '' : 'none'; + const searchInVisible = queryPrev && query.includes(queryPrev); + const entries = container && container.children || container || + (searchInVisible ? $$('.entry:not(.hidden)') : installed.children); + let needsRefilter = false; + for (const entry of entries) { + let isMatching = !query; + if (!isMatching) { + const style = BG.cachedStyles.byId.get(entry.styleId) || {}; + isMatching = Boolean(style && ( + isMatchingText(style.name) || + style.url && isMatchingText(style.url) || + isMatchingStyle(style))); + } + if (entry.classList.contains('not-matching') != !isMatching) { + entry.classList.toggle('not-matching', !isMatching); + needsRefilter = true; } } + if (needsRefilter && !container) { + handleEvent.filterOnChange({forceRefilter: true}); + } + return; function isMatchingStyle(style) { for (const section of style.sections) { @@ -686,39 +716,159 @@ function searchStyles({immediately, container}) { } +function filterAndAppend({entry, container}) { + if (!container) { + container = document.createElement('div'); + container.appendChild(entry); + // reverse the visibility, otherwise reapplyFilter will see no need to work + if (!filtersSelector.hide || !entry.matches(filtersSelector.hide)) { + entry.classList.add('hidden'); + } + } + if ($('#search').value.trim()) { + searchStyles({immediately: true, container}); + } + reapplyFilter(container); +} + + +function reapplyFilter(container = installed) { + $('#check-all-updates').disabled = !$('.updatable:not(.can-update)'); + // A: show + const toUnhide = filtersSelector.hide ? $$(filtersSelector.unhide, container) : container; + // showStyles() is building the page and no filters are active + if (toUnhide instanceof DocumentFragment) { + installed.appendChild(toUnhide); + return; + } + // filtering needed or a single-element job from handleUpdate() + const entries = installed.children; + const numEntries = entries.length; + let numVisible = numEntries - $$('.entry.hidden').length; + for (const entry of toUnhide.children || toUnhide) { + const next = findInsertionPoint(entry); + if (entry.nextElementSibling !== next) { + installed.insertBefore(entry, next); + } + if (entry.classList.contains('hidden')) { + entry.classList.remove('hidden'); + numVisible++; + } + } + // B: hide + const toHide = filtersSelector.hide ? $$(filtersSelector.hide, container) : []; + if (!toHide.length) { + return; + } + for (const entry of toHide) { + entry.classList.add('hidden'); + } + // showStyles() is building the page with filters active so we need to: + // 1. add all hidden entries to the end + // 2. add the visible entries before the first hidden entry + if (container instanceof DocumentFragment) { + for (const entry of toHide) { + installed.appendChild(entry); + } + installed.insertBefore(container, $('.entry.hidden')); + return; + } + // normal filtering of the page or a single-element job from handleUpdate() + // we need to keep the visible entries together at the start + // first pass only moves one hidden entry in hidden groups with odd number of items + shuffle(false); + setTimeout(shuffle, 0, true); + // single-element job from handleEvent(): add the last wraith + if (toHide.length == 1 && toHide[0].parentElement != installed) { + installed.appendChild(toHide[0]); + } + return; + + function shuffle(fullPass) { + // 1. skip the visible group on top + let firstHidden = $('#installed > .hidden'); + let entry = firstHidden; + let i = [...entries].indexOf(entry); + let horizon = entries[numVisible]; + const skipGroup = state => { + const start = i; + const first = entry; + while (entry && entry.classList.contains('hidden') == state) { + entry = entry.nextElementSibling; + i++; + } + return {first, start, len: i - start}; + }; + let prevGroup = i ? {first: entries[0], start: 0, len: i} : skipGroup(true); + // eslint-disable-next-line no-unmodified-loop-condition + while (entry) { + // 2a. find the next hidden group's start and end + // 2b. find the next visible group's start and end + const isHidden = entry.classList.contains('hidden'); + const group = skipGroup(isHidden); + const hidden = isHidden ? group : prevGroup; + const visible = isHidden ? prevGroup : group; + // 3. move the shortest group; repeat 2-3 + if (hidden.len < visible.len && (fullPass || hidden.len % 2)) { + // 3a. move hidden under the horizon + for (let j = 0; j < (fullPass ? hidden.len : 1); j++) { + const entry = entries[hidden.start]; + installed.insertBefore(entry, horizon); + horizon = entry; + i--; + } + prevGroup = isHidden ? skipGroup(false) : group; + firstHidden = entry; + } else if (isHidden || !fullPass) { + prevGroup = group; + } else { + // 3b. move visible above the horizon + for (let j = 0; j < visible.len; j++) { + const entry = entries[visible.start + j]; + installed.insertBefore(entry, firstHidden); + } + prevGroup = { + first: firstHidden, + start: hidden.start + visible.len, + len: hidden.len + skipGroup(true).len, + }; + } + } + } + + function findInsertionPoint(entry) { + const nameLLC = entry.styleNameLowerCase; + let a = 0; + let b = Math.min(numEntries, numVisible) - 1; + if (b < 0) { + return entries[numVisible]; + } + if (entries[0].styleNameLowerCase > nameLLC) { + return entries[0]; + } + if (entries[b].styleNameLowerCase <= nameLLC) { + return entries[numVisible]; + } + // bisect + while (a < b - 1) { + const c = (a + b) / 2 | 0; + if (nameLLC < entries[c].styleNameLowerCase) { + b = c; + } else { + a = c; + } + } + if (entries[a].styleNameLowerCase > nameLLC) { + return entries[a]; + } + while (a <= b && entries[a].styleNameLowerCase < nameLLC) { + a++; + } + return entries[entries[a].styleNameLowerCase <= nameLLC ? a + 1 : a]; + } +} + + function rememberScrollPosition() { history.replaceState({scrollY: window.scrollY}, document.title); } - - -function findNextElement(style) { - const nameLLC = style.name.toLocaleLowerCase(); - const elements = installed.children; - let a = 0; - let b = elements.length - 1; - if (b < 0) { - return undefined; - } - if (elements[0].styleNameLowerCase > nameLLC) { - return elements[0]; - } - if (elements[b].styleNameLowerCase <= nameLLC) { - return undefined; - } - // bisect - while (a < b - 1) { - const c = (a + b) / 2 | 0; - if (nameLLC < elements[c].styleNameLowerCase) { - b = c; - } else { - a = c; - } - } - if (elements[a].styleNameLowerCase > nameLLC) { - return elements[a]; - } - while (a <= b && elements[a].name < nameLLC) { - a++; - } - return elements[elements[a].styleNameLowerCase <= nameLLC ? a + 1 : a]; -} From 4eae87e6063762a98b1023069ed393559ed9f043 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 17 Apr 2017 18:08:49 +0300 Subject: [PATCH 155/235] own page load microopt: reduce blocking on prefs postpone noncritical init stuff to window.load because first access to chrome API takes time to initialize API bindings --- prefs.js | 87 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/prefs.js b/prefs.js index f3e817d6..dfa94ac8 100644 --- a/prefs.js +++ b/prefs.js @@ -146,54 +146,69 @@ var prefs = new function Prefs() { } else { value = defaultValue; } - this.set(key, value, {noBroadcast: true}); + if (BG == window) { + // when in bg page, .set() will write to localStorage + this.set(key, value, {noBroadcast: true, noSync: true}); + } else { + values[key] = value; + defineReadonlyProperty(this.readOnlyValues, key, value); + } } - getSync().get('settings', ({settings: synced} = {}) => { - if (synced) { - for (const key in defaults) { - if (key == 'popupWidth' && synced[key] != values.popupWidth) { - // this is a fix for the period when popupWidth wasn't synced - // TODO: remove it in a couple of months - continue; - } - if (key in synced) { - this.set(key, synced[key], {noSync: true}); - } - } - } - if (typeof contextMenus !== 'undefined') { - for (const id in contextMenus) { - if (typeof values[id] == 'boolean') { - this.broadcast(id, values[id], {noSync: true}); - } - } - } - }); + // any access to chrome API takes time due to initialization of bindings + let lazyInit = () => { + window.removeEventListener('load', lazyInit); + lazyInit = null; - chrome.storage.onChanged.addListener((changes, area) => { - if (area == 'sync' && 'settings' in changes) { - const synced = changes.settings.newValue; + getSync().get('settings', ({settings: synced} = {}) => { if (synced) { for (const key in defaults) { + if (key == 'popupWidth' && synced[key] != values.popupWidth) { + // this is a fix for the period when popupWidth wasn't synced + // TODO: remove it in a couple of months + continue; + } if (key in synced) { this.set(key, synced[key], {noSync: true}); } } - } else { - // user manually deleted our settings, we'll recreate them - getSync().set({'settings': values}); } - } - }); + if (typeof contextMenus !== 'undefined') { + for (const id in contextMenus) { + if (typeof values[id] == 'boolean') { + this.broadcast(id, values[id], {noSync: true}); + } + } + } + }); - chrome.runtime.onMessage.addListener(msg => { - if (msg.prefs) { - for (const id in msg.prefs) { - this.set(id, msg.prefs[id], {noBroadcast: true, noSync: true}); + chrome.storage.onChanged.addListener((changes, area) => { + if (area == 'sync' && 'settings' in changes) { + const synced = changes.settings.newValue; + if (synced) { + for (const key in defaults) { + if (key in synced) { + this.set(key, synced[key], {noSync: true}); + } + } + } else { + // user manually deleted our settings, we'll recreate them + getSync().set({'settings': values}); + } } - } - }); + }); + + chrome.runtime.onMessage.addListener(msg => { + if (msg.prefs) { + for (const id in msg.prefs) { + this.set(id, msg.prefs[id], {noBroadcast: true, noSync: true}); + } + } + }); + }; + + window.addEventListener('load', lazyInit); + return; function doBroadcast() { const affects = {all: 'disableAll' in broadcastPrefs}; From b3b1d4a62800b6dba916494977677febc4bcf714 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 17 Apr 2017 18:54:39 +0300 Subject: [PATCH 156/235] own page load microopt: reduce blocking on i18n * localStorage cache is faster than chrome.i18n.get * TreeWalker is faster than tHTML for removing extraneous whitespace * simple for() is faster than for-of with [...iterable] --- background.js | 12 ++++++++++++ localization.js | 43 +++++++++++++++++++++++++++++++++---------- options/index.html | 2 +- popup.html | 2 +- 4 files changed, 47 insertions(+), 12 deletions(-) diff --git a/background.js b/background.js index 4eee2c94..e8eafe44 100644 --- a/background.js +++ b/background.js @@ -37,6 +37,18 @@ function webNavigationListener(method, data) { }); } +// reset i18n cache on language change + +setTimeout(() => { + const {browserUIlanguage} = tryJSONparse(localStorage.L10N) || {}; + const UIlang = chrome.i18n.getUILanguage(); + if (browserUIlanguage != UIlang) { + localStorage.L10N = JSON.stringify({ + browserUIlanguage: UIlang, + }); + } +}); + // messaging chrome.runtime.onMessage.addListener(onRuntimeMessage); diff --git a/localization.js b/localization.js index 9df3b395..2a6ac4c4 100644 --- a/localization.js +++ b/localization.js @@ -5,10 +5,14 @@ tDocLoader(); function t(key, params) { - const s = chrome.i18n.getMessage(key, params); + const cache = !params && t.cache[key]; + const s = cache || chrome.i18n.getMessage(key, params); if (s == '') { throw `Missing string "${key}"`; } + if (!params && !cache) { + t.cache[key] = s; + } return s; } @@ -35,24 +39,38 @@ function tHTML(html) { function tNodeList(nodes) { - for (const node of [...nodes]) { + const PREFIX = 'i18n-'; + for (let n = nodes.length; --n >= 0;) { + const node = nodes[n]; // skip non-ELEMENT_NODE if (node.nodeType != 1) { continue; } if (node.localName == 'template') { + const elements = node.content.querySelectorAll('*'); + tNodeList(elements); + template[node.dataset.id] = elements[0]; // compress inter-tag whitespace to reduce number of DOM nodes by 25% - template[node.dataset.id] = tHTML(node.innerHTML); + const walker = document.createTreeWalker(elements[0], NodeFilter.SHOW_TEXT); + const toRemove = []; + while (walker.nextNode()) { + const textNode = walker.currentNode; + if (!textNode.nodeValue.trim()) { + toRemove.push(textNode); + } + } + toRemove.forEach(el => el.remove()); continue; } - for (const attr of [...node.attributes]) { - let name = attr.nodeName; - if (name.indexOf('i18n-') != 0) { + for (let a = node.attributes.length; --a >= 0;) { + const attr = node.attributes[a]; + const name = attr.nodeName; + if (!name.startsWith(PREFIX)) { continue; } - name = name.substr(5); // 'i18n-'.length + const type = name.substr(PREFIX.length); const value = t(attr.value); - switch (name) { + switch (type) { case 'text': node.insertBefore(document.createTextNode(value), node.firstChild); break; @@ -63,15 +81,17 @@ function tNodeList(nodes) { node.insertAdjacentHTML('afterbegin', value); break; default: - node.setAttribute(name, value); + node.setAttribute(type, value); } - node.removeAttribute(attr.nodeName); + node.removeAttribute(name); } } } function tDocLoader() { + t.cache = tryJSONparse(localStorage.L10N) || {}; + const cacheLength = Object.keys(t.cache).length; // localize HEAD tNodeList(document.getElementsByTagName('*')); @@ -85,6 +105,9 @@ function tDocLoader() { const onLoad = () => { tDocLoader.stop(); process(observer.takeRecords()); + if (cacheLength != Object.keys(t.cache).length) { + localStorage.L10N = JSON.stringify(t.cache); + } }; tDocLoader.start = () => { observer.observe(document, {subtree: true, childList: true}); diff --git a/options/index.html b/options/index.html index 527f2c17..a476b4a6 100644 --- a/options/index.html +++ b/options/index.html @@ -4,8 +4,8 @@ Stylus - + diff --git a/popup.html b/popup.html index 4b56fcc0..9ad62b9a 100644 --- a/popup.html +++ b/popup.html @@ -56,8 +56,8 @@ - + From dca6aecd20292b1da02c3912279c842b26744d35 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 17 Apr 2017 18:56:37 +0300 Subject: [PATCH 157/235] Show active NTP icon correctly on startup --- background.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/background.js b/background.js index e8eafe44..02e09042 100644 --- a/background.js +++ b/background.js @@ -17,7 +17,6 @@ chrome.webNavigation.onReferenceFragmentUpdated.addListener(data => { webNavigationListener('styleReplaceAll', data); }); - function webNavigationListener(method, data) { getStyles({matchUrl: data.url, enabled: true, asHash: true}, styles => { // we can't inject chrome:// and chrome-extension:// pages @@ -263,10 +262,7 @@ function refreshAllTabs() { function updateIcon(tab, styles) { - // while NTP is still loading only process the request for its main frame with a real url - // (but when it's loaded we should process style toggle requests from popups, for example) - const isNTP = tab.url == 'chrome://newtab/'; - if (isNTP && tab.status != 'complete' || tab.id < 0) { + if (tab.id < 0) { return; } if (styles) { @@ -278,12 +274,9 @@ function updateIcon(tab, styles) { }); return; } - if (isNTP) { - getTabRealURL(tab).then(url => - getStyles({matchUrl: url, enabled: true, asHash: true}, stylesReceived)); - } else { - getStyles({matchUrl: tab.url, enabled: true, asHash: true}, stylesReceived); - } + getTabRealURL(tab).then(url => + getStyles({matchUrl: url, enabled: true, asHash: true}, + stylesReceived)); function stylesReceived(styles) { let numStyles = styles.length; From 01d59192a3328ca98b9de93a4d1ec7441c8939a0 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 17 Apr 2017 19:17:28 +0300 Subject: [PATCH 158/235] Chrome 49 fixup for updateIcon --- messaging.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/messaging.js b/messaging.js index a06d154c..94c058dd 100644 --- a/messaging.js +++ b/messaging.js @@ -28,6 +28,11 @@ let BG = chrome.extension.getBackgroundPage(); if (!BG || BG != window) { document.documentElement.classList.toggle('firefox', FIREFOX); document.documentElement.classList.toggle('opera', OPERA); + // TODO: remove once our manifest's minimum_chrome_version is 50+ + // Chrome 49 doesn't report own extension pages in webNavigation apparently + if (navigator.userAgent.includes('Chrome/49.')) { + getActiveTab().then(BG.updateIcon); + } } function notifyAllTabs(msg) { From 0d6f7e0a4b61e6063170c74977bfc48e055c76a3 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 17 Apr 2017 19:48:39 +0300 Subject: [PATCH 159/235] popup: right-click / ctrl-click on a name opens editor --- popup.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/popup.js b/popup.js index 241860a9..de54567e 100644 --- a/popup.js +++ b/popup.js @@ -201,8 +201,7 @@ function createStyleElement({ id: 'style-' + style.id, styleId: style.id, className: entry.className + ' ' + (style.enabled ? 'enabled' : 'disabled'), - onmousedown: handleEvent.middleClick, - onauxclick: handleEvent.middleClick, + onmousedown: handleEvent.maybeEdit, }); const checkbox = $('.checker', entry); @@ -324,13 +323,16 @@ Object.assign(handleEvent, { close(); }, - middleClick(event) { - if (event.button != 1) { + maybeEdit(event) { + if (!( + event.button == 0 && event.ctrlKey || + event.button == 1 || + event.button == 2)) { return; } // open an editor on middleclick if (event.target.matches('.entry, .style-name, .style-edit-link')) { - $('.style-edit-link', this).click(); + this.onmouseup = () => $('.style-edit-link', this).click(); event.preventDefault(); return; } From 021f50015b2d3ce86c11a8e7d868679a350d5fed Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 17 Apr 2017 21:06:00 +0300 Subject: [PATCH 160/235] restore editor window size when reopened via Ctrl-Shift-T --- edit.html | 16 ++++++++------ edit.js | 65 +++++++++++++++++++++++++++++++------------------------ 2 files changed, 46 insertions(+), 35 deletions(-) diff --git a/edit.html b/edit.html index 3402392f..79b26790 100644 --- a/edit.html +++ b/edit.html @@ -1,6 +1,14 @@ + + + + + + + + @@ -644,14 +652,8 @@ - - - - - - - +

    diff --git a/manage.js b/manage.js index 58e03b50..164656c9 100644 --- a/manage.js +++ b/manage.js @@ -10,6 +10,7 @@ const filtersSelector = { 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); @@ -75,15 +76,11 @@ function initGlobalEvents() { el.onclick = () => target.classList.toggle('hidden'); }); + // triggered automatically by setupLivePrefs() below enforceInputRange($('#manage.newUI.targets')); - setupLivePrefs([ - 'manage.onlyEnabled', - 'manage.onlyEdited', - 'manage.newUI', - 'manage.newUI.favicons', - 'manage.newUI.targets', - ]); + // N.B. triggers existing onchange listeners + setupLivePrefs($$('input[id^="manage."]').map(el => el.id)); $$('[data-filter]').forEach(el => { el.onchange = handleEvent.filterOnChange; @@ -401,39 +398,55 @@ function handleDelete(id) { function switchUI({styleOnly} = {}) { - const enabled = $('#manage.newUI').checked; - const favicons = $('#manage.newUI.favicons').checked; - const targets = Number($('#manage.newUI.targets').value); + 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); + const valueChanged = value !== newUI[id] && (id == 'enabled' || current.enabled); + current[id] = value; + changed[id] = valueChanged; + someChanged |= valueChanged; + } - const stateToggled = newUI.enabled != enabled; - const targetsChanged = enabled && targets != newUI.targets; - const faviconsChanged = enabled && favicons != newUI.favicons; - const missingFavicons = enabled && favicons && !$('.applies-to img'); - - if (!styleOnly && !stateToggled && !targetsChanged && !faviconsChanged) { + if (!styleOnly && !someChanged) { return; } - Object.assign(newUI, {enabled, favicons, targets}); - + Object.assign(newUI, current); newUI.renderClass(); - installed.classList.toggle('has-favicons', favicons); + installed.classList.toggle('has-favicons', newUI.favicons); $('#style-overrides').textContent = ` .newUI .targets { max-height: ${newUI.targets * 18}px; } - `; + ` + (newUI.faviconsGray ? ` + .newUI .target img { + -webkit-filter: grayscale(1); + filter: grayscale(1); + opacity: .25; + } + ` : ` + .newUI .target img { + -webkit-filter: none; + filter: none; + opacity: 1; + } + `); if (styleOnly) { return; } - if (stateToggled || missingFavicons && !createStyleElement.parts) { + const missingFavicons = newUI.enabled && newUI.favicons && !$('.applies-to img'); + if (changed.enabled || (missingFavicons && !createStyleElement.parts)) { installed.innerHTML = ''; getStylesSafe().then(showStyles); return; } - if (targetsChanged) { + if (changed.targets) { for (const targets of $$('.entry .targets')) { const hasMore = targets.children.length > newUI.targets; targets.parentElement.classList.toggle('has-more', hasMore); diff --git a/prefs.js b/prefs.js index f460779d..17510093 100644 --- a/prefs.js +++ b/prefs.js @@ -18,6 +18,7 @@ var prefs = new function Prefs() { 'manage.onlyEdited': false, // display only styles created locally 'manage.newUI': true, // use the new compact layout 'manage.newUI.favicons': false, // show favicons for the sites in applies-to + 'manage.newUI.faviconsGray': true, // gray out favicons 'manage.newUI.targets': 3, // max number of applies-to targets visible: 0 = none 'editor.options': {}, // CodeMirror.defaults.* From 6f74cb8b29c2e506be3188bda391219e74736887 Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 18 Apr 2017 21:56:32 +0300 Subject: [PATCH 173/235] event.keyCode doesn't work in Firefox --- edit.js | 4 +++- manage.js | 2 +- msgbox/msgbox.js | 5 +++-- popup.js | 5 +++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/edit.js b/edit.js index 99308bd4..ae41418a 100644 --- a/edit.js +++ b/edit.js @@ -1783,7 +1783,9 @@ function showHelp(title, text) { return div; function closeHelp(e) { - if (!e || e.type == "click" || (e.keyCode == 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)) { + if (!e + || e.type == "click" + || ((e.keyCode || e.which) == 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)) { div.style.display = ""; document.querySelector(".contents").innerHTML = ""; document.removeEventListener("keydown", closeHelp); diff --git a/manage.js b/manage.js index 164656c9..97b6f4eb 100644 --- a/manage.js +++ b/manage.js @@ -59,7 +59,7 @@ function initGlobalEvents() { // focus search field on / key document.onkeypress = event => { - if (event.keyCode == 47 + if ((event.keyCode || event.which) == 47 && !event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.target.matches('[type="text"], [type="search"]')) { event.preventDefault(); diff --git a/msgbox/msgbox.js b/msgbox/msgbox.js index 46807b1b..640add2e 100644 --- a/msgbox/msgbox.js +++ b/msgbox/msgbox.js @@ -28,10 +28,11 @@ function messageBox({ resolveWith({button: this.buttonIndex}); }, key(event) { + const keyCode = event.keyCode || event.which; if (!event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey - && (event.keyCode == 13 || event.keyCode == 27)) { + && (keyCode == 13 || keyCode == 27)) { event.preventDefault(); - resolveWith(event.keyCode == 13 ? {enter: true} : {esc: true}); + resolveWith(keyCode == 13 ? {enter: true} : {esc: true}); } }, scroll() { diff --git a/popup.js b/popup.js index 813bbd62..28a2d3ac 100644 --- a/popup.js +++ b/popup.js @@ -275,10 +275,11 @@ Object.assign(handleEvent, { $('[data-cmd="ok"]', box).onclick = () => confirm(true); $('[data-cmd="cancel"]', box).onclick = () => confirm(false); window.onkeydown = event => { + const keyCode = event.keyCode || event.which; if (!event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey - && (event.keyCode == 13 || event.keyCode == 27)) { + && (keyCode == 13 || keyCode == 27)) { event.preventDefault(); - confirm(event.keyCode == 13); + confirm(keyCode == 13); } }; function confirm(ok) { From fe3f5121e58dd1672ff0cf86b9d75983af72bdd1 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 19 Apr 2017 16:34:48 +0300 Subject: [PATCH 174/235] manage: shorten "Find editor styles" as "Theme" --- manage.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manage.html b/manage.html index dc0365eb..52416635 100644 --- a/manage.html +++ b/manage.html @@ -175,11 +175,11 @@

    +

    -

    - -

    From f5da135e81db4cf5e84b6f7f0056630ff050b131 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 19 Apr 2017 19:03:00 +0300 Subject: [PATCH 175/235] invalidateCache: minor refactor & fix deletedId case --- storage.js | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/storage.js b/storage.js index 4fc861cc..f2f2d8e4 100644 --- a/storage.js +++ b/storage.js @@ -432,32 +432,31 @@ function compileStyleRegExps({style, compileAll}) { function invalidateCache({added, updated, deletedId} = {}) { - // prevent double-add on echoed invalidation - const cached = added && cachedStyles.byId.get(added.id); - if (cached) { - return; - } if (!cachedStyles.list) { return; } + const id = added ? added.id : updated ? updated.id : deletedId; + const cached = cachedStyles.byId.get(id); if (updated) { - const cached = cachedStyles.byId.get(updated.id); if (cached) { Object.assign(cached, updated); + cachedStyles.filters.clear(); + return; + } else { + added = updated; } - cachedStyles.filters.clear(); - return; } if (added) { - cachedStyles.list.push(added); - cachedStyles.byId.set(added.id, added); - cachedStyles.filters.clear(); + if (!cached) { + cachedStyles.list.push(added); + cachedStyles.byId.set(added.id, added); + cachedStyles.filters.clear(); + } return; } - if (deletedId != undefined) { - const deletedStyle = (cachedStyles.byId.get(deletedId) || {}).style; - if (deletedStyle) { - const cachedIndex = cachedStyles.list.indexOf(deletedStyle); + if (deletedId !== undefined) { + if (cached) { + const cachedIndex = cachedStyles.list.indexOf(cached); cachedStyles.list.splice(cachedIndex, 1); cachedStyles.byId.delete(deletedId); cachedStyles.filters.clear(); From 4fd1a3db6268bb47ad72b9d80d494d9fd5354be8 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 19 Apr 2017 19:13:11 +0300 Subject: [PATCH 176/235] remove fixBoolean() --- storage.js | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/storage.js b/storage.js index f2f2d8e4..e7b7babd 100644 --- a/storage.js +++ b/storage.js @@ -79,14 +79,15 @@ function getStyles(options, callback) { function filterStyles({ - enabled, + enabled = null, url = null, id = null, matchUrl = null, asHash = null, strictRegexp = true, // used by the popup to detect bad regexps } = {}) { - enabled = fixBoolean(enabled); + enabled = enabled === null || typeof enabled == 'boolean' ? enabled : + typeof enabled == 'string' ? enabled == 'true' : null; id = id === null ? null : Number(id); if (enabled === null @@ -504,14 +505,6 @@ function reportError(...args) { } -function fixBoolean(b) { - if (typeof b != 'undefined') { - return b != 'false'; - } - return null; -} - - function getDomains(url) { if (url.indexOf('file:') == 0) { return []; From 3a8ac2d9dcf60e3665eb8a0c1ed02215aa401607 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 19 Apr 2017 19:22:13 +0300 Subject: [PATCH 177/235] inline reportError() --- background.js | 7 +++++-- storage.js | 10 ---------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/background.js b/background.js index 02e09042..b4a7f552 100644 --- a/background.js +++ b/background.js @@ -1,4 +1,4 @@ -/* global getDatabase, getStyles, saveStyle, reportError */ +/* global getDatabase, getStyles, saveStyle */ 'use strict'; chrome.webNavigation.onBeforeNavigate.addListener(data => { @@ -161,7 +161,10 @@ Object.keys(contextMenus).forEach(id => { // Get the DB so that any first run actions will be performed immediately // when the background page loads. -getDatabase(function() {}, reportError); +getDatabase(() => {}, (...args) => { + args.forEach(arg => 'message' in arg && console.error(arg.message)); +}); + // When an edit page gets attached or detached, remember its state // so we can do the same to the next one to open. diff --git a/storage.js b/storage.js index e7b7babd..2ea84ecc 100644 --- a/storage.js +++ b/storage.js @@ -1,4 +1,3 @@ -/* global cachedStyles: true */ 'use strict'; const RX_NAMESPACE = new RegExp([/[\s\r\n]*/, @@ -496,15 +495,6 @@ function cleanupCachedFilters({force = false} = {}) { } -function reportError(...args) { - for (const arg of args) { - if ('message' in arg) { - console.log(arg.message); - } - } -} - - function getDomains(url) { if (url.indexOf('file:') == 0) { return []; From a80c677b3e6c57b3fdabc14b559191ec638e2d1e Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 19 Apr 2017 23:54:05 +0300 Subject: [PATCH 178/235] render at least 10 style entries on slower machines --- .eslintrc | 2 +- manage.js | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.eslintrc b/.eslintrc index 0ba14e06..2dfd33c9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -204,7 +204,7 @@ rules: no-undefined: [0] no-underscore-dangle: [0] no-unexpected-multiline: [2] - no-unmodified-loop-condition: [2] + no-unmodified-loop-condition: [1] no-unneeded-ternary: [2] no-unreachable: [2] no-unsafe-finally: [2] diff --git a/manage.js b/manage.js index 97b6f4eb..0a995de4 100644 --- a/manage.js +++ b/manage.js @@ -104,11 +104,10 @@ function showStyles(styles = []) { function renderStyles(index) { const t0 = performance.now(); - while (index < sorted.length) { + let rendered = 0; + while (index < sorted.length + && (shouldRenderAll || performance.now() - t0 < 10 || ++rendered < 10)) { renderBin.appendChild(createStyleElement(sorted[index++])); - if (!shouldRenderAll && performance.now() - t0 > 10) { - break; - } } filterAndAppend({container: renderBin}); if (index < sorted.length) { From 98c34da9e7f890b41447404c63dcfd5e6036690f Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 20 Apr 2017 01:19:33 +0300 Subject: [PATCH 179/235] simplify and speed up USO rebrand observer --- install.js | 45 +++++++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/install.js b/install.js index 0b9e7c91..f78ca970 100644 --- a/install.js +++ b/install.js @@ -21,12 +21,11 @@ function waitForBody() { this.disconnect(); rebrand([{addedNodes: [document.body]}]); - const rebrandObserver = new MutationObserver(rebrand); - rebrandObserver.observe(document.body, {childList: true, subtree: true}); + new MutationObserver(rebrand) + .observe(document.body, {childList: true, subtree: true}); document.addEventListener('DOMContentLoaded', function _() { document.removeEventListener('DOMContentLoaded', _); - rebrandObserver.disconnect(); chrome.runtime.sendMessage({ method: 'getStyles', url: getMeta('stylish-id-url') || location.href @@ -141,30 +140,24 @@ function getResource(url) { } -function rebrand(mutations) { +function rebrand(mutations, observer) { /* stylish to stylus; https://github.com/schomery/stylish-chrome/issues/12 */ - for (let m = mutations.length; --m >= 0;) { - const added = mutations[m].addedNodes; - for (let n = added.length; --n >= 0;) { - const addedNode = added[n]; - if (addedNode.nodeType != Node.ELEMENT_NODE) { - continue; - } - const elementsToCheck = addedNode.matches('.install-status') ? [addedNode] - : addedNode.getElementsByClassName('install-status'); - for (let i = elementsToCheck.length; --i >= 0;) { - const el = elementsToCheck[i]; - if (!el.textContent.includes('Stylish')) { - continue; - } - const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); - while (walker.nextNode()) { - const node = walker.currentNode; - const text = node.nodeValue; - if (text.includes('Stylish') && node.parentNode.localName != 'a') { - node.nodeValue = text.replace(/Stylish/g, 'Stylus'); - } - } + if (!document.getElementById('hidden-meta') && document.readyState == 'loading') { + return; + } + observer.disconnect(); + const elements = document.getElementsByClassName('install-status'); + for (let i = elements.length; --i >= 0;) { + const el = elements[i]; + if (!el.textContent.includes('Stylish')) { + continue; + } + const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); + while (walker.nextNode()) { + const node = walker.currentNode; + const text = node.nodeValue; + if (text.includes('Stylish') && node.parentNode.localName != 'a') { + node.nodeValue = text.replace(/Stylish/g, 'Stylus'); } } } From aa5fc9f640065e96b0df29ce295665d60364246f Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 20 Apr 2017 04:46:04 +0300 Subject: [PATCH 180/235] notify USO earlier in install.js by relaying xhr --- .eslintrc | 1 + background.js | 6 ++++++ install.js | 58 +++++++++++++++++++++++++++------------------------ manage.js | 23 ++------------------ messaging.js | 15 +++++++++++++ 5 files changed, 55 insertions(+), 48 deletions(-) diff --git a/.eslintrc b/.eslintrc index 2dfd33c9..b20ffc9d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -33,6 +33,7 @@ globals: getStylesSafe: false saveStyleSafe: false sessionStorageHash: false + download: false # localization.js template: false t: false diff --git a/background.js b/background.js index b4a7f552..88acd85d 100644 --- a/background.js +++ b/background.js @@ -86,6 +86,12 @@ function onRuntimeMessage(request, sender, sendResponse) { } } break; + + case 'download': + download(request.url) + .then(sendResponse) + .catch(() => sendResponse(null)); + return KEEP_CHANNEL_OPEN; } } diff --git a/install.js b/install.js index f78ca970..9927bb8a 100644 --- a/install.js +++ b/install.js @@ -18,19 +18,16 @@ function waitForBody() { if (!document.body) { return; } - this.disconnect(); + rebrand([{addedNodes: [document.body]}]); new MutationObserver(rebrand) .observe(document.body, {childList: true, subtree: true}); - document.addEventListener('DOMContentLoaded', function _() { - document.removeEventListener('DOMContentLoaded', _); - chrome.runtime.sendMessage({ - method: 'getStyles', - url: getMeta('stylish-id-url') || location.href - }, checkUpdatability); - }); + chrome.runtime.sendMessage({ + method: 'getStyles', + url: getMeta('stylish-id-url') || location.href + }, checkUpdatability); } @@ -71,7 +68,9 @@ function sendEvent(type, detail = null) { // because USO tries to use a global "event" variable deprecated in Firefox detail = cloneInto(detail, document); // eslint-disable-line no-undef } - document.dispatchEvent(new CustomEvent(type, detail)); + onDOMready().then(() => { + document.dispatchEvent(new CustomEvent(type, detail)); + }); } @@ -121,20 +120,11 @@ function getMeta(name) { function getResource(url) { - if (url.startsWith('#')) { - return Promise.resolve(document.getElementById(url.slice(1)).textContent); - } return new Promise(resolve => { - const xhr = new XMLHttpRequest(); - xhr.onloadend = () => resolve(xhr.status < 400 ? xhr.responseText : null); - if (url.length > 2000) { - const [mainUrl, query] = url.split('?'); - xhr.open('POST', mainUrl, true); - xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); - xhr.send(query); + if (url.startsWith('#')) { + resolve(document.getElementById(url.slice(1)).textContent); } else { - xhr.open('GET', url); - xhr.send(); + chrome.runtime.sendMessage({method: 'download', url}, resolve); } }); } @@ -148,17 +138,18 @@ function rebrand(mutations, observer) { observer.disconnect(); const elements = document.getElementsByClassName('install-status'); for (let i = elements.length; --i >= 0;) { - const el = elements[i]; - if (!el.textContent.includes('Stylish')) { - continue; - } - const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); + const walker = document.createTreeWalker(elements[i], NodeFilter.SHOW_TEXT); while (walker.nextNode()) { const node = walker.currentNode; const text = node.nodeValue; - if (text.includes('Stylish') && node.parentNode.localName != 'a') { + const parent = node.parentNode; + const extensionHelp = /stylish_chrome/.test(parent.href); + if (text.includes('Stylish') && (parent.localName != 'a' || extensionHelp)) { node.nodeValue = text.replace(/Stylish/g, 'Stylus'); } + if (extensionHelp) { + parent.href = 'http://add0n.com/stylus.html'; + } } } } @@ -213,6 +204,19 @@ function styleSectionsEqual({sections: a}, {sections: b}) { } +function onDOMready() { + if (document.readyState != 'loading') { + return Promise.resolve(); + } + return new Promise(resolve => { + document.addEventListener('DOMContentLoaded', function _() { + document.removeEventListener('DOMContentLoaded', _); + resolve(); + }); + }); +} + + function orphanCheck() { const port = chrome.runtime.connect(); if (port) { diff --git a/manage.js b/manage.js index 0a995de4..464f47ca 100644 --- a/manage.js +++ b/manage.js @@ -558,7 +558,7 @@ class Updater { } checkMd5() { - return Updater.download(this.md5Url).then( + return download(this.md5Url).then( md5 => (md5.length == 32 ? this.decideOnMd5(md5 != this.md5) : this.onFailure(-1)), @@ -573,7 +573,7 @@ class Updater { } checkFullCode({forceUpdate = false} = {}) { - return Updater.download(this.url).then( + return download(this.url).then( text => this.handleJson(forceUpdate, JSON.parse(text)), status => this.onFailure(status)); } @@ -620,25 +620,6 @@ class Updater { filterAndAppend({entry: this.element}); } } - - static download(url) { - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.onloadend = () => (xhr.status == 200 - ? resolve(xhr.responseText) - : reject(xhr.status)); - if (url.length > 2000) { - const [mainUrl, query] = url.split('?'); - xhr.open('POST', mainUrl, true); - xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); - xhr.send(query); - } else { - xhr.open('GET', url); - xhr.send(); - } - }); - } - } diff --git a/messaging.js b/messaging.js index a5062b99..c0020345 100644 --- a/messaging.js +++ b/messaging.js @@ -306,3 +306,18 @@ function deleteStyleSafe({id, notify = true} = {}) { return id; }); } + + +function download(url) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.timeout = 10e3; + xhr.onloadend = () => (xhr.status == 200 + ? resolve(xhr.responseText) + : reject(xhr.status)); + const [mainUrl, query] = url.split('?'); + xhr.open(query ? 'POST' : 'GET', mainUrl, true); + xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + xhr.send(query); + }); +} From c52b8c453f5ee28f05392ab063aaf36f55188712 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 20 Apr 2017 17:00:43 +0300 Subject: [PATCH 181/235] refactor background.js * use runtime.onInstalled to open FAQ * extract refreshAllTabs (now it may call applyOnMessage for own tab) * extract getCodeMirrorThemes and use localStorage to cache the names * put one-time init stuff inside blocks to help GC --- background.js | 439 ++++++++++++++++------------------------- backup/fileSaveLoad.js | 27 ++- edit.js | 86 +++++++- 3 files changed, 277 insertions(+), 275 deletions(-) diff --git a/background.js b/background.js index 88acd85d..1162cc08 100644 --- a/background.js +++ b/background.js @@ -1,44 +1,73 @@ /* global getDatabase, getStyles, saveStyle */ 'use strict'; -chrome.webNavigation.onBeforeNavigate.addListener(data => { - webNavigationListener(null, data); +// eslint-disable-next-line no-var +var browserCommands, contextMenus; + +// ************************************************************************* +// preload the DB and report errors +getDatabase(() => {}, (...args) => { + args.forEach(arg => 'message' in arg && console.error(arg.message)); }); -chrome.webNavigation.onCommitted.addListener(data => { - webNavigationListener('styleApply', data); -}); +// ************************************************************************* +// register all listeners +chrome.runtime.onMessage.addListener(onRuntimeMessage); -chrome.webNavigation.onHistoryStateUpdated.addListener(data => { - webNavigationListener('styleReplaceAll', data); -}); +chrome.webNavigation.onBeforeNavigate.addListener(data => + webNavigationListener(null, data)); -chrome.webNavigation.onReferenceFragmentUpdated.addListener(data => { - webNavigationListener('styleReplaceAll', data); -}); +chrome.webNavigation.onCommitted.addListener(data => + webNavigationListener('styleApply', data)); -function webNavigationListener(method, data) { - getStyles({matchUrl: data.url, enabled: true, asHash: true}, styles => { - // we can't inject chrome:// and chrome-extension:// pages - // so we'll only inform our page of the change - // and it'll retrieve the styles directly - if (method && !data.url.startsWith('chrome:') && data.tabId >= 0) { - const isOwnPage = data.url.startsWith(URLS.ownOrigin); - chrome.tabs.sendMessage( - data.tabId, - {method, styles: isOwnPage ? 'DIY' : styles}, - {frameId: data.frameId}); - } - // main page frame id is 0 - if (data.frameId == 0) { - updateIcon({id: data.tabId, url: data.url}, styles); +chrome.webNavigation.onHistoryStateUpdated.addListener(data => + webNavigationListener('styleReplaceAll', data)); + +chrome.webNavigation.onReferenceFragmentUpdated.addListener(data => + webNavigationListener('styleReplaceAll', data)); + +chrome.tabs.onAttached.addListener((tabId, data) => { + // When an edit page gets attached or detached, remember its state + // so we can do the same to the next one to open. + chrome.tabs.get(tabId, tab => { + if (tab.url.startsWith(URLS.ownOrigin + 'edit.html')) { + chrome.windows.get(tab.windowId, {populate: true}, win => { + // If there's only one tab in this window, it's been dragged to new window + prefs.set('openEditInWindow', win.tabs.length == 1); + }); } }); +}); + +chrome.contextMenus.onClicked.addListener((info, tab) => + contextMenus[info.menuItemId].click(info, tab)); + +if ('commands' in chrome) { + // Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350 + chrome.commands.onCommand.addListener(command => browserCommands[command]()); } -// reset i18n cache on language change +// ************************************************************************* +// Open FAQs page once after installation to guide new users. +// Do not display it in development mode. +if (chrome.runtime.getManifest().update_url) { + const openHomepageOnInstall = ({reason}) => { + chrome.runtime.onInstalled.removeListener(openHomepageOnInstall); + if (reason == 'install') { + const version = chrome.runtime.getManifest().version; + setTimeout(openURL, 100, { + url: `http://add0n.com/stylus.html?version=${version}&type=install` + }); + } + }; + // bind for 60 seconds max and auto-unbind if it's a normal run + chrome.runtime.onInstalled.addListener(openHomepageOnInstall); + setTimeout(openHomepageOnInstall, 60e3, {reason: 'unbindme'}); +} -setTimeout(() => { +// ************************************************************************* +// reset L10N cache on UI language change +{ const {browserUIlanguage} = tryJSONparse(localStorage.L10N) || {}; const UIlang = chrome.i18n.getUILanguage(); if (browserUIlanguage != UIlang) { @@ -46,74 +75,22 @@ setTimeout(() => { browserUIlanguage: UIlang, }); } -}); - -// messaging - -chrome.runtime.onMessage.addListener(onRuntimeMessage); - -function onRuntimeMessage(request, sender, sendResponse) { - switch (request.method) { - - case 'getStyles': - getStyles(request, styles => { - sendResponse(styles); - // check if this is a main content frame style enumeration - if (request.matchUrl && !request.id - && sender && sender.tab && sender.frameId == 0 - && sender.tab.url == request.matchUrl) { - updateIcon(sender.tab, styles); - } - }); - return KEEP_CHANNEL_OPEN; - - case 'saveStyle': - saveStyle(request).then(sendResponse); - return KEEP_CHANNEL_OPEN; - - case 'healthCheck': - getDatabase( - () => sendResponse(true), - () => sendResponse(false)); - return KEEP_CHANNEL_OPEN; - - case 'prefChanged': - for (var prefName in request.prefs) { // eslint-disable-line no-var - if (prefName in contextMenus) { // eslint-disable-line no-use-before-define - chrome.contextMenus.update(prefName, { - checked: request.prefs[prefName], - }, ignoreChromeError); - } - } - break; - - case 'download': - download(request.url) - .then(sendResponse) - .catch(() => sendResponse(null)); - return KEEP_CHANNEL_OPEN; - } } -// commands (global hotkeys) - -const browserCommands = { +// ************************************************************************* +// browser commands +browserCommands = { openManage() { openURL({url: '/manage.html'}); }, - styleDisableAll(state) { - prefs.set('disableAll', - typeof state == 'boolean' ? state : !prefs.get('disableAll')); + styleDisableAll(info) { + prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll')); }, }; -// Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350 -if ('commands' in chrome) { - chrome.commands.onCommand.addListener(command => browserCommands[command]()); -} +// ************************************************************************* // context menus -// eslint-disable-next-line no-var -var contextMenus = { +contextMenus = Object.assign({ 'show-badge': { title: 'menuShowBadge', click: info => prefs.set(info.menuItemId, info.checked), @@ -126,29 +103,25 @@ var contextMenus = { title: 'openStylesManager', click: browserCommands.openManage, }, -}; - -// detect browsers without Delete by looking at the end of UA string -// Google Chrome: Safari/# -// but skip CentBrowser: Safari/# plus Shockwave Flash in plugins -// Vivaldi: Vivaldi/# -if (/Vivaldi\/[\d.]+$/.test(navigator.userAgent) - || /Safari\/[\d.]+$/.test(navigator.userAgent) - && !Array.from(navigator.plugins).some(p => p.name == 'Shockwave Flash')) { - contextMenus.editDeleteText = { +}, + // detect browsers without Delete by looking at the end of UA string + /Vivaldi\/[\d.]+$/.test(navigator.userAgent) || + // Chrome and co. + /Safari\/[\d.]+$/.test(navigator.userAgent) && + // skip forks with Flash as those are likely to have the menu e.g. CentBrowser + !Array.from(navigator.plugins).some(p => p.name == 'Shockwave Flash') +&& { + 'editDeleteText': { title: 'editDeleteText', contexts: ['editable'], documentUrlPatterns: [URLS.ownOrigin + 'edit*'], click: (info, tab) => { chrome.tabs.sendMessage(tab.id, {method: 'editDeleteText'}); }, - }; -} + } +}); -chrome.contextMenus.onClicked.addListener((info, tab) => - contextMenus[info.menuItemId].click(info, tab)); - -Object.keys(contextMenus).forEach(id => { +for (const id of Object.keys(contextMenus)) { const item = Object.assign({id}, contextMenus[id]); const prefValue = prefs.readOnlyValues[id]; const isBoolean = typeof prefValue == 'boolean'; @@ -162,110 +135,80 @@ Object.keys(contextMenus).forEach(id => { } delete item.click; chrome.contextMenus.create(item, ignoreChromeError); -}); +} - -// Get the DB so that any first run actions will be performed immediately -// when the background page loads. -getDatabase(() => {}, (...args) => { - args.forEach(arg => 'message' in arg && console.error(arg.message)); -}); - - -// When an edit page gets attached or detached, remember its state -// so we can do the same to the next one to open. -const editFullUrl = URLS.ownOrigin + 'edit.html'; -chrome.tabs.onAttached.addListener((tabId, data) => { - chrome.tabs.get(tabId, tabData => { - if (tabData.url.startsWith(editFullUrl)) { - chrome.windows.get(tabData.windowId, {populate: true}, win => { - // If there's only one tab in this window, it's been dragged to new window - prefs.set('openEditInWindow', win.tabs.length == 1); - }); - } - }); -}); - -// eslint-disable-next-line no-var -var codeMirrorThemes; -getCodeMirrorThemes().then(themes => { - codeMirrorThemes = themes; -}); - -// do not use prefs.get('version', null) as it might not yet be available -chrome.storage.local.get('version', prefs => { - // Open FAQs page once after installation to guide new users, - // https://github.com/schomery/stylish-chrome/issues/22#issuecomment-279936160 - if (!prefs.version) { - // do not display the FAQs page in development mode - if ('update_url' in chrome.runtime.getManifest()) { - const version = chrome.runtime.getManifest().version; - chrome.storage.local.set({version}, () => { - window.setTimeout(() => { - chrome.tabs.create({ - url: `http://add0n.com/stylus.html?version=${version}&type=install` - }); - }, 3000); - }); - } - } -}); - - -injectContentScripts(); - -function injectContentScripts() { - // expand * as .*? - const wildcardAsRegExp = (s, flags) => - new RegExp(s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&').replace(/\*/g, '.*?'), flags); - const contentScripts = chrome.runtime.getManifest().content_scripts; - for (const cs of contentScripts) { - cs.matches = cs.matches.map(m => ( - m == '' ? m : wildcardAsRegExp(m) - )); - } - chrome.tabs.query({}, tabs => { - for (const tab of tabs) { - for (const cs of contentScripts) { - for (const m of cs.matches) { - if ((m == '' || tab.url.match(m)) - && (!tab.url.startsWith('chrome') || tab.url == 'chrome://newtab/')) { - chrome.tabs.sendMessage(tab.id, {method: 'ping'}, pong => { - if (!pong) { - chrome.tabs.executeScript(tab.id, { - file: cs.js[0], - runAt: cs.run_at, - allFrames: cs.all_frames, - matchAboutBlank: cs.match_about_blank, - }, ignoreChromeError); - } - }); - // inject the content script just once - break; - } - } +Object.defineProperty(contextMenus, 'updateOnPrefChanged', { + value: changedPrefs => { + for (const id in changedPrefs) { + if (id in contextMenus) { + chrome.contextMenus.update(id, { + checked: changedPrefs[id], + }, ignoreChromeError); } } - }); + } +}); + +// ************************************************************************* +// [re]inject content scripts +{ + const NTP = 'chrome://newtab/'; + const PING = {method: 'ping'}; + const ALL_URLS = ''; + const contentScripts = chrome.runtime.getManifest().content_scripts; + // expand * as .*? + const wildcardAsRegExp = (s, flags) => new RegExp( + s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&') + .replace(/\*/g, '.*?'), flags); + for (const cs of contentScripts) { + cs.matches = cs.matches.map(m => ( + m == ALL_URLS ? m : wildcardAsRegExp(m) + )); + } + + const injectCS = (cs, tabId) => { + chrome.tabs.executeScript(tabId, { + file: cs.js[0], + runAt: cs.run_at, + allFrames: cs.all_frames, + matchAboutBlank: cs.match_about_blank, + }, ignoreChromeError); + }; + + const pingCS = (cs, {id, url}) => { + cs.matches.some(match => { + if ((match == ALL_URLS || url.match(match)) + && (!url.startsWith('chrome') || url == NTP)) { + chrome.tabs.sendMessage(id, PING, pong => !pong && injectCS(cs, id)); + return true; + } + }); + }; + + chrome.tabs.query({}, tabs => + tabs.forEach(tab => + contentScripts.forEach(cs => + pingCS(cs, tab)))); } -function refreshAllTabs() { - return new Promise(resolve => { - // list all tabs including chrome-extension:// which can be ours - chrome.tabs.query({}, tabs => { - const lastTab = tabs[tabs.length - 1]; - for (const tab of tabs) { - getStyles({matchUrl: tab.url, enabled: true, asHash: true}, styles => { - const message = {method: 'styleReplaceAll', styles}; - chrome.tabs.sendMessage(tab.id, message); - updateIcon(tab, styles); - if (tab == lastTab) { - resolve(); - } - }); - } - }); +// ************************************************************************* + +function webNavigationListener(method, {url, tabId, frameId}) { + getStyles({matchUrl: url, enabled: true, asHash: true}, styles => { + if (method && !url.startsWith('chrome:') && tabId >= 0) { + chrome.tabs.sendMessage(tabId, { + method, + // ping own page so it retrieves the styles directly + styles: url.startsWith(URLS.ownOrigin) ? 'DIY' : styles, + }, { + frameId + }); + } + // main page frame id is 0 + if (frameId == 0) { + updateIcon({id: tabId, url}, styles); + } }); } @@ -322,73 +265,31 @@ function updateIcon(tab, styles) { } -function getCodeMirrorThemes() { - if (!chrome.runtime.getPackageDirectoryEntry) { - return Promise.resolve([ - chrome.i18n.getMessage('defaultTheme'), - '3024-day', - '3024-night', - 'abcdef', - 'ambiance', - 'ambiance-mobile', - 'base16-dark', - 'base16-light', - 'bespin', - 'blackboard', - 'cobalt', - 'colorforth', - 'dracula', - 'duotone-dark', - 'duotone-light', - 'eclipse', - 'elegant', - 'erlang-dark', - 'hopscotch', - 'icecoder', - 'isotope', - 'lesser-dark', - 'liquibyte', - 'material', - 'mbo', - 'mdn-like', - 'midnight', - 'monokai', - 'neat', - 'neo', - 'night', - 'panda-syntax', - 'paraiso-dark', - 'paraiso-light', - 'pastel-on-dark', - 'railscasts', - 'rubyblue', - 'seti', - 'solarized', - 'the-matrix', - 'tomorrow-night-bright', - 'tomorrow-night-eighties', - 'ttcn', - 'twilight', - 'vibrant-ink', - 'xq-dark', - 'xq-light', - 'yeti', - 'zenburn', - ]); +function onRuntimeMessage(request, sender, sendResponse) { + switch (request.method) { + + case 'getStyles': + getStyles(request, sendResponse); + return KEEP_CHANNEL_OPEN; + + case 'saveStyle': + saveStyle(request).then(sendResponse); + return KEEP_CHANNEL_OPEN; + + case 'healthCheck': + getDatabase( + () => sendResponse(true), + () => sendResponse(false)); + return KEEP_CHANNEL_OPEN; + + case 'prefChanged': + contextMenus.updateOnPrefChanged(request.prefs); + break; + + case 'download': + download(request.url) + .then(sendResponse) + .catch(() => sendResponse(null)); + return KEEP_CHANNEL_OPEN; } - return new Promise(resolve => { - chrome.runtime.getPackageDirectoryEntry(rootDir => { - rootDir.getDirectory('codemirror/theme', {create: false}, themeDir => { - themeDir.createReader().readEntries(entries => { - resolve([ - chrome.i18n.getMessage('defaultTheme') - ].concat( - entries.filter(entry => entry.isFile) - .sort((a, b) => (a.name < b.name ? -1 : 1)) - .map(entry => entry.name.replace(/\.css$/, '')) - )); - }); - }); - }); - }); } diff --git a/backup/fileSaveLoad.js b/backup/fileSaveLoad.js index 004daad0..ce5f2032 100644 --- a/backup/fileSaveLoad.js +++ b/backup/fileSaveLoad.js @@ -1,4 +1,4 @@ -/* global messageBox, handleUpdate */ +/* global messageBox, handleUpdate, applyOnMessage */ 'use strict'; const STYLISH_DUMP_FILE_EXT = '.txt'; @@ -151,7 +151,7 @@ function importFromString(jsonString) { stats.metaOnly.names.length + stats.codeOnly.names.length + stats.added.names.length; - Promise.resolve(numChanged && BG.refreshAllTabs()).then(() => { + Promise.resolve(numChanged && refreshAllTabs()).then(() => { const report = Object.keys(stats) .filter(kind => stats[kind].names.length) .map(kind => { @@ -248,6 +248,29 @@ function importFromString(jsonString) { ? oldStyle.name + ' —> ' + newStyle.name : oldStyle.name; } + + function refreshAllTabs() { + return getActiveTab().then(activeTab => new Promise(resolve => { + // list all tabs including chrome-extension:// which can be ours + chrome.tabs.query({}, tabs => { + const lastTab = tabs[tabs.length - 1]; + for (const tab of tabs) { + getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => { + const message = {method: 'styleReplaceAll', styles}; + if (tab.id == activeTab.id) { + applyOnMessage(message); + } else { + chrome.tabs.sendMessage(tab.id, message); + } + BG.updateIcon(tab, styles); + if (tab == lastTab) { + resolve(); + } + }); + } + }); + })); + } } diff --git a/edit.js b/edit.js index ae41418a..15f58376 100644 --- a/edit.js +++ b/edit.js @@ -47,6 +47,8 @@ new MutationObserver((mutations, observer) => { } }).observe(document, {subtree: true, childList: true}); +getCodeMirrorThemes(); + // reroute handling to nearest editor when keypress resolves to one of these commands var hotkeyRerouter = { commands: { @@ -254,14 +256,15 @@ function initCodeMirror() { return options.map(function(opt) { return ""; }).join(""); } var themeControl = document.getElementById("editor.theme"); - if (BG && BG.codeMirrorThemes) { - themeControl.innerHTML = optionsHtmlFromArray(BG.codeMirrorThemes); + const themeList = localStorage.codeMirrorThemes; + if (themeList) { + themeControl.innerHTML = optionsHtmlFromArray(themeList.split(/\s+/)); } else { // Chrome is starting up and shows our edit.html, but the background page isn't loaded yet const theme = prefs.get("editor.theme"); themeControl.innerHTML = optionsHtmlFromArray([theme == "default" ? t("defaultTheme") : theme]); - BG.getCodeMirrorThemes().then(themes => { - BG.codeMirrorThemes = themes; + getCodeMirrorThemes().then(() => { + const themes = (localStorage.codeMirrorThemes || '').split(/\s+/); themeControl.innerHTML = optionsHtmlFromArray(themes); themeControl.selectedIndex = Math.max(0, themes.indexOf(theme)); }); @@ -1868,3 +1871,78 @@ function getComputedHeight(el) { return el.getBoundingClientRect().height + parseFloat(compStyle.marginTop) + parseFloat(compStyle.marginBottom); } + + +function getCodeMirrorThemes() { + if (!chrome.runtime.getPackageDirectoryEntry) { + const themes = Promise.resolve([ + chrome.i18n.getMessage('defaultTheme'), + '3024-day', + '3024-night', + 'abcdef', + 'ambiance', + 'ambiance-mobile', + 'base16-dark', + 'base16-light', + 'bespin', + 'blackboard', + 'cobalt', + 'colorforth', + 'dracula', + 'duotone-dark', + 'duotone-light', + 'eclipse', + 'elegant', + 'erlang-dark', + 'hopscotch', + 'icecoder', + 'isotope', + 'lesser-dark', + 'liquibyte', + 'material', + 'mbo', + 'mdn-like', + 'midnight', + 'monokai', + 'neat', + 'neo', + 'night', + 'panda-syntax', + 'paraiso-dark', + 'paraiso-light', + 'pastel-on-dark', + 'railscasts', + 'rubyblue', + 'seti', + 'solarized', + 'the-matrix', + 'tomorrow-night-bright', + 'tomorrow-night-eighties', + 'ttcn', + 'twilight', + 'vibrant-ink', + 'xq-dark', + 'xq-light', + 'yeti', + 'zenburn', + ]); + localStorage.codeMirrorThemes = themes.join(' '); + } + return new Promise(resolve => { + chrome.runtime.getPackageDirectoryEntry(rootDir => { + rootDir.getDirectory('codemirror/theme', {create: false}, themeDir => { + themeDir.createReader().readEntries(entries => { + const themes = [ + chrome.i18n.getMessage('defaultTheme') + ].concat( + entries.filter(entry => entry.isFile) + .sort((a, b) => (a.name < b.name ? -1 : 1)) + .map(entry => entry.name.replace(/\.css$/, '')) + ); + localStorage.codeMirrorThemes = themes.join(' '); + resolve(themes); + }); + }); + }); + }); +} From 2e60af40f06591afaadd837bbd9f6a1d6b80d52e Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 20 Apr 2017 21:27:10 +0300 Subject: [PATCH 182/235] refactor bg updater; add prefs.subscribe() --- .eslintrc | 2 +- background.js | 18 +---- manage.js | 5 +- messaging.js | 4 + options/index.js | 72 ++++++++---------- prefs.js | 38 +++++++--- update.js | 186 +++++++++++++++++++---------------------------- 7 files changed, 141 insertions(+), 184 deletions(-) diff --git a/.eslintrc b/.eslintrc index b20ffc9d..8cc63436 100644 --- a/.eslintrc +++ b/.eslintrc @@ -112,7 +112,7 @@ rules: no-case-declarations: [2] no-class-assign: [2] no-cond-assign: [2, except-parens] - no-confusing-arrow: [2, {allowParens: true}] + no-confusing-arrow: [1, {allowParens: true}] no-const-assign: [2] no-constant-condition: [0] no-continue: [0] diff --git a/background.js b/background.js index 1162cc08..415de5ae 100644 --- a/background.js +++ b/background.js @@ -137,17 +137,9 @@ for (const id of Object.keys(contextMenus)) { chrome.contextMenus.create(item, ignoreChromeError); } -Object.defineProperty(contextMenus, 'updateOnPrefChanged', { - value: changedPrefs => { - for (const id in changedPrefs) { - if (id in contextMenus) { - chrome.contextMenus.update(id, { - checked: changedPrefs[id], - }, ignoreChromeError); - } - } - } -}); +prefs.subscribe((id, checked) => { + chrome.contextMenus.update(id, {checked}, ignoreChromeError); +}, Object.keys(contextMenus)); // ************************************************************************* // [re]inject content scripts @@ -282,10 +274,6 @@ function onRuntimeMessage(request, sender, sendResponse) { () => sendResponse(false)); return KEEP_CHANNEL_OPEN; - case 'prefChanged': - contextMenus.updateOnPrefChanged(request.prefs); - break; - case 'download': download(request.url) .then(sendResponse) diff --git a/manage.js b/manage.js index 464f47ca..0af0d7f1 100644 --- a/manage.js +++ b/manage.js @@ -499,10 +499,7 @@ function checkUpdateAll() { let updatesFound = false; let checked = 0; processQueue(); - // notify the automatic updater to reset the next automatic update accordingly - chrome.runtime.sendMessage({ - method: 'resetInterval' - }); + BG.updater.resetInterval(); function processQueue(status) { if (status === true) { diff --git a/messaging.js b/messaging.js index c0020345..b688270a 100644 --- a/messaging.js +++ b/messaging.js @@ -203,6 +203,10 @@ function debounce(fn, delay, ...args) { timers.delete(fn); fn(...args); }); + debounce.unregister = debounce.unregister || (fn => { + clearTimeout(timers.get(fn)); + timers.delete(fn); + }); clearTimeout(timers.get(fn)); timers.set(fn, setTimeout(debounce.run, delay, fn, ...args)); } diff --git a/options/index.js b/options/index.js index 6da43d07..2d0b5808 100644 --- a/options/index.js +++ b/options/index.js @@ -22,54 +22,13 @@ document.onclick = e => { // prevent double-triggering in case a sub-element was clicked e.stopPropagation(); - function check() { - let total = 0; - let checked = 0; - let updated = 0; - $('#update-progress').style.width = 0; - $('#updates-installed').dataset.value = ''; - document.body.classList.add('update-in-progress'); - const maxWidth = $('#update-progress').parentElement.clientWidth; - function showProgress() { - $('#update-progress').style.width = Math.round(checked / total * maxWidth) + 'px'; - $('#updates-installed').dataset.value = updated || ''; - } - function done() { - document.body.classList.remove('update-in-progress'); - } - BG.update.perform((cmd, value) => { - switch (cmd) { - case 'count': - total = value; - if (!total) { - done(); - } - break; - case 'single-updated': - updated++; - // fallthrough - case 'single-skipped': - checked++; - if (total && checked === total) { - done(); - } - break; - } - showProgress(); - }); - // notify the automatic updater to reset the next automatic update accordingly - chrome.runtime.sendMessage({ - method: 'resetInterval' - }); - } - switch (target.dataset.cmd) { case 'open-manage': openURL({url: '/manage.html'}); break; case 'check-updates': - check(); + checkUpdates(); break; case 'open-keyboard': @@ -84,3 +43,32 @@ document.onclick = e => { break; } }; + +function checkUpdates() { + let total = 0; + let checked = 0; + let updated = 0; + const installed = $('#updates-installed'); + const progress = $('#update-progress'); + const maxWidth = progress.parentElement.clientWidth; + progress.style.width = 0; + installed.dataset.value = ''; + document.body.classList.add('update-in-progress'); + BG.updater.checkAllStyles((state, value) => { + switch (state) { + case BG.updater.COUNT: + total = value; + break; + case BG.updater.UPDATED: + updated++; + // fallthrough + case BG.updater.SKIPPED: + checked++; + break; + } + progress.style.width = Math.round(checked / total * maxWidth) + 'px'; + installed.dataset.value = updated || ''; + }).then(() => { + document.body.classList.remove('update-in-progress'); + }); +} diff --git a/prefs.js b/prefs.js index 17510093..1c981e63 100644 --- a/prefs.js +++ b/prefs.js @@ -60,6 +60,11 @@ var prefs = new function Prefs() { 'badgeNormal', ]; + const onChange = { + any: new Set(), + specific: new Map(), + }; + // coalesce multiple pref changes in broadcast let broadcastPrefs = {}; @@ -101,16 +106,26 @@ var prefs = new function Prefs() { } values[key] = value; defineReadonlyProperty(this.readOnlyValues, key, value); + const hasChanged = !equal(value, oldValue); if (BG && BG != window) { BG.prefs.set(key, BG.deepCopy(value), {noBroadcast, noSync}); } else { localStorage[key] = typeof defaults[key] == 'object' ? JSON.stringify(value) : value; - if (!noBroadcast && !equal(value, oldValue)) { + if (!noBroadcast && hasChanged) { this.broadcast(key, value, {noSync}); } } + if (hasChanged) { + const listener = onChange.specific.get(key); + if (listener) { + listener(key, value); + } + for (const listener of onChange.any.values()) { + listener(key, value); + } + } }, remove: key => this.set(key, undefined), @@ -124,6 +139,16 @@ var prefs = new function Prefs() { debounce(doSyncSet); } }, + + subscribe(listener, keys) { + if (keys) { + for (const key of keys) { + onChange.specific.set(key, listener); + } + } else { + onChange.any.add(listener); + } + }, }); // Unlike sync, HTML5 localStorage is ready at browser startup @@ -289,15 +314,8 @@ function setupLivePrefs(IDs) { updateElement({id, element, force: true}); element.addEventListener('change', onChange); } - chrome.runtime.onMessage.addListener(msg => { - if (msg.prefs) { - for (const id in msg.prefs) { - if (id in checkedProps) { - updateElement({id, value: msg.prefs[id]}); - } - } - } - }); + prefs.subscribe((id, value) => updateElement({id, value}), IDs); + function onChange() { const value = this[checkedProps[this.id]]; if (prefs.get(this.id) != value) { diff --git a/update.js b/update.js index 3ff46ebf..02214985 100644 --- a/update.js +++ b/update.js @@ -1,118 +1,80 @@ -/* eslint brace-style: 1, arrow-parens: 1, space-before-function-paren: 1, arrow-body-style: 1 */ -/* globals getStyles, saveStyle */ +/* globals getStyles, saveStyle, styleSectionsEqual */ 'use strict'; -// TODO: refactor to make usable in manage::Updater -var update = { - fetch: (resource, callback) => { - let req = new XMLHttpRequest(); - let [url, data] = resource.split('?'); - req.open('POST', url, true); - req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); - req.onload = () => callback(req.responseText); - req.onerror = req.ontimeout = () => callback(); - req.send(data); - }, - md5Check: (style, callback, skipped) => { - let req = new XMLHttpRequest(); - req.open('GET', style.md5Url, true); - req.onload = () => { - let md5 = req.responseText; - if (md5 && md5 !== style.originalMd5) { - callback(style); - } - else { - skipped(`"${style.name}" style is up-to-date`); - } - }; - req.onerror = req.ontimeout = () => skipped('Error validating MD5 checksum'); - req.send(); - }, - list: (callback) => { - getStyles({}, (styles) => callback(styles.filter(style => style.updateUrl))); - }, - perform: (observe = function () {}) => { - // TODO: use sectionsAreEqual - // from install.js - function arraysAreEqual (a, b) { - // treat empty array and undefined as equivalent - if (typeof a === 'undefined') { - return (typeof b === 'undefined') || (b.length === 0); - } - if (typeof b === 'undefined') { - return (typeof a === 'undefined') || (a.length === 0); - } - if (a.length !== b.length) { - return false; - } - return a.every(function (entry) { - return b.indexOf(entry) !== -1; +// eslint-disable-next-line no-var +var updater = { + + COUNT: 'count', + UPDATED: 'updated', + SKIPPED: 'skipped', + SKIPPED_SAME_MD5: 'up-to-date: MD5 is unchanged', + SKIPPED_SAME_CODE: 'up-to-date: code sections are unchanged', + SKIPPED_ERROR_MD5: 'error: MD5 is invalid', + SKIPPED_ERROR_JSON: 'error: JSON is invalid', + DONE: 'done', + + lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(), + + checkAllStyles(observe = () => {}) { + updater.resetInterval(); + return new Promise(resolve => { + getStyles({}, styles => { + styles = styles.filter(style => style.updateUrl); + observe(updater.COUNT, styles.length); + Promise.all(styles.map(style => + updater.checkStyle(style) + .then(saveStyle) + .then(saved => observe(updater.UPDATED, saved)) + .catch(err => observe(updater.SKIPPED, style, err)) + )).then(() => { + observe(updater.DONE); + resolve(); + }); }); - } - // from install.js - function sectionsAreEqual(a, b) { - if (a.code !== b.code) { - return false; - } - return ['urls', 'urlPrefixes', 'domains', 'regexps'].every(function (attribute) { - return arraysAreEqual(a[attribute], b[attribute]); - }); - } - - update.list(styles => { - observe('count', styles.length); - styles.forEach(style => update.md5Check(style, style => update.fetch(style.updateUrl, response => { - if (response) { - let json = JSON.parse(response); - - if (json.sections.length === style.sections.length) { - if (json.sections.every((section) => { - return style.sections.some(installedSection => sectionsAreEqual(section, installedSection)); - })) { - return observe('single-skipped', '2'); // everything is the same - } - json.method = 'saveStyle'; - json.id = style.id; - - saveStyle(json).then(style => { - observe('single-updated', style.name); - }); - } - else { - return observe('single-skipped', '3'); // style sections mismatch - } - } - }), () => observe('single-skipped', '1'))); }); - } -}; -// automatically update all user-styles if "updateInterval" pref is set -window.setTimeout(function () { - let id; - function run () { - update.perform(/*(cmd, value) => console.log(cmd, value)*/); - reset(); - } - function reset () { - window.clearTimeout(id); - let interval = prefs.get('updateInterval'); - // if interval === 0 => automatic update is disabled + }, + + checkStyle(style) { + return download(style.md5Url) + .then(md5 => + !md5 || md5.length != 32 ? Promise.reject(updater.SKIPPED_ERROR_MD5) : + md5 == style.originalMd5 ? Promise.reject(updater.SKIPPED_SAME_MD5) : + style.updateUrl) + .then(download) + .then(text => tryJSONparse(text)) + .then(json => + !updater.styleJSONseemsValid(json) ? Promise.reject(updater.SKIPPED_ERROR_JSON) : + styleSectionsEqual(json, style) ? Promise.reject(updater.SKIPPED_SAME_CODE) : + // keep the local name as it could've been customized by the user + Object.assign(json, { + id: style.id, + name: null, + })); + }, + + styleJSONseemsValid(json) { + return json + && json.sections + && json.sections.length + && typeof json.sections.every == 'function' + && typeof json.sections[0].code == 'string'; + }, + + schedule() { + const interval = prefs.get('updateInterval') * 60 * 60 * 1000; if (interval) { - /* console.log('next update', interval); */ - id = window.setTimeout(run, interval * 60 * 60 * 1000); + const elapsed = Math.max(0, Date.now() - updater.lastUpdateTime); + debounce(updater.checkAllStyles, Math.max(10e3, interval - elapsed)); + } else if (debounce.timers) { + debounce.unregister(updater.checkAllStyles); } - } - if (prefs.get('updateInterval')) { - run(); - } - chrome.runtime.onMessage.addListener(request => { - // when user has changed the predefined time interval in the settings page - if (request.method === 'prefChanged' && 'updateInterval' in request.prefs) { - reset(); - } - // when user just manually checked for updates - if (request.method === 'resetInterval') { - reset(); - } - }); -}, 10000); + }, + + resetInterval() { + localStorage.lastUpdateTime = updater.lastUpdateTime = Date.now(); + updater.schedule(); + }, +}; + +updater.schedule(); +prefs.subscribe(updater.schedule, ['updateInterval']); From 9e9723cfd2cb5bdab339b29dc87affa5ca6faf35 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 21 Apr 2017 11:59:01 +0300 Subject: [PATCH 183/235] code cosmetics --- apply.js | 52 ++++++++++++++++++++++++---------------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/apply.js b/apply.js index 143bba3f..7c3b7161 100644 --- a/apply.js +++ b/apply.js @@ -37,10 +37,7 @@ function requestStyles(options, callback = applyStyles) { enabled: true, asHash: true, }, options); - // If this is a Stylish page (Edit Style or Manage Styles), - // we'll request the styles directly to minimize delay and flicker, - // unless Chrome is still starting up and the background page isn't fully loaded. - // (Note: in this case the function may be invoked again from applyStyles.) + // On own pages we request the styles directly to minimize delay and flicker if (typeof getStylesSafe !== 'undefined') { getStylesSafe(request).then(callback); } else { @@ -59,6 +56,7 @@ function applyOnMessage(request, sender, sendResponse) { }); return; } + switch (request.method) { case 'styleDeleted': @@ -122,28 +120,20 @@ function doDisableAll(disable) { function applyStyleState({id, enabled}) { const inCache = disabledElements.get(id) || styleElements.get(id); const inDoc = document.getElementById(ID_PREFIX + id); - if (enabled && inDoc || !enabled && !inDoc) { - return; - } - if (enabled && !inDoc && !inCache) { - requestStyles({id}); - return; - } - if (enabled && inCache) { - addStyleElement(inCache); - disabledElements.delete(id); - return; - } - if (!enabled && inDoc) { - disabledElements.set(id, inDoc); - inDoc.remove(); - if (document.location.href == 'about:srcdoc') { - const original = document.getElementById(ID_PREFIX + id); - if (original) { - original.remove(); - } + if (enabled) { + if (inDoc) { + return; + } else if (inCache) { + addStyleElement(inCache); + disabledElements.delete(id); + } else { + requestStyles({id}); + } + } else { + if (inDoc) { + disabledElements.set(id, inDoc); + inDoc.remove(); } - return; } } @@ -208,12 +198,13 @@ function applySections(styleId, sections) { return; } if (document.documentElement instanceof SVGSVGElement) { - // SVG document, make an SVG style element. + // SVG document style el = document.createElementNS('http://www.w3.org/2000/svg', 'style'); } else if (document instanceof XMLDocument) { + // XML document style el = document.createElementNS('http://www.w3.org/1999/xhtml', 'style'); } else { - // This will make an HTML style element. If there's SVG embedded in an HTML document, this works on the SVG too. + // HTML document style; also works on HTML-embedded SVG el = document.createElement('style'); } Object.assign(el, { @@ -242,6 +233,7 @@ function replaceAll(newStyles) { oldStyles.forEach(el => (el.id += '-ghost')); styleElements.clear(); disabledElements.clear(); + [...retiredStyleTimers.values()].forEach(clearTimeout); retiredStyleTimers.clear(); applyStyles(newStyles); oldStyles.forEach(el => el.remove()); @@ -273,7 +265,11 @@ function initDocRewriteObserver() { }); docRewriteObserver.observe(document, {childList: true}); // detect dynamic iframes rewritten after creation by the embedder i.e. externally - setTimeout(() => document.documentElement != ROOT && reinjectStyles()); + setTimeout(() => { + if (document.documentElement != ROOT) { + reinjectStyles(); + } + }); } From 468a758cec4f25d44bbee521c9d4275f543f0100 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 21 Apr 2017 12:33:24 +0300 Subject: [PATCH 184/235] code cosmetics: inverted "no????" params to straight ones --- prefs.js | 47 ++++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/prefs.js b/prefs.js index 1c981e63..771945b0 100644 --- a/prefs.js +++ b/prefs.js @@ -89,7 +89,7 @@ var prefs = new function Prefs() { return deepCopy(values); }, - set(key, value, {noBroadcast, noSync} = {}) { + set(key, value, {broadcast = true, sync = true, fromBroadcast} = {}) { const oldValue = values[key]; switch (typeof defaults[key]) { case typeof value: @@ -107,14 +107,16 @@ var prefs = new function Prefs() { values[key] = value; defineReadonlyProperty(this.readOnlyValues, key, value); const hasChanged = !equal(value, oldValue); - if (BG && BG != window) { - BG.prefs.set(key, BG.deepCopy(value), {noBroadcast, noSync}); - } else { - localStorage[key] = typeof defaults[key] == 'object' - ? JSON.stringify(value) - : value; - if (!noBroadcast && hasChanged) { - this.broadcast(key, value, {noSync}); + if (!fromBroadcast) { + if (BG && BG != window) { + BG.prefs.set(key, BG.deepCopy(value), {broadcast, sync}); + } else { + localStorage[key] = typeof defaults[key] == 'object' + ? JSON.stringify(value) + : value; + if (broadcast && hasChanged) { + this.broadcast(key, value, {sync}); + } } } if (hasChanged) { @@ -132,10 +134,10 @@ var prefs = new function Prefs() { reset: key => this.set(key, deepCopy(defaults[key])), - broadcast(key, value, {noSync} = {}) { + broadcast(key, value, {sync = true} = {}) { broadcastPrefs[key] = value; debounce(doBroadcast); - if (!noSync) { + if (sync) { debounce(doSyncSet); } }, @@ -174,18 +176,14 @@ var prefs = new function Prefs() { } if (BG == window) { // when in bg page, .set() will write to localStorage - this.set(key, value, {noBroadcast: true, noSync: true}); + this.set(key, value, {broadcast: false, sync: false}); } else { values[key] = value; defineReadonlyProperty(this.readOnlyValues, key, value); } } - // any access to chrome API takes time due to initialization of bindings - let lazyInit = () => { - window.removeEventListener('load', lazyInit); - lazyInit = null; - + if (!BG || BG == window) { getSync().get('settings', ({settings: synced} = {}) => { if (synced) { for (const key in defaults) { @@ -195,14 +193,14 @@ var prefs = new function Prefs() { continue; } if (key in synced) { - this.set(key, synced[key], {noSync: true}); + this.set(key, synced[key], {sync: false}); } } } if (typeof contextMenus !== 'undefined') { for (const id in contextMenus) { if (typeof values[id] == 'boolean') { - this.broadcast(id, values[id], {noSync: true}); + this.broadcast(id, values[id], {sync: false}); } } } @@ -214,7 +212,7 @@ var prefs = new function Prefs() { if (synced) { for (const key in defaults) { if (key in synced) { - this.set(key, synced[key], {noSync: true}); + this.set(key, synced[key], {sync: false}); } } } else { @@ -223,17 +221,20 @@ var prefs = new function Prefs() { } } }); + } + // any access to chrome API takes time due to initialization of bindings + window.addEventListener('load', function _() { + window.removeEventListener('load', _); chrome.runtime.onMessage.addListener(msg => { if (msg.prefs) { for (const id in msg.prefs) { - this.set(id, msg.prefs[id], {noBroadcast: true, noSync: true}); + prefs.set(id, msg.prefs[id], {fromBroadcast: true}); } } }); - }; + }); - window.addEventListener('load', lazyInit); return; function doBroadcast() { From 47eefd1cd2fb518da9525e84c1c90c509ab610e9 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 21 Apr 2017 12:44:06 +0300 Subject: [PATCH 185/235] code cosmetics: contextMenus use prefs.subscribe() --- prefs.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/prefs.js b/prefs.js index 771945b0..b6de008f 100644 --- a/prefs.js +++ b/prefs.js @@ -197,13 +197,6 @@ var prefs = new function Prefs() { } } } - if (typeof contextMenus !== 'undefined') { - for (const id in contextMenus) { - if (typeof values[id] == 'boolean') { - this.broadcast(id, values[id], {sync: false}); - } - } - } }); chrome.storage.onChanged.addListener((changes, area) => { From 3713c252a82066ac12b4bb0d9de7dcc289a0cd81 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 21 Apr 2017 13:02:01 +0300 Subject: [PATCH 186/235] broadcast affectsIcon's keys on startup --- prefs.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/prefs.js b/prefs.js index b6de008f..c838cf7e 100644 --- a/prefs.js +++ b/prefs.js @@ -184,6 +184,8 @@ var prefs = new function Prefs() { } if (!BG || BG == window) { + affectsIcon.forEach(key => this.broadcast(key, values[key], {sync: false})); + getSync().get('settings', ({settings: synced} = {}) => { if (synced) { for (const key in defaults) { From 3b433adb42de1d8d9ecf23cb35f8a7473eb94289 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 21 Apr 2017 13:06:00 +0300 Subject: [PATCH 187/235] check if orphaned in docRewriteObserver --- apply.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apply.js b/apply.js index 7c3b7161..a2f523b2 100644 --- a/apply.js +++ b/apply.js @@ -246,6 +246,9 @@ function initDocRewriteObserver() { } // re-add styles if we detect documentElement being recreated const reinjectStyles = () => { + if (!styleElements) { + return orphanCheck && orphanCheck(); + } ROOT = document.documentElement; for (const el of styleElements.values()) { addStyleElement(document.importNode(el, true)); From fc7793453c90d400b3dd838a3ba7b461cb8646da Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 21 Apr 2017 13:57:01 +0300 Subject: [PATCH 188/235] code cosmetics: simplify import::undo --- backup/fileSaveLoad.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/backup/fileSaveLoad.js b/backup/fileSaveLoad.js index ce5f2032..33c75eb5 100644 --- a/backup/fileSaveLoad.js +++ b/backup/fileSaveLoad.js @@ -194,15 +194,18 @@ function importFromString(jsonString) { ...stats.codeOnly.ids, ...stats.added.ids, ]; + let resolve; index = 0; - return new Promise(undoNextId) - .then(BG.refreshAllTabs) + return new Promise(resolve_ => { + resolve = resolve_; + undoNextId(); + }).then(BG.refreshAllTabs) .then(() => messageBox({ title: t('importReportUndoneTitle'), contents: newIds.length + ' ' + t('importReportUndone'), buttons: [t('confirmOK')], })); - function undoNextId(resolve) { + function undoNextId() { if (index == newIds.length) { resolve(); return; @@ -211,13 +214,10 @@ function importFromString(jsonString) { deleteStyleSafe({id, notify: false}).then(id => { const oldStyle = oldStylesById.get(id); if (oldStyle) { - saveStyleSafe(Object.assign(oldStyle, { - reason: 'import', - notify: false, - })).then(() => - setTimeout(undoNextId, 0, resolve)); + saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS)) + .then(undoNextId); } else { - setTimeout(undoNextId, 0, resolve); + undoNextId(); } }); } From 9617d571f887e7bc8da2568a5b390fa9b5d6a200 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 21 Apr 2017 15:33:28 +0300 Subject: [PATCH 189/235] manage: reuse BG.updater --- manage.js | 175 +++++++++++++++++++++--------------------------------- update.js | 15 ++--- 2 files changed, 75 insertions(+), 115 deletions(-) diff --git a/manage.js b/manage.js index 0af0d7f1..90fed779 100644 --- a/manage.js +++ b/manage.js @@ -484,138 +484,97 @@ function applyUpdateAll() { function checkUpdateAll() { - const btnCheck = $('#check-all-updates'); - const btnApply = $('#apply-all-updates'); - const noUpdates = $('#update-all-no-updates'); - const progress = $('#update-progress'); + $('#check-all-updates').disabled = true; + $('#apply-all-updates').classList.add('hidden'); + $('#update-all-no-updates').classList.add('hidden'); - btnCheck.disabled = true; - btnApply.classList.add('hidden'); - noUpdates.classList.add('hidden'); - const maxWidth = progress.parentElement.clientWidth; - - const queue = $$('.updatable:not(.can-update)').map(checkUpdate); - const total = queue.length; - let updatesFound = false; + let total = 0; let checked = 0; - processQueue(); - BG.updater.resetInterval(); + let updated = 0; - function processQueue(status) { - if (status === true) { - updatesFound = true; - btnApply.disabled = true; - btnApply.classList.remove('hidden'); - renderUpdatesOnlyFilter({check: true}); + $$('.updatable:not(.can-update)').map(el => checkUpdate(el, {single: false})); + BG.updater.checkAllStyles(observe, {save: false}).then(done); + + function observe(state, value, details) { + switch (state) { + case BG.updater.COUNT: + total = value; + break; + case BG.updater.UPDATED: + if (++updated == 1) { + $('#apply-all-updates').disabled = true; + $('#apply-all-updates').classList.remove('hidden'); + } + $('#apply-all-updates').dataset.value = updated; + // fallthrough + case BG.updater.SKIPPED: + checked++; + reportUpdateState(state, value, details); + break; } - if (checked < total) { - queue[checked++].then(status => { - progress.style.width = Math.round(checked / total * maxWidth) + 'px'; - setTimeout(processQueue, 0, status); - }); - return; - } - btnCheck.disabled = false; - btnApply.disabled = false; - if (!updatesFound) { - noUpdates.classList.remove('hidden'); + const progress = $('#update-progress'); + const maxWidth = progress.parentElement.clientWidth; + progress.style.width = Math.round(checked / total * maxWidth) + 'px'; + } + + function done() { + $('#check-all-updates').disabled = false; + $('#apply-all-updates').disabled = false; + renderUpdatesOnlyFilter({check: updated > 0}); + if (!updated) { + $('#update-all-no-updates').classList.remove('hidden'); setTimeout(() => { - noUpdates.classList.add('hidden'); + $('#update-all-no-updates').classList.add('hidden'); }, 10e3); } } } -function checkUpdate(element) { +function checkUpdate(element, {single = true} = {}) { $('.update-note', element).textContent = t('checkingForUpdate'); $('.check-update', element).title = ''; element.classList.remove('checking-update', 'no-update', 'update-problem'); element.classList.add('checking-update'); - return new Updater(element).run(); // eslint-disable-line no-use-before-define + if (single) { + const style = BG.cachedStyles.byId.get(element.styleId); + BG.updater.checkStyle(style, reportUpdateState, {save: false}); + } } -class Updater { - constructor(element) { - const style = BG.cachedStyles.byId.get(element.styleId); - Object.assign(this, { - element, - id: style.id, - url: style.updateUrl, - md5Url: style.md5Url, - md5: style.originalMd5, - }); - } - - run() { - return this.md5Url && this.md5 - ? this.checkMd5() - : this.checkFullCode(); - } - - checkMd5() { - return download(this.md5Url).then( - md5 => (md5.length == 32 - ? this.decideOnMd5(md5 != this.md5) - : this.onFailure(-1)), - status => this.onFailure(status)); - } - - decideOnMd5(md5changed) { - if (md5changed) { - return this.checkFullCode({forceUpdate: true}); - } - this.display(); - } - - checkFullCode({forceUpdate = false} = {}) { - return download(this.url).then( - text => this.handleJson(forceUpdate, JSON.parse(text)), - status => this.onFailure(status)); - } - - handleJson(forceUpdate, json) { - return getStylesSafe({id: this.id}).then(([style]) => { - const needsUpdate = forceUpdate || !BG.styleSectionsEqual(style, json); - this.display({json: needsUpdate && json}); - return needsUpdate; - }); - } - - onFailure(status) { - this.display({ - message: status == 0 - ? t('updateCheckFailServerUnreachable') - : t('updateCheckFailBadResponseCode', [status]), - }); - } - - display({json, message} = {}) { - // json on success - // message on failure - // none on update not needed - this.element.classList.remove('checking-update'); - if (json) { - this.element.classList.add('can-update'); - this.element.updatedCode = json; - $('.update-note', this.element).textContent = ''; +function reportUpdateState(state, style, details) { + const entry = $('#style-' + style.id); + entry.classList.remove('checking-update'); + switch (state) { + case BG.updater.UPDATED: + entry.classList.add('can-update'); + entry.updatedCode = style; + $('.update-note', entry).textContent = ''; $('#onlyUpdates').classList.remove('hidden'); - } else { - this.element.classList.add('no-update'); - this.element.classList.toggle('update-problem', Boolean(message)); - $('.update-note', this.element).textContent = message || t('updateCheckSucceededNoUpdate'); - if (newUI.enabled) { - $('.check-update', this.element).title = message; + break; + case BG.updater.SKIPPED: { + if (!details) { + details = t('updateCheckFailServerUnreachable'); + } else if (typeof details == 'number') { + details = t('updateCheckFailBadResponseCode', [details]); } - // don't hide if check-all is running + const same = + details == BG.updater.SKIPPED_SAME_MD5 || + details == BG.updater.SKIPPED_SAME_CODE; + const message = same ? t('updateCheckSucceededNoUpdate') : details; + entry.classList.add('no-update'); + entry.classList.toggle('update-problem', !same); + $('.update-note', entry).textContent = message; + $('.check-update', entry).title = newUI.enabled ? message : ''; if (!$('#check-all-updates').disabled) { + // this is a single update job so we can decide whether to hide the filter $('#onlyUpdates').classList.toggle('hidden', !$('.can-update')); } } - if (filtersSelector.hide) { - filterAndAppend({entry: this.element}); - } + } + if (filtersSelector.hide) { + filterAndAppend({entry}); } } diff --git a/update.js b/update.js index 02214985..37a65a07 100644 --- a/update.js +++ b/update.js @@ -15,17 +15,14 @@ var updater = { lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(), - checkAllStyles(observe = () => {}) { + checkAllStyles(observe = () => {}, {save = true} = {}) { updater.resetInterval(); return new Promise(resolve => { getStyles({}, styles => { styles = styles.filter(style => style.updateUrl); observe(updater.COUNT, styles.length); Promise.all(styles.map(style => - updater.checkStyle(style) - .then(saveStyle) - .then(saved => observe(updater.UPDATED, saved)) - .catch(err => observe(updater.SKIPPED, style, err)) + updater.checkStyle(style, observe, {save}) )).then(() => { observe(updater.DONE); resolve(); @@ -34,7 +31,7 @@ var updater = { }); }, - checkStyle(style) { + checkStyle(style, observe = () => {}, {save = true} = {}) { return download(style.md5Url) .then(md5 => !md5 || md5.length != 32 ? Promise.reject(updater.SKIPPED_ERROR_MD5) : @@ -49,7 +46,11 @@ var updater = { Object.assign(json, { id: style.id, name: null, - })); + reason: 'update', + })) + .then(json => save ? saveStyle(json) : json) + .then(saved => observe(updater.UPDATED, saved)) + .catch(err => observe(updater.SKIPPED, style, err)); }, styleJSONseemsValid(json) { From 09670f59dc54388f6fdfc1a2c66334f927150909 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 21 Apr 2017 15:34:58 +0300 Subject: [PATCH 190/235] manage: adjust .update-problem colors --- manage.css | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/manage.css b/manage.css index c890e477..f60135d8 100644 --- a/manage.css +++ b/manage.css @@ -295,7 +295,15 @@ summary { } .update-problem .check-update svg { - fill: darkred; + fill: #ef6969; +} + +.newUI .entry.update-problem:hover .check-update svg { + fill: #fd4040; +} + +.newUI .entry.update-problem:hover .check-update svg:hover { + fill: red; } .updater-icons > :not(.check-update):after { From 5eb55baa95cb3cefdc24d6dad3a3fa27d3bb4578 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 21 Apr 2017 15:36:02 +0300 Subject: [PATCH 191/235] optionsUI+Opera: keep the status inside its block --- options/index.css | 1 + 1 file changed, 1 insertion(+) diff --git a/options/index.css b/options/index.css index 7d20ed45..a7c3a2af 100644 --- a/options/index.css +++ b/options/index.css @@ -32,6 +32,7 @@ body { margin: 1em 0; border-bottom: 1px dotted #ccc; padding: 0 0 1em 16px; + position: relative; } .block:last-child { From 135423860de55cf20fce85c37baf6ad005a0584d Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 21 Apr 2017 19:39:34 +0300 Subject: [PATCH 192/235] setupLivePrefs() now automatically finds the elements To make an element a live pref discoverable by setupLivePrefs() just use the corresponding pref's id as the element's HTML id attribute. --- edit.js | 5 +---- manage.js | 2 +- options/index.js | 9 +-------- popup.js | 2 +- prefs.js | 5 ++++- 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/edit.js b/edit.js index 15f58376..451e45a0 100644 --- a/edit.js +++ b/edit.js @@ -271,10 +271,7 @@ function initCodeMirror() { } document.getElementById("editor.keyMap").innerHTML = optionsHtmlFromArray(Object.keys(CM.keyMap).sort()); document.getElementById("options").addEventListener("change", acmeEventListener, false); - setupLivePrefs( - document.querySelectorAll("#options *[data-option][id^='editor.']") - .map(function(option) { return option.id }) - ); + setupLivePrefs(); hotkeyRerouter.setState(true); } diff --git a/manage.js b/manage.js index 90fed779..b9f4bfc5 100644 --- a/manage.js +++ b/manage.js @@ -80,7 +80,7 @@ function initGlobalEvents() { enforceInputRange($('#manage.newUI.targets')); // N.B. triggers existing onchange listeners - setupLivePrefs($$('input[id^="manage."]').map(el => el.id)); + setupLivePrefs(); $$('[data-filter]').forEach(el => { el.onchange = handleEvent.filterOnChange; diff --git a/options/index.js b/options/index.js index 2d0b5808..030edcf5 100644 --- a/options/index.js +++ b/options/index.js @@ -1,13 +1,6 @@ 'use strict'; -setupLivePrefs([ - 'show-badge', - 'popup.stylesFirst', - 'badgeNormal', - 'badgeDisabled', - 'popupWidth', - 'updateInterval', -]); +setupLivePrefs(); enforceInputRange($('#popupWidth')); // overwrite the default URL if browser is Opera diff --git a/popup.js b/popup.js index 28a2d3ac..6a9eabf8 100644 --- a/popup.js +++ b/popup.js @@ -62,7 +62,7 @@ function initPopup(url) { $('#disableAll').onchange = function() { installed.classList.toggle('disabled', this.checked); }; - setupLivePrefs(['disableAll']); + setupLivePrefs(); $('#find-styles-link').onclick = handleEvent.openURLandHide; $('#popup-manage-button').onclick = handleEvent.openURLandHide; diff --git a/prefs.js b/prefs.js index c838cf7e..6f407c35 100644 --- a/prefs.js +++ b/prefs.js @@ -302,7 +302,10 @@ var prefs = new function Prefs() { // Accepts an array of pref names (values are fetched via prefs.get) // and establishes a two-way connection between the document elements and the actual prefs -function setupLivePrefs(IDs) { +function setupLivePrefs( + IDs = Object.getOwnPropertyNames(prefs.readOnlyValues) + .filter(id => document.getElementById(id)) +) { const checkedProps = {}; for (const id of IDs) { const element = document.getElementById(id); From e3c135e87e3d7ad120f8d0e38486612dafc3325e Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 21 Apr 2017 20:35:22 +0300 Subject: [PATCH 193/235] code cosmetics --- dom.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dom.js b/dom.js index d302ba6e..1919d408 100644 --- a/dom.js +++ b/dom.js @@ -1,6 +1,6 @@ 'use strict'; -if (!/Windows/i.test(navigator.userAgent)) { +if (!navigator.userAgent.includes('Windows')) { document.documentElement.classList.add('non-windows'); } @@ -62,7 +62,6 @@ function enforceInputRange(element) { doNotify(); } }; - onChange({}); element.addEventListener('change', onChange); element.addEventListener('input', onChange); } From 3dc934369bdedd0578ba1babe2262297cf150f0c Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 22 Apr 2017 15:09:48 +0300 Subject: [PATCH 194/235] manage: simplify DOM (append icons only when needed) --- manage.css | 2 +- manage.html | 83 +++++++++++++++++++++++++++++------------------------ manage.js | 31 ++++++++++++++------ 3 files changed, 69 insertions(+), 47 deletions(-) diff --git a/manage.css b/manage.css index f60135d8..4af0ffb2 100644 --- a/manage.css +++ b/manage.css @@ -275,7 +275,7 @@ summary { } .newUI .can-update .update, -.newUI .no-update:not(.update-problem) .up-to-date, +.newUI .no-update:not(.update-problem):not(.update-done) .up-to-date, .newUI .no-update.update-problem .check-update, .newUI .update-done .updated { display: inline; diff --git a/manage.html b/manage.html index 52416635..34965c73 100644 --- a/manage.html +++ b/manage.html @@ -18,12 +18,7 @@

    - - - - - - +

    @@ -50,44 +45,13 @@

    - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - -

    @@ -96,6 +60,49 @@
    + + + + + + diff --git a/manage.js b/manage.js index b9f4bfc5..f1a42779 100644 --- a/manage.js +++ b/manage.js @@ -98,22 +98,29 @@ function showStyles(styles = []) { const sorted = styles .map(style => ({name: style.name.toLocaleLowerCase(), style})) .sort((a, b) => (a.name < b.name ? -1 : a.name == b.name ? 0 : 1)); + let index = 0; const shouldRenderAll = (history.state || {}).scrollY > window.innerHeight; const renderBin = document.createDocumentFragment(); - renderStyles(0); + if (shouldRenderAll) { + renderStyles(); + } else { + requestAnimationFrame(renderStyles); + } - function renderStyles(index) { + function renderStyles() { const t0 = performance.now(); let rendered = 0; while (index < sorted.length - && (shouldRenderAll || performance.now() - t0 < 10 || ++rendered < 10)) { + && (shouldRenderAll || ++rendered < 10 || performance.now() - t0 < 10)) { renderBin.appendChild(createStyleElement(sorted[index++])); } filterAndAppend({container: renderBin}); if (index < sorted.length) { - setTimeout(renderStyles, 0, index); - } else if (shouldRenderAll && 'scrollY' in (history.state || {})) { - setTimeout(() => scrollTo(0, history.state.scrollY)); + requestAnimationFrame(renderStyles); + return; + } + if ('scrollY' in (history.state || {})) { + setTimeout(window.scrollTo, 0, 0, history.state.scrollY); } if (newUI.enabled && newUI.favicons) { debounce(handleEvent.loadFavicons, 16); @@ -125,7 +132,7 @@ function showStyles(styles = []) { function createStyleElement({style, name}) { // query the sub-elements just once, then reuse the references if ((createStyleElement.parts || {}).newUI !== newUI.enabled) { - const entry = template[`style${newUI.enabled ? 'Compact' : ''}`].cloneNode(true); + const entry = template[`style${newUI.enabled ? 'Compact' : ''}`]; createStyleElement.parts = { newUI: newUI.enabled, entry, @@ -133,8 +140,9 @@ function createStyleElement({style, name}) { checker: $('.checker', entry) || {}, nameLink: $('.style-name-link', entry), editLink: $('.style-edit-link', entry) || {}, - editHrefBase: $('.style-name-link, .style-edit-link', entry).getAttribute('href'), + editHrefBase: $('.style-name-link', entry).getAttribute('href'), homepage: $('.homepage', entry), + homepageIcon: template[`homepageIcon${newUI.enabled ? 'Small' : 'Big'}`], appliesTo: $('.applies-to', entry), targets: $('.targets', entry), expander: $('.expander', entry), @@ -159,6 +167,13 @@ function createStyleElement({style, name}) { (style.enabled ? 'enabled' : 'disabled') + (style.updateUrl ? ' updatable' : ''); + if (style.url) { + $('.homepage', entry).appendChild(parts.homepageIcon.cloneNode(true)); + } + if (style.updateUrl && newUI.enabled) { + $('.actions', entry).appendChild(template.updaterIcons.cloneNode(true)); + } + // name being supplied signifies we're invoked by showStyles() // which debounces its main loop thus loading the postponed favicons createStyleTargetsElement({entry, style, postponeFavicons: name}); From ceed8b565cd5c982a3aaec64507e607b7ada5f3e Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 22 Apr 2017 16:06:05 +0300 Subject: [PATCH 195/235] a bit darker svg-icon --- manage.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manage.css b/manage.css index 4af0ffb2..2f32e78a 100644 --- a/manage.css +++ b/manage.css @@ -256,8 +256,8 @@ summary { display: none; } -.newUI .svg-icon { - fill: #aaa; +.newUI .entry .svg-icon { + fill: #999; } .newUI .entry:hover .svg-icon { From fdc15d24d9f0f3b50d238e611b4c8ebe4538c120 Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 23 Apr 2017 13:54:20 +0300 Subject: [PATCH 196/235] try to avoid setBadgeText errors --- .eslintrc | 1 + background.js | 18 ++++++++---------- messaging.js | 7 +++++++ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.eslintrc b/.eslintrc index 8cc63436..73a2efac 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,6 +16,7 @@ globals: URLS: false BG: false notifyAllTabs: false + getTab: false getActiveTab: false getActiveTabRealURL: false getTabRealURL: false diff --git a/background.js b/background.js index 415de5ae..0790cea1 100644 --- a/background.js +++ b/background.js @@ -210,12 +210,7 @@ function updateIcon(tab, styles) { return; } if (styles) { - // check for not-yet-existing tabs e.g. omnibox instant search - chrome.tabs.get(tab.id, () => { - if (!chrome.runtime.lastError) { - stylesReceived(styles); - } - }); + stylesReceived(styles); return; } getTabRealURL(tab).then(url => @@ -247,11 +242,14 @@ function updateIcon(tab, styles) { // TODO: add Edge preferred sizes: 20, 25, 30, 40 }, }, () => { - if (!chrome.runtime.lastError) { - // Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor - chrome.browserAction.setBadgeBackgroundColor({color}); - chrome.browserAction.setBadgeText({text, tabId: tab.id}); + if (chrome.runtime.lastError) { + return; } + // Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor + chrome.browserAction.setBadgeBackgroundColor({color}); + getTab(tab.id).then(() => { + chrome.browserAction.setBadgeText({text, tabId: tab.id}); + }); }); } } diff --git a/messaging.js b/messaging.js index b688270a..0a1ad059 100644 --- a/messaging.js +++ b/messaging.js @@ -87,6 +87,13 @@ function notifyAllTabs(msg) { } +function getTab(id) { + return new Promise(resolve => + chrome.tabs.get(id, tab => + !chrome.runtime.lastError && resolve(tab))); +} + + function getActiveTab() { return new Promise(resolve => chrome.tabs.query({currentWindow: true, active: true}, tabs => From f9e90f9cd080417b868a15938a86ab66132be42b Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 22 Apr 2017 21:02:49 +0300 Subject: [PATCH 197/235] code cosmetics: simplify debounce() --- messaging.js | 25 +++++++++++++------------ update.js | 2 +- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/messaging.js b/messaging.js index 0a1ad059..81fe30ca 100644 --- a/messaging.js +++ b/messaging.js @@ -204,19 +204,20 @@ function tryJSONparse(jsonString) { } -function debounce(fn, delay, ...args) { - const timers = debounce.timers = debounce.timers || new Map(); - debounce.run = debounce.run || ((fn, ...args) => { - timers.delete(fn); +const debounce = Object.assign((fn, delay, ...args) => { + clearTimeout(debounce.timers.get(fn)); + debounce.timers.set(fn, setTimeout(debounce.run, delay, fn, ...args)); +}, { + timers: new Map(), + run(fn, ...args) { + debounce.timers.delete(fn); fn(...args); - }); - debounce.unregister = debounce.unregister || (fn => { - clearTimeout(timers.get(fn)); - timers.delete(fn); - }); - clearTimeout(timers.get(fn)); - timers.set(fn, setTimeout(debounce.run, delay, fn, ...args)); -} + }, + unregister(fn) { + clearTimeout(debounce.timers.get(fn)); + debounce.timers.delete(fn); + }, +}); function deepCopy(obj) { diff --git a/update.js b/update.js index 37a65a07..aeee808b 100644 --- a/update.js +++ b/update.js @@ -66,7 +66,7 @@ var updater = { if (interval) { const elapsed = Math.max(0, Date.now() - updater.lastUpdateTime); debounce(updater.checkAllStyles, Math.max(10e3, interval - elapsed)); - } else if (debounce.timers) { + } else { debounce.unregister(updater.checkAllStyles); } }, From 736302962c0900e18dc94b6c8eedd3405b47a337 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 24 Apr 2017 14:07:35 +0300 Subject: [PATCH 198/235] restore "x" in messageBox(), add namespace#tag to $element() --- dom.js | 18 +++++++++++++++--- msgbox/msgbox.js | 9 ++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/dom.js b/dom.js index 1919d408..7f5dd80c 100644 --- a/dom.js +++ b/dom.js @@ -81,11 +81,16 @@ function $$(selector, base = document) { function $element(opt) { - // tag: string, default 'div' + // tag: string, default 'div', may include namespace like 'ns#tag' // appendChild: element or an array of elements // dataset: object // any DOM property: assigned as is - const element = document.createElement(opt.tag || 'div'); + const [ns, tag] = opt.tag && opt.tag.includes('#') + ? opt.tag.split('#') + : [null, opt.tag]; + const element = ns + ? document.createElementNS(ns == 'SVG' || ns == 'svg' ? 'http://www.w3.org/2000/svg' : ns, tag) + : document.createElement(tag || 'div'); (opt.appendChild instanceof Array ? opt.appendChild : [opt.appendChild]) .forEach(child => child && element.appendChild(child)); delete opt.appendChild; @@ -94,5 +99,12 @@ function $element(opt) { Object.assign(element.dataset, opt.dataset); delete opt.dataset; } - return Object.assign(element, opt); + if (ns) { + for (const attr in opt) { + element.setAttributeNS(null, attr, opt[attr]); + } + } else { + Object.assign(element, opt); + } + return element; } diff --git a/msgbox/msgbox.js b/msgbox/msgbox.js index 640add2e..a7ebbe2a 100644 --- a/msgbox/msgbox.js +++ b/msgbox/msgbox.js @@ -56,9 +56,12 @@ function messageBox({ messageBox.element = $element({id, className, appendChild: [ $element({appendChild: [ $element({id: `${id}-title`, innerHTML: title}), - $element({ - id: `${id}-close-icon`, - innerHTML: '', + $element({id: `${id}-close-icon`, appendChild: + $element({tag: 'SVG#svg', class: 'svg-icon', viewBox: '0 0 20 20', appendChild: + $element({tag: 'SVG#path', d: 'M11.69,10l4.55,4.55-1.69,1.69L10,11.69,' + + '5.45,16.23,3.77,14.55,8.31,10,3.77,5.45,5.45,3.77,10,8.31l4.55-4.55,1.69,1.69Z', + }) + }), onclick: messageBox.listeners.closeIcon}), $element({id: `${id}-contents`, [putAs]: contents}), $element({id: `${id}-buttons`, appendChild: From 36285649617f4ddbb217354c32a47c380a6a6681 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 24 Apr 2017 16:26:59 +0300 Subject: [PATCH 199/235] reset L10N cache on update --- background.js | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/background.js b/background.js index 0790cea1..f709b398 100644 --- a/background.js +++ b/background.js @@ -48,33 +48,29 @@ if ('commands' in chrome) { } // ************************************************************************* -// Open FAQs page once after installation to guide new users. -// Do not display it in development mode. -if (chrome.runtime.getManifest().update_url) { - const openHomepageOnInstall = ({reason}) => { - chrome.runtime.onInstalled.removeListener(openHomepageOnInstall); - if (reason == 'install') { - const version = chrome.runtime.getManifest().version; +{ + const onInstall = ({reason}) => { + chrome.runtime.onInstalled.removeListener(onInstall); + const manifest = chrome.runtime.getManifest(); + // Open FAQs page once after installation to guide new users. + // Do not display it in development mode. + if (reason == 'install' && manifest.update_url) { setTimeout(openURL, 100, { - url: `http://add0n.com/stylus.html?version=${version}&type=install` + url: `http://add0n.com/stylus.html?version=${manifest.version}&type=install` + }); + } + // reset L10N cache on UI language change or update + const {browserUIlanguage} = tryJSONparse(localStorage.L10N) || {}; + const UIlang = chrome.i18n.getUILanguage(); + if (reason == 'update' || browserUIlanguage != UIlang) { + localStorage.L10N = JSON.stringify({ + browserUIlanguage: UIlang, }); } }; // bind for 60 seconds max and auto-unbind if it's a normal run - chrome.runtime.onInstalled.addListener(openHomepageOnInstall); - setTimeout(openHomepageOnInstall, 60e3, {reason: 'unbindme'}); -} - -// ************************************************************************* -// reset L10N cache on UI language change -{ - const {browserUIlanguage} = tryJSONparse(localStorage.L10N) || {}; - const UIlang = chrome.i18n.getUILanguage(); - if (browserUIlanguage != UIlang) { - localStorage.L10N = JSON.stringify({ - browserUIlanguage: UIlang, - }); - } + chrome.runtime.onInstalled.addListener(onInstall); + setTimeout(onInstall, 60e3, {reason: 'unbindme'}); } // ************************************************************************* From 36667dece1aee98417824dd82ca8a6fd2b2a055b Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 24 Apr 2017 16:28:58 +0300 Subject: [PATCH 200/235] install.js: fix onclick after orphanCheck --- install.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.js b/install.js index 9927bb8a..770758a4 100644 --- a/install.js +++ b/install.js @@ -75,7 +75,7 @@ function sendEvent(type, detail = null) { function onInstallClicked() { - if (!orphanCheck()) { + if (!orphanCheck || !orphanCheck()) { return; } getResource(getMeta('stylish-description')) @@ -85,7 +85,7 @@ function onInstallClicked() { function onUpdateClicked() { - if (!orphanCheck()) { + if (!orphanCheck || !orphanCheck()) { return; } chrome.runtime.sendMessage({ From 32ae088c03de537c0cd5ff6a253ccce4b5b2833c Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 23 Apr 2017 15:19:18 +0300 Subject: [PATCH 201/235] Detect and don't update locally edited styles --- _locales/en/messages.json | 12 ++++++ install.js | 5 ++- manage.js | 4 ++ storage.js | 60 +++++++++++++++++++++++++++-- update.js | 79 ++++++++++++++++++++++++++------------- 5 files changed, 131 insertions(+), 29 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index b1cd5297..ae1f59bd 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -583,6 +583,18 @@ "message": "Update failed - server unreachable.", "description": "Text that displays when an update check failed because the update server is unreachable" }, + "updateCheckSkippedLocallyEdited": { + "message": "This style was edited locally.", + "description": "Text that displays when an update check skipped updating the style to avoid losing local modifications" + }, + "updateCheckSkippedMaybeLocallyEdited": { + "message": "This style might have been edited locally.", + "description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications" + }, + "updateCheckManualUpdateHint": { + "message": "Do a one-time manual update on its userstyles.org page (your edits will be lost)", + "description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications" + }, "updateCheckSucceededNoUpdate": { "message": "Style is up to date.", "description": "Text that displays when an update check completed and no update is available" diff --git a/install.js b/install.js index 770758a4..9923bad3 100644 --- a/install.js +++ b/install.js @@ -104,7 +104,10 @@ function saveStyleCode(message, name, addProps) { } getResource(getMeta('stylish-code-chrome')).then(code => { chrome.runtime.sendMessage( - Object.assign(JSON.parse(code), addProps, {method: 'saveStyle'}), + Object.assign(JSON.parse(code), addProps, { + method: 'saveStyle', + reason: 'update', + }), () => sendEvent('styleInstalledChrome') ); resolve(); diff --git a/manage.js b/manage.js index f1a42779..cfb04e66 100644 --- a/manage.js +++ b/manage.js @@ -573,6 +573,10 @@ function reportUpdateState(state, style, details) { details = t('updateCheckFailServerUnreachable'); } else if (typeof details == 'number') { details = t('updateCheckFailBadResponseCode', [details]); + } else if (details == BG.updater.SKIPPED_EDITED) { + details = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); + } else if (details == BG.updater.SKIPPED_MAYBE_EDITED) { + details = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); } const same = details == BG.updater.SKIPPED_SAME_MD5 || diff --git a/storage.js b/storage.js index 2ea84ecc..a98e95f1 100644 --- a/storage.js +++ b/storage.js @@ -5,6 +5,7 @@ const RX_NAMESPACE = new RegExp([/[\s\r\n]*/, /[\s\r\n]*/].map(rx => rx.source).join(''), 'g'); const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g; const SLOPPY_REGEXP_PREFIX = '\0'; +const DIGEST_KEY_PREFIX = 'originalDigest'; // Note, only 'var'-declared variables are visible from another extension page // eslint-disable-next-line no-var @@ -21,6 +22,26 @@ var cachedStyles = { }, }; +// eslint-disable-next-line no-var +var chromeLocal = { + get(options) { + return new Promise(resolve => { + chrome.storage.local.get(options, data => resolve(data)); + }); + }, + set(data) { + return new Promise(resolve => { + chrome.storage.local.set(data, () => resolve(data)); + }); + }, + getValue(key) { + return chromeLocal.get(key).then(data => data[key]); + }, + setValue(key, value) { + return chromeLocal.set({[key]: value}); + }, +}; + function getDatabase(ready, error) { const dbOpenRequest = window.indexedDB.open('stylish', 2); @@ -214,7 +235,7 @@ function saveStyle(style) { const existed = Boolean(eventGet.target.result); const oldStyle = Object.assign({}, eventGet.target.result); const codeIsUpdated = 'sections' in style && !styleSectionsEqual(style, oldStyle); - write(Object.assign(oldStyle, style), {existed, codeIsUpdated}); + write(Object.assign(oldStyle, style), {reason, existed, codeIsUpdated}); }; } else { // Create @@ -226,10 +247,10 @@ function saveStyle(style) { md5Url: null, url: null, originalMd5: null, - }, style)); + }, style), {reason}); } - function write(style, {existed, codeIsUpdated} = {}) { + function write(style, {reason, existed, codeIsUpdated} = {}) { style.sections = (style.sections || []).map(section => Object.assign({ urls: [], @@ -248,6 +269,9 @@ function saveStyle(style) { style, codeIsUpdated, reason, }); } + if (reason == 'update') { + updateStyleDigest(style); + } resolve(style); }; } @@ -257,6 +281,7 @@ function saveStyle(style) { function deleteStyle({id, notify = true}) { + chrome.storage.local.remove(DIGEST_KEY_PREFIX + id, ignoreChromeError); return new Promise(resolve => getDatabase(db => { const tx = db.transaction(['styles'], 'readwrite'); @@ -507,3 +532,32 @@ function getDomains(url) { } return domains; } + + +function getStyleDigests(style) { + return Promise.all([ + chromeLocal.getValue(DIGEST_KEY_PREFIX + style.id), + calcStyleDigest(style), + ]); +} + + +function updateStyleDigest(style) { + calcStyleDigest(style).then(digest => + chromeLocal.set({[DIGEST_KEY_PREFIX + style.id]: digest})); +} + + +function calcStyleDigest({sections}) { + const text = new TextEncoder('utf-8').encode(JSON.stringify(sections)); + return crypto.subtle.digest('SHA-1', text).then(hex); + function hex(buffer) { + const parts = []; + const PAD8 = '00000000'; + const view = new DataView(buffer); + for (let i = 0; i < view.byteLength; i += 4) { + parts.push((PAD8 + view.getUint32(i).toString(16)).slice(-8)); + } + return parts.join(''); + } +} diff --git a/update.js b/update.js index aeee808b..1055a55b 100644 --- a/update.js +++ b/update.js @@ -1,4 +1,4 @@ -/* globals getStyles, saveStyle, styleSectionsEqual */ +/* globals getStyles, saveStyle, styleSectionsEqual, getStyleDigests, updateStyleDigest */ 'use strict'; // eslint-disable-next-line no-var @@ -7,6 +7,8 @@ var updater = { COUNT: 'count', UPDATED: 'updated', SKIPPED: 'skipped', + SKIPPED_EDITED: 'locally edited', + SKIPPED_MAYBE_EDITED: 'maybe locally edited', SKIPPED_SAME_MD5: 'up-to-date: MD5 is unchanged', SKIPPED_SAME_CODE: 'up-to-date: code sections are unchanged', SKIPPED_ERROR_MD5: 'error: MD5 is invalid', @@ -32,33 +34,60 @@ var updater = { }, checkStyle(style, observe = () => {}, {save = true} = {}) { - return download(style.md5Url) - .then(md5 => - !md5 || md5.length != 32 ? Promise.reject(updater.SKIPPED_ERROR_MD5) : - md5 == style.originalMd5 ? Promise.reject(updater.SKIPPED_SAME_MD5) : - style.updateUrl) - .then(download) - .then(text => tryJSONparse(text)) - .then(json => - !updater.styleJSONseemsValid(json) ? Promise.reject(updater.SKIPPED_ERROR_JSON) : - styleSectionsEqual(json, style) ? Promise.reject(updater.SKIPPED_SAME_CODE) : - // keep the local name as it could've been customized by the user - Object.assign(json, { - id: style.id, - name: null, - reason: 'update', - })) - .then(json => save ? saveStyle(json) : json) + let hasDigest; + return getStyleDigests(style) + .then(fetchMd5IfNotEdited) + .then(fetchCodeIfMd5Changed) + .then(saveIfUpdated) .then(saved => observe(updater.UPDATED, saved)) .catch(err => observe(updater.SKIPPED, style, err)); - }, - styleJSONseemsValid(json) { - return json - && json.sections - && json.sections.length - && typeof json.sections.every == 'function' - && typeof json.sections[0].code == 'string'; + function fetchMd5IfNotEdited([originalDigest, current]) { + hasDigest = Boolean(originalDigest); + if (hasDigest && originalDigest != current) { + return Promise.reject(updater.SKIPPED_EDITED); + } + return download(style.md5Url); + } + + function fetchCodeIfMd5Changed(md5) { + if (!md5 || md5.length != 32) { + return Promise.reject(updater.SKIPPED_ERROR_MD5); + } + if (md5 == style.originalMd5 && hasDigest) { + return Promise.reject(updater.SKIPPED_SAME_MD5); + } + return download(style.updateUrl); + } + + function saveIfUpdated(text) { + const json = tryJSONparse(text); + if (!styleJSONseemsValid(json)) { + return Promise.reject(updater.SKIPPED_ERROR_JSON); + } + json.id = style.id; + if (styleSectionsEqual(json, style)) { + if (!hasDigest) { + updateStyleDigest(json); + } + return Promise.reject(updater.SKIPPED_SAME_CODE); + } else if (!hasDigest) { + return Promise.reject(updater.SKIPPED_MAYBE_EDITED); + } + return !save ? json : + saveStyle(Object.assign(json, { + name: null, // keep local name customizations + reason: 'update', + })); + } + + function styleJSONseemsValid(json) { + return json + && json.sections + && json.sections.length + && typeof json.sections.every == 'function' + && typeof json.sections[0].code == 'string'; + } }, schedule() { From 7677f0dece78912023b413a96e51c101aaf28913 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 24 Apr 2017 16:29:48 +0300 Subject: [PATCH 202/235] updater: add 'ignoreDigest' to force-update on manage page * saveStyle: retain only known properties in sections[] and normalize their order * remove styleDigest on import * shorten detailed status names in updater * don't autohide update status message --- _locales/en/messages.json | 10 +++-- manage.css | 6 ++- manage.html | 16 ++++++-- manage.js | 81 ++++++++++++++++++++++----------------- options/index.js | 23 ++++++----- storage.js | 56 +++++++++++++++++---------- update.js | 67 ++++++++++++++++++++------------ 7 files changed, 158 insertions(+), 101 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index ae1f59bd..50229ff4 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -323,7 +323,7 @@ "description": "Checkbox to show only locally edited styles" }, "manageOnlyUpdates": { - "message": "Only with updates", + "message": "Only with updates or problems", "description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed" }, "manageNewUI": { @@ -592,7 +592,7 @@ "description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications" }, "updateCheckManualUpdateHint": { - "message": "Do a one-time manual update on its userstyles.org page (your edits will be lost)", + "message": "To force an update (and lose your edits) update each style individually.", "description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications" }, "updateCheckSucceededNoUpdate": { @@ -600,7 +600,11 @@ "description": "Text that displays when an update check completed and no update is available" }, "updateAllCheckSucceededNoUpdate": { - "message": "All styles are up to date.", + "message": "No updates found.", + "description": "Text that displays when an update all check completed and no updates are available" + }, + "updateAllCheckSucceededSomeEdited": { + "message": "Some updatable styles weren't checked to avoid losing possible local edits.", "description": "Text that displays when an update all check completed and no updates are available" }, "updateCompleted": { diff --git a/manage.css b/manage.css index 2f32e78a..c590126a 100644 --- a/manage.css +++ b/manage.css @@ -294,7 +294,7 @@ summary { cursor: pointer; } -.update-problem .check-update svg { +.newUI .update-problem .check-update svg { fill: #ef6969; } @@ -500,6 +500,10 @@ input[id^="manage.newUI"] { opacity: .35; } +#update-all-no-updates[data-skipped-edited="true"]:after { + content: " __MSG_updateAllCheckSucceededSomeEdited__ __MSG_updateCheckManualUpdateHint__"; +} + /* highlight updated/added styles */ .highlight { animation: highlight 10s cubic-bezier(0,.82,.47,.98); diff --git a/manage.html b/manage.html index 34965c73..6688c987 100644 --- a/manage.html +++ b/manage.html @@ -135,18 +135,26 @@
    - +

    diff --git a/manage.js b/manage.js index cfb04e66..217ab3ec 100644 --- a/manage.js +++ b/manage.js @@ -365,10 +365,15 @@ Object.assign(handleEvent, { el.lastValue = value; } const enabledFilters = $$('#header [data-filter]').filter(el => getValue(el)); + const buildFilter = hide => + [...enabledFilters.map(el => + el.dataset[hide ? 'filterHide' : 'filter'] + .split(/,\s*/) + .map(s => '.entry' + (hide ? '' : '.hidden') + s)) + ].join(','); Object.assign(filtersSelector, { - hide: enabledFilters.map(el => '.entry:not(.hidden)' + el.dataset.filter).join(','), - unhide: '.entry.hidden' + enabledFilters.map(el => - (':not(' + el.dataset.filter + ')').replace(/^:not\(:not\((.+?)\)\)$/, '$1')).join(''), + hide: buildFilter(true), + unhide: buildFilter(false), }); reapplyFilter(); }, @@ -505,12 +510,13 @@ function checkUpdateAll() { let total = 0; let checked = 0; + let skippedEdited = 0; let updated = 0; - $$('.updatable:not(.can-update)').map(el => checkUpdate(el, {single: false})); - BG.updater.checkAllStyles(observe, {save: false}).then(done); + $$('.updatable:not(.can-update):not(.update-problem)').map(el => checkUpdate(el, {single: false})); + BG.updater.checkAllStyles({observer, save: false}); - function observe(state, value, details) { + function observer(state, value, details) { switch (state) { case BG.updater.COUNT: total = value; @@ -524,37 +530,41 @@ function checkUpdateAll() { // fallthrough case BG.updater.SKIPPED: checked++; + if (details == BG.updater.EDITED || details == BG.updater.MAYBE_EDITED) { + skippedEdited++; + } reportUpdateState(state, value, details); break; + case BG.updater.DONE: + $('#check-all-updates').disabled = false; + $('#apply-all-updates').disabled = false; + renderUpdatesOnlyFilter({check: updated + skippedEdited > 0}); + if (!updated) { + $('#update-all-no-updates').dataset.skippedEdited = skippedEdited > 0; + $('#update-all-no-updates').classList.remove('hidden'); + } + return; } const progress = $('#update-progress'); const maxWidth = progress.parentElement.clientWidth; progress.style.width = Math.round(checked / total * maxWidth) + 'px'; } - - function done() { - $('#check-all-updates').disabled = false; - $('#apply-all-updates').disabled = false; - renderUpdatesOnlyFilter({check: updated > 0}); - if (!updated) { - $('#update-all-no-updates').classList.remove('hidden'); - setTimeout(() => { - $('#update-all-no-updates').classList.add('hidden'); - }, 10e3); - } - } } -function checkUpdate(element, {single = true} = {}) { - $('.update-note', element).textContent = t('checkingForUpdate'); - $('.check-update', element).title = ''; - element.classList.remove('checking-update', 'no-update', 'update-problem'); - element.classList.add('checking-update'); +function checkUpdate(entry, {single = true} = {}) { + $('.update-note', entry).textContent = t('checkingForUpdate'); + $('.check-update', entry).title = ''; if (single) { - const style = BG.cachedStyles.byId.get(element.styleId); - BG.updater.checkStyle(style, reportUpdateState, {save: false}); + BG.updater.checkStyle({ + save: false, + ignoreDigest: entry.classList.contains('update-problem'), + style: BG.cachedStyles.byId.get(entry.styleId), + observer: reportUpdateState, + }); } + entry.classList.remove('checking-update', 'no-update', 'update-problem'); + entry.classList.add('checking-update'); } @@ -569,18 +579,19 @@ function reportUpdateState(state, style, details) { $('#onlyUpdates').classList.remove('hidden'); break; case BG.updater.SKIPPED: { + if (entry.classList.contains('can-update')) { + break; + } if (!details) { details = t('updateCheckFailServerUnreachable'); } else if (typeof details == 'number') { details = t('updateCheckFailBadResponseCode', [details]); - } else if (details == BG.updater.SKIPPED_EDITED) { + } else if (details == BG.updater.EDITED) { details = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); - } else if (details == BG.updater.SKIPPED_MAYBE_EDITED) { + } else if (details == BG.updater.MAYBE_EDITED) { details = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); } - const same = - details == BG.updater.SKIPPED_SAME_MD5 || - details == BG.updater.SKIPPED_SAME_CODE; + const same = details == BG.updater.SAME_MD5 || details == BG.updater.SAME_CODE; const message = same ? t('updateCheckSucceededNoUpdate') : details; entry.classList.add('no-update'); entry.classList.toggle('update-problem', !same); @@ -588,7 +599,7 @@ function reportUpdateState(state, style, details) { $('.check-update', entry).title = newUI.enabled ? message : ''; if (!$('#check-all-updates').disabled) { // this is a single update job so we can decide whether to hide the filter - $('#onlyUpdates').classList.toggle('hidden', !$('.can-update')); + renderUpdatesOnlyFilter({show: $('.can-update, .update-problem')}); } } } @@ -600,10 +611,10 @@ function reportUpdateState(state, style, details) { function renderUpdatesOnlyFilter({show, check} = {}) { const numUpdatable = $$('.can-update').length; - const canUpdate = numUpdatable > 0; + const mightUpdate = numUpdatable > 0 || $('.update-problem'); const checkbox = $('#onlyUpdates input'); - show = show !== undefined ? show : canUpdate; - check = check !== undefined ? show && check : checkbox.checked && canUpdate; + show = show !== undefined ? show : mightUpdate; + check = check !== undefined ? show && check : checkbox.checked && mightUpdate; $('#onlyUpdates').classList.toggle('hidden', !show); checkbox.checked = check; @@ -611,7 +622,7 @@ function renderUpdatesOnlyFilter({show, check} = {}) { const btnApply = $('#apply-all-updates'); if (!btnApply.matches('.hidden')) { - if (canUpdate) { + if (numUpdatable > 0) { btnApply.dataset.value = numUpdatable; } else { btnApply.classList.add('hidden'); diff --git a/options/index.js b/options/index.js index 030edcf5..886badb5 100644 --- a/options/index.js +++ b/options/index.js @@ -41,16 +41,14 @@ function checkUpdates() { let total = 0; let checked = 0; let updated = 0; - const installed = $('#updates-installed'); - const progress = $('#update-progress'); - const maxWidth = progress.parentElement.clientWidth; - progress.style.width = 0; - installed.dataset.value = ''; - document.body.classList.add('update-in-progress'); - BG.updater.checkAllStyles((state, value) => { + const maxWidth = $('#update-progress').parentElement.clientWidth; + BG.updater.checkAllStyles({observer}); + + function observer(state, value) { switch (state) { case BG.updater.COUNT: total = value; + document.body.classList.add('update-in-progress'); break; case BG.updater.UPDATED: updated++; @@ -58,10 +56,11 @@ function checkUpdates() { case BG.updater.SKIPPED: checked++; break; + case BG.updater.DONE: + document.body.classList.remove('update-in-progress'); + return; } - progress.style.width = Math.round(checked / total * maxWidth) + 'px'; - installed.dataset.value = updated || ''; - }).then(() => { - document.body.classList.remove('update-in-progress'); - }); + $('#update-progress').style.width = Math.round(checked / total * maxWidth) + 'px'; + $('#updates-installed').dataset.value = updated || ''; + } } diff --git a/storage.js b/storage.js index a98e95f1..a64c2abc 100644 --- a/storage.js +++ b/storage.js @@ -218,7 +218,7 @@ function saveStyle(style) { const tx = db.transaction(['styles'], 'readwrite'); const os = tx.objectStore('styles'); - const id = style.id !== undefined && style.id !== null ? Number(style.id) : null; + const id = style.id == '0' ? 0 : Number(style.id) || null; const reason = style.reason; const notify = style.notify !== false; delete style.method; @@ -227,15 +227,16 @@ function saveStyle(style) { if (!style.name) { delete style.name; } + let existed, codeIsUpdated; if (id !== null) { // Update or create style.id = id; os.get(id).onsuccess = eventGet => { - const existed = Boolean(eventGet.target.result); - const oldStyle = Object.assign({}, eventGet.target.result); - const codeIsUpdated = 'sections' in style && !styleSectionsEqual(style, oldStyle); - write(Object.assign(oldStyle, style), {reason, existed, codeIsUpdated}); + const oldStyle = eventGet.target.result; + existed = Boolean(oldStyle); + codeIsUpdated = !existed || style.sections && !styleSectionsEqual(style, oldStyle); + write(Object.assign({}, oldStyle, style)); }; } else { // Create @@ -247,18 +248,11 @@ function saveStyle(style) { md5Url: null, url: null, originalMd5: null, - }, style), {reason}); + }, style)); } - function write(style, {reason, existed, codeIsUpdated} = {}) { - style.sections = (style.sections || []).map(section => - Object.assign({ - urls: [], - urlPrefixes: [], - domains: [], - regexps: [], - }, section) - ); + function write(style) { + style.sections = normalizeStyleSections(style); os.put(style).onsuccess = event => { style.id = style.id || event.target.result; invalidateCache(existed ? {updated: style} : {added: style}); @@ -271,6 +265,8 @@ function saveStyle(style) { } if (reason == 'update') { updateStyleDigest(style); + } else if (reason == 'import') { + chrome.storage.local.remove(DIGEST_KEY_PREFIX + style.id, ignoreChromeError); } resolve(style); }; @@ -308,10 +304,14 @@ function getApplicableSections({style, matchUrl, strictRegexp = true, stopOnFirs for (const section of style.sections) { const {urls, domains, urlPrefixes, regexps, code} = section; if ((!urls.length && !urlPrefixes.length && !domains.length && !regexps.length - || urls.length && urls.indexOf(matchUrl) >= 0 - || urlPrefixes.length && arraySomeIsPrefix(urlPrefixes, matchUrl) - || domains.length && arraySomeIn(cachedStyles.urlDomains.get(matchUrl) || getDomains(matchUrl), domains) - || regexps.length && arraySomeMatches(regexps, matchUrl, strictRegexp) + || urls.length + && urls.indexOf(matchUrl) >= 0 + || urlPrefixes.length + && arraySomeIsPrefix(urlPrefixes, matchUrl) + || domains.length + && arraySomeIn(cachedStyles.urlDomains.get(matchUrl) || getDomains(matchUrl), domains) + || regexps.length + && arraySomeMatches(regexps, matchUrl, strictRegexp) ) && !styleCodeEmpty(code)) { sections.push(section); if (stopOnFirst) { @@ -534,6 +534,18 @@ function getDomains(url) { } +function normalizeStyleSections({sections}) { + // retain known properties in an arbitrarily predefined order + return (sections || []).map(section => ({ + code: section.code || '', + urls: section.urls || [], + urlPrefixes: section.urlPrefixes || [], + domains: section.domains || [], + regexps: section.regexps || [], + })); +} + + function getStyleDigests(style) { return Promise.all([ chromeLocal.getValue(DIGEST_KEY_PREFIX + style.id), @@ -548,9 +560,11 @@ function updateStyleDigest(style) { } -function calcStyleDigest({sections}) { - const text = new TextEncoder('utf-8').encode(JSON.stringify(sections)); +function calcStyleDigest(style) { + const jsonString = JSON.stringify(normalizeStyleSections(style)); + const text = new TextEncoder('utf-8').encode(jsonString); return crypto.subtle.digest('SHA-1', text).then(hex); + function hex(buffer) { const parts = []; const PAD8 = '00000000'; diff --git a/update.js b/update.js index 1055a55b..7056efa7 100644 --- a/update.js +++ b/update.js @@ -1,4 +1,5 @@ -/* globals getStyles, saveStyle, styleSectionsEqual, getStyleDigests, updateStyleDigest */ +/* global getStyles, saveStyle, styleSectionsEqual */ +/* global getStyleDigests, updateStyleDigest */ 'use strict'; // eslint-disable-next-line no-var @@ -7,55 +8,71 @@ var updater = { COUNT: 'count', UPDATED: 'updated', SKIPPED: 'skipped', - SKIPPED_EDITED: 'locally edited', - SKIPPED_MAYBE_EDITED: 'maybe locally edited', - SKIPPED_SAME_MD5: 'up-to-date: MD5 is unchanged', - SKIPPED_SAME_CODE: 'up-to-date: code sections are unchanged', - SKIPPED_ERROR_MD5: 'error: MD5 is invalid', - SKIPPED_ERROR_JSON: 'error: JSON is invalid', DONE: 'done', + // details for SKIPPED status + EDITED: 'locally edited', + MAYBE_EDITED: 'maybe locally edited', + SAME_MD5: 'up-to-date: MD5 is unchanged', + SAME_CODE: 'up-to-date: code sections are unchanged', + ERROR_MD5: 'error: MD5 is invalid', + ERROR_JSON: 'error: JSON is invalid', + lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(), - checkAllStyles(observe = () => {}, {save = true} = {}) { + checkAllStyles({observer = () => {}, save = true, ignoreDigest} = {}) { updater.resetInterval(); return new Promise(resolve => { getStyles({}, styles => { styles = styles.filter(style => style.updateUrl); - observe(updater.COUNT, styles.length); + observer(updater.COUNT, styles.length); Promise.all(styles.map(style => - updater.checkStyle(style, observe, {save}) + updater.checkStyle({style, observer, save, ignoreDigest}) )).then(() => { - observe(updater.DONE); + observer(updater.DONE); resolve(); }); }); }); }, - checkStyle(style, observe = () => {}, {save = true} = {}) { + checkStyle({style, observer = () => {}, save = true, ignoreDigest}) { let hasDigest; + /* + Original style digests are calculated in these cases: + * style is installed or updated from server + * style is checked for an update and its code is equal to the server code + + Update check proceeds in these cases: + * style has the original digest and it's equal to the current digest + * [ignoreDigest: true] style doesn't yet have the original digest but we ignore it + * [ignoreDigest: none/false] style doesn't yet have the original digest + so we compare the code to the server code and if it's the same we save the digest, + otherwise we skip the style and report MAYBE_EDITED status + + 'ignoreDigest' option is set on the second manual individual update check on the manage page. + */ return getStyleDigests(style) .then(fetchMd5IfNotEdited) .then(fetchCodeIfMd5Changed) .then(saveIfUpdated) - .then(saved => observe(updater.UPDATED, saved)) - .catch(err => observe(updater.SKIPPED, style, err)); + .then(saved => observer(updater.UPDATED, saved)) + .catch(err => observer(updater.SKIPPED, style, err)); function fetchMd5IfNotEdited([originalDigest, current]) { hasDigest = Boolean(originalDigest); - if (hasDigest && originalDigest != current) { - return Promise.reject(updater.SKIPPED_EDITED); + if (hasDigest && !ignoreDigest && originalDigest != current) { + return Promise.reject(updater.EDITED); } return download(style.md5Url); } function fetchCodeIfMd5Changed(md5) { if (!md5 || md5.length != 32) { - return Promise.reject(updater.SKIPPED_ERROR_MD5); + return Promise.reject(updater.ERROR_MD5); } if (md5 == style.originalMd5 && hasDigest) { - return Promise.reject(updater.SKIPPED_SAME_MD5); + return Promise.reject(updater.SAME_MD5); } return download(style.updateUrl); } @@ -63,16 +80,16 @@ var updater = { function saveIfUpdated(text) { const json = tryJSONparse(text); if (!styleJSONseemsValid(json)) { - return Promise.reject(updater.SKIPPED_ERROR_JSON); + return Promise.reject(updater.ERROR_JSON); } json.id = style.id; if (styleSectionsEqual(json, style)) { - if (!hasDigest) { - updateStyleDigest(json); - } - return Promise.reject(updater.SKIPPED_SAME_CODE); - } else if (!hasDigest) { - return Promise.reject(updater.SKIPPED_MAYBE_EDITED); + // JSONs may have different order of items even if sections are effectively equal + // so we'll update the digest anyway + updateStyleDigest(json); + return Promise.reject(updater.SAME_CODE); + } else if (!hasDigest && !ignoreDigest) { + return Promise.reject(updater.MAYBE_EDITED); } return !save ? json : saveStyle(Object.assign(json, { From 5a9930c6082248f387e7cd2687a04a89e92f675d Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 25 Apr 2017 13:22:20 +0300 Subject: [PATCH 203/235] fixup 2a7231a8: remove z-index --- manage.css | 1 - 1 file changed, 1 deletion(-) diff --git a/manage.css b/manage.css index c590126a..d1dc12b1 100644 --- a/manage.css +++ b/manage.css @@ -240,7 +240,6 @@ summary { width: 60px; height: 20px; white-space: nowrap; - z-index: 999; } .newUI .actions > * { From 6000bb33ab0d8646cb05ffda16b56c46643b35d4 Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 25 Apr 2017 14:16:22 +0300 Subject: [PATCH 204/235] editor: dim Save button when not modified --- edit.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/edit.html b/edit.html index 79b26790..f64f31d2 100644 --- a/edit.html +++ b/edit.html @@ -101,6 +101,14 @@ #url:not([href^="http"]) { display: none; } + #save-button { + opacity: .5; + pointer-events: none; + } + .dirty #save-button { + opacity: 1; + pointer-events: all; + } .svg-icon { cursor: pointer; vertical-align: middle; From 02fd4f1abe2cbdc796f73b855cd018650f604cb1 Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 25 Apr 2017 14:17:37 +0300 Subject: [PATCH 205/235] Display "force-install" for locally edited styles on update * Allow manually resetting locally edited style even if up-to-date * "Check again, I didn't edit any styles!" button --- _locales/en/messages.json | 10 +++++++++- manage.css | 19 +++++++++++++++---- manage.html | 5 +++-- manage.js | 14 +++++++++++--- update.js | 2 +- 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 50229ff4..4d49dbd0 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -76,6 +76,10 @@ "message": "Check all styles for updates", "description": "Label for the button to check all styles for updates" }, + "checkAllUpdatesForce": { + "message": "Check again, I didn't edit any styles!", + "description": "Label for the button to apply all detected updates" + }, "checkForUpdate": { "message": "Check for update", "description": "Label for the button to check a single style for an update" @@ -323,7 +327,7 @@ "description": "Checkbox to show only locally edited styles" }, "manageOnlyUpdates": { - "message": "Only with updates or problems", + "message": "Only with updates or issues", "description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed" }, "manageNewUI": { @@ -591,6 +595,10 @@ "message": "This style might have been edited locally.", "description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications" }, + "updateCheckManualUpdateForce": { + "message": "Force-install update (and lose your edits)", + "description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications" + }, "updateCheckManualUpdateHint": { "message": "To force an update (and lose your edits) update each style individually.", "description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications" diff --git a/manage.css b/manage.css index d1dc12b1..5ef1549f 100644 --- a/manage.css +++ b/manage.css @@ -293,19 +293,22 @@ summary { cursor: pointer; } +.newUI .can-update[data-details$="locally edited"] .update svg, .newUI .update-problem .check-update svg { fill: #ef6969; } +.newUI .can-update[data-details$="locally edited"]:hover .update svg, .newUI .entry.update-problem:hover .check-update svg { fill: #fd4040; } +.newUI .can-update[data-details$="locally edited"]:hover .update svg:hover, .newUI .entry.update-problem:hover .check-update svg:hover { fill: red; } -.updater-icons > :not(.check-update):after { +.newUI .updater-icons > :not(.check-update):after { content: attr(title); position: absolute; margin-top: 18px; @@ -321,14 +324,14 @@ summary { z-index: 999; } -.update-problem .check-update:after { +.newUI .update-problem .check-update:after { background-color: red; border: 1px solid #d40000; color: white; animation: none; } -.can-update .update:after { +.newUI .can-update .update:after { background-color: #c0fff0; border: 1px solid #89cac9; animation: none; @@ -467,12 +470,16 @@ input[id^="manage.newUI"] { display: inline; } +.can-update[data-details$="locally edited"] button.update:after { + content: "*"; +} + .can-update .check-update { display: none; } /* Updates not available */ -.no-update .check-update { +.no-update:not(.update-problem) .check-update { display: none; } @@ -503,6 +510,10 @@ input[id^="manage.newUI"] { content: " __MSG_updateAllCheckSucceededSomeEdited__ __MSG_updateCheckManualUpdateHint__"; } +#check-all-updates-force { + margin-top: 1ex; +} + /* highlight updated/added styles */ .highlight { animation: highlight 10s cubic-bezier(0,.82,.47,.98); diff --git a/manage.html b/manage.html index 6688c987..58ac2a63 100644 --- a/manage.html +++ b/manage.html @@ -148,8 +148,8 @@ +

    diff --git a/manage.js b/manage.js index 217ab3ec..789a1eb1 100644 --- a/manage.js +++ b/manage.js @@ -51,6 +51,7 @@ function initGlobalEvents() { installed = $('#installed'); installed.onclick = handleEvent.entryClicked; $('#check-all-updates').onclick = checkUpdateAll; + $('#check-all-updates-force').onclick = checkUpdateAll; $('#apply-all-updates').onclick = applyUpdateAll; $('#search').oninput = searchStyles; $('#manage-options-button').onclick = () => chrome.runtime.openOptionsPage(); @@ -504,7 +505,9 @@ function applyUpdateAll() { function checkUpdateAll() { + const ignoreDigest = this && this.id == 'check-all-updates-force'; $('#check-all-updates').disabled = true; + $('#check-all-updates-force').classList.add('hidden'); $('#apply-all-updates').classList.add('hidden'); $('#update-all-no-updates').classList.add('hidden'); @@ -513,8 +516,9 @@ function checkUpdateAll() { let skippedEdited = 0; let updated = 0; - $$('.updatable:not(.can-update):not(.update-problem)').map(el => checkUpdate(el, {single: false})); - BG.updater.checkAllStyles({observer, save: false}); + $$('.updatable:not(.can-update)' + (ignoreDigest ? '' : ':not(.update-problem)')) + .map(el => checkUpdate(el, {single: false})); + BG.updater.checkAllStyles({observer, save: false, ignoreDigest}); function observer(state, value, details) { switch (state) { @@ -542,6 +546,7 @@ function checkUpdateAll() { if (!updated) { $('#update-all-no-updates').dataset.skippedEdited = skippedEdited > 0; $('#update-all-no-updates').classList.remove('hidden'); + $('#check-all-updates-force').classList.toggle('hidden', skippedEdited == 0); } return; } @@ -582,6 +587,9 @@ function reportUpdateState(state, style, details) { if (entry.classList.contains('can-update')) { break; } + const same = details == BG.updater.SAME_MD5 || details == BG.updater.SAME_CODE; + const edited = details == BG.updater.EDITED || details == BG.updater.MAYBE_EDITED; + entry.dataset.details = details; if (!details) { details = t('updateCheckFailServerUnreachable'); } else if (typeof details == 'number') { @@ -591,12 +599,12 @@ function reportUpdateState(state, style, details) { } else if (details == BG.updater.MAYBE_EDITED) { details = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); } - const same = details == BG.updater.SAME_MD5 || details == BG.updater.SAME_CODE; const message = same ? t('updateCheckSucceededNoUpdate') : details; entry.classList.add('no-update'); entry.classList.toggle('update-problem', !same); $('.update-note', entry).textContent = message; $('.check-update', entry).title = newUI.enabled ? message : ''; + $('.update', entry).title = t(edited ? 'updateCheckManualUpdateForce' : 'installUpdate'); if (!$('#check-all-updates').disabled) { // this is a single update job so we can decide whether to hide the filter renderUpdatesOnlyFilter({show: $('.can-update, .update-problem')}); diff --git a/update.js b/update.js index 7056efa7..8c40a210 100644 --- a/update.js +++ b/update.js @@ -71,7 +71,7 @@ var updater = { if (!md5 || md5.length != 32) { return Promise.reject(updater.ERROR_MD5); } - if (md5 == style.originalMd5 && hasDigest) { + if (md5 == style.originalMd5 && hasDigest && !ignoreDigest) { return Promise.reject(updater.SAME_MD5); } return download(style.updateUrl); From acc4d83b9de053dbfda6c4cc26339c23227447c9 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 26 Apr 2017 00:48:27 +0300 Subject: [PATCH 206/235] promisify DB access --- background.js | 20 ++--- messaging.js | 23 ++--- storage.js | 236 ++++++++++++++++++++++++++------------------------ update.js | 19 ++-- 4 files changed, 149 insertions(+), 149 deletions(-) diff --git a/background.js b/background.js index f709b398..e0367f3e 100644 --- a/background.js +++ b/background.js @@ -1,4 +1,4 @@ -/* global getDatabase, getStyles, saveStyle */ +/* global dbExec, getStyles, saveStyle */ 'use strict'; // eslint-disable-next-line no-var @@ -6,7 +6,7 @@ var browserCommands, contextMenus; // ************************************************************************* // preload the DB and report errors -getDatabase(() => {}, (...args) => { +dbExec().catch((...args) => { args.forEach(arg => 'message' in arg && console.error(arg.message)); }); @@ -183,7 +183,7 @@ prefs.subscribe((id, checked) => { // ************************************************************************* function webNavigationListener(method, {url, tabId, frameId}) { - getStyles({matchUrl: url, enabled: true, asHash: true}, styles => { + getStyles({matchUrl: url, enabled: true, asHash: true}).then(styles => { if (method && !url.startsWith('chrome:') && tabId >= 0) { chrome.tabs.sendMessage(tabId, { method, @@ -209,9 +209,9 @@ function updateIcon(tab, styles) { stylesReceived(styles); return; } - getTabRealURL(tab).then(url => - getStyles({matchUrl: url, enabled: true, asHash: true}, - stylesReceived)); + getTabRealURL(tab) + .then(url => getStyles({matchUrl: url, enabled: true, asHash: true})) + .then(stylesReceived); function stylesReceived(styles) { let numStyles = styles.length; @@ -255,7 +255,7 @@ function onRuntimeMessage(request, sender, sendResponse) { switch (request.method) { case 'getStyles': - getStyles(request, sendResponse); + getStyles(request).then(sendResponse); return KEEP_CHANNEL_OPEN; case 'saveStyle': @@ -263,9 +263,9 @@ function onRuntimeMessage(request, sender, sendResponse) { return KEEP_CHANNEL_OPEN; case 'healthCheck': - getDatabase( - () => sendResponse(true), - () => sendResponse(false)); + dbExec() + .then(() => sendResponse(true)) + .catch(() => sendResponse(false)); return KEEP_CHANNEL_OPEN; case 'download': diff --git a/messaging.js b/messaging.js index 81fe30ca..5c95ad33 100644 --- a/messaging.js +++ b/messaging.js @@ -268,13 +268,13 @@ function sessionStorageHash(name) { } -function onBackgroundReady() { - return BG ? Promise.resolve() : new Promise(ping); +function onBackgroundReady(...dataPassthru) { + return BG ? Promise.resolve(...dataPassthru) : new Promise(ping); function ping(resolve) { chrome.runtime.sendMessage({method: 'healthCheck'}, health => { if (health !== undefined) { BG = chrome.extension.getBackgroundPage(); - resolve(); + resolve(...dataPassthru); } else { ping(resolve); } @@ -285,20 +285,13 @@ function onBackgroundReady() { // in case Chrome haven't yet loaded the bg page and displays our page like edit/manage function getStylesSafe(options) { - return new Promise(resolve => { - if (BG) { - BG.getStyles(options, resolve); - } else { - onBackgroundReady().then(() => - BG.getStyles(options, resolve)); - } - }); + return onBackgroundReady(options).then(BG.getStyles); } function saveStyleSafe(style) { - return onBackgroundReady() - .then(() => BG.saveStyle(BG.deepCopy(style))) + return onBackgroundReady(BG.deepCopy(style)) + .then(BG.saveStyle) .then(savedStyle => { if (style.notify === false) { handleUpdate(savedStyle, style); @@ -309,8 +302,8 @@ function saveStyleSafe(style) { function deleteStyleSafe({id, notify = true} = {}) { - return onBackgroundReady() - .then(() => BG.deleteStyle({id, notify})) + return onBackgroundReady({id, notify}) + .then(BG.deleteStyle) .then(() => { if (!notify) { handleDelete(id); diff --git a/storage.js b/storage.js index a64c2abc..0479c9cf 100644 --- a/storage.js +++ b/storage.js @@ -43,58 +43,69 @@ var chromeLocal = { }; -function getDatabase(ready, error) { - const dbOpenRequest = window.indexedDB.open('stylish', 2); - dbOpenRequest.onsuccess = event => { - ready(event.target.result); - }; - dbOpenRequest.onerror = event => { - console.warn(event.target.errorCode); - if (error) { - error(event); - } - }; - dbOpenRequest.onupgradeneeded = event => { - if (event.oldVersion == 0) { - event.target.result.createObjectStore('styles', { - keyPath: 'id', - autoIncrement: true, - }); - } - }; +function dbExec(method, data) { + return new Promise((resolve, reject) => { + Object.assign(indexedDB.open('stylish', 2), { + onsuccess(event) { + const database = event.target.result; + if (!method) { + resolve(database); + } else { + const transaction = database.transaction(['styles'], 'readwrite'); + const store = transaction.objectStore('styles'); + Object.assign(store[method](data), { + onsuccess: event => resolve(event, store, transaction, database), + onerror: reject, + }); + } + }, + onerror(event) { + console.warn(event.target.errorCode); + reject(event); + }, + onupgradeneeded(event) { + if (event.oldVersion == 0) { + event.target.result.createObjectStore('styles', { + keyPath: 'id', + autoIncrement: true, + }); + } + }, + }); + }); } -function getStyles(options, callback) { +function getStyles(options) { if (cachedStyles.list) { - callback(filterStyles(options)); - return; + return Promise.resolve(filterStyles(options)); } if (cachedStyles.mutex.inProgress) { - cachedStyles.mutex.onDone.push({options, callback}); - return; + return new Promise(resolve => { + cachedStyles.mutex.onDone.push({options, resolve}); + }); } cachedStyles.mutex.inProgress = true; - getDatabase(db => { - const tx = db.transaction(['styles'], 'readonly'); - const os = tx.objectStore('styles'); - os.getAll().onsuccess = event => { - cachedStyles.list = event.target.result || []; - cachedStyles.byId.clear(); - for (const style of cachedStyles.list) { - cachedStyles.byId.set(style.id, style); - compileStyleRegExps({style}); + return dbExec('getAll').then(event => { + cachedStyles.list = event.target.result || []; + cachedStyles.byId.clear(); + const t0 = performance.now(); + let hasTimeToCompile = true; + for (const style of cachedStyles.list) { + cachedStyles.byId.set(style.id, style); + if (hasTimeToCompile) { + hasTimeToCompile = !compileStyleRegExps({style}) || performance.now() - t0 > 100; } - callback(filterStyles(options)); + } - cachedStyles.mutex.inProgress = false; - for (const {options, callback} of cachedStyles.mutex.onDone) { - callback(filterStyles(options)); - } - cachedStyles.mutex.onDone = []; - }; - }, null); + cachedStyles.mutex.inProgress = false; + for (const {options, resolve} of cachedStyles.mutex.onDone) { + resolve(filterStyles(options)); + } + cachedStyles.mutex.onDone = []; + return filterStyles(options); + }); } @@ -213,83 +224,81 @@ function filterStylesInternal({ function saveStyle(style) { - return new Promise(resolve => { - getDatabase(db => { - const tx = db.transaction(['styles'], 'readwrite'); - const os = tx.objectStore('styles'); - - const id = style.id == '0' ? 0 : Number(style.id) || null; - const reason = style.reason; - const notify = style.notify !== false; - delete style.method; - delete style.reason; - delete style.notify; - if (!style.name) { - delete style.name; - } - let existed, codeIsUpdated; - - if (id !== null) { - // Update or create - style.id = id; - os.get(id).onsuccess = eventGet => { - const oldStyle = eventGet.target.result; - existed = Boolean(oldStyle); - codeIsUpdated = !existed || style.sections && !styleSectionsEqual(style, oldStyle); - write(Object.assign({}, oldStyle, style)); - }; - } else { - // Create - delete style.id; - write(Object.assign({ - // Set optional things if they're undefined - enabled: true, - updateUrl: null, - md5Url: null, - url: null, - originalMd5: null, - }, style)); - } - - function write(style) { - style.sections = normalizeStyleSections(style); - os.put(style).onsuccess = event => { - style.id = style.id || event.target.result; - invalidateCache(existed ? {updated: style} : {added: style}); - compileStyleRegExps({style}); - if (notify) { - notifyAllTabs({ - method: existed ? 'styleUpdated' : 'styleAdded', - style, codeIsUpdated, reason, - }); - } - if (reason == 'update') { - updateStyleDigest(style); - } else if (reason == 'import') { - chrome.storage.local.remove(DIGEST_KEY_PREFIX + style.id, ignoreChromeError); - } - resolve(style); - }; - } + const id = Number(style.id) >= 0 ? Number(style.id) : null; + const reason = style.reason; + const notify = style.notify !== false; + delete style.method; + delete style.reason; + delete style.notify; + if (!style.name) { + delete style.name; + } + let existed, codeIsUpdated; + if (id !== null) { + // Update or create + style.id = id; + return dbExec('get', id).then((event, store) => { + const oldStyle = event.target.result; + existed = Boolean(oldStyle); + codeIsUpdated = !existed || style.sections && !styleSectionsEqual(style, oldStyle); + style = Object.assign({}, oldStyle, style); + return write(style, store); }); - }); + } else { + // Create + delete style.id; + style = Object.assign({ + // Set optional things if they're undefined + enabled: true, + updateUrl: null, + md5Url: null, + url: null, + originalMd5: null, + }, style); + return write(style); + } + + function write(style, store) { + style.sections = normalizeStyleSections(style); + if (store) { + return new Promise(resolve => { + store.put(style).onsuccess = event => resolve(done(event)); + }); + } else { + return dbExec('put', style).then(done); + } + } + + function done(event) { + style.id = style.id || event.target.result; + invalidateCache(existed ? {updated: style} : {added: style}); + compileStyleRegExps({style}); + if (notify) { + notifyAllTabs({ + method: existed ? 'styleUpdated' : 'styleAdded', + style, codeIsUpdated, reason, + }); + } + if (reason == 'update') { + updateStyleDigest(style); + } else if (reason == 'import') { + chrome.storage.local.remove(DIGEST_KEY_PREFIX + style.id, ignoreChromeError); + } + return style; + } } function deleteStyle({id, notify = true}) { + id = Number(id); chrome.storage.local.remove(DIGEST_KEY_PREFIX + id, ignoreChromeError); - return new Promise(resolve => - getDatabase(db => { - const tx = db.transaction(['styles'], 'readwrite'); - const os = tx.objectStore('styles'); - os.delete(Number(id)).onsuccess = () => { - invalidateCache({deletedId: id}); - if (notify) { - notifyAllTabs({method: 'styleDeleted', id}); - } - resolve(id); - }; - })); + return dbExec('delete', id).then(() => { + invalidateCache({deletedId: id}); + if (notify) { + notifyAllTabs({method: 'styleDeleted', id}); + } + return id; + }); } @@ -448,11 +457,12 @@ function compileStyleRegExps({style, compileAll}) { const rx = tryRegExp(anchored); cachedStyles.regexps.set(cacheKey, rx || false); if (!compileAll && performance.now() - t0 > 100) { - return; + return false; } } } } + return true; } diff --git a/update.js b/update.js index 8c40a210..5996aa02 100644 --- a/update.js +++ b/update.js @@ -22,17 +22,14 @@ var updater = { checkAllStyles({observer = () => {}, save = true, ignoreDigest} = {}) { updater.resetInterval(); - return new Promise(resolve => { - getStyles({}, styles => { - styles = styles.filter(style => style.updateUrl); - observer(updater.COUNT, styles.length); - Promise.all(styles.map(style => - updater.checkStyle({style, observer, save, ignoreDigest}) - )).then(() => { - observer(updater.DONE); - resolve(); - }); - }); + return getStyles({}).then(styles => { + styles = styles.filter(style => style.updateUrl); + observer(updater.COUNT, styles.length); + return Promise.all( + styles.map(style => + updater.checkStyle({style, observer, save, ignoreDigest}))); + }).then(() => { + observer(updater.DONE); }); }, From f08312ab00ad11edb105f0e481d975043dd72215 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 26 Apr 2017 01:06:16 +0300 Subject: [PATCH 207/235] rephrase update messages --- _locales/en/messages.json | 6 +++--- manage.css | 2 +- update.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 4d49dbd0..ad15ba62 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -596,11 +596,11 @@ "description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications" }, "updateCheckManualUpdateForce": { - "message": "Force-install update (and lose your edits)", + "message": "Install update (local edits will be overwritten)", "description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications" }, "updateCheckManualUpdateHint": { - "message": "To force an update (and lose your edits) update each style individually.", + "message": "Forcing an update will overwrite any local edits.", "description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications" }, "updateCheckSucceededNoUpdate": { @@ -612,7 +612,7 @@ "description": "Text that displays when an update all check completed and no updates are available" }, "updateAllCheckSucceededSomeEdited": { - "message": "Some updatable styles weren't checked to avoid losing possible local edits.", + "message": "Some updatable styles weren't checked to avoid losing possible local edits. Updates can be forced by checking individually, or by running another check for all styles (local edits will be overwritten).", "description": "Text that displays when an update all check completed and no updates are available" }, "updateCompleted": { diff --git a/manage.css b/manage.css index 5ef1549f..005f11d6 100644 --- a/manage.css +++ b/manage.css @@ -507,7 +507,7 @@ input[id^="manage.newUI"] { } #update-all-no-updates[data-skipped-edited="true"]:after { - content: " __MSG_updateAllCheckSucceededSomeEdited__ __MSG_updateCheckManualUpdateHint__"; + content: " __MSG_updateAllCheckSucceededSomeEdited__"; } #check-all-updates-force { diff --git a/update.js b/update.js index 5996aa02..742978e7 100644 --- a/update.js +++ b/update.js @@ -12,7 +12,7 @@ var updater = { // details for SKIPPED status EDITED: 'locally edited', - MAYBE_EDITED: 'maybe locally edited', + MAYBE_EDITED: 'may be locally edited', SAME_MD5: 'up-to-date: MD5 is unchanged', SAME_CODE: 'up-to-date: code sections are unchanged', ERROR_MD5: 'error: MD5 is invalid', From af365568fb4c521c2ae49d4a0cd2d8a7a2f31ff7 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 26 Apr 2017 01:44:09 +0300 Subject: [PATCH 208/235] re-use normal bg in update tooltip when locally edited --- manage.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/manage.css b/manage.css index 005f11d6..9c022bbb 100644 --- a/manage.css +++ b/manage.css @@ -332,9 +332,12 @@ summary { } .newUI .can-update .update:after { + animation: none; +} + +.newUI .can-update:not([data-details$="locally edited"]) .update:after { background-color: #c0fff0; border: 1px solid #89cac9; - animation: none; } .newUI .applies-to { From 50ec32a7b2124a8d2e54527d3c3d6858133793fc Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 26 Apr 2017 03:05:41 +0300 Subject: [PATCH 209/235] Rephrase the misleading "only edited styles" option --- _locales/ar/messages.json | 4 ---- _locales/cs/messages.json | 4 ---- _locales/de/messages.json | 6 +----- _locales/el/messages.json | 4 ---- _locales/en/messages.json | 10 +++++++--- _locales/es/messages.json | 4 ---- _locales/fi/messages.json | 4 ---- _locales/fr/messages.json | 4 ---- _locales/it/messages.json | 4 ---- _locales/ja/messages.json | 4 ---- _locales/nl/messages.json | 4 ---- _locales/pt_BR/messages.json | 4 ---- _locales/ru/messages.json | 4 ---- _locales/sr/messages.json | 7 ++----- _locales/sv/messages.json | 4 ---- _locales/sv_SE/messages.json | 4 ---- _locales/te/messages.json | 4 ---- _locales/tr/messages.json | 4 ---- _locales/zh/messages.json | 4 ---- _locales/zh_CN/messages.json | 4 ---- _locales/zh_TW/messages.json | 4 ---- dom.js | 2 +- manage.html | 4 ++-- prefs.js | 2 +- 24 files changed, 14 insertions(+), 89 deletions(-) diff --git a/_locales/ar/messages.json b/_locales/ar/messages.json index a808bf3c..50c8f547 100644 --- a/_locales/ar/messages.json +++ b/_locales/ar/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index d2ab037c..2cf3eebe 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -7,10 +7,6 @@ "message": "výchozí", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Pouze upravené styly.", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Exportovat", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 11c75031..e924449f 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -11,10 +11,6 @@ "message": "Styles Exportieren", "description": "" }, - "manageOnlyEdited": { - "message": "Nur bearbeitete Styles", - "description": "Checkbox to show only locally edited styles" - }, "optionsUpdateInterval": { "message": "Automatischer Update- und Installations-Intervall (in Stunden)", "description": "" @@ -520,4 +516,4 @@ "message": "Gestalten Sie das Web mit Stylus, einem Manager für Benutzer-Styles, um. Stylus lässt Sie ganz einfach Themes und Skins für viele beliebte Webseiten installieren.", "description": "Extension description" } -} \ No newline at end of file +} diff --git a/_locales/el/messages.json b/_locales/el/messages.json index 1ce194ef..ac685072 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Μόνο επεξεργασμενα στυλ", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" diff --git a/_locales/en/messages.json b/_locales/en/messages.json index ad15ba62..74032fd9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -322,9 +322,13 @@ "message": "Only enabled styles", "description": "Checkbox to show only enabled styles" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" + "manageOnlyLocal": { + "message": "Only locally created styles", + "description": "Checkbox to show only locally created styles i.e. non-updatable" + }, + "manageOnlyLocalTooltip": { + "message": "(the styles not installed through a userstyles.org page)", + "description": "Tooltip for the checkbox to show only locally created styles i.e. non-updatable" }, "manageOnlyUpdates": { "message": "Only with updates or issues", diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 8eb8fb4e..5ac6f5d1 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -11,10 +11,6 @@ "message": "Exportar estilos", "description": "" }, - "manageOnlyEdited": { - "message": "Sólo estilos editados", - "description": "Checkbox to show only locally edited styles" - }, "optionsUpdateInterval": { "message": "Buscar e instalar automáticamente todas las actualizaciones disponibles de estilos de usuario (en horas)", "description": "" diff --git a/_locales/fi/messages.json b/_locales/fi/messages.json index 05f23581..9d7e0ea8 100644 --- a/_locales/fi/messages.json +++ b/_locales/fi/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index bcefde28..b399a0d5 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -7,10 +7,6 @@ "message": "défaut", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Exportez", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 0c0e3c96..3a485c49 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 0bff027c..db2a6210 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" diff --git a/_locales/nl/messages.json b/_locales/nl/messages.json index 6361e18d..618b81bd 100644 --- a/_locales/nl/messages.json +++ b/_locales/nl/messages.json @@ -7,10 +7,6 @@ "message": "standaard", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Alleen bewerkte stijlen", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Exporteren", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" diff --git a/_locales/pt_BR/messages.json b/_locales/pt_BR/messages.json index 1781676d..729f3dd4 100644 --- a/_locales/pt_BR/messages.json +++ b/_locales/pt_BR/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 1de473da..1b5797c2 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -7,10 +7,6 @@ "message": "по-умолчанию", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Только отредактированные стили", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Экспорт", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" diff --git a/_locales/sr/messages.json b/_locales/sr/messages.json index 34e16b6d..01682cad 100644 --- a/_locales/sr/messages.json +++ b/_locales/sr/messages.json @@ -7,10 +7,6 @@ "message": "подразумевано", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Само уређени стилови", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Извези", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -295,7 +291,8 @@ "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, "stylusUnavailableForURL": { - "message": "Stylus не ради на страницама као што је ова.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" + "message": "Stylus не ради на страницама као што је ова.", + "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { "message": "Уклони одељак", diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index 14ad05cb..5b015421 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Endast ändrade stilar", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" diff --git a/_locales/sv_SE/messages.json b/_locales/sv_SE/messages.json index 913f6b8c..1016654e 100644 --- a/_locales/sv_SE/messages.json +++ b/_locales/sv_SE/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Endast ändrade stilar", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" diff --git a/_locales/te/messages.json b/_locales/te/messages.json index 82268516..ed0550f2 100644 --- a/_locales/te/messages.json +++ b/_locales/te/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" diff --git a/_locales/tr/messages.json b/_locales/tr/messages.json index 7a15d65c..efbc2fd3 100644 --- a/_locales/tr/messages.json +++ b/_locales/tr/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" diff --git a/_locales/zh/messages.json b/_locales/zh/messages.json index fbc30b4c..a28bf22c 100644 --- a/_locales/zh/messages.json +++ b/_locales/zh/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 99b9dc9c..98cb6163 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -11,10 +11,6 @@ "message": "导出所有样式", "description": "" }, - "manageOnlyEdited": { - "message": "仅修改过的样式", - "description": "Checkbox to show only locally edited styles" - }, "optionsUpdateInterval": { "message": "每 N 小时,检查所有样式更新(0 为关闭检查)", "description": "" diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json index 07e51fad..e326dfc8 100644 --- a/_locales/zh_TW/messages.json +++ b/_locales/zh_TW/messages.json @@ -7,10 +7,6 @@ "message": "默認", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "只顯示已禁用的樣式", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "導出", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" diff --git a/dom.js b/dom.js index 7f5dd80c..d2475415 100644 --- a/dom.js +++ b/dom.js @@ -68,7 +68,7 @@ function enforceInputRange(element) { function $(selector, base = document) { - // we have ids with . like #manage.onlyEdited which look like #id.class + // we have ids with . like #manage.onlyEnabled which looks like #id.class // so since getElementById is superfast we'll try it anyway const byId = selector.startsWith('#') && document.getElementById(selector.slice(1)); return byId || base.querySelector(selector); diff --git a/manage.html b/manage.html index 58ac2a63..d7fc425c 100644 --- a/manage.html +++ b/manage.html @@ -141,10 +141,10 @@

    + + + + +

    diff --git a/manage.js b/manage.js index 309e87ae..cc298d10 100644 --- a/manage.js +++ b/manage.js @@ -53,6 +53,7 @@ function initGlobalEvents() { $('#check-all-updates').onclick = checkUpdateAll; $('#check-all-updates-force').onclick = checkUpdateAll; $('#apply-all-updates').onclick = applyUpdateAll; + $('#update-history').onclick = showUpdateHistory; $('#search').oninput = searchStyles; $('#manage-options-button').onclick = () => chrome.runtime.openOptionsPage(); $('#manage-shortcuts-button').onclick = () => openURL({url: URLS.configureCommands}); @@ -675,6 +676,21 @@ function renderUpdatesOnlyFilter({show, check} = {}) { } +function showUpdateHistory() { + BG.chromeLocal.getValue('updateLog').then((lines = []) => { + messageBox({ + title: t('updateCheckHistory'), + contents: $element({ + className: 'update-history-log', + textContent: lines.join('\n'), + }), + buttons: [t('confirmOK')], + onshow: () => ($('#message-box-contents').scrollTop = 1e9), + }); + }); +} + + function searchStyles({immediately, container}) { const searchElement = $('#search'); const query = searchElement.value.toLocaleLowerCase(); diff --git a/update.js b/update.js index 742978e7..46fb0230 100644 --- a/update.js +++ b/update.js @@ -1,4 +1,4 @@ -/* global getStyles, saveStyle, styleSectionsEqual */ +/* global getStyles, saveStyle, styleSectionsEqual, chromeLocal */ /* global getStyleDigests, updateStyleDigest */ 'use strict'; @@ -25,11 +25,13 @@ var updater = { return getStyles({}).then(styles => { styles = styles.filter(style => style.updateUrl); observer(updater.COUNT, styles.length); + updater.log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`); return Promise.all( styles.map(style => updater.checkStyle({style, observer, save, ignoreDigest}))); }).then(() => { observer(updater.DONE); + updater.log(''); }); }, @@ -53,8 +55,15 @@ var updater = { .then(fetchMd5IfNotEdited) .then(fetchCodeIfMd5Changed) .then(saveIfUpdated) - .then(saved => observer(updater.UPDATED, saved)) - .catch(err => observer(updater.SKIPPED, style, err)); + .then(saved => { + observer(updater.UPDATED, saved); + updater.log(updater.UPDATED + ` #${saved.id} ${saved.name}`); + }) + .catch(err => { + observer(updater.SKIPPED, style, err); + err = err === 0 ? 'server unreachable' : err; + updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`); + }); function fetchMd5IfNotEdited([originalDigest, current]) { hasDigest = Boolean(originalDigest); @@ -118,6 +127,18 @@ var updater = { localStorage.lastUpdateTime = updater.lastUpdateTime = Date.now(); updater.schedule(); }, + + log(text) { + chromeLocal.getValue('updateLog').then((lines = []) => { + const time = text && performance.now() - (updater.log.lastWriteTime || 0) > 10e3 + ? new Date().toLocaleString() + '\t' + : ''; + lines.splice(0, lines.length - 1000); + lines.push(time + text); + chromeLocal.setValue('updateLog', lines); + updater.log.lastWriteTime = performance.now(); + }); + }, }; updater.schedule(); From a22874a898d6ee612a92184205897384f065a684 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 27 Apr 2017 00:49:03 +0300 Subject: [PATCH 215/235] write/read styleDigest in the backup file --- backup/fileSaveLoad.js | 16 +++++++++++++++- storage.js | 14 +++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/backup/fileSaveLoad.js b/backup/fileSaveLoad.js index 62de23d4..9aa88603 100644 --- a/backup/fileSaveLoad.js +++ b/backup/fileSaveLoad.js @@ -59,6 +59,10 @@ function importFromString(jsonString) { const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []); const oldStylesByName = json.length && new Map( oldStyles.map(style => [style.name.trim(), style])); + + let oldDigests; + chrome.storage.local.get(null, data => (oldDigests = data)); + const stats = { added: {names: [], ids: [], legend: 'importReportLegendAdded'}, unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'}, @@ -67,12 +71,14 @@ function importFromString(jsonString) { codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode'}, invalid: {names: [], legend: 'importReportLegendInvalid'}, }; + let index = 0; let lastRenderTime = performance.now(); const renderQueue = []; const RENDER_NAP_TIME_MAX = 1000; // ms const RENDER_QUEUE_MAX = 50; // number of styles const SAVE_OPTIONS = {reason: 'import', notify: false}; + return new Promise(proceed); function proceed(resolve) { @@ -214,6 +220,7 @@ function importFromString(jsonString) { deleteStyleSafe({id, notify: false}).then(id => { const oldStyle = oldStylesById.get(id); if (oldStyle) { + oldStyle.styleDigest = oldDigests[BG.DIGEST_KEY_PREFIX + id]; saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS)) .then(undoNextId); } else { @@ -275,7 +282,14 @@ function importFromString(jsonString) { $('#file-all-styles').onclick = () => { - getStylesSafe().then(styles => { + Promise.all([ + BG.chromeLocal.get(null), + getStylesSafe(), + ]).then(([data, styles]) => { + styles = styles.map(style => { + const styleDigest = data[BG.DIGEST_KEY_PREFIX + style.id]; + return styleDigest ? Object.assign({styleDigest}, style) : style; + }); const text = JSON.stringify(styles, null, '\t'); const fileName = generateFileName(); diff --git a/storage.js b/storage.js index 0479c9cf..0959c640 100644 --- a/storage.js +++ b/storage.js @@ -5,7 +5,9 @@ const RX_NAMESPACE = new RegExp([/[\s\r\n]*/, /[\s\r\n]*/].map(rx => rx.source).join(''), 'g'); const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g; const SLOPPY_REGEXP_PREFIX = '\0'; -const DIGEST_KEY_PREFIX = 'originalDigest'; + +// eslint-disable-next-line no-var +var DIGEST_KEY_PREFIX = 'originalDigest'; // Note, only 'var'-declared variables are visible from another extension page // eslint-disable-next-line no-var @@ -227,9 +229,11 @@ function saveStyle(style) { const id = Number(style.id) >= 0 ? Number(style.id) : null; const reason = style.reason; const notify = style.notify !== false; + const styleDigest = style.styleDigest; delete style.method; delete style.reason; delete style.notify; + delete style.styleDigest; if (!style.name) { delete style.name; } @@ -282,7 +286,11 @@ function saveStyle(style) { if (reason == 'update') { updateStyleDigest(style); } else if (reason == 'import') { - chrome.storage.local.remove(DIGEST_KEY_PREFIX + style.id, ignoreChromeError); + if (typeof styleDigest == 'string' && styleDigest.length == 40) { + chromeLocal.setValue(DIGEST_KEY_PREFIX + style.id, styleDigest); + } else { + chrome.storage.local.remove(DIGEST_KEY_PREFIX + style.id); + } } return style; } @@ -566,7 +574,7 @@ function getStyleDigests(style) { function updateStyleDigest(style) { calcStyleDigest(style).then(digest => - chromeLocal.set({[DIGEST_KEY_PREFIX + style.id]: digest})); + chromeLocal.setValue(DIGEST_KEY_PREFIX + style.id, digest)); } From f0dc13cd2ee5577a40f83dcea5ecd877f76d4eaa Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 27 Apr 2017 02:06:16 +0300 Subject: [PATCH 216/235] debounce update log writer --- update.js | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/update.js b/update.js index 46fb0230..ec7b9641 100644 --- a/update.js +++ b/update.js @@ -128,17 +128,27 @@ var updater = { updater.schedule(); }, - log(text) { - chromeLocal.getValue('updateLog').then((lines = []) => { - const time = text && performance.now() - (updater.log.lastWriteTime || 0) > 10e3 - ? new Date().toLocaleString() + '\t' - : ''; - lines.splice(0, lines.length - 1000); - lines.push(time + text); - chromeLocal.setValue('updateLog', lines); - updater.log.lastWriteTime = performance.now(); - }); - }, + log: (() => { + let queue = []; + let lastWriteTime = 0; + return text => { + queue.push(text); + debounce(flushQueue, 1e3); + }; + function flushQueue() { + chromeLocal.getValue('updateLog').then((lines = []) => { + // our XHR timeout is 10 seconds + const time = performance.now() - lastWriteTime > 11e3 + ? new Date().toLocaleString() + '\t' + : ''; + lines.splice(0, lines.length - 1000); + lines.push(...queue.map(item => item ? time + item : '')); + chromeLocal.setValue('updateLog', lines); + lastWriteTime = performance.now(); + queue = []; + }); + } + })(), }; updater.schedule(); From 459c2e5ef3ea59c7783986795651729acee977ba Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 27 Apr 2017 15:54:55 +0300 Subject: [PATCH 217/235] update log timestamp adjustments --- update.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/update.js b/update.js index ec7b9641..f71ed5e0 100644 --- a/update.js +++ b/update.js @@ -132,19 +132,17 @@ var updater = { let queue = []; let lastWriteTime = 0; return text => { - queue.push(text); + queue.push({text, time: new Date().toLocaleString()}); debounce(flushQueue, 1e3); }; function flushQueue() { chromeLocal.getValue('updateLog').then((lines = []) => { - // our XHR timeout is 10 seconds - const time = performance.now() - lastWriteTime > 11e3 - ? new Date().toLocaleString() + '\t' - : ''; + const time = Date.now() - lastWriteTime > 11e3 ? queue[0].time + ' ' : ''; lines.splice(0, lines.length - 1000); - lines.push(...queue.map(item => item ? time + item : '')); + lines.push(time + queue[0].text); + lines.push(...queue.slice(1).map(item => item.text)); chromeLocal.setValue('updateLog', lines); - lastWriteTime = performance.now(); + lastWriteTime = Date.now(); queue = []; }); } From 738788f289b56ad3dd9419e90470f39f5a4862b1 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 27 Apr 2017 20:31:56 +0300 Subject: [PATCH 218/235] tweak no-unused-vars --- .eslintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 73a2efac..24b34542 100644 --- a/.eslintrc +++ b/.eslintrc @@ -213,7 +213,7 @@ rules: no-unsafe-negation: [2] no-unused-expressions: [2] no-unused-labels: [0] - no-unused-vars: [1, {args: all, vars: local, varsIgnorePattern: clearError, argsIgnorePattern: ^_}] + no-unused-vars: [1, {args: after-used, vars: local, argsIgnorePattern: ^_}] no-use-before-define: [2, nofunc] no-useless-call: [2] no-useless-computed-key: [2] From e0124f66ba7f7c1bbd6162ecee381276e15ccb84 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 27 Apr 2017 01:09:52 +0300 Subject: [PATCH 219/235] code cosmetics --- backup/fileSaveLoad.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/backup/fileSaveLoad.js b/backup/fileSaveLoad.js index 9aa88603..203ff095 100644 --- a/backup/fileSaveLoad.js +++ b/backup/fileSaveLoad.js @@ -291,22 +291,20 @@ $('#file-all-styles').onclick = () => { return styleDigest ? Object.assign({styleDigest}, style) : style; }); const text = JSON.stringify(styles, null, '\t'); - const fileName = generateFileName(); - const url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text); + return url; // for long URLs; https://github.com/schomery/stylish-chrome/issues/13#issuecomment-284582600 - fetch(url) + }).then(fetch) .then(res => res.blob()) .then(blob => { const objectURL = URL.createObjectURL(blob); Object.assign(document.createElement('a'), { - download: fileName, + download: generateFileName(), href: objectURL, type: 'application/json', }).dispatchEvent(new MouseEvent('click')); setTimeout(() => URL.revokeObjectURL(objectURL)); }); - }); function generateFileName() { const today = new Date(); From 0ce99afbf57464b7d1eefe7ee6f5b302bf04eaf9 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 27 Apr 2017 14:20:10 +0300 Subject: [PATCH 220/235] No need for Stylish->Stylus substitution on new USO --- install.js | 54 +++++++++--------------------------------------------- 1 file changed, 9 insertions(+), 45 deletions(-) diff --git a/install.js b/install.js index 9923bad3..61d031c5 100644 --- a/install.js +++ b/install.js @@ -3,9 +3,6 @@ document.addEventListener('stylishUpdateChrome', onUpdateClicked); document.addEventListener('stylishInstallChrome', onInstallClicked); -new MutationObserver(waitForBody) - .observe(document.documentElement, {childList: true}); - chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { // orphaned content script check if (msg.method == 'ping') { @@ -13,22 +10,15 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { } }); - -function waitForBody() { - if (!document.body) { - return; +new MutationObserver((mutations, observer) => { + if (document.body) { + observer.disconnect(); + chrome.runtime.sendMessage({ + method: 'getStyles', + url: getMeta('stylish-id-url') || location.href + }, checkUpdatability); } - this.disconnect(); - - rebrand([{addedNodes: [document.body]}]); - new MutationObserver(rebrand) - .observe(document.body, {childList: true, subtree: true}); - - chrome.runtime.sendMessage({ - method: 'getStyles', - url: getMeta('stylish-id-url') || location.href - }, checkUpdatability); -} +}).observe(document.documentElement, {childList: true}); function checkUpdatability([installedStyle]) { @@ -133,31 +123,6 @@ function getResource(url) { } -function rebrand(mutations, observer) { - /* stylish to stylus; https://github.com/schomery/stylish-chrome/issues/12 */ - if (!document.getElementById('hidden-meta') && document.readyState == 'loading') { - return; - } - observer.disconnect(); - const elements = document.getElementsByClassName('install-status'); - for (let i = elements.length; --i >= 0;) { - const walker = document.createTreeWalker(elements[i], NodeFilter.SHOW_TEXT); - while (walker.nextNode()) { - const node = walker.currentNode; - const text = node.nodeValue; - const parent = node.parentNode; - const extensionHelp = /stylish_chrome/.test(parent.href); - if (text.includes('Stylish') && (parent.localName != 'a' || extensionHelp)) { - node.nodeValue = text.replace(/Stylish/g, 'Stylus'); - } - if (extensionHelp) { - parent.href = 'http://add0n.com/stylus.html'; - } - } - } -} - - function styleSectionsEqual({sections: a}, {sections: b}) { if (!a || !b) { return undefined; @@ -236,13 +201,12 @@ function orphanCheck() { 'checkUpdatability', 'getMeta', 'getResource', + 'onDOMready', 'onInstallClicked', 'onUpdateClicked', 'orphanCheck', - 'rebrand', 'saveStyleCode', 'sendEvent', 'styleSectionsEqual', - 'waitForBody', ].forEach(fn => (window[fn] = null)); } From cff3d13d4b0163e7c1e3bbd82dc8f39ef81b7684 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 27 Apr 2017 14:39:51 +0300 Subject: [PATCH 221/235] optionsUI: add a post-import update hint --- _locales/en/messages.json | 3 +++ options/index.css | 16 +++++++++++----- options/index.html | 5 ++++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 39122199..8cd0ccd7 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -663,6 +663,9 @@ "optionsUpdateIntervalNote": { "message": "To disable the automatic userstyle update checks, set interval to 0" }, + "optionsUpdateImportNote": { + "message": "When importing style backups from old version or from Stylish, do a one-time check for updates manually in the styles manager to ensure all styles are updated." + }, "optionsCustomizeBadge": { "message": "Badge on the toolbar icon" }, diff --git a/options/index.css b/options/index.css index a7c3a2af..bd649bbc 100644 --- a/options/index.css +++ b/options/index.css @@ -99,6 +99,11 @@ input[type=number]:invalid { color: darkred; } +input[type="color"] { + box-sizing: border-box; + height: 2em; +} + [data-cmd="check-updates"] button { position: relative; } @@ -152,11 +157,6 @@ input[type=number]:invalid { margin-bottom: 1ex; } -input[type="color"] { - box-sizing: border-box; - height: 2em; -} - #notes a { color: inherit; } @@ -165,6 +165,12 @@ input[type="color"] { color: black; } +#notes p { + line-height: 1.25; + margin-top: 1ex; + margin-bottom: 1ex; +} + @keyframes fadeinout { 0% { opacity: 0 } 10% { opacity: 1 } diff --git a/options/index.html b/options/index.html index a476b4a6..f63b55f2 100644 --- a/options/index.html +++ b/options/index.html @@ -79,7 +79,10 @@

      -
    1. +
    2. +

      +

      +
    3. Date: Thu, 27 Apr 2017 20:31:42 +0300 Subject: [PATCH 222/235] remove health.js --- health.js | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 health.js diff --git a/health.js b/health.js deleted file mode 100644 index ac1dc96c..00000000 --- a/health.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -setTimeout(healthCheck, 0); - -function healthCheck() { - chrome.runtime.sendMessage({method: 'healthCheck'}, ok => { - if (ok === undefined) { - // Chrome is starting up - healthCheck(); - } else if (!ok && confirm(t('dbError'))) { - window.open('http://userstyles.org/dberror'); - } - }); -} From 77ffd3004d8191f6ced424efbaf577b1024659b5 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 27 Apr 2017 22:12:32 +0300 Subject: [PATCH 223/235] fixup 3dc93436: immediately render a chunk if ScrollY>0 --- manage.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/manage.js b/manage.js index cc298d10..20d97a03 100644 --- a/manage.js +++ b/manage.js @@ -104,9 +104,10 @@ function showStyles(styles = []) { .map(style => ({name: style.name.toLocaleLowerCase(), style})) .sort((a, b) => (a.name < b.name ? -1 : a.name == b.name ? 0 : 1)); let index = 0; - const shouldRenderAll = (history.state || {}).scrollY > window.innerHeight; + const scrollY = (history.state || {}).scrollY; + const shouldRenderAll = scrollY > window.innerHeight; const renderBin = document.createDocumentFragment(); - if (shouldRenderAll) { + if (scrollY) { renderStyles(); } else { requestAnimationFrame(renderStyles); From 33fa5693ed0df8bdd5bce2443bad0ad8c133ffe7 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 28 Apr 2017 14:05:25 +0300 Subject: [PATCH 224/235] make sure style.id is not 0 in saveStyle --- storage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage.js b/storage.js index 0959c640..194b300d 100644 --- a/storage.js +++ b/storage.js @@ -226,7 +226,7 @@ function filterStylesInternal({ function saveStyle(style) { - const id = Number(style.id) >= 0 ? Number(style.id) : null; + const id = Number(style.id) || null; const reason = style.reason; const notify = style.notify !== false; const styleDigest = style.styleDigest; From 9740144e63c2b3e135ec0a5c52c8a6c800a593b0 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 28 Apr 2017 19:44:03 +0300 Subject: [PATCH 225/235] Add an empty line before check-all in update log --- update.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/update.js b/update.js index f71ed5e0..cf2e0a97 100644 --- a/update.js +++ b/update.js @@ -21,10 +21,12 @@ var updater = { lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(), checkAllStyles({observer = () => {}, save = true, ignoreDigest} = {}) { + updater.checkAllStyles.running = true; updater.resetInterval(); return getStyles({}).then(styles => { styles = styles.filter(style => style.updateUrl); observer(updater.COUNT, styles.length); + updater.log(''); updater.log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`); return Promise.all( styles.map(style => @@ -32,6 +34,7 @@ var updater = { }).then(() => { observer(updater.DONE); updater.log(''); + updater.checkAllStyles.running = false; }); }, @@ -133,11 +136,17 @@ var updater = { let lastWriteTime = 0; return text => { queue.push({text, time: new Date().toLocaleString()}); - debounce(flushQueue, 1e3); + debounce(flushQueue, text && updater.checkAllStyles.running ? 1e3 : 0); }; function flushQueue() { chromeLocal.getValue('updateLog').then((lines = []) => { const time = Date.now() - lastWriteTime > 11e3 ? queue[0].time + ' ' : ''; + if (!queue[0].text) { + queue.shift(); + if (lines[lines.length - 1]) { + lines.push(''); + } + } lines.splice(0, lines.length - 1000); lines.push(time + queue[0].text); lines.push(...queue.slice(1).map(item => item.text)); From 93695da69aab403fb9464168901a9bddf39df2b2 Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 29 Apr 2017 00:04:01 +0300 Subject: [PATCH 226/235] code cosmetics --- update.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/update.js b/update.js index cf2e0a97..5a6e03fb 100644 --- a/update.js +++ b/update.js @@ -21,8 +21,8 @@ var updater = { lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(), checkAllStyles({observer = () => {}, save = true, ignoreDigest} = {}) { - updater.checkAllStyles.running = true; updater.resetInterval(); + updater.checkAllStyles.running = true; return getStyles({}).then(styles => { styles = styles.filter(style => style.updateUrl); observer(updater.COUNT, styles.length); @@ -55,9 +55,9 @@ var updater = { 'ignoreDigest' option is set on the second manual individual update check on the manage page. */ return getStyleDigests(style) - .then(fetchMd5IfNotEdited) - .then(fetchCodeIfMd5Changed) - .then(saveIfUpdated) + .then(maybeFetchMd5) + .then(maybeFetchCode) + .then(maybeSave) .then(saved => { observer(updater.UPDATED, saved); updater.log(updater.UPDATED + ` #${saved.id} ${saved.name}`); @@ -68,7 +68,7 @@ var updater = { updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`); }); - function fetchMd5IfNotEdited([originalDigest, current]) { + function maybeFetchMd5([originalDigest, current]) { hasDigest = Boolean(originalDigest); if (hasDigest && !ignoreDigest && originalDigest != current) { return Promise.reject(updater.EDITED); @@ -76,7 +76,7 @@ var updater = { return download(style.md5Url); } - function fetchCodeIfMd5Changed(md5) { + function maybeFetchCode(md5) { if (!md5 || md5.length != 32) { return Promise.reject(updater.ERROR_MD5); } @@ -86,7 +86,7 @@ var updater = { return download(style.updateUrl); } - function saveIfUpdated(text) { + function maybeSave(text) { const json = tryJSONparse(text); if (!styleJSONseemsValid(json)) { return Promise.reject(updater.ERROR_JSON); From fbc3ac0070f7cd79ed0f2114eb89bb43056bec64 Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 29 Apr 2017 00:54:37 +0300 Subject: [PATCH 227/235] match-highlighter: do nothing if token is same --- .../addon/search/match-highlighter.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/codemirror-overwrites/addon/search/match-highlighter.js b/codemirror-overwrites/addon/search/match-highlighter.js index a3de4c03..f1b49d92 100644 --- a/codemirror-overwrites/addon/search/match-highlighter.js +++ b/codemirror-overwrites/addon/search/match-highlighter.js @@ -119,16 +119,24 @@ function highlightMatches(cm) { cm.operation(function() { var state = cm.state.matchHighlighter; - removeOverlay(cm); if (!cm.somethingSelected() && state.options.showToken) { var re = state.options.showToken === true ? /[\w$]/ : state.options.showToken; var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start; while (start && re.test(line.charAt(start - 1))) --start; while (end < line.length && re.test(line.charAt(end))) ++end; - if (start < end) - addOverlay(cm, line.slice(start, end), re, state.options.style); + /* STYLUS: hack start */ + const token = line.slice(start, end); + if (token !== state.lastToken) { + state.lastToken = token; + removeOverlay(cm); + if (token) { + addOverlay(cm, token, re, state.options.style); + } + } return; } + removeOverlay(cm); + /* STYLUS: hack end */ var from = cm.getCursor("from"), to = cm.getCursor("to"); if (from.line != to.line) return; if (state.options.wordsOnly && !isWord(cm, from, to)) return; From 6d65d2a2b60d02a9667a3ba8f77f5ad097ebbf96 Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 29 Apr 2017 02:36:10 +0300 Subject: [PATCH 228/235] Expose iframes via HTML[stylus-iframe] * convert actions to buttons --- _locales/en/messages.json | 17 +++++++++++---- apply.js | 24 +++++++++++++++++++- options/index.css | 17 +++++++++++++-- options/index.html | 46 ++++++++++++++++++++++++--------------- prefs.js | 6 ++++- storage.js | 13 ++++++----- 6 files changed, 92 insertions(+), 31 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 8cd0ccd7..f6f4ff42 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -655,7 +655,7 @@ "message": "Background color when disabled" }, "optionsPopupWidth": { - "message": "Width (in pixels)" + "message": "Popup width (in pixels)" }, "optionsUpdateInterval": { "message": "Automatically check for and install all available userstyle updates (in hrs)" @@ -675,6 +675,15 @@ "optionsCustomizeUpdate": { "message": "Updates" }, + "optionsAdvanced": { + "message": "Advanced" + }, + "optionsAdvancedExposeIframes": { + "message": "Expose iframes via HTML[stylus-iframe]" + }, + "optionsAdvancedExposeIframesNote": { + "message": "Enables writing iframe-specific CSS like 'html[stylus-iframe] h1 { display:none }'" + }, "optionsActions": { "message": "Actions" }, @@ -682,10 +691,10 @@ "message": "Reset the options to default values" }, "optionsResetButton": { - "message": "Reset" + "message": "Reset options" }, "optionsOpenManager": { - "message": "Open styles manager" + "message": "Manage styles" }, "optionsOpenManagerNote": { "message": "Define a keyboard shortcut" @@ -697,6 +706,6 @@ "message": "Open" }, "optionsCheck": { - "message": "Check" + "message": "Update styles" } } diff --git a/apply.js b/apply.js index a2f523b2..6fc05cbe 100644 --- a/apply.js +++ b/apply.js @@ -7,6 +7,7 @@ var ID_PREFIX = 'stylus-'; var ROOT = document.documentElement; var isOwnPage = location.href.startsWith('chrome-extension:'); var disableAll = false; +var exposeIframes = false; var styleElements = new Map(); var disabledElements = new Map(); var retiredStyleTimers = new Map(); @@ -94,6 +95,9 @@ function applyOnMessage(request, sender, sendResponse) { if ('disableAll' in request.prefs) { doDisableAll(request.prefs.disableAll); } + if ('exposeIframes' in request.prefs) { + doExposeIframes(request.prefs.exposeIframes); + } break; case 'ping': @@ -103,7 +107,7 @@ function applyOnMessage(request, sender, sendResponse) { } -function doDisableAll(disable) { +function doDisableAll(disable = disableAll) { if (!disable === !disableAll) { return; } @@ -117,6 +121,20 @@ function doDisableAll(disable) { } +function doExposeIframes(state = exposeIframes) { + if (state === exposeIframes || window == parent) { + return; + } + exposeIframes = state; + const attr = document.documentElement.getAttribute('stylus-iframe'); + if (state && attr != '') { + document.documentElement.setAttribute('stylus-iframe', ''); + } else if (!state && attr == '') { + document.documentElement.removeAttribute('stylus-iframe'); + } +} + + function applyStyleState({id, enabled}) { const inCache = disabledElements.get(id) || styleElements.get(id); const inDoc = document.getElementById(ID_PREFIX + id); @@ -169,6 +187,10 @@ function applyStyles(styles) { doDisableAll(styles.disableAll); delete styles.disableAll; } + if ('exposeIframes' in styles) { + doExposeIframes(styles.exposeIframes); + delete styles.exposeIframes; + } if (document.head && document.head.firstChild && document.head.firstChild.id == 'xml-viewer-style') { diff --git a/options/index.css b/options/index.css index bd649bbc..919d8541 100644 --- a/options/index.css +++ b/options/index.css @@ -37,7 +37,7 @@ body { .block:last-child { border-bottom: none; - padding-bottom: 4px; + padding-bottom: 0; } h1 { @@ -104,6 +104,14 @@ input[type="color"] { height: 2em; } +#actions { + justify-content: space-around; +} + +#actions button { + width: auto; +} + [data-cmd="check-updates"] button { position: relative; } @@ -126,7 +134,6 @@ input[type="color"] { #updates-installed { position: absolute; font-size: 85%; - right: 16px; margin-top: 1px; } @@ -171,6 +178,12 @@ input[type="color"] { margin-bottom: 1ex; } +sup { + vertical-align: baseline; + position: relative; + top: -0.4em; +} + @keyframes fadeinout { 0% { opacity: 0 } 10% { opacity: 1 } diff --git a/options/index.html b/options/index.html index f63b55f2..95919c47 100644 --- a/options/index.html +++ b/options/index.html @@ -12,9 +12,17 @@
      +

      + -
      +

      @@ -48,6 +50,7 @@
      +

      @@ -57,21 +60,27 @@
      +
      -

      +

      - - +
      +
      + +
      + + +
      +
      @@ -83,6 +92,7 @@

    4. +
    5. Date: Sat, 29 Apr 2017 19:27:42 +0300 Subject: [PATCH 229/235] optionsUI: make "Shortcuts" a button; unify translations --- _locales/en/messages.json | 18 +++++++----------- manage.html | 4 +++- options/index.html | 10 ++++------ options/index.js | 2 +- popup.html | 4 +++- 5 files changed, 18 insertions(+), 20 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f6f4ff42..71a79aa3 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -389,14 +389,6 @@ "message": "Options", "description": "Go to Options UI" }, - "openOptionsShortcuts": { - "message": "Shortcuts", - "description": "Go to shortcut configuration" - }, - "openShortcutsPopup": { - "message": "Shortcuts", - "description": "Go to shortcut configuration" - }, "optionsHeading": { "message": "Options", "description": "Heading for options section on manage page." @@ -453,6 +445,13 @@ "message": "Remove section", "description": "Label for the button to remove a section" }, + "shortcuts": { + "message": "Shortcuts", + "description": "Go to shortcut configuration" + }, + "shortcutsNote": { + "message": "Define keyboard shortcuts" + }, "styleBadRegexp": { "message": "Regexp is invalid.", "description": "Validation message for a bad regexp in a style" @@ -696,9 +695,6 @@ "optionsOpenManager": { "message": "Manage styles" }, - "optionsOpenManagerNote": { - "message": "Define a keyboard shortcut" - }, "optionsCheckUpdate": { "message": "Check for and install all available updates" }, diff --git a/manage.html b/manage.html index d9d95ec2..eb405e0a 100644 --- a/manage.html +++ b/manage.html @@ -195,7 +195,9 @@

    - + - +

    + + +
    @@ -93,11 +96,6 @@

  • -
  • - -
  • diff --git a/options/index.js b/options/index.js index 886badb5..f9afb81e 100644 --- a/options/index.js +++ b/options/index.js @@ -25,7 +25,7 @@ document.onclick = e => { break; case 'open-keyboard': - openURL({url: e.target.href}); + openURL({url: target.closest('a').href}); e.preventDefault(); break; diff --git a/popup.html b/popup.html index 9ad62b9a..7684eb05 100644 --- a/popup.html +++ b/popup.html @@ -100,7 +100,9 @@ From ee86ef303701b695d557f516adbc8a1c97787ee4 Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 29 Apr 2017 19:54:16 +0300 Subject: [PATCH 230/235] optionsUI: add 'editor.contextDelete' --- _locales/en/messages.json | 3 ++ background.js | 59 +++++++++++++++++++++------------------ edit.js | 4 +-- options/index.html | 7 +++++ prefs.js | 12 ++++++++ 5 files changed, 56 insertions(+), 29 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 71a79aa3..32fc1343 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -683,6 +683,9 @@ "optionsAdvancedExposeIframesNote": { "message": "Enables writing iframe-specific CSS like 'html[stylus-iframe] h1 { display:none }'" }, + "optionsAdvancedContextDelete": { + "message": "Add 'Delete' in editor context menu" + }, "optionsActions": { "message": "Actions" }, diff --git a/background.js b/background.js index e0367f3e..2f70e159 100644 --- a/background.js +++ b/background.js @@ -99,16 +99,10 @@ contextMenus = Object.assign({ title: 'openStylesManager', click: browserCommands.openManage, }, -}, - // detect browsers without Delete by looking at the end of UA string - /Vivaldi\/[\d.]+$/.test(navigator.userAgent) || - // Chrome and co. - /Safari\/[\d.]+$/.test(navigator.userAgent) && - // skip forks with Flash as those are likely to have the menu e.g. CentBrowser - !Array.from(navigator.plugins).some(p => p.name == 'Shockwave Flash') -&& { - 'editDeleteText': { +}, prefs.get('editor.contextDelete') && { + 'editor.contextDelete': { title: 'editDeleteText', + type: 'normal', contexts: ['editable'], documentUrlPatterns: [URLS.ownOrigin + 'edit*'], click: (info, tab) => { @@ -117,26 +111,37 @@ contextMenus = Object.assign({ } }); -for (const id of Object.keys(contextMenus)) { - const item = Object.assign({id}, contextMenus[id]); - const prefValue = prefs.readOnlyValues[id]; - const isBoolean = typeof prefValue == 'boolean'; - item.title = chrome.i18n.getMessage(item.title); - if (isBoolean) { - item.type = 'checkbox'; - item.checked = prefValue; - } - if (!item.contexts) { - item.contexts = ['browser_action']; - } - delete item.click; - chrome.contextMenus.create(item, ignoreChromeError); +{ + const createContextMenus = (ids = Object.keys(contextMenus)) => { + for (const id of ids) { + const item = Object.assign({id}, contextMenus[id]); + const prefValue = prefs.readOnlyValues[id]; + item.title = chrome.i18n.getMessage(item.title); + if (!item.type && typeof prefValue == 'boolean') { + item.type = 'checkbox'; + item.checked = prefValue; + } + if (!item.contexts) { + item.contexts = ['browser_action']; + } + delete item.click; + chrome.contextMenus.create(item, ignoreChromeError); + } + }; + createContextMenus(); + prefs.subscribe((id, checked) => { + if (id == 'editor.contextDelete') { + if (checked) { + createContextMenus([id]); + } else { + chrome.contextMenus.remove(id, ignoreChromeError); + } + } else { + chrome.contextMenus.update(id, {checked}, ignoreChromeError); + } + }, Object.keys(contextMenus).filter(key => typeof prefs.readOnlyValues[key] == 'boolean')); } -prefs.subscribe((id, checked) => { - chrome.contextMenus.update(id, {checked}, ignoreChromeError); -}, Object.keys(contextMenus)); - // ************************************************************************* // [re]inject content scripts { diff --git a/edit.js b/edit.js index 451e45a0..b9575c18 100644 --- a/edit.js +++ b/edit.js @@ -1234,8 +1234,8 @@ function initHooks() { function toggleContextMenuDelete(event) { - if (event.button == 2) { - chrome.contextMenus.update('editDeleteText', { + if (event.button == 2 && prefs.get('editor.contextDelete')) { + chrome.contextMenus.update('editor.contextDelete', { enabled: Boolean( this.selectionStart != this.selectionEnd || this.somethingSelected && this.somethingSelected() diff --git a/options/index.html b/options/index.html index 37be4bba..ff31c16b 100644 --- a/options/index.html +++ b/options/index.html @@ -71,6 +71,13 @@ + diff --git a/prefs.js b/prefs.js index ef4a89cd..c632d1e9 100644 --- a/prefs.js +++ b/prefs.js @@ -44,6 +44,7 @@ var prefs = new function Prefs() { 'editor.matchHighlight': 'token', // token = token/word under cursor even if nothing is selected // selection = only when something is selected // '' (empty string) = disabled + 'editor.contextDelete': contextDeleteMissing(), // "Delete" item in context menu 'badgeDisabled': '#8B0000', // badge background color when disabled 'badgeNormal': '#006666', // badge background color @@ -301,6 +302,17 @@ var prefs = new function Prefs() { } return true; } + + function contextDeleteMissing() { + return ( + // detect browsers without Delete by looking at the end of UA string + /Vivaldi\/[\d.]+$/.test(navigator.userAgent) || + // Chrome and co. + /Safari\/[\d.]+$/.test(navigator.userAgent) && + // skip forks with Flash as those are likely to have the menu e.g. CentBrowser + !Array.from(navigator.plugins).some(p => p.name == 'Shockwave Flash') + ); + } }(); From cb79b3561ceab127c3e1c90fd4ea1cb8bf72f1d2 Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 29 Apr 2017 20:05:42 +0300 Subject: [PATCH 231/235] code cosmetics: simplify onoffswitch --- options/index.css | 14 +++++++------- options/index.html | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/options/index.css b/options/index.css index 919d8541..db445f10 100644 --- a/options/index.css +++ b/options/index.css @@ -201,11 +201,11 @@ sup { -ms-user-select: none; } -.onoffswitch-checkbox { +.onoffswitch input { display: none; } -.onoffswitch-label { +.onoffswitch span { display: block; overflow: hidden; cursor: pointer; @@ -218,7 +218,7 @@ sup { box-shadow: inset 2px 2px 4px rgba(0,0,0,0.1); } -.onoffswitch-label:before { +.onoffswitch span:before { content: ""; display: block; width: 18px; @@ -232,19 +232,19 @@ sup { box-shadow: 0 3px 13px 0 rgba(0, 0, 0, 0.4); } -.onoffswitch-checkbox:checked + .onoffswitch-label { +.onoffswitch input:checked + span { background-color: #CAEBE3; } -.onoffswitch-checkbox:checked + .onoffswitch-label, .onoffswitch-checkbox:checked + .onoffswitch-label:before { +.onoffswitch input:checked + span, .onoffswitch input:checked + span:before { border-color: #CAEBE3; } -.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner { +.onoffswitch input:checked + span .onoffswitch-inner { margin-left: 0; } -.onoffswitch-checkbox:checked + .onoffswitch-label:before { +.onoffswitch input:checked + span:before { right: 0; background-color: #04BA9F; box-shadow: 3px 6px 18px 0 rgba(0, 0, 0, 0.2); diff --git a/options/index.html b/options/index.html index ff31c16b..75fd75b4 100644 --- a/options/index.html +++ b/options/index.html @@ -19,8 +19,8 @@