From 6a838e9d5e8afc2a757b6f2b654fc2db174afdd0 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 7 Dec 2017 20:26:41 +0300 Subject: [PATCH 01/10] make sure all pre-create hooks finished --- edit/codemirror-editing-hooks.js | 4 +- edit/edit.js | 65 ++++++++++++++++---------------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/edit/codemirror-editing-hooks.js b/edit/codemirror-editing-hooks.js index 46550b8e..7cb66a69 100644 --- a/edit/codemirror-editing-hooks.js +++ b/edit/codemirror-editing-hooks.js @@ -5,7 +5,9 @@ global save toggleStyle setupAutocomplete makeSectionVisible getSectionForChild */ 'use strict'; -onDOMready().then(() => { +addEventListener('init:allDone', function _() { + removeEventListener('init:allDone', _); + CodeMirror.defaults.lint = linterConfig.getForCodeMirror(); const COMMANDS = { diff --git a/edit/edit.js b/edit/edit.js index 0e95c192..255cc975 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -26,47 +26,23 @@ const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'do let editor; -preinit(); window.onbeforeunload = beforeUnload; chrome.runtime.onMessage.addListener(onRuntimeMessage); +preinit(); + Promise.all([ - initStyleData().then(style => { - styleId = style.id; - sessionStorage.justEditedStyleId = styleId; - // we set "usercss" class on when is empty - // so there'll be no flickering of the elements that depend on it - if (isUsercss(style)) { - document.documentElement.classList.add('usercss'); - } - // strip URL parameters when invoked for a non-existent id - if (!styleId) { - history.replaceState({}, document.title, location.pathname); - } - return style; - }), + initStyleData(), onDOMready(), - onBackgroundReady(), ]) .then(([style]) => Promise.all([ style, initColorpicker(), initCollapsibles(), initHooksCommon(), + dispatchEvent(new Event('init:allDone')), ])) -.then(([style]) => { - const usercss = isUsercss(style); - $('#heading').textContent = t(styleId ? 'editStyleHeading' : 'addStyleTitle'); - $('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName'); - $('#name').title = usercss ? t('usercssReplaceTemplateName') : ''; - $('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true}); - if (usercss) { - editor = createSourceEditor(style); - } else { - initWithSectionStyle({style}); - document.addEventListener('wheel', scrollEntirePageOnCtrlShift); - } -}); +.then(createEditor); function preinit() { // make querySelectorAll enumeration code readable @@ -178,6 +154,20 @@ function preinit() { }); } +function createEditor([style]) { + const usercss = isUsercss(style); + $('#heading').textContent = t(styleId ? 'editStyleHeading' : 'addStyleTitle'); + $('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName'); + $('#name').title = usercss ? t('usercssReplaceTemplateName') : ''; + $('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true}); + if (usercss) { + editor = createSourceEditor(style); + } else { + initWithSectionStyle({style}); + document.addEventListener('wheel', scrollEntirePageOnCtrlShift); + } +} + function onRuntimeMessage(request) { switch (request.method) { case 'styleUpdated': @@ -258,9 +248,20 @@ function initStyleData() { ) ], }); - return !id ? - Promise.resolve(createEmptyStyle()) : - getStylesSafe({id}).then(([style]) => style || createEmptyStyle()); + return getStylesSafe({id: id || -1}) + .then(([style = createEmptyStyle()]) => { + styleId = sessionStorage.justEditedStyleId = style.id; + // we set "usercss" class on when is empty + // so there'll be no flickering of the elements that depend on it + if (isUsercss(style)) { + document.documentElement.classList.add('usercss'); + } + // strip URL parameters when invoked for a non-existent id + if (!styleId) { + history.replaceState({}, document.title, location.pathname); + } + return style; + }); } function initHooks() { From fbcd3cc96524d0cbba9a58aabff5161efd43087e Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 7 Dec 2017 20:58:02 +0300 Subject: [PATCH 02/10] clarify the tooltip for USO userstyle "configure" icon --- _locales/en/messages.json | 6 +++++- popup/popup.js | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index a1529384..72953360 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -105,7 +105,11 @@ }, "configureStyle": { "message": "Configure", - "description": "Label for the button to configure userstyle" + "description": "Label for the button to configure usercss userstyle" + }, + "configureStyleOnHomepage": { + "message": "Configure on homepage", + "description": "Label for the button to configure userstyles.org userstyle" }, "checkForUpdate": { "message": "Check for update", diff --git a/popup/popup.js b/popup/popup.js index 3041696f..bd7c06be 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -282,6 +282,7 @@ function createStyleElement({ if (!style.usercssData && style.updateUrl && style.updateUrl.includes('?') && style.url) { config.href = style.url; config.target = '_blank'; + config.title = t('configureStyleOnHomepage'); $('use', config).attributes['xlink:href'].nodeValue = '#svg-icon-config-uso'; } else if (!style.usercssData || !Object.keys(style.usercssData.vars || {}).length) { config.style.display = 'none'; From 99cce55a8e93ec7ecbc5eb7e0838bcc840177ff6 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 7 Dec 2017 23:21:27 +0300 Subject: [PATCH 03/10] ensure long words break before breaking the layout supersedes 40075a0d --- .eslintrc | 1 + edit/edit.js | 24 -------- js/localization.js | 136 +++++++++++++++++++++++++++++++-------------- manage/manage.js | 2 +- 4 files changed, 95 insertions(+), 68 deletions(-) diff --git a/.eslintrc b/.eslintrc index 6eb52b33..0d866fff 100644 --- a/.eslintrc +++ b/.eslintrc @@ -48,6 +48,7 @@ globals: tHTML: false tNodeList: false tDocLoader: false + tWordBreak: false # dom.js onDOMready: false scrollElementIntoView: false diff --git a/edit/edit.js b/edit/edit.js index 255cc975..a050e2fe 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -75,30 +75,6 @@ function preinit() { 'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css' })); - // forcefully break long labels in aligned options to prevent the entire block layout from breaking - onDOMready().then(() => new Promise(requestAnimationFrame)).then(() => { - const maxWidth2ndChild = $$('#options .aligned > :nth-child(2)') - .sort((a, b) => b.offsetWidth - a.offsetWidth)[0].offsetWidth; - const widthFor1stChild = $('#options').offsetWidth - maxWidth2ndChild; - if (widthFor1stChild > 50) { - for (const el of $$('#options .aligned > :nth-child(1)')) { - if (el.offsetWidth > widthFor1stChild) { - el.style.cssText = 'word-break: break-all; hyphens: auto;'; - } - } - } else { - const width = $('#options').clientWidth; - document.head.appendChild($create('style', ` - #options .aligned > nth-child(1) { - max-width: 70px; - } - #options .aligned > nth-child(2) { - max-width: ${width - 70}px; - } - `)); - } - }); - if (chrome.windows) { queryTabs({currentWindow: true}).then(tabs => { const windowId = tabs[0].windowId; diff --git a/js/localization.js b/js/localization.js index 79b242e9..c0c5a28f 100644 --- a/js/localization.js +++ b/js/localization.js @@ -48,26 +48,14 @@ function tHTML(html, tag) { function tNodeList(nodes) { const PREFIX = 'i18n-'; + for (let n = nodes.length; --n >= 0;) { const node = nodes[n]; - // skip non-ELEMENT_NODE - if (node.nodeType !== 1) { + if (node.nodeType !== Node.ELEMENT_NODE) { 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% - 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()); + createTemplate(node); continue; } for (let a = node.attributes.length; --a >= 0;) { @@ -78,26 +66,71 @@ function tNodeList(nodes) { } const type = name.substr(PREFIX.length); const value = t(attr.value); + let toInsert, before; switch (type) { + case 'word-break': + // we already know that: hasWordBreak + break; case 'text': - node.insertBefore(document.createTextNode(value), node.firstChild); - break; + before = node.firstChild; + // fallthrough to text-append case 'text-append': - node.appendChild(document.createTextNode(value)); + toInsert = createText(value); break; - case 'html': - // localized strings only allow having text nodes and links - node.textContent = ''; - [...tHTML(value, 'div').childNodes] - .filter(a => a.nodeType === a.TEXT_NODE || a.tagName === 'A') - .forEach(n => node.appendChild(n)); + case 'html': { + toInsert = createHtml(value); break; + } default: node.setAttribute(type, value); } + tDocLoader.pause(); + if (toInsert) { + node.insertBefore(toInsert, before || null); + } node.removeAttribute(name); } } + + function createTemplate(node) { + 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% + 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); + } + } + tDocLoader.pause(); + toRemove.forEach(el => el.remove()); + } + + function createText(str) { + return document.createTextNode(tWordBreak(str)); + } + + function createHtml(value) { + // bar are the only recognizable HTML elements + const rx = /(?:]*)>([^<]*)<\/a>)?([^<]*)/gi; + const bin = document.createDocumentFragment(); + for (let m; (m = rx.exec(value)) && m[0];) { + const [, linkParams, linkText, nextText] = m; + if (linkText) { + const href = /\bhref\s*=\s*(\S+)/.exec(linkParams); + const a = bin.appendChild(document.createElement('a')); + a.href = href && href[1].replace(/^(["'])(.*)\1$/, '$2') || ''; + a.appendChild(createText(linkText)); + } + if (nextText) { + bin.appendChild(createText(nextText)); + } + } + return bin; + } } @@ -115,33 +148,50 @@ function tDocLoader() { t.cache = {browserUIlanguage: UIlang}; localStorage.L10N = JSON.stringify(t.cache); } - const cacheLength = Object.keys(t.cache).length; - // localize HEAD - tNodeList(document.getElementsByTagName('*')); + Object.assign(tDocLoader, { + observer: new MutationObserver(process), + start() { + if (!tDocLoader.observing) { + tDocLoader.observing = true; + tDocLoader.observer.observe(document, {subtree: true, childList: true}); + } + }, + stop() { + tDocLoader.pause(); + document.removeEventListener('DOMContentLoaded', onLoad); + }, + pause() { + if (tDocLoader.observing) { + tDocLoader.observing = false; + tDocLoader.observer.disconnect(); + } + }, + }); - // localize BODY - const process = mutations => { + tNodeList(document.getElementsByTagName('*')); + tDocLoader.start(); + document.addEventListener('DOMContentLoaded', onLoad); + + function process(mutations) { for (const mutation of mutations) { tNodeList(mutation.addedNodes); } - }; - const observer = new MutationObserver(process); - const onLoad = () => { + tDocLoader.start(); + } + + function onLoad() { tDocLoader.stop(); - process(observer.takeRecords()); + process(tDocLoader.observer.takeRecords()); if (cacheLength !== Object.keys(t.cache).length) { localStorage.L10N = JSON.stringify(t.cache); } - }; - tDocLoader.start = () => { - observer.observe(document, {subtree: true, childList: true}); - }; - tDocLoader.stop = () => { - observer.disconnect(); - document.removeEventListener('DOMContentLoaded', onLoad); - }; - tDocLoader.start(); - document.addEventListener('DOMContentLoaded', onLoad); + } +} + + +function tWordBreak(text) { + // adds soft hyphens every 10 characters to ensure the long words break before breaking the layout + return text.length <= 10 ? text : text.replace(/[\d\w\u0080-\uFFFF]{10}|((?!\s)\W){10}/g, '$&\u00AD'); } diff --git a/manage/manage.js b/manage/manage.js index 4069337a..506bdc31 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -179,7 +179,7 @@ function createStyleElement({style, name}) { } const parts = createStyleElement.parts; parts.checker.checked = style.enabled; - parts.nameLink.textContent = style.name; + parts.nameLink.textContent = tWordBreak(style.name); parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id; parts.homepage.href = parts.homepage.title = style.url || ''; From 3318db19995fe52e212b98e01285f3457425c73a Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 8 Dec 2017 01:08:25 +0300 Subject: [PATCH 04/10] show .config-error on failure to save --- manage/config-dialog.css | 32 +++++++++++++++++++++++ manage/config-dialog.js | 55 +++++++++++++++++++++++++++------------- 2 files changed, 69 insertions(+), 18 deletions(-) diff --git a/manage/config-dialog.css b/manage/config-dialog.css index 84849efb..4c7cc5bd 100644 --- a/manage/config-dialog.css +++ b/manage/config-dialog.css @@ -102,6 +102,29 @@ display: inline-flex; } +#message-box-buttons { + position: relative; +} + +.config-error { + position: absolute; + z-index: 99; + left: 0; + right: 0; + bottom: -1rem; + padding: 0 .75rem; + line-height: 24px; + height: 24px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + background-color: red; + color: white; + font-weight: bold; + text-shadow: 0.5px 0.5px 6px #400; + animation: fadein .5s; +} + .cm-colorview::before, .color-swatch { width: var(--onoffswitch-width) !important; @@ -124,3 +147,12 @@ border: none !important; box-shadow: 3px 3px 50px rgba(0,0,0,.5) !important; } + +@keyframes fadein { + from { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/manage/config-dialog.js b/manage/config-dialog.js index 08da4c69..7216c158 100644 --- a/manage/config-dialog.js +++ b/manage/config-dialog.js @@ -2,6 +2,7 @@ 'use strict'; function configDialog(style) { + const AUTOSAVE_DELAY = 500; const data = style.usercssData; const varsHash = deepCopy(data.vars) || {}; const varNames = Object.keys(varsHash); @@ -77,7 +78,7 @@ function configDialog(style) { if (va) { va.dirty = varsInitial[va.name] !== (isDefault(va) ? va.default : va.value); if (prefs.get('config.autosave')) { - debounce(save); + debounce(save, 0, {anyChangeIsDirty: true}); } else { target.closest('label').classList.toggle('dirty', va.dirty); updateButtons(); @@ -92,8 +93,9 @@ function configDialog(style) { buttons.close.textContent = t(someDirty ? 'confirmCancel' : 'confirmClose'); } - function save() { - if (!vars.length || !vars.some(va => va.dirty)) { + function save({anyChangeIsDirty = false} = {}) { + if (!vars.length || + !vars.some(va => va.dirty || anyChangeIsDirty && va.value !== va.savedValue)) { return; } style.enabled = true; @@ -117,10 +119,11 @@ function configDialog(style) { !isDefault(va) && bgva.options.every(o => o.name !== va.value)) { error = `'${va.value}' not in the updated '${va.type}' list`; - } else if (!va.dirty) { + } else if (!va.dirty && (!anyChangeIsDirty || va.value === va.savedValue)) { continue; } else { styleVars[va.name].value = va.value; + va.savedValue = va.value; numValid++; continue; } @@ -147,8 +150,13 @@ function configDialog(style) { varsInitial = getInitialValues(deepCopy(saved.usercssData.vars)); vars.forEach(va => onchange({target: va.input})); updateButtons(); + $.remove('.config-error'); }) - .catch(errors => onhide() + messageBox.alert(Array.isArray(errors) ? errors.join('\n') : errors)); + .catch(errors => { + const el = $('.config-error', messageBox.element) || + $('#message-box-buttons').insertAdjacentElement('afterbegin', $create('.config-error')); + el.textContent = el.title = Array.isArray(errors) ? errors.join('\n') : errors; + }); } function useDefault() { @@ -184,9 +192,7 @@ function configDialog(style) { va.input = $create('input.slider', { va, type: 'checkbox', - onchange() { - va.value = va.input.checked ? '1' : '0'; - }, + onchange: updateVarOnChange, }), $create('span'), ]), @@ -201,9 +207,7 @@ function configDialog(style) { $create('.select-resizer', [ va.input = $create('select', { va, - onchange() { - va.value = this.value; - } + onchange: updateVarOnChange, }, va.options.map(o => $create('option', {value: o.name}, o.label))), @@ -218,10 +222,8 @@ function configDialog(style) { va.input = $create('input', { va, type: 'text', - oninput() { - va.value = this.value; - this.dispatchEvent(new Event('change', {bubbles: true})); - }, + onchange: updateVarOnChange, + oninput: updateVarOnInput, }), ]; break; @@ -234,6 +236,18 @@ function configDialog(style) { } } + function updateVarOnChange() { + this.va.value = this.value; + } + + function updateVarOnInput(event, debounced = false) { + if (debounced) { + event.target.dispatchEvent(new Event('change', {bubbles: true})); + } else { + debounce(updateVarOnInput, AUTOSAVE_DELAY, event, true); + } + } + function renderValues() { for (const va of vars) { const value = isDefault(va) ? va.default : va.value; @@ -287,13 +301,18 @@ function configDialog(style) { const colorpicker = document.body.appendChild( $create('.colorpicker-popup', {style: 'display: none!important'})); + const PADDING = 50; const MIN_WIDTH = parseFloat(getComputedStyle(colorpicker).width) || 350; - const MIN_HEIGHT = 250; + const MIN_HEIGHT = 250 + PADDING; colorpicker.remove(); - width = Math.max(Math.min(width / 0.9 + 2, 800), MIN_WIDTH); - height = Math.max(Math.min(height / 0.9 + 2, 600), MIN_HEIGHT); + width = constrain(MIN_WIDTH, 800, width + PADDING); + height = constrain(MIN_HEIGHT, 600, height + PADDING); document.body.style.setProperty('min-width', width + 'px', 'important'); document.body.style.setProperty('min-height', height + 'px', 'important'); } + + function constrain(min, max, value) { + return value < min ? min : value > max ? max : value; + } } From 2deffbc6228fc8e1465f6d2357f347cac9211369 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 8 Dec 2017 03:23:09 +0300 Subject: [PATCH 05/10] show "x" to reset non-default values in usercss config individually also: * simplified CSS selectors where possible * .config-name = var name, 1st element in