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
This commit is contained in:
tophf 2020-10-11 17:13:25 +03:00 committed by GitHub
parent ad24ee0c15
commit 60fc6f2456
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 76 additions and 123 deletions

View File

@ -91,7 +91,6 @@
<script src="edit/colorpicker-helper.js"></script> <script src="edit/colorpicker-helper.js"></script>
<script src="edit/beautify.js"></script> <script src="edit/beautify.js"></script>
<script src="edit/show-keymap-help.js"></script> <script src="edit/show-keymap-help.js"></script>
<script src="edit/refresh-on-view.js"></script>
<script src="edit/codemirror-themes.js"></script> <script src="edit/codemirror-themes.js"></script>
<script src="edit/source-editor.js"></script> <script src="edit/source-editor.js"></script>
@ -305,7 +304,7 @@
</section> </section>
<section id="actions"> <section id="actions">
<div> <div>
<button id="save-button" i18n-text="styleSaveLabel" data-hotkey-tooltip="save"></button> <button id="save-button" i18n-text="styleSaveLabel" data-hotkey-tooltip="save" disabled></button>
<button id="beautify" i18n-text="styleBeautify"></button> <button id="beautify" i18n-text="styleBeautify"></button>
<a href="manage.html" tabindex="-1"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a> <a href="manage.html" tabindex="-1"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
</div> </div>

View File

@ -15,8 +15,7 @@
-webkit-animation: highlight 3s cubic-bezier(.18, .02, 0, .94); -webkit-animation: highlight 3s cubic-bezier(.18, .02, 0, .94);
} }
.CodeMirror-focused { .CodeMirror-focused {
outline: -webkit-focus-ring-color auto 5px; outline: #7dadd9 auto 1px; /* not using the ring-color hack as it became ugly in new Chrome */
outline-offset: -2px;
} }
.CodeMirror-bookmark { .CodeMirror-bookmark {
background: linear-gradient(to right, currentColor, transparent); background: linear-gradient(to right, currentColor, transparent);
@ -24,13 +23,6 @@
width: 2em; width: 2em;
opacity: .5; opacity: .5;
} }
@supports (-moz-appearance:none) {
/* restrict to FF */
.CodeMirror-focused {
outline: #7dadd9 auto 1px;
outline-offset: -1px;
}
}
.CodeMirror-search-field { .CodeMirror-search-field {
width: 10em; width: 10em;
} }

View File

@ -63,6 +63,7 @@ label {
#sections { #sections {
padding-left: 280px; padding-left: 280px;
min-height: 0; min-height: 0;
height: 100%;
} }
#sections h2 { #sections h2 {
margin-top: 1rem; margin-top: 1rem;
@ -278,12 +279,6 @@ input:invalid {
.section-editor .section:not(:first-child) { .section-editor .section:not(:first-child) {
border-top: 2px solid hsl(0, 0%, 80%); 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 { .add-section:after {
content: attr(short-text); content: attr(short-text);
} }
@ -817,13 +812,8 @@ body.linter-disabled .hidden-unless-compact {
color: #888; color: #888;
} }
/* FIXME: remove the ID selector */ .single-editor {
#sections .single-editor {
height: 100%; height: 100%;
margin: 0;
padding: 0;
display: flex;
box-sizing: border-box;
} }
.single-editor .CodeMirror { .single-editor .CodeMirror {
@ -843,7 +833,6 @@ body.linter-disabled .hidden-unless-compact {
} }
.usercss.firefox #sections, .usercss.firefox #sections,
.usercss.firefox .single-editor,
.usercss.firefox .CodeMirror { .usercss.firefox .CodeMirror {
height: 100%; height: 100%;
} }
@ -996,7 +985,7 @@ body.linter-disabled .hidden-unless-compact {
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
} }
#sections > * { #sections > :not(.single-editor) {
margin: 0 .5rem; margin: 0 .5rem;
padding: .5rem 0; padding: .5rem 0;
} }

View File

@ -541,7 +541,7 @@ function detectLayout() {
body.classList.add('fixed-header'); body.classList.add('fixed-header');
} }
}, 250); }, 250);
window.addEventListener('scroll', fixedHeader); window.addEventListener('scroll', fixedHeader, {passive: true});
} }
} else { } else {
body.classList.remove('compact-layout'); body.classList.remove('compact-layout');

View File

@ -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);
});

View File

