From b161d17bbfe4c2fb1fdaf84e9db1e0894a62c435 Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 3 Dec 2017 17:12:43 +0300 Subject: [PATCH 01/28] update zh_TW locale --- _locales/zh_TW/messages.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json index eb2bf237..0c6fd026 100644 --- a/_locales/zh_TW/messages.json +++ b/_locales/zh_TW/messages.json @@ -36,7 +36,7 @@ "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" }, "installButton": { - "message": "安裝", + "message": "安裝樣式", "description": "Label for install button" }, "styleMetaErrorCheckbox": { @@ -47,6 +47,10 @@ "message": "無效的 JSON 格式", "description": "Setting linter config with invalid JSON" }, + "popupHotkeysTooltip": { + "message": "點選以檢視可用的快捷鍵", + "description": "Tooltip displayed when hovering the right edge of the extension popup" + }, "optionsBadgeNormal": { "message": "背景顏色", "description": "" @@ -330,6 +334,10 @@ "message": "顯示生效的樣式數目", "description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text." }, + "popupHotkeysInfo": { + "message": "<1>-<9>、<0>,數字鍵盤也可以 — 切換第 N 個樣式(0 是切換到第 10 個)\n- 切換以該字母為名稱第一個字的樣式\n 開啟編輯器而非切換\n 啟用列出的樣式\n 停用列出的樣式\n 與 <`> (倒引號)— 切換初始啟用的樣式;不要在彈出式視窗開啟時套用到後來啟用的樣式,這樣您就可以在測試完後復原初始選擇:僅停用全部,然後切換。 \n更多資訊請見 wiki", + "description": "NOTE1: preserve < and > symbols so that is styled as a key.\nNOTE2: the last line is displayed as a text of the link to the wiki page.\nNOTE3: this is the list of hotkeys displayed after clicking the right edge of the extension popup." + }, "cm_lineWrapping": { "message": "自動換行", "description": "Label for the checkbox controlling word wrap option for the style editor." @@ -934,7 +942,7 @@ "description": "Label for the checkbox in the style editor." }, "installButtonReinstall": { - "message": "重新安裝", + "message": "重新安裝樣式", "description": "Label for reinstall button" }, "linterInvalidConfigError": { @@ -1036,7 +1044,7 @@ "description": "" }, "installButtonUpdate": { - "message": "更新", + "message": "更新樣式", "description": "Label for update button" }, "backupButtons": { @@ -1052,7 +1060,7 @@ "description": "Label for the button to go to the edit style page" }, "installButtonInstalled": { - "message": "已安裝", + "message": "樣式已安裝", "description": "Text displayed when the style is successfully installed" }, "author": { From 0f3ddb9c03beb6d0f3e371c8cb753da0faa414b9 Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 3 Dec 2017 17:13:59 +0300 Subject: [PATCH 02/28] bump version to 1.1.7.1 --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 3086cea3..d6b953ca 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "name": "Stylus", - "version": "1.1.7", + "version": "1.1.7.1", "minimum_chrome_version": "49", "description": "__MSG_description__", "homepage_url": "http://add0n.com/stylus.html", From f4677a7b7b43337e5b622cf47213272cc6b3d059 Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 3 Dec 2017 20:32:50 +0300 Subject: [PATCH 03/28] actually use the name when saving --- background/storage.js | 3 +++ edit/edit.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/background/storage.js b/background/storage.js index eee6a011..3d728473 100644 --- a/background/storage.js +++ b/background/storage.js @@ -238,6 +238,9 @@ function getStyles(options) { cachedStyles.byId.clear(); for (const style of cachedStyles.list) { cachedStyles.byId.set(style.id, style); + if (!style.name) { + style.name = 'ID: ' + style.id; + } } cachedStyles.mutex.inProgress = false; diff --git a/edit/edit.js b/edit/edit.js index 7b41edd0..78378c13 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -387,7 +387,7 @@ function save() { saveStyleSafe({ id: styleId, - name: name, + name: $('#name').value.trim(), enabled: $('#enabled').checked, reason: 'editSave', sections: getSectionsHashes() From c723ec58cee1e025406e7b4fe4b3aeab8f5e7f2e Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 3 Dec 2017 20:36:20 +0300 Subject: [PATCH 04/28] bump version to 1.1.7.2 --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index d6b953ca..cd2c8f13 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "name": "Stylus", - "version": "1.1.7.1", + "version": "1.1.7.2", "minimum_chrome_version": "49", "description": "__MSG_description__", "homepage_url": "http://add0n.com/stylus.html", From 489546e35c718d0307a10f5508416b90d7725807 Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 3 Dec 2017 18:49:26 +0300 Subject: [PATCH 05/28] use just one event listener per applies-to widget in usercss editor --- edit/applies-to-line-widget.js | 404 +++++++++++++++++---------------- edit/edit.css | 9 +- edit/regexp-tester.js | 10 +- 3 files changed, 218 insertions(+), 205 deletions(-) diff --git a/edit/applies-to-line-widget.js b/edit/applies-to-line-widget.js index 60161020..d23668bc 100644 --- a/edit/applies-to-line-widget.js +++ b/edit/applies-to-line-widget.js @@ -1,79 +1,12 @@ -/* global regExpTester debounce messageBox CodeMirror */ +/* global regExpTester debounce messageBox CodeMirror template */ 'use strict'; -function templateCache(cache) { - function clone(id) { - if (typeof cache[id] === 'function') { - cache[id] = cache[id](); - } - return cache[id].cloneNode(true); - } - return {clone}; -} - function createAppliesToLineWidget(cm) { - const APPLIES_TYPE = [ - [t('appliesUrlOption'), 'url'], - [t('appliesUrlPrefixOption'), 'url-prefix'], - [t('appliesDomainOption'), 'domain'], - [t('appliesRegexpOption'), 'regexp'] - ]; const THROTTLE_DELAY = 400; + let TPL, EVENTS, CLICK_ROUTE; let widgets = []; let fromLine, toLine, styleVariables; let initialized = false; - - const template = templateCache({ - container: () => - $element({className: 'applies-to', appendChild: [ - $element({tag: 'label', appendChild: t('appliesLabel')}), - $element({ - tag: 'ul', - className: 'applies-to-list' - }) - ]}), - listItem: () => - $element({tag: 'li', appendChild: [ - $element({ - tag: 'select', - className: 'applies-type', - appendChild: APPLIES_TYPE.map(([label, value]) => $element({ - tag: 'option', - value: value, - textContent: label - })) - }), - $element({ - tag: 'input', - className: 'applies-value' - }), - $element({ - tag: 'button', - type: 'button', - className: 'applies-to-regexp-test', - textContent: t('styleRegexpTestButton') - }), - $element({ - tag: 'button', - type: 'button', - className: 'applies-to-remove', - textContent: t('appliesRemove') - }), - $element({ - tag: 'button', - type: 'button', - className: 'applies-to-add', - textContent: t('appliesAdd') - }) - ]}), - appliesToEverything: () => - $element({ - tag: 'li', - className: 'applies-to-everything', - textContent: t('appliesToEverything') - }) - }); - return {toggle}; function toggle(newState = !initialized) { @@ -90,6 +23,147 @@ function createAppliesToLineWidget(cm) { function init() { initialized = true; + TPL = { + container: + $element({className: 'applies-to', appendChild: [ + $element({tag: 'label', appendChild: t('appliesLabel')}), + $element({tag: 'ul', className: 'applies-to-list'}), + ]}), + listItem: $element({ + tag: 'li', + className: 'applies-to-item', + appendChild: [ + $element({ + tag: 'select', + className: 'applies-type', + appendChild: [ + [t('appliesUrlOption'), 'url'], + [t('appliesUrlPrefixOption'), 'url-prefix'], + [t('appliesDomainOption'), 'domain'], + [t('appliesRegexpOption'), 'regexp'] + ].map(([textContent, value]) => $element({ + tag: 'option', + value, + textContent, + })), + }), + $element({ + tag: 'input', + className: 'applies-value', + }), + $element({ + tag: 'button', + className: 'test-regexp', + textContent: t('styleRegexpTestButton'), + }), + $element({ + tag: 'button', + className: 'remove-applies-to', + textContent: t('appliesRemove'), + }), + $element({ + tag: 'button', + className: 'add-applies-to', + textContent: t('appliesAdd'), + }) + ]}), + appliesToEverything: $element({ + tag: 'li', + className: 'applies-to-everything', + textContent: t('appliesToEverything'), + }), + }; + + CLICK_ROUTE = { + '.test-regexp': (item, apply) => { + regExpTester.toggle(); + regExpTester.update([apply.value.text]); + }, + + '.remove-applies-to': (item, apply) => { + const applies = item.closest('.applies-to').__applies; + const i = applies.indexOf(apply); + let repl; + let from; + let to; + if (applies.length < 2) { + messageBox({ + contents: t('appliesRemoveError'), + buttons: [t('confirmClose')] + }); + return; + } + if (i === 0) { + from = apply.mark.find().from; + to = applies[i + 1].mark.find().from; + repl = ''; + } else if (i === applies.length - 1) { + from = applies[i - 1].mark.find().to; + to = apply.mark.find().to; + repl = ''; + } else { + from = applies[i - 1].mark.find().to; + to = applies[i + 1].mark.find().from; + repl = ', '; + } + cm.replaceRange(repl, from, to, 'appliesTo'); + clearApply(apply); + item.remove(); + applies.splice(i, 1); + }, + + '.add-applies-to': (item, apply) => { + const applies = this.closest('.applies-to').__applies; + const i = applies.indexOf(apply); + const pos = apply.mark.find().to; + const text = `, ${apply.type.text}("")`; + cm.replaceRange(text, pos, pos, 'appliesTo'); + const newApply = createApply( + cm.indexFromPos(pos) + 2, + apply.type.text, + '', + true + ); + setupApplyMarkers(newApply); + applies.splice(i + 1, 0, newApply); + item.insertAdjacentElement('afterend', buildChildren(applies, newApply)); + }, + }; + + EVENTS = { + onchange({target}) { + const typeElement = target.closest('.applies-type'); + if (typeElement) { + const item = target.closest('.applies-to-item'); + const apply = item.__apply; + changeItem(apply, 'type', typeElement.value); + item.dataset.type = apply.type.text; + } + }, + oninput({target}) { + if (target.matches('.applies-value')) { + const apply = target.closest('.applies-to-item').__apply; + debounce(changeItem, THROTTLE_DELAY, apply, 'value', target.value); + } + }, + onfocus({target}) { + if (target.matches('.test-regexp')) { + const apply = target.closest('.applies-to-item').__apply; + updateRegexpTest(apply); + } + }, + onclick({target}) { + for (const selector in CLICK_ROUTE) { + const routed = target.closest(selector); + if (routed) { + const item = routed.closest('.applies-to-item'); + CLICK_ROUTE[selector].call(routed, item, item.__apply); + return; + } + } + } + }; + styleVariables = $element({tag: 'style'}); fromLine = 0; toLine = cm.doc.size; @@ -175,7 +249,7 @@ function createAppliesToLineWidget(cm) { inOp = true; cm.startOperation(); } - cm.operation(doUpdate); + doUpdate(); } if (inOp) { cm.endOperation(); @@ -245,26 +319,29 @@ function createAppliesToLineWidget(cm) { let i = 0; let itemHeight; for (const section of findAppliesTo(start, end)) { - while (removed[i] && removed[i].line.lineNo() < section.pos.line) { - clearWidget(removed[i++]); + let removedWidget = removed[i]; + while (removedWidget && removedWidget.line.lineNo() < section.pos.line) { + clearWidget(removed[i]); + removedWidget = removed[++i]; } for (const a of section.applies) { setupApplyMarkers(a, lineIndexes); } - if (removed[i] && removed[i].line.lineNo() === section.pos.line) { + if (removedWidget && removedWidget.line.lineNo() === section.pos.line) { // reuse old widget - removed[i].section.applies.forEach(apply => { + removedWidget.section.applies.forEach(apply => { apply.type.mark.clear(); apply.value.mark.clear(); }); - removed[i].section = section; + removedWidget.section = section; const newNode = buildElement(section); - if (removed[i].node.parentNode) { - removed[i].node.parentNode.replaceChild(newNode, removed[i].node); + const removedNode = removedWidget.node; + if (removedNode.parentNode) { + removedNode.parentNode.replaceChild(newNode, removedNode); } - removed[i].node = newNode; - removed[i].changed(); - yield removed[i]; + removedWidget.node = newNode; + removedWidget.changed(); + yield removedWidget; i++; continue; } @@ -337,128 +414,55 @@ function createAppliesToLineWidget(cm) { } function buildElement({applies}) { - const el = template.clone('container'); - const appliesToList = $('.applies-to-list', el); - applies.map(makeLi) - .forEach(item => appliesToList.appendChild(item)); - if (!appliesToList.childNodes.length) { - appliesToList.appendChild(template.clone('appliesToEverything')); + const container = TPL.container.cloneNode(true); + const list = $('.applies-to-list', container); + for (const apply of applies) { + list.appendChild(buildChildren(applies, apply)); } + if (!list.children[0]) { + list.appendChild(TPL.appliesToEverything.cloneNode(true)); + } + return Object.assign(container, EVENTS, {__applies: applies}); + } + + function buildChildren(applies, apply) { + const el = TPL.listItem.cloneNode(true); + el.dataset.type = apply.type.text; + el.__apply = apply; + $('.applies-type', el).value = apply.type.text; + $('.applies-value', el).value = apply.value.text; return el; + } - function makeLi(apply) { - const el = template.clone('listItem'); - el.dataset.type = apply.type.text; - el.addEventListener('change', e => { - if (e.target.classList.contains('applies-type')) { - el.dataset.type = apply.type.text; - } - }); + function changeItem(apply, part, newText) { + part = apply[part]; + const range = part.mark.find(); + part.mark.clear(); + cm.replaceRange(newText, range.from, range.to, 'appliesTo'); + part.mark = cm.markText( + range.from, + cm.findPosH(range.from, newText.length, 'char'), + {clearWhenEmpty: false} + ); + part.text = newText; - const typeInput = $('.applies-type', el); - typeInput.value = apply.type.text; - typeInput.onchange = function () { - applyChange(apply.type, this.value); - }; + if (part === apply.type) { + const range = apply.mark.find(); + apply.mark.clear(); + apply.mark = cm.markText( + part.mark.find().from, + range.to, + {clearWhenEmpty: false} + ); + } - const valueInput = $('.applies-value', el); - valueInput.value = apply.value.text; - valueInput.oninput = function () { - debounce(applyChange, THROTTLE_DELAY, apply.value, this.value); - }; - valueInput.onfocus = updateRegexpTest; + updateRegexpTest(apply); + } - const regexpTestButton = $('.applies-to-regexp-test', el); - regexpTestButton.onclick = () => { - regExpTester.toggle(); - regExpTester.update([apply.value.text]); - }; - - const removeButton = $('.applies-to-remove', el); - removeButton.onclick = function () { - const i = applies.indexOf(apply); - let repl; - let from; - let to; - if (applies.length < 2) { - messageBox({ - contents: chrome.i18n.getMessage('appliesRemoveError'), - buttons: [t('confirmClose')] - }); - return; - } - if (i === 0) { - from = apply.mark.find().from; - to = applies[i + 1].mark.find().from; - repl = ''; - } else if (i === applies.length - 1) { - from = applies[i - 1].mark.find().to; - to = apply.mark.find().to; - repl = ''; - } else { - from = applies[i - 1].mark.find().to; - to = applies[i + 1].mark.find().from; - repl = ', '; - } - cm.replaceRange(repl, from, to, 'appliesTo'); - clearApply(apply); - this.closest('li').remove(); - applies.splice(i, 1); - }; - - const addButton = $('.applies-to-add', el); - addButton.onclick = function () { - const i = applies.indexOf(apply); - const pos = apply.mark.find().to; - const text = `, ${apply.type.text}("")`; - cm.replaceRange(text, pos, pos, 'appliesTo'); - const newApply = createApply( - cm.indexFromPos(pos) + 2, - apply.type.text, - '', - true - ); - setupApplyMarkers(newApply); - applies.splice(i + 1, 0, newApply); - this.closest('li').insertAdjacentElement('afterend', makeLi(newApply)); - }; - - return el; - - function updateRegexpTest() { - if (apply.type.text === 'regexp') { - const re = apply.value.text.trim(); - if (re) { - regExpTester.update([re]); - } else { - regExpTester.update([]); - } - } - } - - function applyChange(input, newText) { - const range = input.mark.find(); - input.mark.clear(); - cm.replaceRange(newText, range.from, range.to, 'appliesTo'); - input.mark = cm.markText( - range.from, - cm.findPosH(range.from, newText.length, 'char'), - {clearWhenEmpty: false} - ); - input.text = newText; - - if (input === apply.type) { - const range = apply.mark.find(); - apply.mark.clear(); - apply.mark = cm.markText( - input.mark.find().from, - range.to, - {clearWhenEmpty: false} - ); - } - - updateRegexpTest(); - } + function updateRegexpTest(apply) { + if (apply.type.text === 'regexp') { + const rx = apply.value.text.trim(); + regExpTester.update(rx ? [rx] : {}); } } @@ -487,8 +491,12 @@ function createAppliesToLineWidget(cm) { function *findAppliesTo(posStart, posEnd) { const text = cm.getValue(); - const re = /^[\t ]*@-moz-document\s+/mg; - const applyRe = /(url|url-prefix|domain|regexp)\(((['"])(?:\\\\|\\\n|\\\3|[^\n])*?\3|[^)\n]*)\)[\s,]*/iyg; + const re = /^[\t ]*@-moz-document[\s\n]+/gm; + const applyRe = new RegExp([ + /(?:\/\*[^*]*\*\/[\s\n]*)*/, + /(url|url-prefix|domain|regexp)/, + /\(((['"])(?:\\\\|\\\n|\\\3|[^\n])*?\3|[^)\n]*)\)\s*(,\s*)?/, + ].map(rx => rx.source).join(''), 'giy'); let match; re.lastIndex = posStart; while ((match = re.exec(text))) { diff --git a/edit/edit.css b/edit/edit.css index e6da6d60..299b1ec4 100644 --- a/edit/edit.css +++ b/edit/edit.css @@ -331,6 +331,7 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar .test-regexp { display: none; } +.single-editor .test-regexp, .has-regexp .test-regexp { display: inline-block; } @@ -610,11 +611,15 @@ html:not(.usercss) .usercss-only, margin-top: 0.35rem; } -.CodeMirror-linewidget .applies-to li:not([data-type="regexp"]) .applies-to-regexp-test { +.CodeMirror-linewidget .applies-to li:not([data-type="regexp"]) .test-regexp { display: none; } -.CodeMirror-linewidget li.applies-to-everything { +.CodeMirror-linewidget li .add-applies-to { + visibility: visible; +} + +.CodeMirror-linewidget .applies-to-everything { margin-top: 0.2rem; } diff --git a/edit/regexp-tester.js b/edit/regexp-tester.js index e578bcaf..80ad7a70 100644 --- a/edit/regexp-tester.js +++ b/edit/regexp-tester.js @@ -25,17 +25,17 @@ var regExpTester = (() => { } } - function isShowed() { + function isShown() { return Boolean($('.regexp-report')); } - function toggle(state = !isShowed()) { - if (state && !isShowed()) { + function toggle(state = !isShown()) { + if (state && !isShown()) { if (!isInit) { init(); } showHelp('', $element({className: 'regexp-report'})); - } else if (!state && isShowed()) { + } else if (!state && isShown()) { if (isInit) { uninit(); } @@ -45,7 +45,7 @@ var regExpTester = (() => { } function update(newRegexps) { - if (!isShowed()) { + if (!isShown()) { if (isInit) { uninit(); } From e905e4e07906ecfe22eb8e6d6d7d3ceae911fe4a Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 3 Dec 2017 21:34:01 +0300 Subject: [PATCH 06/28] properly escape/unescape regexp in applies-to widgets --- _locales/en/messages.json | 4 ++++ edit/applies-to-line-widget.js | 8 +++++++- edit/edit.css | 6 ++++++ edit/regexp-tester.js | 8 +++++++- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e7f5877e..697c870e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -742,6 +742,10 @@ "message": "Invalid regexps skipped", "description": "RegExp test report: label for the invalid expressions" }, + "styleRegexpTestNote": { + "message": "Note: use a single \\ for escaping in the regexp input field, which will be automatically converted to \\\\ in the style code as per specification for quoted strings in CSS.", + "description": "RegExp test report: a note displayed at the bottom of the dialog" + }, "styleRegexpPartialExplanation": { "message": "This style uses partially matching regexps in violation of CSS4 @document specification which requires a full URL match. The affected CSS sections were not applied to the page. This style was probably created in Stylish-for-Chrome which incorrectly checks 'regexp()' rules since the very first version (known bug)." }, diff --git a/edit/applies-to-line-widget.js b/edit/applies-to-line-widget.js index d23668bc..34340626 100644 --- a/edit/applies-to-line-widget.js +++ b/edit/applies-to-line-widget.js @@ -435,9 +435,13 @@ function createAppliesToLineWidget(cm) { } function changeItem(apply, part, newText) { + if (!apply) { + return; + } part = apply[part]; const range = part.mark.find(); part.mark.clear(); + newText = newText.replace(/\\/g, '\\\\'); cm.replaceRange(newText, range.from, range.to, 'appliesTo'); part.mark = cm.markText( range.from, @@ -467,12 +471,14 @@ function createAppliesToLineWidget(cm) { } function createApply(pos, typeText, valueText, isQuoted = false) { + typeText = typeText.toLowerCase(); const start = pos; const typeStart = start; const typeEnd = typeStart + typeText.length; const valueStart = typeEnd + 1 + Number(isQuoted); const valueEnd = valueStart + valueText.length; const end = valueEnd + Number(isQuoted) + 1; + const hasSingleEscapes = /([^\\]|^)\\([^\\]|$)/.test(valueText); return { start, type: { @@ -481,7 +487,7 @@ function createAppliesToLineWidget(cm) { end: typeEnd, }, value: { - text: valueText, + text: hasSingleEscapes ? valueText : valueText.replace(/\\\\/g, '\\'), start: valueStart, end: valueEnd, }, diff --git a/edit/edit.css b/edit/edit.css index 299b1ec4..4a892adb 100644 --- a/edit/edit.css +++ b/edit/edit.css @@ -385,6 +385,12 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar margin-left: -20px; margin-top: -1px; } +.regexp-report-note { + font-size: 90%; + margin: 1em 1em .5em; + color: #999; +} + /************ help popup ************/ #help-popup { top: 3rem; diff --git a/edit/regexp-tester.js b/edit/regexp-tester.js index 80ad7a70..a11527c1 100644 --- a/edit/regexp-tester.js +++ b/edit/regexp-tester.js @@ -165,9 +165,15 @@ var regExpTester = (() => { } } } + report.appendChild($element({ + tag: 'p', + className: 'regexp-report-note', + appendChild: t('styleRegexpTestNote').split(/(\\+)/) + .map(s => s.startsWith('\\') ? $element({tag: 'code', textContent: s}) : s), + })); showHelp(t('styleRegexpTestTitle'), report); - $('.regexp-report').onclick = event => { + report.onclick = event => { const target = event.target.closest('a, .regexp-report div'); if (target) { openURL({ From 6e142a7444063ae278a9b730456e86fb118922d8 Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 3 Dec 2017 23:29:36 +0300 Subject: [PATCH 07/28] install-usercss: show error position and source fixes #276 --- install-usercss/install-usercss.css | 1 + install-usercss/install-usercss.js | 49 +++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/install-usercss/install-usercss.css b/install-usercss/install-usercss.css index e0561892..db94718b 100644 --- a/install-usercss/install-usercss.css +++ b/install-usercss/install-usercss.css @@ -79,6 +79,7 @@ input:disabled + span { font-weight: bold; font-size: 125%; margin-bottom: 1em; + white-space: pre-wrap; } .warning pre { diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js index baecd3e6..c3c98833 100644 --- a/install-usercss/install-usercss.js +++ b/install-usercss/install-usercss.js @@ -220,8 +220,8 @@ function initSourceCode(sourceCode) { cm.setValue(sourceCode); cm.refresh(); - sendMessage({method: 'buildUsercss', sourceCode, checkDup: true}) - .then(init) + BG.usercssHelper.build(BG.deepCopy({sourceCode, checkDup: true})) + .then(r => init(deepCopy(r))) .catch(err => { $('.header').classList.add('meta-init-error'); showError(err); @@ -229,12 +229,48 @@ } function buildWarning(err) { + const contents = Array.isArray(err) ? + $element({tag: 'pre', textContent: err.join('\n')}) : + [err && err.message || err || 'Unknown error']; + if (Number.isInteger(err.index)) { + const pos = cm.posFromIndex(err.index); + contents[0] = `${pos.line + 1}:${pos.ch + 1} ` + contents[0]; + contents.push($element({ + tag: 'pre', + textContent: drawLinePointer(pos) + })); + setTimeout(() => { + cm.scrollIntoView({line: pos.line + 1, ch: pos.ch}, window.innerHeight / 4); + cm.setCursor(pos.line, pos.ch + 1); + cm.focus(); + }); + } return $element({className: 'warning', appendChild: [ t('parseUsercssError'), - $element({tag: 'pre', textContent: String(err)}) + '\n', + ...contents, ]}); } + function drawLinePointer(pos) { + const SIZE = 60; + const line = cm.getLine(pos.line); + const numTabs = pos.ch + 1 - line.slice(0, pos.ch + 1).replace(/\t/g, '').length; + const pointer = ' '.repeat(pos.ch) + '^'; + const start = Math.max(Math.min(pos.ch - SIZE / 2, line.length - SIZE), 0); + const end = Math.min(Math.max(pos.ch + SIZE / 2, SIZE), line.length); + const leftPad = start !== 0 ? '...' : ''; + const rightPad = end !== line.length ? '...' : ''; + return ( + leftPad + + line.slice(start, end).replace(/\t/g, ' '.repeat(cm.options.tabSize)) + + rightPad + + '\n' + + ' '.repeat(leftPad.length + numTabs * cm.options.tabSize) + + pointer.slice(start, end) + ); + } + function init({style, dup}) { const data = style.usercssData; const dupData = dup && dup.usercssData; @@ -258,9 +294,10 @@ data.version, ])) ).then(ok => ok && - sendMessage(Object.assign(style, {method: 'saveUsercss', reason: 'update'})) - .then(install) - .catch(err => messageBox.alert(t('styleInstallFailed', err)))); + BG.usercssHelper.save(BG.deepCopy(Object.assign(style, {reason: 'update'}))) + .then(r => install(deepCopy(r))) + .catch(err => messageBox.alert(t('styleInstallFailed', err))) + ); }; // set updateUrl From c0c60fb7a2ae27964bed389dd980236ef0359963 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 4 Dec 2017 00:12:09 +0300 Subject: [PATCH 08/28] add a terse invocation syntax for $element and rename it to $create --- .eslintrc | 5 +- edit/applies-to-line-widget.js | 68 +++++---------- edit/codemirror-editing-hooks.js | 15 +--- edit/colorpicker-helper.js | 3 +- edit/edit.js | 18 ++-- edit/lint.js | 134 +++++++++-------------------- edit/regexp-tester.js | 50 +++++------ edit/show-keymap-help.js | 2 +- edit/source-editor.js | 13 +-- install-usercss/install-usercss.js | 74 ++++++---------- js/dom.js | 87 +++++++++++++++---- manage/config-dialog.js | 88 ++++++++----------- manage/import-export.js | 24 +++--- manage/incremental-search.js | 3 +- manage/manage.js | 4 +- manage/updater-ui.js | 5 +- msgbox/msgbox.js | 40 ++++----- popup/hotkeys.js | 8 +- 18 files changed, 275 insertions(+), 366 deletions(-) diff --git a/.eslintrc b/.eslintrc index 3d844c56..6eb52b33 100644 --- a/.eslintrc +++ b/.eslintrc @@ -55,7 +55,8 @@ globals: animateElement: false $: false $$: false - $element: false + $create: false + $createLink: false # prefs.js prefs: false setupLivePrefs: false @@ -236,7 +237,7 @@ rules: one-var: [0] operator-assignment: [2, always] operator-linebreak: [2, after, overrides: {"?": ignore, ":": ignore, "&&": ignore, "||": ignore}] - padded-blocks: [2, never] + padded-blocks: [0] prefer-numeric-literals: [2] prefer-rest-params: [0] prefer-const: [1, {destructuring: any, ignoreReadBeforeAssign: true}] diff --git a/edit/applies-to-line-widget.js b/edit/applies-to-line-widget.js index 34340626..f3687213 100644 --- a/edit/applies-to-line-widget.js +++ b/edit/applies-to-line-widget.js @@ -25,53 +25,25 @@ function createAppliesToLineWidget(cm) { TPL = { container: - $element({className: 'applies-to', appendChild: [ - $element({tag: 'label', appendChild: t('appliesLabel')}), - $element({tag: 'ul', className: 'applies-to-list'}), - ]}), - listItem: $element({ - tag: 'li', - className: 'applies-to-item', - appendChild: [ - $element({ - tag: 'select', - className: 'applies-type', - appendChild: [ - [t('appliesUrlOption'), 'url'], - [t('appliesUrlPrefixOption'), 'url-prefix'], - [t('appliesDomainOption'), 'domain'], - [t('appliesRegexpOption'), 'regexp'] - ].map(([textContent, value]) => $element({ - tag: 'option', - value, - textContent, - })), - }), - $element({ - tag: 'input', - className: 'applies-value', - }), - $element({ - tag: 'button', - className: 'test-regexp', - textContent: t('styleRegexpTestButton'), - }), - $element({ - tag: 'button', - className: 'remove-applies-to', - textContent: t('appliesRemove'), - }), - $element({ - tag: 'button', - className: 'add-applies-to', - textContent: t('appliesAdd'), - }) - ]}), - appliesToEverything: $element({ - tag: 'li', - className: 'applies-to-everything', - textContent: t('appliesToEverything'), - }), + $create('div.applies-to', [ + $create('label', t('appliesLabel')), + $create('ul.applies-to-list'), + ]), + listItem: + $create('li.applies-to-item', [ + $create('select.applies-type', [ + $create('option', {value: 'url'}, t('appliesUrlOption')), + $create('option', {value: 'url-prefix'}, t('appliesUrlPrefixOption')), + $create('option', {value: 'domain'}, t('appliesDomainOption')), + $create('option', {value: 'regexp'}, t('appliesRegexpOption')), + ]), + $create('input.applies-value'), + $create('button.test-regexp', t('styleRegexpTestButton')), + $create('button.remove-applies-to', t('appliesRemove')), + $create('button.add-applies-to', t('appliesAdd')), + ]), + appliesToEverything: + $create('li.applies-to-everything', t('appliesToEverything')), }; CLICK_ROUTE = { @@ -164,7 +136,7 @@ function createAppliesToLineWidget(cm) { } }; - styleVariables = $element({tag: 'style'}); + styleVariables = $create('style'); fromLine = 0; toLine = cm.doc.size; diff --git a/edit/codemirror-editing-hooks.js b/edit/codemirror-editing-hooks.js index a47b2e12..8071d7d2 100644 --- a/edit/codemirror-editing-hooks.js +++ b/edit/codemirror-editing-hooks.js @@ -115,10 +115,8 @@ onDOMready().then(() => { if (option.type === 'checkbox') { option = (option.labels || [])[0] || option.nextElementSibling || option; } - progress = document.body.appendChild($element({ - className: 'set-option-progress', - targetElement: option, - })); + progress = document.body.appendChild( + $create('.set-option-progress', {targetElement: option})); } } if (progress) { @@ -222,12 +220,7 @@ onDOMready().then(() => { break; } // avoid flicker: wait for the second stylesheet to load, then apply the theme - document.head.appendChild($element({ - tag: 'link', - id: 'cm-theme2', - rel: 'stylesheet', - href: url - })); + document.head.appendChild($create('link#cm-theme2', {rel: 'stylesheet', href: url})); setTimeout(() => { CodeMirror.setOption(option, value); themeLink.remove(); @@ -287,7 +280,7 @@ onDOMready().then(() => { function optionsFromArray(parent, options) { const fragment = document.createDocumentFragment(); for (const opt of options) { - fragment.appendChild($element({tag: 'option', textContent: opt})); + fragment.appendChild($create('option', opt)); } parent.appendChild(fragment); } diff --git a/edit/colorpicker-helper.js b/edit/colorpicker-helper.js index 51d95e3b..ef53452a 100644 --- a/edit/colorpicker-helper.js +++ b/edit/colorpicker-helper.js @@ -69,8 +69,7 @@ var initColorpicker = () => { } function configureColorpicker() { - const input = $element({ - tag: 'input', + const input = $create('input', { type: 'search', spellcheck: false, value: prefs.get('editor.colorpicker.hotkey'), diff --git a/edit/edit.js b/edit/edit.js index 78378c13..492d8958 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -457,22 +457,20 @@ function toMozillaFormat() { function fromMozillaFormat() { const popup = showCodeMirrorPopup(t('styleFromMozillaFormatPrompt'), - $element({appendChild: [ - $element({ - tag: 'button', + $create([ + $create('button', { name: 'import-append', textContent: t('importAppendLabel'), title: 'Ctrl-Enter:\n' + t('importAppendTooltip'), onclick: doImport, }), - $element({ - tag: 'button', + $create('button', { name: 'import-replace', textContent: t('importReplaceLabel'), title: 'Ctrl-Shift-Enter:\n' + t('importReplaceTooltip'), onclick: () => doImport({replaceOldStyle: true}), }), - ]})); + ])); const contents = $('.contents', popup); contents.insertBefore(popup.codebox.display.wrapper, contents.firstElementChild); popup.codebox.focus(); @@ -522,10 +520,8 @@ function fromMozillaFormat() { } function showError(errors) { - showHelp(t('styleFromMozillaFormatError'), $element({ - tag: 'pre', - textContent: Array.isArray(errors) ? errors.join('\n') : errors, - })); + showHelp(t('styleFromMozillaFormatError'), + $create('pre', Array.isArray(errors) ? errors.join('\n') : errors)); } } @@ -617,7 +613,7 @@ function showCodeMirrorPopup(title, html, options) { function setGlobalProgress(done, total) { const progressElement = $('#global-progress') || - total && document.body.appendChild($element({id: 'global-progress'})); + total && document.body.appendChild($create('#global-progress')); if (total) { const progress = (done / Math.max(done, total) * 100).toFixed(1); progressElement.style.borderLeftWidth = progress + 'vw'; diff --git a/edit/lint.js b/edit/lint.js index f39ec685..814807b2 100644 --- a/edit/lint.js +++ b/edit/lint.js @@ -1,7 +1,6 @@ /* global CodeMirror messageBox */ /* global editors makeSectionVisible showCodeMirrorPopup showHelp */ /* global loadScript require CSSLint stylelint */ -/* global makeLink */ 'use strict'; onDOMready().then(loadLinterAssets); @@ -231,9 +230,7 @@ function updateLinter({immediately, linter = linterConfig.getName()} = {}) { cm.options.gutters = guttersOption; const el = $('.' + GUTTERS_CLASS, cm.display.gutters); if (linter && !el) { - cm.display.gutters.appendChild($element({ - className: 'CodeMirror-gutter ' + GUTTERS_CLASS - })); + cm.display.gutters.appendChild($create('.CodeMirror-gutter ' + GUTTERS_CLASS)); } else if (!linter && el) { el.remove(); } @@ -281,9 +278,8 @@ function updateLintReportInternal(scope, {postponeNewIssues} = {}) { const newMarkers = lintState.stylusMarkers = new Map(); const oldText = (lintState.body || {}).textContentCached || ''; const activeLine = cm.getCursor().line; - const body = !(lintState.marked || {}).length ? {} : $element({ - tag: 'tbody', - appendChild: lintState.marked.map(mark => { + const body = !(lintState.marked || {}).length ? {} : + $create('tbody', lintState.marked.map(mark => { const info = mark.__annotation; const {line, ch} = info.from; const isActiveLine = line === activeLine; @@ -294,27 +290,15 @@ function updateLintReportInternal(scope, {postponeNewIssues} = {}) { oldMarkers.delete(pos); } newMarkers.set(pos, message); - return $element({ - tag: 'tr', - className: info.severity, - appendChild: [ - $element({ - tag: 'td', - attributes: {role: 'severity'}, - dataset: {rule: info.rule}, - appendChild: $element({ - className: 'CodeMirror-lint-marker-' + info.severity, - textContent: info.severity, - }), - }), - $element({tag: 'td', attributes: {role: 'line'}, textContent: line + 1}), - $element({tag: 'td', attributes: {role: 'sep'}, textContent: ':'}), - $element({tag: 'td', attributes: {role: 'col'}, textContent: ch + 1}), - $element({tag: 'td', attributes: {role: 'message'}, textContent: message, title}), - ], - }); - }) - }); + return $create(`tr.${info.severity}`, [ + $create('td', {attributes: {role: 'severity'}, dataset: {rule: info.rule}}, + $create('.CodeMirror-lint-marker-' + info.severity, info.severity)), + $create('td', {attributes: {role: 'line'}}, line + 1), + $create('td', {attributes: {role: 'sep'}}, ':'), + $create('td', {attributes: {role: 'col'}}, ch + 1), + $create('td', {attributes: {role: 'message'}, title}, message), + ]); + })); body.textContentCached = body.textContent || ''; lintState.body = body.textContentCached && body; result.changed |= oldText !== body.textContentCached; @@ -340,14 +324,10 @@ function renderLintReport(someBlockChanged) { if (!body) { return; } - const newBlock = $element({ - tag: 'table', - appendChild: [ - $element({tag: 'caption', textContent: label + ' ' + (index + 1)}), - body, - ], - cm, - }); + const newBlock = $create('table', {cm}, [ + $create('caption', label + ' ' + (index + 1)), + body, + ]); newContent.appendChild(newBlock); issueCount += newBlock.rows.length; @@ -388,35 +368,29 @@ function showLintHelp() { : 'https://github.com/CSSLint/csslint/issues/535'; let headerLink, template; if (linter === 'csslint') { - headerLink = makeLink('https://github.com/CSSLint/csslint/wiki/Rules-by-ID', 'CSSLint'); + headerLink = $createLink('https://github.com/CSSLint/csslint/wiki/Rules-by-ID', 'CSSLint'); template = ruleID => { const rule = linterConfig.allRuleIds.csslint.find(rule => rule.id === ruleID); return rule && - $element({tag: 'li', appendChild: [ - $element({tag: 'b', appendChild: makeLink(rule.url || baseUrl, rule.name)}), - $element({tag: 'br'}), + $create('li', [ + $create('b', $createLink(rule.url || baseUrl, rule.name)), + $create('br'), rule.desc, - ]}); + ]); }; } else { - headerLink = makeLink(baseUrl, 'stylelint'); + headerLink = $createLink(baseUrl, 'stylelint'); template = rule => - $element({ - tag: 'li', - appendChild: makeLink(baseUrl + rule, rule), - }); + $create('li', + $createLink(baseUrl + rule, rule)); } const header = t('linterIssuesHelp', '\x01').split('\x01'); const activeRules = new Set($$('#lint td[role="severity"]').map(el => el.dataset.rule)); return showHelp(t('linterIssues'), - $element({appendChild: [ + $create([ header[0], headerLink, header[1], - $element({ - tag: 'ul', - className: 'rules', - appendChild: [...activeRules.values()].map(template), - }), - ]}) + $create('ul.rules', [...activeRules.values()].map(template)), + ]) ); } @@ -465,41 +439,21 @@ function setupLinterPopup(config) { }); function makeFooter() { - const makeButton = (className, onclick, text, options = {}) => - $element(Object.assign(options, { - className, - onclick, - tag: 'button', - type: 'button', - textContent: t(text), - })); - return $element({ - appendChild: [ - $element({ - tag: 'p', - appendChild: [ - t('linterRulesLink') + ' ', - $element({ - tag: 'a', - target: '_blank', - href: linter === 'stylelint' - ? 'https://stylelint.io/user-guide/rules/' - : 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID', - textContent: linterTitle - }), - linter === 'csslint' ? ' ' + t('linterCSSLintSettings') : '' - ] - }), - makeButton('save', save, 'styleSaveLabel', {title: 'Ctrl-Enter'}), - makeButton('cancel', cancel, 'confirmClose'), - makeButton('reset', reset, 'genericResetLabel', {title: t('linterResetMessage')}), - $element({ - tag: 'span', - className: 'saved-message', - textContent: t('genericSavedMessage') - }) - ] - }); + return $create('div', [ + $create('p', [ + t('linterRulesLink') + ' ', + $createLink( + linter === 'stylelint' + ? 'https://stylelint.io/user-guide/rules/' + : 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID', + linterTitle), + linter === 'csslint' ? ' ' + t('linterCSSLintSettings') : '', + ]), + $create('button.save', {onclick: save, title: 'Ctrl-Enter'}, t('styleSaveLabel')), + $create('button.cancel', {onclick: cancel}, t('confirmClose')), + $create('button.reset', {onclick: reset, title: t('linterResetMessage')}, t('genericResetLabel')), + $create('span.saved-message', t('genericSavedMessage')), + ]); } function save(event) { @@ -516,9 +470,7 @@ function setupLinterPopup(config) { if (invalid.length) { showLinterErrorMessage(linter, [ t('linterInvalidConfigError'), - $element({tag: 'ul', appendChild: invalid.map(name => - $element({tag: 'li', textContent: name})), - }), + $create('ul', invalid.map(name => $create('li', name))), ], popup); return; } diff --git a/edit/regexp-tester.js b/edit/regexp-tester.js index a11527c1..4c5e63ff 100644 --- a/edit/regexp-tester.js +++ b/edit/regexp-tester.js @@ -34,7 +34,7 @@ var regExpTester = (() => { if (!isInit) { init(); } - showHelp('', $element({className: 'regexp-report'})); + showHelp('', $create('.regexp-report')); } else if (!state && isShown()) { if (isInit) { uninit(); @@ -108,19 +108,19 @@ var regExpTester = (() => { const faviconUrl = url.startsWith(URLS.ownOrigin) ? OWN_ICON : GET_FAVICON_URL + new URL(url).hostname; - const icon = $element({tag: 'img', src: faviconUrl}); + const icon = $create('img', {src: faviconUrl}); if (match.text.length === url.length) { - full.push($element({appendChild: [ + full.push($create('div', [ icon, url, - ]})); + ])); } else { - partial.push($element({appendChild: [ + partial.push($create('div', [ icon, url.substr(0, match.pos), - $element({tag: 'mark', textContent: match.text}), + $create('mark', match.text), url.substr(match.pos + match.text.length), - ]})); + ])); } } if (full.length) { @@ -131,33 +131,28 @@ var regExpTester = (() => { } } // render stats - const report = $element({className: 'regexp-report'}); - const br = $element({tag: 'br'}); + const report = $create('.regexp-report'); + const br = $create('br'); for (const type in stats) { // top level groups: full, partial, none, invalid const {label, data} = stats[type]; if (!data.length) { continue; } - const block = report.appendChild($element({ - tag: 'details', - open: true, - dataset: {type}, - appendChild: $element({tag: 'summary', appendChild: label}), - })); + const block = report.appendChild( + $create('details', {open: true, dataset: {type}}, [ + $create('summary', label), + ])); // 2nd level: regexp text for (const {text, urls} of data) { if (urls) { // type is partial or full - block.appendChild($element({ - tag: 'details', - open: true, - appendChild: [ - $element({tag: 'summary', textContent: text}), + block.appendChild( + $create('details', {open: true}, [ + $create('summary', text), // 3rd level: tab urls ...urls, - ], - })); + ])); } else { // type is none or invalid block.appendChild(document.createTextNode(text)); @@ -165,12 +160,11 @@ var regExpTester = (() => { } } } - report.appendChild($element({ - tag: 'p', - className: 'regexp-report-note', - appendChild: t('styleRegexpTestNote').split(/(\\+)/) - .map(s => s.startsWith('\\') ? $element({tag: 'code', textContent: s}) : s), - })); + report.appendChild( + $create('p.regexp-report-note', + t('styleRegexpTestNote') + .split(/(\\+)/) + .map(s => (s.startsWith('\\') ? $create('code', s) : s)))); showHelp(t('styleRegexpTestTitle'), report); report.onclick = event => { diff --git a/edit/show-keymap-help.js b/edit/show-keymap-help.js index 8c73cbfa..0929d9d0 100644 --- a/edit/show-keymap-help.js +++ b/edit/show-keymap-help.js @@ -64,7 +64,7 @@ function showKeyMapHelp() { if (index > offset) { cell.appendChild(document.createTextNode(text.substring(offset, index))); } - cell.appendChild($element({tag: 'mark', textContent: match})); + cell.appendChild($create('mark', match)); offset = index + match.length; }); if (offset + 1 !== text.length) { diff --git a/edit/source-editor.js b/edit/source-editor.js index b09bf838..5b1d5385 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -1,7 +1,7 @@ /* global CodeMirror dirtyReporter initLint */ /* global showToggleStyleHelp goBackToManage updateLintReportIfEnabled */ /* global editors linterConfig updateLinter regExpTester mozParser */ -/* global makeLink createAppliesToLineWidget messageBox */ +/* global createAppliesToLineWidget messageBox */ 'use strict'; function createSourceEditor(style) { @@ -12,9 +12,7 @@ function createSourceEditor(style) { $('#name').disabled = true; $('#mozilla-format-container').remove(); $('#sections').textContent = ''; - $('#sections').appendChild( - $element({className: 'single-editor'}) - ); + $('#sections').appendChild($create('.single-editor')); const dirty = dirtyReporter(); dirty.onChange(() => { @@ -236,15 +234,12 @@ function createSourceEditor(style) { return; } const contents = Array.isArray(err) ? - $element({tag: 'pre', textContent: err.join('\n')}) : + $create('pre', err.join('\n')) : [String(err)]; if (Number.isInteger(err.index)) { const pos = cm.posFromIndex(err.index); contents[0] += ` (line ${pos.line + 1} col ${pos.ch + 1})`; - contents.push($element({ - tag: 'pre', - textContent: drawLinePointer(pos) - })); + contents.push($create('pre', drawLinePointer(pos))); } messageBox.alert(contents); }); diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js index c3c98833..3d347955 100644 --- a/install-usercss/install-usercss.js +++ b/install-usercss/install-usercss.js @@ -1,4 +1,4 @@ -/* global CodeMirror semverCompare makeLink closeCurrentTab */ +/* global CodeMirror semverCompare closeCurrentTab */ /* global messageBox download chromeLocal */ 'use strict'; @@ -44,11 +44,8 @@ setTimeout(() => { if (!installed) { - const div = $element({}); - $('.header').appendChild($element({ - className: 'lds-spinner', - appendChild: new Array(12).fill(div).map(e => e.cloneNode()), - })); + $('.header').appendChild($create('.lds-spinner', + new Array(12).fill($create('div')).map(e => e.cloneNode()))); } }, 200); @@ -101,8 +98,7 @@ $('.applies-to').textContent = ''; getAppliesTo(style).forEach(pattern => - $('.applies-to').appendChild($element({tag: 'li', textContent: pattern})) - ); + $('.applies-to').appendChild($create('li', pattern))); $('.external-link').textContent = ''; const externalLink = makeExternalLink(); @@ -125,46 +121,35 @@ const [, name, email, url] = match; const frag = document.createDocumentFragment(); if (email) { - frag.appendChild(makeLink(`mailto:${email}`, name)); + frag.appendChild($createLink(`mailto:${email}`, name)); } else { - frag.appendChild($element({ - tag: 'span', - textContent: name - })); + frag.appendChild($create('span', name)); } if (url) { - frag.appendChild(makeLink( - url, - $element({ - tag: 'svg#svg', - viewBox: '0 0 20 20', - class: 'svg-icon', - appendChild: $element({ - tag: 'svg#path', + frag.appendChild($createLink(url, + $create('SVG:svg.svg-icon', {viewBox: '0 0 20 20'}, + $create('SVG:path', { d: 'M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z' - }) - }) + })) )); } return frag; } function makeExternalLink() { - const urls = []; - if (data.homepageURL) { - urls.push([data.homepageURL, t('externalHomepage')]); - } - if (data.supportURL) { - urls.push([data.supportURL, t('externalSupport')]); - } - if (urls.length) { - return $element({appendChild: [ - $element({tag: 'h3', textContent: t('externalLink')}), - $element({tag: 'ul', appendChild: urls.map(args => - $element({tag: 'li', appendChild: makeLink(...args)}) - )}) - ]}); - } + const urls = [ + data.homepageURL && [data.homepageURL, t('externalHomepage')], + data.supportURL && [data.supportURL, t('externalSupport')], + ]; + return (data.homepageURL || data.supportURL) && ( + $create('div', [ + $create('h3', t('externalLink')), + $create('ul', urls.map(args => args && + $create('li', + $createLink(...args) + ) + )) + ])); } function installButtonClass() { @@ -230,26 +215,23 @@ function buildWarning(err) { const contents = Array.isArray(err) ? - $element({tag: 'pre', textContent: err.join('\n')}) : + $create('pre', err.join('\n')) : [err && err.message || err || 'Unknown error']; if (Number.isInteger(err.index)) { const pos = cm.posFromIndex(err.index); contents[0] = `${pos.line + 1}:${pos.ch + 1} ` + contents[0]; - contents.push($element({ - tag: 'pre', - textContent: drawLinePointer(pos) - })); + contents.push($create('pre', drawLinePointer(pos))); setTimeout(() => { cm.scrollIntoView({line: pos.line + 1, ch: pos.ch}, window.innerHeight / 4); cm.setCursor(pos.line, pos.ch + 1); cm.focus(); }); } - return $element({className: 'warning', appendChild: [ + return $create('.warning', [ t('parseUsercssError'), '\n', ...contents, - ]}); + ]); } function drawLinePointer(pos) { @@ -281,7 +263,7 @@ // update UI if (versionTest < 0) { $('.actions').parentNode.insertBefore( - $element({className: 'warning', textContent: t('versionInvalidOlder')}), + $create('.warning', t('versionInvalidOlder')), $('.actions') ); } diff --git a/js/dom.js b/js/dom.js index 969dc5c0..d81c2bc7 100644 --- a/js/dom.js +++ b/js/dom.js @@ -73,8 +73,7 @@ if (!chrome.app && chrome.windows) { } const iconset = ['', 'light/'][prefs.get('iconset')] || ''; for (const size of [38, 32, 19, 16]) { - document.head.appendChild($element({ - tag: 'link', + document.head.appendChild($create('link', { rel: 'icon', href: `/images/icon/${iconset}${size}.png`, sizes: size + 'x' + size, @@ -167,35 +166,88 @@ function $$(selector, base = document) { } -function $element(opt) { - // tag: string, default 'div', may include namespace like 'ns#tag' - // appendChild: element/string or an array of elements/strings - // dataset: object - // any DOM property: assigned as is - const [ns, tag] = opt.tag && opt.tag.includes('#') - ? opt.tag.split('#') - : [null, opt.tag]; +function $create(selector = 'div', properties, children) { +/* + $create('tag#id.class.class', ?[children]) + $create('tag#id.class.class', ?textContentOrChildNode) + $create('tag#id.class.class', {properties}, ?[children]) + $create('tag#id.class.class', {properties}, ?textContentOrChildNode) + tag is 'div' by default, #id and .class are optional + + $create([children]) + + $create({propertiesAndOptions}) + $create({propertiesAndOptions}, ?[children]) + tag: string, default 'div' + appendChild: element/string or an array of elements/strings + dataset: object + any DOM property: assigned as is + + tag may include namespace like 'ns:tag' +*/ + let ns, tag, opt; + + if (typeof selector === 'string') { + if (Array.isArray(properties) || + properties instanceof Node || + typeof properties !== 'object') { + opt = {}; + children = properties; + } else { + opt = properties || {}; + } + const idStart = (selector.indexOf('#') + 1 || selector.length + 1) - 1; + const classStart = (selector.indexOf('.') + 1 || selector.length + 1) - 1; + const id = selector.slice(idStart + 1, classStart); + if (id) { + opt.id = id; + } + const cls = selector.slice(classStart + 1); + if (cls) { + opt[selector.includes(':') ? 'class' : 'className'] = + cls.includes('.') ? cls.replace(/\./g, ' ') : cls; + } + tag = selector.slice(0, Math.min(idStart, classStart)); + + } else if (Array.isArray(selector)) { + tag = 'div'; + opt = {}; + children = selector; + + } else { + opt = selector; + tag = opt.tag; + delete opt.tag; + children = opt.appendChild || properties; + delete opt.appendChild; + } + + if (tag && tag.includes(':')) { + ([ns, tag] = tag.split(':')); + } + const element = ns ? document.createElementNS(ns === 'SVG' || ns === 'svg' ? 'http://www.w3.org/2000/svg' : ns, tag) : document.createElement(tag || 'div'); - const children = Array.isArray(opt.appendChild) ? opt.appendChild : [opt.appendChild]; - for (const child of children) { + + for (const child of Array.isArray(children) ? children : [children]) { if (child) { element.appendChild(child instanceof Node ? child : document.createTextNode(child)); } } - delete opt.appendChild; - delete opt.tag; + if (opt.dataset) { Object.assign(element.dataset, opt.dataset); delete opt.dataset; } + if (opt.attributes) { for (const attr in opt.attributes) { element.setAttribute(attr, opt.attributes[attr]); } delete opt.attributes; } + if (ns) { for (const attr in opt) { element.setAttributeNS(null, attr, opt[attr]); @@ -203,11 +255,12 @@ function $element(opt) { } else { Object.assign(element, opt); } + return element; } -function makeLink(href = '', content) { +function $createLink(href = '', content) { const opt = { tag: 'a', target: '_blank', @@ -217,9 +270,9 @@ function makeLink(href = '', content) { Object.assign(opt, href); } else { opt.href = href; - opt.appendChild = content; } - return $element(opt); + opt.appendChild = opt.appendChild || content; + return $create(opt); } diff --git a/manage/config-dialog.js b/manage/config-dialog.js index ebb9dbe0..18ddf0eb 100644 --- a/manage/config-dialog.js +++ b/manage/config-dialog.js @@ -1,8 +1,9 @@ -/* global messageBox makeLink */ +/* global messageBox */ 'use strict'; function configDialog(style) { - const varsHash = deepCopy(style.usercssData.vars) || {}; + const data = style.usercssData; + const varsHash = deepCopy(data.vars) || {}; const varNames = Object.keys(varsHash); const vars = varNames.map(name => varsHash[name]); const elements = []; @@ -12,21 +13,12 @@ function configDialog(style) { renderValues(); return messageBox({ - title: `${style.name} v${style.usercssData.version}`, + title: `${style.name} v${data.version}`, className: 'config-dialog', contents: [ - $element({ - className: 'config-heading', - appendChild: style.usercssData.supportURL && makeLink({ - className: 'external-support', - href: style.usercssData.supportURL, - textContent: t('externalFeedback') - }) - }), - $element({ - className: 'config-body', - appendChild: elements - }) + $create('.config-heading', data.supportURL && + $createLink({className: '.external-support', href: data.supportURL}, t('externalFeedback'))), + $create('.config-body', elements) ], buttons: [ t('confirmSave'), @@ -71,19 +63,17 @@ function configDialog(style) { continue; } invalid.push(['*' + va.name, ': ', ...error].map(e => - e[0] === '*' && $element({tag: 'b', textContent: e.slice(1)}) || e)); + e[0] === '*' && $create('b', e.slice(1)) || e)); if (bgva) { styleVars[va.name].value = deepCopy(bgva); } } if (invalid.length) { messageBox.alert([ - $element({textContent: t('usercssConfigIncomplete'), style: 'max-width: 34em'}), - $element({ - tag: 'ol', - style: 'text-align: left', - appendChild: invalid.map(msg => $element({tag: 'li', appendChild: msg})), - }), + $create('div', {style: 'max-width: 34em'}, t('usercssConfigIncomplete')), + $create('ol', {style: 'text-align: left'}, + invalid.map(msg => + $create({tag: 'li', appendChild: msg}))), ]); } return numValid && BG.usercssHelper.save(style); @@ -91,30 +81,28 @@ function configDialog(style) { function buildConfigForm() { for (const va of vars) { - let appendChild; + let children; switch (va.type) { case 'color': - appendChild = [$element({ - className: 'cm-colorview', - appendChild: va.inputColor = $element({ - va, - className: 'color-swatch', - onclick: showColorpicker, - }) - })]; + va.inputColor = $create('.color-swatch', {va, onclick: showColorpicker}); + children = [ + $create('.cm-colorview', [ + va.inputColor, + ]), + ]; break; case 'checkbox': - va.input = $element({tag: 'input', type: 'checkbox', className: 'slider'}); + va.input = $create('input.slider', {type: 'checkbox'}); va.input.onchange = () => { va.dirty = true; va.value = String(Number(va.input.checked)); }; - appendChild = [ - $element({tag: 'span', className: 'onoffswitch', appendChild: [ + children = [ + $create('span.onoffswitch', [ va.input, - $element({tag: 'span'}) - ]}) + $create('span'), + ]) ]; break; @@ -122,36 +110,30 @@ function configDialog(style) { case 'dropdown': case 'image': // TODO: a image picker input? - va.input = $element({ - tag: 'select', - appendChild: va.options.map(o => $element({ - tag: 'option', value: o.name, textContent: o.label - })) - }); + va.input = $create('select', + va.options.map(o => + $create('option', {value: o.name}, o.label))); va.input.onchange = () => { va.dirty = true; va.value = va.input.value; }; - appendChild = [va.input]; + children = [va.input]; break; default: - va.input = $element({tag: 'input', type: 'text'}); + va.input = $create('input', {type: 'text'}); va.input.oninput = () => { va.dirty = true; va.value = va.input.value; }; - appendChild = [va.input]; + children = [va.input]; break; } - elements.push($element({ - tag: 'label', - className: `config-${va.type}`, - appendChild: [ - $element({tag: 'span', appendChild: va.label}), - ...appendChild, - ], - })); + elements.push( + $create(`label.config-${va.type}`, [ + $create('span', va.label), + ...children, + ])); } } diff --git a/manage/import-export.js b/manage/import-export.js index b7e80031..a4c91a70 100644 --- a/manage/import-export.js +++ b/manage/import-export.js @@ -175,18 +175,16 @@ function importFromString(jsonString) { .map(kind => { const {ids, names, legend} = stats[kind]; const listItemsWithId = (name, i) => - $element({dataset: {id: ids[i]}, textContent: name}); + $create('div', {dataset: {id: ids[i]}}, name); const listItems = name => - $element({textContent: name}); + $create('div', name); const block = - $element({tag: 'details', dataset: {id: kind}, appendChild: [ - $element({tag: 'summary', appendChild: - $element({tag: 'b', textContent: names.length + ' ' + t(legend)}) - }), - $element({tag: 'small', appendChild: - names.map(ids ? listItemsWithId : listItems) - }), - ]}); + $create('details', {dataset: {id: kind}}, [ + $create('summary', + $create('b', names.length + ' ' + t(legend))), + $create('small', + names.map(ids ? listItemsWithId : listItems)), + ]); return block; }); scrollTo(0, 0); @@ -308,8 +306,7 @@ $('#file-all-styles').onclick = () => { const text = JSON.stringify(styles, null, '\t'); const blob = new Blob([text], {type: 'application/json'}); const objectURL = URL.createObjectURL(blob); - let link = $element({ - tag:'a', + let link = $create('a', { href: objectURL, type: 'application/json', download: generateFileName(), @@ -319,8 +316,7 @@ $('#file-all-styles').onclick = () => { link.dispatchEvent(new MouseEvent('click')); setTimeout(() => URL.revokeObjectURL(objectURL)); } else { - const iframe = document.body.appendChild($element({ - tag: 'iframe', + const iframe = document.body.appendChild($create('iframe', { style: 'width: 0; height: 0; position: fixed; opacity: 0;'.replace(/;/g, '!important;'), })); doTimeout() diff --git a/manage/incremental-search.js b/manage/incremental-search.js index 4f61ff6e..9dc5312c 100644 --- a/manage/incremental-search.js +++ b/manage/incremental-search.js @@ -5,8 +5,7 @@ onDOMready().then(() => { let prevText, focusedLink, focusedEntry; let prevTime = performance.now(); let focusedName = ''; - const input = $element({ - tag: 'textarea', + const input = $create('textarea', { spellcheck: false, oninput: incrementalSearch, }); diff --git a/manage/manage.js b/manage/manage.js index e8fc983a..86cc6b18 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -84,14 +84,14 @@ function initGlobalEvents() { switchUI({styleOnly: true}); // translate CSS manually - document.head.appendChild($element({tag: 'style', textContent: ` + document.head.appendChild($create('style', ` .disabled h2::after { content: "${t('genericDisabledLabel')}"; } #update-all-no-updates[data-skipped-edited="true"]:after { content: " ${t('updateAllCheckSucceededSomeEdited')}"; } - `})); + `)); } diff --git a/manage/updater-ui.js b/manage/updater-ui.js index 3f4b5292..7b5360dd 100644 --- a/manage/updater-ui.js +++ b/manage/updater-ui.js @@ -174,10 +174,7 @@ function showUpdateHistory() { BG.chromeLocal.getValue('updateLog').then((lines = []) => { messageBox({ title: t('updateCheckHistory'), - contents: $element({ - className: 'update-history-log', - textContent: lines.join('\n'), - }), + contents: $create('.update-history-log', lines.join('\n')), buttons: [t('confirmOK')], onshow: () => ($('#message-box-contents').scrollTop = 1e9), }); diff --git a/msgbox/msgbox.js b/msgbox/msgbox.js index 4354d895..cbe3b44f 100644 --- a/msgbox/msgbox.js +++ b/msgbox/msgbox.js @@ -58,27 +58,25 @@ function messageBox({ removeSelf(); } const id = 'message-box'; - messageBox.element = $element({id, className, appendChild: [ - $element({appendChild: [ - $element({id: `${id}-title`, textContent: title}), - $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`, appendChild: tHTML(contents)}), - $element({id: `${id}-buttons`, appendChild: - buttons.map((content, buttonIndex) => content && $element({ - tag: 'button', - buttonIndex, - textContent: content.textContent || content, - onclick: content.onclick || messageBox.listeners.button, - })) - }), - ]}), - ]}); + messageBox.element = + $create({id, className}, [ + $create([ + $create(`#${id}-title`, title), + $create(`#${id}-close-icon`, {onclick: messageBox.listeners.closeIcon}, + $create('SVG:svg.svg-icon', {viewBox: '0 0 20 20'}, + $create('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', + }))), + $create(`#${id}-contents`, tHTML(contents)), + $create(`#${id}-buttons`, + buttons.map((content, buttonIndex) => content && + $create('button', { + buttonIndex, + textContent: content.textContent || content, + onclick: content.onclick || messageBox.listeners.button, + }))), + ]), + ]); } function bindGlobalListeners() { diff --git a/popup/hotkeys.js b/popup/hotkeys.js index 4508d151..053d7e1c 100644 --- a/popup/hotkeys.js +++ b/popup/hotkeys.js @@ -142,13 +142,13 @@ window.addEventListener('showStyles:done', function _() { line .split(/(<.*?>)/) .map(s => (!s.startsWith('<') ? s : - $element({tag: 'mark', textContent: s.slice(1, -1)}))); + $create('mark', s.slice(1, -1)))); const linesToElements = text => text .trim() .split('\n') .map((line, i, array) => - $element(i < array.length - 1 ? { + $create(i < array.length - 1 ? { tag: 'p', appendChild: keysToElements(line), } : { @@ -159,9 +159,9 @@ window.addEventListener('showStyles:done', function _() { })); [ linesToElements(t('popupHotkeysInfo')), - $element({tag: 'button', textContent: t('confirmOK')}), + $create('button', t('confirmOK')), ].forEach(child => { - container.appendChild($element({appendChild: child})); + container.appendChild($create('div', child)); }); } } From 48dda41e2ec49efe3e94a1fdcaa3f1936d533c42 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 4 Dec 2017 08:32:27 +0300 Subject: [PATCH 09/28] fixup 99512da9 and 493c1a65: show csslint rule info in issues info box --- edit/lint.js | 22 ++++++++++++--------- vendor-overwrites/csslint/csslint-worker.js | 5 +++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/edit/lint.js b/edit/lint.js index 814807b2..afc44211 100644 --- a/edit/lint.js +++ b/edit/lint.js @@ -366,11 +366,11 @@ function showLintHelp() { ? 'https://stylelint.io/user-guide/rules/' // some CSSLint rules do not have a url : 'https://github.com/CSSLint/csslint/issues/535'; - let headerLink, template; + let headerLink, template, csslintRules; if (linter === 'csslint') { headerLink = $createLink('https://github.com/CSSLint/csslint/wiki/Rules-by-ID', 'CSSLint'); template = ruleID => { - const rule = linterConfig.allRuleIds.csslint.find(rule => rule.id === ruleID); + const rule = csslintRules.find(rule => rule.id === ruleID); return rule && $create('li', [ $create('b', $createLink(rule.url || baseUrl, rule.name)), @@ -382,16 +382,20 @@ function showLintHelp() { headerLink = $createLink(baseUrl, 'stylelint'); template = rule => $create('li', - $createLink(baseUrl + rule, rule)); + rule === 'CssSyntaxError' ? rule : $createLink(baseUrl + rule, rule)); } const header = t('linterIssuesHelp', '\x01').split('\x01'); const activeRules = new Set($$('#lint td[role="severity"]').map(el => el.dataset.rule)); - return showHelp(t('linterIssues'), - $create([ - header[0], headerLink, header[1], - $create('ul.rules', [...activeRules.values()].map(template)), - ]) - ); + Promise.resolve(linter !== 'csslint' || linterConfig.invokeWorker({action: 'getAllRuleInfos'})) + .then(data => { + csslintRules = data; + showHelp(t('linterIssues'), + $create([ + header[0], headerLink, header[1], + $create('ul.rules', [...activeRules.values()].map(template)), + ]) + ); + }); } function showLinterErrorMessage(title, contents, popup) { diff --git a/vendor-overwrites/csslint/csslint-worker.js b/vendor-overwrites/csslint/csslint-worker.js index e150c755..03d92460 100644 --- a/vendor-overwrites/csslint/csslint-worker.js +++ b/vendor-overwrites/csslint/csslint-worker.js @@ -10994,6 +10994,11 @@ self.onmessage = ({data: {action = 'run', code, config}}) => { self.postMessage(CSSLint.getRules().map(rule => rule.id)); return; + case 'getAllRuleInfos': + // the functions are non-tranferable + self.postMessage(CSSLint.getRules().map(rule => JSON.parse(JSON.stringify(rule)))); + return; + case 'run': Object.defineProperty(config, 'errors', {get: () => 0, set: () => 0}); config['uso-vars'] = 1; From 01e8c3b080e750e76f73222e3170f3298cc8f8d9 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 4 Dec 2017 09:00:03 +0300 Subject: [PATCH 10/28] disable spellchecking in input fields --- edit.html | 4 ++-- edit/applies-to-line-widget.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/edit.html b/edit.html index 008a8cc7..ef364bf9 100644 --- a/edit.html +++ b/edit.html @@ -80,7 +80,7 @@ - + @@ -157,7 +157,7 @@ - + diff --git a/edit/applies-to-line-widget.js b/edit/applies-to-line-widget.js index f3687213..91d26f63 100644 --- a/edit/applies-to-line-widget.js +++ b/edit/applies-to-line-widget.js @@ -37,7 +37,7 @@ function createAppliesToLineWidget(cm) { $create('option', {value: 'domain'}, t('appliesDomainOption')), $create('option', {value: 'regexp'}, t('appliesRegexpOption')), ]), - $create('input.applies-value'), + $create('input.applies-value', {spellcheck: false}), $create('button.test-regexp', t('styleRegexpTestButton')), $create('button.remove-applies-to', t('appliesRemove')), $create('button.add-applies-to', t('appliesAdd')), From 1785bac9d2fc5c471c489ef0000ed2dfd19ebfdf Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 4 Dec 2017 09:55:11 +0300 Subject: [PATCH 11/28] fix usercss updater: skip if code hasn't changed --- background/update.js | 74 +++++++++++++++++++++----------------------- manage/updater-ui.js | 4 +-- 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/background/update.js b/background/update.js index e9018d55..945db534 100644 --- a/background/update.js +++ b/background/update.js @@ -56,26 +56,26 @@ var updater = { 'ignoreDigest' option is set on the second manual individual update check on the manage page. */ - const maybeUpdate = style.usercssData ? maybeUpdateUsercss : maybeUpdateUSO; - return (ignoreDigest ? Promise.resolve() : calcStyleDigest(style)) - .then(checkIfEdited) - .then(maybeUpdate) - .then(maybeValidate) + return Promise.resolve(style) + .then([calcStyleDigest][!ignoreDigest ? 0 : 'skip']) + .then([checkIfEdited][!ignoreDigest ? 0 : 'skip']) + .then([maybeUpdateUSO, maybeUpdateUsercss][style.usercssData ? 1 : 0]) .then(maybeSave) - .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}`); - }); + .then(reportSuccess) + .catch(reportFailure); + + function reportSuccess(saved) { + observer(updater.UPDATED, saved); + updater.log(updater.UPDATED + ` #${saved.id} ${saved.name}`); + } + + function reportFailure(err) { + observer(updater.SKIPPED, style, err); + err = err === 0 ? 'server unreachable' : err; + updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`); + } function checkIfEdited(digest) { - if (ignoreDigest) { - return; - } if (style.originalDigest && style.originalDigest !== digest) { return Promise.reject(updater.EDITED); } @@ -95,6 +95,7 @@ var updater = { } function maybeUpdateUsercss() { + // TODO: when sourceCode is > 100kB use http range request(s) for version check return download(style.updateUrl).then(text => { const json = usercss.buildMeta(text); const {usercssData: {version}} = style; @@ -104,6 +105,8 @@ var updater = { // re-install is invalid in a soft upgrade if (!ignoreDigest) { return Promise.reject(updater.SAME_VERSION); + } else if (text === style.sourceCode) { + return Promise.reject(updater.SAME_CODE); } break; case 1: @@ -114,38 +117,31 @@ var updater = { }); } - function maybeValidate(json) { - if (json.usercssData) { - // usercss is already validated while building - return json; - } - if (!styleJSONseemsValid(json)) { + function maybeSave(json = {}) { + // usercss is already validated while building + if (!json.usercssData && !styleJSONseemsValid(json)) { return Promise.reject(updater.ERROR_JSON); } - return json; - } - - function maybeSave(json) { json.id = style.id; json.updateDate = Date.now(); + json.reason = 'update'; + // keep current state + delete json.enabled; + // keep local name customizations + delete json.name; + if (styleSectionsEqual(json, style)) { - // JSONs may have different order of items even if sections are effectively equal - // so we'll update the digest anyway - // always update digest even if (save === false) + // update digest even if save === false as there might be just a space added etc. saveStyle(Object.assign(json, {reason: 'update-digest'})); return Promise.reject(updater.SAME_CODE); } else if (!style.originalDigest && !ignoreDigest) { return Promise.reject(updater.MAYBE_EDITED); } - if (!save) { - return json; - } - json.reason = 'update'; - if (json.usercssData) { - return usercssHelper.save(json); - } - json.name = null; // keep local name customizations - return saveStyle(json); + + return !save ? json : + json.usercssData + ? usercssHelper.save(json) + : saveStyle(json); } function styleJSONseemsValid(json) { diff --git a/manage/updater-ui.js b/manage/updater-ui.js index 7b5360dd..8149000e 100644 --- a/manage/updater-ui.js +++ b/manage/updater-ui.js @@ -122,9 +122,9 @@ function reportUpdateState(state, style, details) { const edited = details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED; entry.dataset.details = details; if (!details) { - details = t('updateCheckFailServerUnreachable'); + details = t('updateCheckFailServerUnreachable') + '\n' + style.updateUrl; } else if (typeof details === 'number') { - details = t('updateCheckFailBadResponseCode', [details]); + details = t('updateCheckFailBadResponseCode', [details]) + '\n' + style.updateUrl; } else if (details === BG.updater.EDITED) { details = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); } else if (details === BG.updater.MAYBE_EDITED) { From 1951bc89bba784b40166b35da03e7b5f9255dcf9 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 4 Dec 2017 02:38:44 +0300 Subject: [PATCH 12/28] manage: inform when hiding all styles due to active filters --- _locales/en/messages.json | 4 ++++ manage/filters.js | 7 ++----- manage/manage.css | 31 +++++++++++++++++++++++++++++++ manage/manage.js | 3 +++ 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 697c870e..36ce385a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -349,6 +349,10 @@ } } }, + "filteredStylesAllHidden": { + "message": "Currently applied filters match no styles", + "description": "Text shown when no styles match currently applied filter in the style manager" + }, "findStylesForSite": { "message": "Find more styles for this site", "description": "Text for a link that gets a list of styles for the current site" diff --git a/manage/filters.js b/manage/filters.js index d7282e7f..77f294a3 100644 --- a/manage/filters.js +++ b/manage/filters.js @@ -295,11 +295,7 @@ function reapplyFilter(container = installed) { } -function showFiltersStats({immediately} = {}) { - if (!immediately) { - debounce(showFiltersStats, 100, {immediately: true}); - return; - } +function showFiltersStats() { $('#filters').classList.toggle('active', filtersSelector.hide !== ''); const numTotal = BG.cachedStyles.list.length; const numHidden = installed.getElementsByClassName('entry hidden').length; @@ -309,6 +305,7 @@ function showFiltersStats({immediately} = {}) { filtersSelector.numShown = numShown; filtersSelector.numTotal = numTotal; $('#filters-stats').textContent = t('filteredStyles', [numShown, numTotal]); + document.body.classList.toggle('all-styles-hidden-by-filters', !numShown && numTotal); } } diff --git a/manage/manage.css b/manage/manage.css index 8de33df9..951aef59 100644 --- a/manage/manage.css +++ b/manage/manage.css @@ -15,6 +15,26 @@ body { height: 100%; } +body.all-styles-hidden-by-filters:before, +body.all-styles-hidden-by-filters:after { + position: absolute; + left: calc(3rem + var(--header-width)); + color: hsla(180, 40%, 45%, .3); + animation: fadein 3s; +} + +body.all-styles-hidden-by-filters:before { + content: "\2190"; /* left arrow */ + font-size: 3rem; + top: 3.5rem; +} + +body.all-styles-hidden-by-filters:after { + font-size: 1.5rem; + position: absolute; + top: 3rem; +} + a, .disabled a:hover { color: #000; transition: color .5s; @@ -898,6 +918,17 @@ fieldset select { flex-direction: column; } + body.all-styles-hidden-by-filters:before { + content: "\2191"; /* up arrow */ + top: calc(50% + 2.75rem); + left: 2rem; + } + + body.all-styles-hidden-by-filters:after { + top: calc(50% + 4rem); + left: 3.75rem; + } + #header { height: auto; position: static; diff --git a/manage/manage.js b/manage/manage.js index 86cc6b18..58435e1c 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -91,6 +91,9 @@ function initGlobalEvents() { #update-all-no-updates[data-skipped-edited="true"]:after { content: " ${t('updateAllCheckSucceededSomeEdited')}"; } + body.all-styles-hidden-by-filters:after { + content: "${t('filteredStylesAllHidden')}"; + } `)); } From 81c6d9912c9f213f5db07af884618bf467663428 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 4 Dec 2017 11:28:32 +0300 Subject: [PATCH 13/28] fixup 1951bc89: wait for bg when the browser is starting up --- manage/filters.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/manage/filters.js b/manage/filters.js index 77f294a3..20fa7a47 100644 --- a/manage/filters.js +++ b/manage/filters.js @@ -296,6 +296,10 @@ function reapplyFilter(container = installed) { function showFiltersStats() { + if (!BG.cachedStyles.list) { + debounce(showFiltersStats); + return; + } $('#filters').classList.toggle('active', filtersSelector.hide !== ''); const numTotal = BG.cachedStyles.list.length; const numHidden = installed.getElementsByClassName('entry hidden').length; From 294917ed4dac2bef3115148252bea4f16e47ed65 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 4 Dec 2017 19:51:52 +0300 Subject: [PATCH 14/28] fixup 1951bc89: update the note twice --- manage/filters.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/manage/filters.js b/manage/filters.js index 20fa7a47..71b7c101 100644 --- a/manage/filters.js +++ b/manage/filters.js @@ -260,6 +260,9 @@ function reapplyFilter(container = installed) { }; } } + if (fullPass) { + showFiltersStats(); + } } function findInsertionPoint(entry) { @@ -297,7 +300,7 @@ function reapplyFilter(container = installed) { function showFiltersStats() { if (!BG.cachedStyles.list) { - debounce(showFiltersStats); + debounce(showFiltersStats, 100); return; } $('#filters').classList.toggle('active', filtersSelector.hide !== ''); From e50de59669884dbe7bfabe92179f1c774f912c54 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 4 Dec 2017 20:04:17 +0300 Subject: [PATCH 15/28] fixup 1951bc89: always debounce note update --- manage/filters.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manage/filters.js b/manage/filters.js index 71b7c101..05cc1dc2 100644 --- a/manage/filters.js +++ b/manage/filters.js @@ -261,7 +261,7 @@ function reapplyFilter(container = installed) { } } if (fullPass) { - showFiltersStats(); + showFiltersStats({immediately: true}); } } @@ -298,9 +298,9 @@ function reapplyFilter(container = installed) { } -function showFiltersStats() { - if (!BG.cachedStyles.list) { - debounce(showFiltersStats, 100); +function showFiltersStats({immediately} = {}) { + if (!immediately || !BG.cachedStyles.list) { + debounce(showFiltersStats, 100, {immediately: true}); return; } $('#filters').classList.toggle('active', filtersSelector.hide !== ''); From dd05955a38b34bb2422b2c492e111d999815e2a4 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 4 Dec 2017 20:12:47 +0300 Subject: [PATCH 16/28] fixup d97337de: preceding comment may be absent --- js/moz-parser.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/js/moz-parser.js b/js/moz-parser.js index bf023fdc..1aa2aa47 100644 --- a/js/moz-parser.js +++ b/js/moz-parser.js @@ -23,10 +23,11 @@ var mozParser = (() => { const section = {code: '', start: {line, col}}; // move last comment before @-moz-document inside the section if (!/\/\*[\s\n]*AGENT_SHEET[\s\n]*\*\//.test(lastCmt)) { - section.code = lastCmt + '\n'; - const indent = outerText.match(/^\s*/)[0]; - outerText = outerText.slice(0, -lastCmt.length); - outerText = indent + outerText.trim(); + if (lastCmt) { + section.code = lastCmt + '\n'; + outerText = outerText.slice(0, -lastCmt.length); + } + outerText = outerText.match(/^\s*/)[0] + outerText.trim(); } if (outerText.trim()) { lastSection.code = outerText; From 3e9810678ad887a3e63b82479d05accf2adf7d65 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 4 Dec 2017 20:13:56 +0300 Subject: [PATCH 17/28] fixup 0e61de29: use break/continue instead of return fixes #282 --- edit/sections.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/edit/sections.js b/edit/sections.js index 696c3fea..93d5b956 100644 --- a/edit/sections.js +++ b/edit/sections.js @@ -390,11 +390,11 @@ function getSections() { function getSectionsHashes() { const sections = []; - getSections().forEach(div => { + for (const div of getSections()) { const meta = {urls: [], urlPrefixes: [], domains: [], regexps: []}; for (const li of $('.applies-to-list', div).childNodes) { if (li.className === template.appliesToEverything.className) { - return; + break; } const type = $('[name=applies-type]', li).value; const value = $('[name=applies-value]', li).value; @@ -404,11 +404,11 @@ function getSectionsHashes() { } const code = div.CodeMirror.getValue(); if (/^\s*$/.test(code) && Object.keys(meta).length === 0) { - return; + continue; } meta.code = code; sections.push(meta); - }); + } return sections; } From 30d84d15fa31ea53a13011ddc4df51b5793ad2d4 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 4 Dec 2017 20:24:07 +0300 Subject: [PATCH 18/28] remove the note sooner when unhiding entries --- manage/filters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manage/filters.js b/manage/filters.js index 05cc1dc2..a198db84 100644 --- a/manage/filters.js +++ b/manage/filters.js @@ -158,7 +158,7 @@ function reapplyFilter(container = installed) { filterContainer({hide: true}); } if (!toHide.length) { - showFiltersStats(); + showFiltersStats({immediately: true}); return; } for (const entry of toHide) { From a0c6450fc13967af07c1032d88b33875708dabe1 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 4 Dec 2017 21:00:06 +0300 Subject: [PATCH 19/28] only show the note if filtering something --- manage/filters.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manage/filters.js b/manage/filters.js index a198db84..8dd191b9 100644 --- a/manage/filters.js +++ b/manage/filters.js @@ -312,7 +312,8 @@ function showFiltersStats({immediately} = {}) { filtersSelector.numShown = numShown; filtersSelector.numTotal = numTotal; $('#filters-stats').textContent = t('filteredStyles', [numShown, numTotal]); - document.body.classList.toggle('all-styles-hidden-by-filters', !numShown && numTotal); + document.body.classList.toggle('all-styles-hidden-by-filters', + !numShown && numTotal && filtersSelector.hide); } } From 8ca4253be4636a96c643641492b95ea6b5f693c4 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 4 Dec 2017 21:14:31 +0300 Subject: [PATCH 20/28] fix the note for good, hopefully --- manage/filters.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manage/filters.js b/manage/filters.js index 8dd191b9..aae54d43 100644 --- a/manage/filters.js +++ b/manage/filters.js @@ -158,7 +158,7 @@ function reapplyFilter(container = installed) { filterContainer({hide: true}); } if (!toHide.length) { - showFiltersStats({immediately: true}); + showFiltersStats(); return; } for (const entry of toHide) { @@ -298,9 +298,9 @@ function reapplyFilter(container = installed) { } -function showFiltersStats({immediately} = {}) { - if (!immediately || !BG.cachedStyles.list) { - debounce(showFiltersStats, 100, {immediately: true}); +function showFiltersStats() { + if (!BG.cachedStyles.list) { + debounce(showFiltersStats, 100); return; } $('#filters').classList.toggle('active', filtersSelector.hide !== ''); From 7339643ec38d036cd259799f004aa82c6d1a4743 Mon Sep 17 00:00:00 2001 From: narcolepticinsomniac Date: Mon, 4 Dec 2017 02:40:22 +0300 Subject: [PATCH 21/28] Custom checkbox and select elements --- edit.html | 116 +++++++++++------ edit/beautify.js | 15 ++- edit/edit.css | 168 +++++++++++++++++++------ install-usercss.html | 10 ++ install-usercss/install-usercss.css | 56 ++++++++- manage.html | 137 +++++++++++++++----- manage/manage.css | 186 +++++++++++++++++++++++++--- popup.html | 24 ++-- popup/popup.css | 84 +++++-------- 9 files changed, 608 insertions(+), 188 deletions(-) diff --git a/edit.html b/edit.html index ef364bf9..9010800e 100644 --- a/edit.html +++ b/edit.html @@ -74,12 +74,15 @@