Drastically speed up tag selector refresh with many tags
When refreshing, if fewer than 100 tags to show, just create them from scratch instead of updating the full set. Otherwise, remove the full set from DOM and add it back in after updates to avoid reflows (from #1204). There are various things that could be done to optimize this further (avoiding unnecessary sorting during full refreshes, calculating a hash of the full set and not updating it every time), but we should probably just replace it with @tnajdek's React version first. Closes #1204
This commit is contained in:
parent
8edd4b0523
commit
fe186333be
5 changed files with 113 additions and 61 deletions
|
@ -267,64 +267,64 @@
|
|||
fetch = true;
|
||||
}
|
||||
|
||||
var emptyColored = true;
|
||||
var emptyRegular = true;
|
||||
this._emptyRegular = true;
|
||||
var tagsBox = this.id('tags-box');
|
||||
|
||||
var tagColors = Zotero.Tags.getColors(this.libraryID);
|
||||
|
||||
// If new data, rebuild boxes
|
||||
// If new data, rebuild buttons
|
||||
if (fetch || this._dirty) {
|
||||
this._tags = yield Zotero.Tags.getAll(this.libraryID, this._types)
|
||||
.tap(() => Zotero.Promise.check(this.mode));
|
||||
tagsBox.textContent = "";
|
||||
|
||||
// Add colored tags that aren't already real tags
|
||||
let regularTags = new Set(this._tags.map(tag => tag.tag));
|
||||
let coloredTags = new Set(tagColors.keys());
|
||||
[for (x of coloredTags) if (!regularTags.has(x)) x].forEach(x =>
|
||||
this._tags.push(Zotero.Tags.cleanData({ tag: x }))
|
||||
);
|
||||
let { div, emptyRegular } = this.createTagsList(this._tags);
|
||||
|
||||
// Sort by name
|
||||
let collation = Zotero.getLocaleCollation();
|
||||
this._tags.sort(function (a, b) {
|
||||
return collation.compareString(1, a.tag, b.tag);
|
||||
});
|
||||
|
||||
var fragment = document.createDocumentFragment();
|
||||
let lastTag;
|
||||
for (let i = 0; i < this._tags.length; i++) {
|
||||
let tagData = this._tags[i];
|
||||
|
||||
// Only show tags of different types once
|
||||
if (tagData.tag === lastTag) {
|
||||
continue;
|
||||
}
|
||||
lastTag = tagData.tag;
|
||||
|
||||
let elem = this._insertClickableTag(fragment, tagData);
|
||||
let visible = this._updateClickableTag(
|
||||
elem, tagData.tag, tagColors
|
||||
);
|
||||
if (visible) {
|
||||
emptyRegular = false;
|
||||
}
|
||||
}
|
||||
tagsBox.appendChild(fragment);
|
||||
this._emptyRegular = emptyRegular;
|
||||
tagsBox.innerHTML = "";
|
||||
tagsBox.appendChild(div);
|
||||
this._dirty = false;
|
||||
this._tagsDiv = null;
|
||||
}
|
||||
// Otherwise just update based on visibility
|
||||
else {
|
||||
elems = tagsBox.childNodes;
|
||||
for (let i = 0; i < elems.length; i++) {
|
||||
let elem = elems[i];
|
||||
let visible = this._updateClickableTag(
|
||||
elem, elem.textContent, tagColors
|
||||
);
|
||||
if (visible) {
|
||||
emptyRegular = false;
|
||||
// If only a few tags, regenerate buttons from scratch
|
||||
if (Object.keys(this.scope).length <= 100) {
|
||||
// If full set is currently displayed, store it for later
|
||||
if (!this._tagsDiv) {
|
||||
this._tagsDiv = tagsBox.firstChild;
|
||||
}
|
||||
|
||||
let tags = [];
|
||||
for (let name in this.scope) {
|
||||
tags.push(...this.scope[name].map(type => {
|
||||
return {
|
||||
tag: name,
|
||||
type: type
|
||||
};
|
||||
}));
|
||||
}
|
||||
let { div, emptyRegular } = this.createTagsList(tags);
|
||||
tagsBox.replaceChild(div, tagsBox.firstChild);
|
||||
this._emptyRegular = emptyRegular;
|
||||
}
|
||||
// Otherwise swap in the stored buttons from the last full run,
|
||||
// after updating their visibility
|
||||
else {
|
||||
let oldDiv = tagsBox.removeChild(tagsBox.firstChild);
|
||||
if (!this._tagsDiv) {
|
||||
this._tagsDiv = oldDiv;
|
||||
}
|
||||
|
||||
let elems = this._tagsDiv.childNodes;
|
||||
let tagColors = Zotero.Tags.getColors(this.libraryID);
|
||||
for (let i = 0; i < elems.length; i++) {
|
||||
let elem = elems[i];
|
||||
let visible = this._updateClickableTag(
|
||||
elem, elem.textContent, tagColors
|
||||
);
|
||||
if (visible) {
|
||||
this._emptyRegular = false;
|
||||
}
|
||||
}
|
||||
tagsBox.appendChild(this._tagsDiv);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -397,9 +397,8 @@
|
|||
//end tag cloud code
|
||||
|
||||
this.updateNumSelected();
|
||||
var empty = this._emptyRegular = emptyRegular;
|
||||
// TODO: Show loading again when switching libraries/collections?
|
||||
this.id('tags-deck').selectedIndex = empty ? 1 : 2;
|
||||
this.id('tags-deck').selectedIndex = this._emptyRegular ? 1 : 2;
|
||||
|
||||
if (this.onRefresh) {
|
||||
this.onRefresh();
|
||||
|
@ -415,7 +414,53 @@
|
|||
</body>
|
||||
</method>
|
||||
|
||||
<method name="createTagsList">
|
||||
<parameter name="tags"/>
|
||||
<body><![CDATA[
|
||||
var tagColors = Zotero.Tags.getColors(this.libraryID);
|
||||
|
||||
// Add colored tags that aren't already real tags
|
||||
var regularTags = new Set(tags.map(tag => tag.tag));
|
||||
[...new Set(tagColors.keys())].filter(x => !regularTags.has(x)).forEach((x) => {
|
||||
tags.push(Zotero.Tags.cleanData({ tag: x }));
|
||||
});
|
||||
|
||||
// Sort by name
|
||||
var t = new Date();
|
||||
Zotero.debug("Sorting tags");
|
||||
var collation = Zotero.getLocaleCollation();
|
||||
tags.sort(function (a, b) {
|
||||
return collation.compareString(1, a.tag, b.tag);
|
||||
});
|
||||
Zotero.debug(`Sorted tags in ${new Date() - t} ms`);
|
||||
|
||||
var div = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
|
||||
var emptyRegular = true;
|
||||
var lastTag;
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
let tagData = tags[i];
|
||||
|
||||
// Only show tags of different types once
|
||||
if (tagData.tag === lastTag) {
|
||||
continue;
|
||||
}
|
||||
lastTag = tagData.tag;
|
||||
|
||||
let elem = this._insertClickableTag(div, tagData);
|
||||
let visible = this._updateClickableTag(
|
||||
elem, tagData.tag, tagColors
|
||||
);
|
||||
if (visible) {
|
||||
emptyRegular = false;
|
||||
}
|
||||
}
|
||||
return { div, emptyRegular };
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="insertSorted">
|
||||
<parameter name="tagsList"/>
|
||||
<parameter name="tagObjs"/>
|
||||
<body><![CDATA[
|
||||
var tagColors = Zotero.Tags.getColors(this._libraryID);
|
||||
|
@ -426,8 +471,7 @@
|
|||
});
|
||||
|
||||
// Create tag elements in sorted order
|
||||
var tagsBox = this.id('tags-box');
|
||||
var tagElems = tagsBox.childNodes;
|
||||
var tagElems = tagsList.childNodes;
|
||||
var j = 0;
|
||||
loop:
|
||||
for (let i = 0; i < tagObjs.length; i++) {
|
||||
|
@ -450,7 +494,7 @@
|
|||
}
|
||||
j++;
|
||||
}
|
||||
this._insertClickableTag(tagsBox, tagObj, tagElems[j]);
|
||||
this._insertClickableTag(tagsList, tagObj, tagElems[j]);
|
||||
this._updateClickableTag(
|
||||
tagElems[j], tagElems[j].textContent, tagColors
|
||||
);
|
||||
|
@ -553,7 +597,11 @@
|
|||
}.bind(this));
|
||||
|
||||
if (tagObjs.length) {
|
||||
this.insertSorted(tagObjs);
|
||||
this.insertSorted(this.id('tags-box').firstChild, tagObjs);
|
||||
// If full set isn't currently displayed, update it too
|
||||
if (this._tagsDiv) {
|
||||
this.insertSorted(this._tagsDiv, tagObjs);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Don't add anything for item or collection-item; just update scope
|
||||
|
|
|
@ -351,16 +351,16 @@ Zotero.CollectionTreeRow.prototype.getSearchObject = Zotero.Promise.coroutine(fu
|
|||
/**
|
||||
* Returns all the tags used by items in the current view
|
||||
*
|
||||
* @return {Promise}
|
||||
* @return {Promise<Object[]>}
|
||||
*/
|
||||
Zotero.CollectionTreeRow.prototype.getChildTags = Zotero.Promise.coroutine(function* () {
|
||||
switch (this.type) {
|
||||
// TODO: implement?
|
||||
case 'share':
|
||||
return false;
|
||||
return [];
|
||||
|
||||
case 'bucket':
|
||||
return false;
|
||||
return [];
|
||||
}
|
||||
var results = yield this.getSearchResults();
|
||||
return Zotero.Tags.getAllWithinItemsList(results);
|
||||
|
|
|
@ -171,7 +171,7 @@ Zotero.Tags = new function() {
|
|||
throw new Error("ids must be an array");
|
||||
}
|
||||
if (!ids.length) {
|
||||
return {};
|
||||
return [];
|
||||
}
|
||||
|
||||
var prefix = "SELECT DISTINCT name AS tag, type FROM itemTags "
|
||||
|
|
|
@ -16,13 +16,16 @@ groupbox
|
|||
}
|
||||
|
||||
#tags-box {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: -moz-field;
|
||||
}
|
||||
|
||||
#tags-box > div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
align-content: flex-start;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: -moz-field;
|
||||
}
|
||||
|
||||
#tags-box button {
|
||||
|
|
|
@ -48,6 +48,7 @@ describe("Tag Selector", function () {
|
|||
beforeEach(function* () {
|
||||
var libraryID = Zotero.Libraries.userLibraryID;
|
||||
yield clearTagColors(libraryID);
|
||||
yield doc.getElementById('zotero-tag-selector').refresh(true);
|
||||
})
|
||||
after(function () {
|
||||
win.close();
|
||||
|
@ -194,7 +195,7 @@ describe("Tag Selector", function () {
|
|||
var libraryID = Zotero.Libraries.userLibraryID;
|
||||
|
||||
var tagSelector = doc.getElementById('zotero-tag-selector');
|
||||
var tagElems = tagSelector.id('tags-box').childNodes;
|
||||
var tagElems = tagSelector.id('tags-box').getElementsByTagName('button');
|
||||
var count = tagElems.length;
|
||||
|
||||
yield Zotero.Tags.setColor(libraryID, "Top", '#AAAAAA');
|
||||
|
@ -228,7 +229,7 @@ describe("Tag Selector", function () {
|
|||
yield promise;
|
||||
|
||||
var tagSelector = doc.getElementById('zotero-tag-selector');
|
||||
var tagElems = tagSelector.id('tags-box').childNodes;
|
||||
var tagElems = tagSelector.id('tags-box').getElementsByTagName('button');
|
||||
|
||||
// Make sure the colored tags are still in the right position
|
||||
var tags = new Map();
|
||||
|
|
Loading…
Reference in a new issue