Make preferences less janky, preload panes on hover, allow panes to delay visibility until promise resolves (#3363)

Prevents flashes of unlocalized labels and controls without values set.
Makes switching panes feel speedier overall because of preloading.

I thought there was an issue for the flashes of uninitialized content but can't
find it now.
This commit is contained in:
Abe Jellinek 2023-08-26 05:57:38 -04:00 committed by GitHub
parent b79e0b3d71
commit 85cade3fb2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 90 additions and 44 deletions

View file

@ -37,7 +37,8 @@ var Zotero_Preferences = {
this.content = document.getElementById('prefs-content'); this.content = document.getElementById('prefs-content');
this.helpContainer = document.getElementById('prefs-help-container'); this.helpContainer = document.getElementById('prefs-help-container');
this.navigation.addEventListener('select', () => this._onNavigationSelect()); this.navigation.addEventListener('mouseover', event => this._handleNavigationMouseOver(event));
this.navigation.addEventListener('select', () => this._handleNavigationSelect());
document.getElementById('prefs-search').addEventListener('command', document.getElementById('prefs-search').addEventListener('command',
event => this._search(event.target.value)); event => this._search(event.target.value));
@ -124,18 +125,28 @@ var Zotero_Preferences = {
} }
}, },
_onNavigationSelect() { async _handleNavigationMouseOver(event) {
for (let child of this.content.children) { if (event.target.tagName === 'richlistitem') {
if (child !== this.helpContainer) { await this._loadPane(event.target.value);
child.setAttribute('hidden', true);
}
} }
},
async _handleNavigationSelect() {
let paneID = this.navigation.value; let paneID = this.navigation.value;
if (paneID) { if (paneID) {
this.content.scrollTop = 0; let pane = this.panes.get(paneID);
document.getElementById('prefs-search').value = ''; document.getElementById('prefs-search').value = '';
this._search(''); await this._search('');
this._loadAndDisplayPane(paneID); await this._showPane(paneID);
this.content.scrollTop = 0;
for (let child of this.content.children) {
if (child !== this.helpContainer && child !== pane.container) {
child.hidden = true;
}
}
this.helpContainer.hidden = !pane.helpURL;
document.getElementById('prefs-subpane-back-button').hidden = !pane.parent;
} }
Zotero.Prefs.set('lastSelectedPrefPane', paneID); Zotero.Prefs.set('lastSelectedPrefPane', paneID);
}, },
@ -201,20 +212,31 @@ var Zotero_Preferences = {
this.panes.set(id, { this.panes.set(id, {
...options, ...options,
imported: false, loaded: false,
container, container,
}); });
}, },
/** /**
* Display a pane's content, alongside any other panes already showing. * Load a pane if not already loaded.
* If the pane is not yet loaded, it will be loaded first.
* *
* @param {String} id * @param {String} id
* @return {Promise<void>}
*/ */
_loadAndDisplayPane(id) { async _loadPane(id) {
let pane = this.panes.get(id); let pane = this.panes.get(id);
if (!pane.imported) { if (pane.loaded) {
return;
}
if (pane.loadPromise) {
await pane.loadPromise;
return;
}
let rest = async () => {
// Hack - make sure the following code does not run synchronously so we can set loadPromise immediately
await Zotero.Promise.delay();
if (pane.scripts) { if (pane.scripts) {
for (let script of pane.scripts) { for (let script of pane.scripts) {
Services.scriptloader.loadSubScript(script, window); Services.scriptloader.loadSubScript(script, window);
@ -237,17 +259,34 @@ var Zotero_Preferences = {
? MozXULElement.parseXULToFragment(markup, dtdFiles) ? MozXULElement.parseXULToFragment(markup, dtdFiles)
: this._parseXHTMLToFragment(markup, dtdFiles); : this._parseXHTMLToFragment(markup, dtdFiles);
contentFragment = document.importNode(contentFragment, true); contentFragment = document.importNode(contentFragment, true);
this._initImportedNodesPreInsert(contentFragment); this._initImportedNodesPreInsert(contentFragment);
pane.container.append(contentFragment); pane.container.append(contentFragment);
pane.imported = true;
this._initImportedNodesPostInsert(pane.container); await document.l10n.ready;
} await document.l10n.translateFragment(pane.container);
await this._initImportedNodesPostInsert(pane.container);
pane.loaded = true;
};
pane.loadPromise = rest();
await pane.loadPromise;
},
/**
* Display a pane's content, alongside any other panes already showing.
* If the pane is not yet loaded, it will be loaded first.
*
* @param {String} id
* @return {Promise<void>}
*/
async _showPane(id) {
await this._loadPane(id);
let pane = this.panes.get(id);
pane.container.hidden = false; pane.container.hidden = false;
pane.container.children[0].dispatchEvent(new Event('showing')); pane.container.children[0].dispatchEvent(new Event('showing'));
let backButton = document.getElementById('prefs-subpane-back-button');
backButton.hidden = !pane.parent;
}, },
_parseXHTMLToFragment(str, entities = []) { _parseXHTMLToFragment(str, entities = []) {
@ -334,10 +373,10 @@ ${str}
* @param {Element} container * @param {Element} container
* @private * @private
*/ */
_initImportedNodesPostInsert(container) { async _initImportedNodesPostInsert(container) {
let attachToPreference = (elem) => { let attachToPreference = (elem) => {
if (this._observerSymbols.has(elem)) { if (this._observerSymbols.has(elem)) {
return; return Promise.resolve();
} }
let preference = elem.getAttribute('preference'); let preference = elem.getAttribute('preference');
@ -382,9 +421,10 @@ ${str}
elem.addEventListener('change', this._syncToPrefOnModify.bind(this)); elem.addEventListener('change', this._syncToPrefOnModify.bind(this));
// Set timeout before populating the value so the pane can add listeners first // Set timeout before populating the value so the pane can add listeners first
setTimeout(() => { return new Promise(resolve => setTimeout(() => {
this._syncFromPref(elem, elem.getAttribute('preference')); this._syncFromPref(elem, elem.getAttribute('preference'));
}); resolve();
}));
}; };
let detachFromPreference = (elem) => { let detachFromPreference = (elem) => {
@ -395,9 +435,13 @@ ${str}
} }
}; };
let awaitBeforeShowing = [];
// Activate `preference` attributes // Activate `preference` attributes
// Do not await anything between here and the 'load' event dispatch below! That would cause 'syncfrompreference'
// events to be fired before 'load'!
for (let elem of container.querySelectorAll('[preference]')) { for (let elem of container.querySelectorAll('[preference]')) {
attachToPreference(elem); awaitBeforeShowing.push(attachToPreference(elem));
} }
new MutationObserver((mutations) => { new MutationObserver((mutations) => {
@ -406,6 +450,7 @@ ${str}
let target = mutation.target; let target = mutation.target;
detachFromPreference(target); detachFromPreference(target);
if (target.hasAttribute('preference')) { if (target.hasAttribute('preference')) {
// Don't bother awaiting these
attachToPreference(target); attachToPreference(target);
} }
} }
@ -443,9 +488,15 @@ ${str}
} }
for (let child of container.children) { for (let child of container.children) {
child.dispatchEvent(new Event('load')); let event = new Event('load');
event.waitUntil = (promise) => {
awaitBeforeShowing.push(promise);
};
child.dispatchEvent(event);
} }
await Promise.allSettled(awaitBeforeShowing);
// If this is the first pane to be loaded, notify anyone waiting // If this is the first pane to be loaded, notify anyone waiting
// (for tests) // (for tests)
this._firstPaneLoadDeferred.resolve(); this._firstPaneLoadDeferred.resolve();
@ -461,7 +512,7 @@ ${str}
* *
* @param {String} [term] * @param {String} [term]
*/ */
_search(term) { _search: Zotero.Utilities.Internal.serial(async function (term) {
// Initial housekeeping: // Initial housekeeping:
// Clear existing highlights // Clear existing highlights
@ -480,11 +531,13 @@ ${str}
hidden.ariaHidden = false; hidden.ariaHidden = false;
} }
// Hide help button by default - _handleNavigationSelect() will show it when appropriate
this.helpContainer.hidden = true;
if (!term) { if (!term) {
if (this.navigation.selectedIndex == -1) { if (this.navigation.selectedIndex == -1) {
this.navigation.selectedIndex = 0; this.navigation.selectedIndex = 0;
} }
this.helpContainer.hidden = !this.panes.get(this.navigation.value)?.helpURL;
return; return;
} }
@ -500,7 +553,7 @@ ${str}
pane.container.hidden = true; pane.container.hidden = true;
} }
else { else {
this._loadAndDisplayPane(id); await this._showPane(id);
} }
} }
@ -580,7 +633,7 @@ ${str}
} }
} }
} }
}, }),
/** /**
* Search for the given term (case-insensitive) in the tree. * Search for the given term (case-insensitive) in the tree.

View file

@ -23,7 +23,7 @@
***** END LICENSE BLOCK ***** ***** END LICENSE BLOCK *****
--> -->
<vbox id="zotero-prefpane-cite" onload="Zotero_Preferences.Cite.init()"> <vbox id="zotero-prefpane-cite" onload="event.waitUntil(Zotero_Preferences.Cite.init())">
<vbox class="main-section" id="styles"> <vbox class="main-section" id="styles">
<html:h1>&zotero.preferences.cite.styles;</html:h1> <html:h1>&zotero.preferences.cite.styles;</html:h1>

View file

@ -31,11 +31,11 @@ var VirtualizedTable = require('components/virtualized-table');
var { makeRowRenderer } = VirtualizedTable; var { makeRowRenderer } = VirtualizedTable;
Zotero_Preferences.Export = { Zotero_Preferences.Export = {
init: Zotero.Promise.coroutine(function* () { init: async function () {
this.updateQuickCopyInstructions(); this.updateQuickCopyInstructions();
yield this.populateQuickCopyList(); await this.populateQuickCopyList();
yield this.populateNoteQuickCopyList(); await this.populateNoteQuickCopyList();
}), },
getQuickCopyTranslators: async function () { getQuickCopyTranslators: async function () {

View file

@ -23,7 +23,7 @@
***** END LICENSE BLOCK ***** ***** END LICENSE BLOCK *****
--> -->
<vbox id="zotero-prefpane-export" onload="Zotero_Preferences.Export.init()"> <vbox id="zotero-prefpane-export" onload="event.waitUntil(Zotero_Preferences.Export.init())">
<groupbox id="zotero-prefpane-export-groupbox"> <groupbox id="zotero-prefpane-export-groupbox">
<vbox> <vbox>
<label><html:h2>&zotero.preferences.quickCopy.caption;</html:h2></label> <label><html:h2>&zotero.preferences.quickCopy.caption;</html:h2></label>

View file

@ -113,14 +113,7 @@ var loadPrefPane = async function (paneName) {
var win = await loadWindow("chrome://zotero/content/preferences/preferences.xhtml", { var win = await loadWindow("chrome://zotero/content/preferences/preferences.xhtml", {
pane: id pane: id
}); });
var doc = win.document; await win.Zotero_Preferences.waitForFirstPaneLoad();
while (true) {
var pane = doc.getElementById(id);
if (pane) {
break;
}
await delay(1);
}
return win; return win;
}; };