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:
parent
b79e0b3d71
commit
85cade3fb2
5 changed files with 90 additions and 44 deletions
|
@ -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.
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue