From 60fc6f2456025c90573a30314b763c8f260d70b3 Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 11 Oct 2020 17:13:25 +0300 Subject: [PATCH] Editor fixes, make sectioned editor open quickly again (#1061) * make usercss editor full-height again * make sectioned editor open quickly again * remove leftovers * autofocus when add/clone button is clicked * don't fit to content on clicking the add button * scroll the window to show a manually added section entirely * autofocus on a manually added applies-to * disable Save button while loading * use standard CSS for a focused CodeMirror outline * trigger refresh sooner by one viewport in advance * declare refreshOnView as a standard function * run fixedHeader asynchronously to prevent self-triggering * account for header in compact mode when fitting to content * code cosmetics --- edit.html | 3 +- edit/codemirror-default.css | 10 +-- edit/edit.css | 17 +---- edit/edit.js | 2 +- edit/refresh-on-view.js | 29 -------- edit/sections-editor-section.js | 14 ++-- edit/sections-editor.js | 124 +++++++++++++++++--------------- 7 files changed, 76 insertions(+), 123 deletions(-) delete mode 100644 edit/refresh-on-view.js diff --git a/edit.html b/edit.html index 12614c99..b87396fe 100644 --- a/edit.html +++ b/edit.html @@ -91,7 +91,6 @@ - @@ -305,7 +304,7 @@
- +
diff --git a/edit/codemirror-default.css b/edit/codemirror-default.css index 6adbf962..0db190f4 100644 --- a/edit/codemirror-default.css +++ b/edit/codemirror-default.css @@ -15,8 +15,7 @@ -webkit-animation: highlight 3s cubic-bezier(.18, .02, 0, .94); } .CodeMirror-focused { - outline: -webkit-focus-ring-color auto 5px; - outline-offset: -2px; + outline: #7dadd9 auto 1px; /* not using the ring-color hack as it became ugly in new Chrome */ } .CodeMirror-bookmark { background: linear-gradient(to right, currentColor, transparent); @@ -24,13 +23,6 @@ width: 2em; opacity: .5; } -@supports (-moz-appearance:none) { - /* restrict to FF */ - .CodeMirror-focused { - outline: #7dadd9 auto 1px; - outline-offset: -1px; - } -} .CodeMirror-search-field { width: 10em; } diff --git a/edit/edit.css b/edit/edit.css index 3635fa61..8db215ba 100644 --- a/edit/edit.css +++ b/edit/edit.css @@ -63,6 +63,7 @@ label { #sections { padding-left: 280px; min-height: 0; + height: 100%; } #sections h2 { margin-top: 1rem; @@ -278,12 +279,6 @@ input:invalid { .section-editor .section:not(:first-child) { border-top: 2px solid hsl(0, 0%, 80%); } -.section-editor:not(.section-editor-ready) .section { - opacity: 0 !important; -} -.section-editor:not(.section-editor-ready) .CodeMirror { - height: 0; -} .add-section:after { content: attr(short-text); } @@ -817,13 +812,8 @@ body.linter-disabled .hidden-unless-compact { color: #888; } -/* FIXME: remove the ID selector */ -#sections .single-editor { +.single-editor { height: 100%; - margin: 0; - padding: 0; - display: flex; - box-sizing: border-box; } .single-editor .CodeMirror { @@ -843,7 +833,6 @@ body.linter-disabled .hidden-unless-compact { } .usercss.firefox #sections, -.usercss.firefox .single-editor, .usercss.firefox .CodeMirror { height: 100%; } @@ -996,7 +985,7 @@ body.linter-disabled .hidden-unless-compact { flex-direction: column; flex: 1; } - #sections > * { + #sections > :not(.single-editor) { margin: 0 .5rem; padding: .5rem 0; } diff --git a/edit/edit.js b/edit/edit.js index 7d3b49d4..b902a1e3 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -541,7 +541,7 @@ function detectLayout() { body.classList.add('fixed-header'); } }, 250); - window.addEventListener('scroll', fixedHeader); + window.addEventListener('scroll', fixedHeader, {passive: true}); } } else { body.classList.remove('compact-layout'); diff --git a/edit/refresh-on-view.js b/edit/refresh-on-view.js deleted file mode 100644 index ffbc0589..00000000 --- a/edit/refresh-on-view.js +++ /dev/null @@ -1,29 +0,0 @@ -/* global CodeMirror */ -/* -Initialization of the multi-sections editor is slow if there are many editors -e.g. https://github.com/openstyles/stylus/issues/178. So we only refresh the -editor when they were scroll into view. -*/ -'use strict'; - -CodeMirror.defineExtension('refreshOnView', function () { - const cm = this; - if (typeof IntersectionObserver === 'undefined') { - // uh - cm.isRefreshed = true; - cm.refresh(); - return; - } - const wrapper = cm.display.wrapper; - const observer = new IntersectionObserver(entries => { - for (const entry of entries) { - if (entry.isIntersecting) { - // wrapper.style.visibility = 'visible'; - cm.isRefreshed = true; - cm.refresh(); - observer.disconnect(); - } - } - }); - observer.observe(wrapper); -}); diff --git a/edit/sections-editor-section.js b/edit/sections-editor-section.js index 8885cfc6..2735ac2c 100644 --- a/edit/sections-editor-section.js +++ b/edit/sections-editor-section.js @@ -296,19 +296,14 @@ function createSection({ function insertApplyAfter(init, base) { const apply = createApply(init); - if (base) { - const index = appliesTo.indexOf(base); - appliesTo.splice(index + 1, 0, apply); - appliesToContainer.insertBefore(apply.el, base.el.nextSibling); - } else { - appliesTo.push(apply); - appliesToContainer.appendChild(apply.el); - } + appliesTo.splice(base ? appliesTo.indexOf(base) + 1 : appliesTo.length, 0, apply); + appliesToContainer.insertBefore(apply.el, base ? base.el.nextSibling : null); dirty.add(apply, apply); if (appliesTo.length > 1 && appliesTo[0].all) { removeApply(appliesTo[0]); } emitSectionChange(); + return apply; } function removeApply(apply) { @@ -380,7 +375,8 @@ function createSection({ } $('.add-applies-to', el).addEventListener('click', e => { e.preventDefault(); - insertApplyAfter({type, value: ''}, apply); + const newApply = insertApplyAfter({type, value: ''}, apply); + $('input', newApply.el).focus(); }); return apply; diff --git a/edit/sections-editor.js b/edit/sections-editor.js index bcf95148..8a04e06c 100644 --- a/edit/sections-editor.js +++ b/edit/sections-editor.js @@ -29,6 +29,9 @@ function createSectionsEditor({style, onTitleChanged}) { updateLivePreview(); }); + updateHeader(); + rerouteHotkeys(true); + $('#to-mozilla').addEventListener('click', showMozillaFormat); $('#to-mozilla-help').addEventListener('click', showToMozillaHelp); $('#from-mozilla').addEventListener('click', () => showMozillaFormatImport()); @@ -47,17 +50,22 @@ function createSectionsEditor({style, onTitleChanged}) { .forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete)); } - let sectionOrder = ''; - const initializing = new Promise(resolve => initSection({ - sections: style.sections.slice(), - done:() => { - dirty.clear(); - rerouteHotkeys(true); - resolve(); - updateHeader(); - sections.forEach(fitToContent); + const xo = window.IntersectionObserver && new IntersectionObserver(entries => { + for (const {isIntersecting, target} of entries) { + if (isIntersecting) { + target.CodeMirror.refresh(); + xo.unobserve(target); + } } - })); + }, {rootMargin: '100%'}); + const refreshOnView = (cm, force) => + force || !xo ? + cm.refresh() : + xo.observe(cm.display.wrapper); + + let sectionOrder = ''; + let headerOffset; // in compact mode the header is at the top so it reduces the available height + const initializing = initSections(style.sections.slice()); const livePreview = createLivePreview(); livePreview.show(Boolean(style.id)); @@ -83,28 +91,26 @@ function createSectionsEditor({style, onTitleChanged}) { }; function fitToContent(section) { - if (section.cm.isRefreshed) { + const {cm, cm: {display: {wrapper, sizer}}} = section; + if (cm.display.renderedView) { resize(); } else { - section.cm.on('update', resize); + cm.on('update', resize); } function resize() { - let contentHeight = section.el.querySelector('.CodeMirror-sizer').offsetHeight; - if (contentHeight < section.cm.defaultTextHeight()) { + let contentHeight = sizer.offsetHeight; + if (contentHeight < cm.defaultTextHeight()) { return; } - contentHeight += 9; // border & resize grip - section.cm.off('update', resize); - const cmHeight = section.cm.getWrapperElement().offsetHeight; - const maxHeight = cmHeight + window.innerHeight - section.el.offsetHeight; - section.cm.setSize(null, Math.min(contentHeight, maxHeight)); - if (sections.every(s => s.cm.isRefreshed)) { - fitToAvailableSpace(); + if (headerOffset == null) { + headerOffset = wrapper.getBoundingClientRect().top; } - setTimeout(() => { - container.classList.add('section-editor-ready'); - }, 50); + contentHeight += 9; // border & resize grip + cm.off('update', resize); + const cmHeight = wrapper.offsetHeight; + const maxHeight = (window.innerHeight - headerOffset) - (section.el.offsetHeight - cmHeight); + cm.setSize(null, Math.min(contentHeight, maxHeight)); } } @@ -367,7 +373,7 @@ function createSectionsEditor({style, onTitleChanged}) { if (replaceOldStyle) { return replaceSections(sections); } - return new Promise(resolve => initSection({sections, done: resolve, focusOn: false})); + return initSections(sections, {focusOn: false}); }) .then(() => { $('.dismiss').dispatchEvent(new Event('click')); @@ -472,36 +478,33 @@ function createSectionsEditor({style, onTitleChanged}) { livePreview.update(getModel()); } - function initSection({ - sections: originalSections, + function initSections(originalSections, { total = originalSections.length, focusOn = 0, - done - }) { - container.classList.add('hidden'); - chunk(); - - function chunk() { - if (!originalSections.length) { - setGlobalProgress(); - if (focusOn !== false) { - setTimeout(() => sections[focusOn].cm.focus()); - } - container.classList.remove('hidden'); - for (const section of sections) { - section.cm.refreshOnView(); - } - if (done) { - done(); - } - return; - } + } = {}) { + let done; + return new Promise(resolve => { + done = resolve; + chunk(true); + }); + function chunk(forceRefresh) { const t0 = performance.now(); while (originalSections.length && performance.now() - t0 < 100) { - insertSectionAfter(originalSections.shift()); + insertSectionAfter(originalSections.shift(), undefined, forceRefresh); + dirty.clear(); + if (focusOn !== false && sections[focusOn]) { + sections[focusOn].cm.focus(); + focusOn = false; + } } setGlobalProgress(total - originalSections.length, total); - setTimeout(chunk); + if (!originalSections.length) { + setGlobalProgress(); + fitToAvailableSpace(); + done(); + } else { + setTimeout(chunk); + } } } @@ -540,7 +543,7 @@ function createSectionsEditor({style, onTitleChanged}) { updateLivePreview(); } - function insertSectionAfter(init, base) { + function insertSectionAfter(init, base, forceRefresh) { if (!init) { init = {code: '', urlPrefixes: ['http://example.com']}; } @@ -557,15 +560,18 @@ function createSectionsEditor({style, onTitleChanged}) { prevEditor, nextEditor }); - if (base) { - const index = sections.indexOf(base); - sections.splice(index + 1, 0, section); - container.insertBefore(section.el, base.el.nextSibling); - } else { - sections.push(section); - container.appendChild(section.el); + const {cm} = section; + sections.splice(base ? sections.indexOf(base) + 1 : sections.length, 0, section); + container.insertBefore(section.el, base ? base.el.nextSibling : null); + refreshOnView(cm, forceRefresh); + if (!base || init.code) { + // Fit a) during startup or b) when the clone button is clicked on a section with some code + fitToContent(section); + } + if (base) { + cm.focus(); + setTimeout(scrollToEditor, 0, cm); } - section.render(); updateSectionOrder(); section.onChange(updateLivePreview); updateLivePreview(); @@ -599,7 +605,7 @@ function createSectionsEditor({style, onTitleChanged}) { } sections.length = 0; container.textContent = ''; - return new Promise(resolve => initSection({sections: originalSections, done: resolve})); + return initSections(originalSections); } function replaceStyle(newStyle, codeIsUpdated) {