@ -296,19 +296,14 @@ function createSection({
function insertApplyAfter(init, base) { function insertApplyAfter(init, base) {
const apply = createApply(init); const apply = createApply(init);
if (base) { appliesTo.splice(base ? appliesTo.indexOf(base) + 1 : appliesTo.length, 0, apply);
const index = appliesTo.indexOf(base); appliesToContainer.insertBefore(apply.el, base ? base.el.nextSibling : null);
appliesTo.splice(index + 1, 0, apply);
appliesToContainer.insertBefore(apply.el, base.el.nextSibling);
} else {
appliesTo.push(apply);
appliesToContainer.appendChild(apply.el);
}
dirty.add(apply, apply); dirty.add(apply, apply);
if (appliesTo.length > 1 && appliesTo[0].all) { if (appliesTo.length > 1 && appliesTo[0].all) {
removeApply(appliesTo[0]); removeApply(appliesTo[0]);
} }
emitSectionChange(); emitSectionChange();
return apply;
} }
function removeApply(apply) { function removeApply(apply) {
@ -380,7 +375,8 @@ function createSection({
} }
$('.add-applies-to', el).addEventListener('click', e => { $('.add-applies-to', el).addEventListener('click', e => {
e.preventDefault(); e.preventDefault();
insertApplyAfter({type, value: ''}, apply); const newApply = insertApplyAfter({type, value: ''}, apply);
$('input', newApply.el).focus();
}); });
return apply; return apply;

View File

@ -29,6 +29,9 @@ function createSectionsEditor({style, onTitleChanged}) {
updateLivePreview(); updateLivePreview();
}); });
updateHeader();
rerouteHotkeys(true);
$('#to-mozilla').addEventListener('click', showMozillaFormat); $('#to-mozilla').addEventListener('click', showMozillaFormat);
$('#to-mozilla-help').addEventListener('click', showToMozillaHelp); $('#to-mozilla-help').addEventListener('click', showToMozillaHelp);
$('#from-mozilla').addEventListener('click', () => showMozillaFormatImport()); $('#from-mozilla').addEventListener('click', () => showMozillaFormatImport());
@ -47,17 +50,22 @@ function createSectionsEditor({style, onTitleChanged}) {
.forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete)); .forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete));
} }
let sectionOrder = ''; const xo = window.IntersectionObserver && new IntersectionObserver(entries => {
const initializing = new Promise(resolve => initSection({ for (const {isIntersecting, target} of entries) {
sections: style.sections.slice(), if (isIntersecting) {
done:() => { target.CodeMirror.refresh();
dirty.clear(); xo.unobserve(target);
rerouteHotkeys(true);
resolve();
updateHeader();
sections.forEach(fitToContent);
} }
})); }
}, {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(); const livePreview = createLivePreview();
livePreview.show(Boolean(style.id)); livePreview.show(Boolean(style.id));
@ -83,28 +91,26 @@ function createSectionsEditor({style, onTitleChanged}) {
}; };
function fitToContent(section) { function fitToContent(section) {
if (section.cm.isRefreshed) { const {cm, cm: {display: {wrapper, sizer}}} = section;
if (cm.display.renderedView) {
resize(); resize();
} else { } else {
section.cm.on('update', resize); cm.on('update', resize);
} }
function resize() { function resize() {
let contentHeight = section.el.querySelector('.CodeMirror-sizer').offsetHeight; let contentHeight = sizer.offsetHeight;
if (contentHeight < section.cm.defaultTextHeight()) { if (contentHeight < cm.defaultTextHeight()) {
return; return;
} }
contentHeight += 9; // border & resize grip if (headerOffset == null) {
section.cm.off('update', resize); headerOffset = wrapper.getBoundingClientRect().top;
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();
} }
setTimeout(() => { contentHeight += 9; // border & resize grip
container.classList.add('section-editor-ready'); cm.off('update', resize);
}, 50); 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) { if (replaceOldStyle) {
return replaceSections(sections); return replaceSections(sections);
} }
return new Promise(resolve => initSection({sections, done: resolve, focusOn: false})); return initSections(sections, {focusOn: false});
}) })
.then(() => { .then(() => {
$('.dismiss').dispatchEvent(new Event('click')); $('.dismiss').dispatchEvent(new Event('click'));
@ -472,38 +478,35 @@ function createSectionsEditor({style, onTitleChanged}) {
livePreview.update(getModel()); livePreview.update(getModel());
} }
function initSection({ function initSections(originalSections, {
sections: originalSections,
total = originalSections.length, total = originalSections.length,
focusOn = 0, focusOn = 0,
done } = {}) {
}) { let done;
container.classList.add('hidden'); return new Promise(resolve => {
chunk(); done = resolve;
chunk(true);
function chunk() { });
if (!originalSections.length) { function chunk(forceRefresh) {
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;
}
const t0 = performance.now(); const t0 = performance.now();
while (originalSections.length && performance.now() - t0 < 100) { 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); setGlobalProgress(total - originalSections.length, total);
if (!originalSections.length) {
setGlobalProgress();
fitToAvailableSpace();
done();
} else {
setTimeout(chunk); setTimeout(chunk);
} }
} }
}
function removeSection(section) { function removeSection(section) {
if (sections.every(s => s.isRemoved() || s === section)) { if (sections.every(s => s.isRemoved() || s === section)) {
@ -540,7 +543,7 @@ function createSectionsEditor({style, onTitleChanged}) {
updateLivePreview(); updateLivePreview();
} }
function insertSectionAfter(init, base) { function insertSectionAfter(init, base, forceRefresh) {
if (!init) { if (!init) {
init = {code: '', urlPrefixes: ['http://example.com']}; init = {code: '', urlPrefixes: ['http://example.com']};
} }
@ -557,15 +560,18 @@ function createSectionsEditor({style, onTitleChanged}) {
prevEditor, prevEditor,
nextEditor nextEditor
}); });
if (base) { const {cm} = section;
const index = sections.indexOf(base); sections.splice(base ? sections.indexOf(base) + 1 : sections.length, 0, section);
sections.splice(index + 1, 0, section); container.insertBefore(section.el, base ? base.el.nextSibling : null);
container.insertBefore(section.el, base.el.nextSibling); refreshOnView(cm, forceRefresh);
} else { if (!base || init.code) {
sections.push(section); // Fit a) during startup or b) when the clone button is clicked on a section with some code
container.appendChild(section.el); fitToContent(section);
}
if (base) {
cm.focus();
setTimeout(scrollToEditor, 0, cm);
} }
section.render();
updateSectionOrder(); updateSectionOrder();
section.onChange(updateLivePreview); section.onChange(updateLivePreview);
updateLivePreview(); updateLivePreview();
@ -599,7 +605,7 @@ function createSectionsEditor({style, onTitleChanged}) {
} }
sections.length = 0; sections.length = 0;
container.textContent = ''; container.textContent = '';
return new Promise(resolve => initSection({sections: originalSections, done: resolve})); return initSections(originalSections);
} }
function replaceStyle(newStyle, codeIsUpdated) { function replaceStyle(newStyle, codeIsUpdated) {