mirror of https://github.com/keeweb/keeweb.git
fix #1580: high memory usage on large files, virtual scrolling of entries
This commit is contained in:
parent
53b5715da0
commit
fddf2892cd
|
@ -251,7 +251,6 @@ class View extends EventEmitter {
|
|||
FocusManager.setModal(null);
|
||||
}
|
||||
}
|
||||
this.emit(visible ? 'show' : 'hide');
|
||||
if (this.el) {
|
||||
this.el.classList.toggle('show', !!visible);
|
||||
this.el.classList.toggle('hide', !visible);
|
||||
|
@ -259,6 +258,7 @@ class View extends EventEmitter {
|
|||
Tip.hideTips(this.el);
|
||||
}
|
||||
}
|
||||
this.emit(visible ? 'show' : 'hide');
|
||||
}
|
||||
|
||||
isHidden() {
|
||||
|
|
|
@ -202,7 +202,6 @@ class AppView extends View {
|
|||
this.views.menu.show();
|
||||
this.views.menuDrag.$el.parent().show();
|
||||
this.views.listWrap.show();
|
||||
this.views.list.show();
|
||||
this.views.listDrag.show();
|
||||
this.views.details.show();
|
||||
this.views.footer.show();
|
||||
|
@ -211,6 +210,8 @@ class AppView extends View {
|
|||
this.hideSettings();
|
||||
this.hideKeyChange();
|
||||
this.hideImportCsv();
|
||||
|
||||
this.views.list.show();
|
||||
}
|
||||
|
||||
hideOpenFile() {
|
||||
|
|
|
@ -68,12 +68,20 @@ class ListView extends View {
|
|||
this.readTableColumnsEnabled();
|
||||
|
||||
this.items = new SearchResultCollection();
|
||||
this.renderedItems = new Map();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isVisible()) {
|
||||
this.pendingRender = true;
|
||||
return;
|
||||
}
|
||||
this.pendingRender = false;
|
||||
|
||||
if (!this.itemsEl) {
|
||||
super.render();
|
||||
this.itemsEl = this.$el.find('.list__items>.scroller');
|
||||
this.itemsEl.on('scroll', () => this.renderVisibleItems());
|
||||
this.views.search.render();
|
||||
this.setTableView();
|
||||
|
||||
|
@ -84,9 +92,9 @@ class ListView extends View {
|
|||
});
|
||||
}
|
||||
if (this.items.length) {
|
||||
const itemTemplate = this.getItemTemplate();
|
||||
const itemsTemplate = this.getItemsTemplate();
|
||||
const noColor = AppSettingsModel.colorfulIcons ? '' : 'grayscale';
|
||||
|
||||
const presenter = new EntryPresenter(
|
||||
this.getDescField(),
|
||||
noColor,
|
||||
|
@ -99,16 +107,28 @@ class ListView extends View {
|
|||
}
|
||||
});
|
||||
presenter.columns = columns;
|
||||
let itemsHtml = '';
|
||||
this.items.forEach((item) => {
|
||||
presenter.present(item);
|
||||
itemsHtml += itemTemplate(presenter, DefaultTemplateOptions);
|
||||
}, this);
|
||||
this.presenter = presenter;
|
||||
|
||||
presenter.present(this.items[0]);
|
||||
const itemTemplate = this.getItemTemplate();
|
||||
const itemsHtml = itemTemplate(presenter, DefaultTemplateOptions);
|
||||
|
||||
const html = itemsTemplate(
|
||||
{ itemsHtml, columns: this.tableColumns },
|
||||
DefaultTemplateOptions
|
||||
);
|
||||
this.itemsEl.html(html);
|
||||
this.itemsContainerEl = this.itemsEl.find('.list__items-container:first')[0];
|
||||
|
||||
const firstListItem = this.itemsContainerEl.firstElementChild;
|
||||
this.itemHeight = firstListItem.getBoundingClientRect().height;
|
||||
|
||||
this.renderedItems = new Map([[0, firstListItem]]);
|
||||
|
||||
const totalHeight = this.itemHeight * this.items.length;
|
||||
this.itemsContainerEl.style.minHeight = totalHeight + 'px';
|
||||
|
||||
this.renderVisibleItems();
|
||||
} else {
|
||||
this.itemsEl.html(this.emptyTemplate({}, DefaultTemplateOptions));
|
||||
}
|
||||
|
@ -117,16 +137,12 @@ class ListView extends View {
|
|||
|
||||
getItemsTemplate() {
|
||||
if (this.model.settings.tableView) {
|
||||
return require('templates/list-table.hbs');
|
||||
return require('templates/list-mode-table.hbs');
|
||||
} else {
|
||||
return this.renderPlainItems;
|
||||
return require('templates/list-mode-list.hbs');
|
||||
}
|
||||
}
|
||||
|
||||
renderPlainItems(data) {
|
||||
return data.itemsHtml;
|
||||
}
|
||||
|
||||
getItemTemplate() {
|
||||
if (this.model.settings.tableView) {
|
||||
return require('templates/list-item-table.hbs');
|
||||
|
@ -135,6 +151,88 @@ class ListView extends View {
|
|||
}
|
||||
}
|
||||
|
||||
renderVisibleItems() {
|
||||
if (!this.isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollEl = this.itemsEl[0];
|
||||
const rect = scrollEl.getBoundingClientRect();
|
||||
|
||||
const pxTop = scrollEl.scrollTop;
|
||||
const pxHeight = rect.height;
|
||||
const itemHeight = this.itemHeight;
|
||||
const renderedItems = this.renderedItems;
|
||||
|
||||
let firstIx = Math.max(0, Math.floor(pxTop / itemHeight));
|
||||
let lastIx = Math.min(this.items.length - 1, Math.ceil((pxTop + pxHeight) / itemHeight));
|
||||
|
||||
const visibleCount = lastIx - firstIx;
|
||||
firstIx = Math.max(0, firstIx - visibleCount);
|
||||
lastIx = Math.min(this.items.length - 1, lastIx + visibleCount);
|
||||
|
||||
const itemTemplate = this.getItemTemplate();
|
||||
const presenter = this.presenter;
|
||||
|
||||
let itemsHtml = '';
|
||||
const renderedIndices = [];
|
||||
|
||||
for (let ix = firstIx; ix <= lastIx; ix++) {
|
||||
const item = this.items[ix];
|
||||
if (renderedItems.has(ix)) {
|
||||
continue;
|
||||
}
|
||||
presenter.present(item);
|
||||
itemsHtml += itemTemplate(presenter, DefaultTemplateOptions);
|
||||
renderedIndices.push(ix);
|
||||
}
|
||||
|
||||
const tempEl = document.createElement('div');
|
||||
tempEl.innerHTML = itemsHtml;
|
||||
const renderedElements = [...tempEl.children];
|
||||
|
||||
for (let i = 0; i < renderedElements.length; i++) {
|
||||
const el = renderedElements[i];
|
||||
const ix = renderedIndices[i];
|
||||
this.itemsContainerEl.append(el);
|
||||
el.style.top = ix * itemHeight + 'px';
|
||||
renderedItems.set(ix, el);
|
||||
}
|
||||
|
||||
const maxRenderedItems = visibleCount * 5;
|
||||
|
||||
if (renderedItems.size > maxRenderedItems) {
|
||||
for (const [ix, el] of this.renderedItems) {
|
||||
if (ix < firstIx || ix > lastIx) {
|
||||
el.remove();
|
||||
renderedItems.delete(ix);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ensureItemRendered(ix) {
|
||||
if (this.renderedItems.has(ix)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = this.items[ix];
|
||||
const itemTemplate = this.getItemTemplate();
|
||||
|
||||
this.presenter.present(item);
|
||||
const itemHtml = itemTemplate(this.presenter, DefaultTemplateOptions);
|
||||
|
||||
const tempEl = document.createElement('div');
|
||||
tempEl.innerHTML = itemHtml;
|
||||
|
||||
const [el] = tempEl.children;
|
||||
|
||||
this.itemsContainerEl.append(el);
|
||||
el.style.top = ix * this.itemHeight + 'px';
|
||||
|
||||
this.renderedItems.set(ix, el);
|
||||
}
|
||||
|
||||
getDescField() {
|
||||
return this.model.sort.replace('-', '');
|
||||
}
|
||||
|
@ -203,7 +301,12 @@ class ListView extends View {
|
|||
}
|
||||
|
||||
selectItem(item) {
|
||||
this.presenter.activeEntryId = item.id;
|
||||
this.model.activeEntryId = item.id;
|
||||
|
||||
const ix = this.items.indexOf(item);
|
||||
this.ensureItemRendered(ix);
|
||||
|
||||
Events.emit('entry-selected', item);
|
||||
this.itemsEl.find('.list__item--active').removeClass('list__item--active');
|
||||
const itemEl = document.getElementById(item.id);
|
||||
|
@ -220,6 +323,9 @@ class ListView extends View {
|
|||
|
||||
viewShown() {
|
||||
this.views.search.show();
|
||||
if (this.pendingRender) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
viewHidden() {
|
||||
|
@ -248,6 +354,7 @@ class ListView extends View {
|
|||
viewResized(size) {
|
||||
this.setSize(size);
|
||||
this.throttleSetViewSizeSetting(size);
|
||||
this.renderVisibleItems();
|
||||
}
|
||||
|
||||
throttleSetViewSizeSetting = throttle((size) => {
|
||||
|
@ -256,6 +363,7 @@ class ListView extends View {
|
|||
|
||||
filterChanged(filter) {
|
||||
this.items = filter.entries;
|
||||
this.renderedItems = new Map();
|
||||
this.render();
|
||||
}
|
||||
|
||||
|
|
|
@ -119,20 +119,29 @@
|
|||
}
|
||||
|
||||
&__table {
|
||||
border-collapse: collapse;
|
||||
width: calc(100% - 2px);
|
||||
td,
|
||||
th {
|
||||
&-body {
|
||||
position: relative;
|
||||
}
|
||||
&-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
&-cell {
|
||||
padding: $base-padding;
|
||||
text-align: left;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
flex-basis: 0;
|
||||
&:first-child {
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
width: 3em;
|
||||
flex-basis: 3em;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
th:first-child {
|
||||
padding: 0;
|
||||
width: 3em;
|
||||
}
|
||||
&-options {
|
||||
@include icon-btn();
|
||||
cursor: pointer;
|
||||
|
@ -140,10 +149,13 @@
|
|||
}
|
||||
|
||||
&__item {
|
||||
padding: $base-padding;
|
||||
left: 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
box-sizing: border-box;
|
||||
@include nomobile {
|
||||
@include area-selectable();
|
||||
&--active,
|
||||
|
@ -163,7 +175,9 @@
|
|||
@include nomobile {
|
||||
border-radius: var(--block-border-radius);
|
||||
}
|
||||
padding: $base-padding;
|
||||
margin: 0 $small-spacing;
|
||||
width: calc(100% - #{$small-spacing * 2});
|
||||
}
|
||||
|
||||
&--expired {
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
<tr class="list__item list__item--table {{#if active}}list__item--active{{/if}} {{#if expired}}list__item--expired{{/if}}" id="{{id}}" draggable="true">
|
||||
<td>
|
||||
<div class="list__item list__item--table list__table-row {{#if active}}list__item--active{{/if}} {{#if expired}}list__item--expired{{/if}}" id="{{id}}" draggable="true">
|
||||
<div class="list__table-cell">
|
||||
{{~#if customIcon~}}
|
||||
<img src="{{customIcon}}" class="list__item-icon list__item-icon--custom {{#if color}}{{color}}{{/if}}" />
|
||||
{{~else~}}
|
||||
<i class="fa fa-{{icon}} {{#if color}}{{color}}-color{{/if}} list__item-icon"></i>
|
||||
{{~/if~}}
|
||||
</td>
|
||||
{{#if columns.title}}<td>{{#if title}}{{title}}{{else}}({{res 'noTitle'}}){{/if}}</td>{{/if}}
|
||||
{{#if columns.user}}<td>{{user}}</td>{{/if}}
|
||||
{{#if columns.url}}<td>{{url}}</td>{{/if}}
|
||||
{{#if columns.tags}}<td>{{tags}}</td>{{/if}}
|
||||
{{#if columns.notes}}<td>{{notes}}</td>{{/if}}
|
||||
{{#if columns.groupName}}<td>{{groupName}}</td>{{/if}}
|
||||
{{#if columns.fileName}}<td>{{fileName}}</td>{{/if}}
|
||||
</tr>
|
||||
</div>
|
||||
{{#if columns.title}}<div class="list__table-cell">{{#if title}}{{title}}{{else}}({{res 'noTitle'}}){{/if}}</div>{{/if}}
|
||||
{{#if columns.user}}<div class="list__table-cell">{{user}}</div>{{/if}}
|
||||
{{#if columns.url}}<div class="list__table-cell">{{url}}</div>{{/if}}
|
||||
{{#if columns.tags}}<div class="list__table-cell">{{tags}}</div>{{/if}}
|
||||
{{#if columns.notes}}<div class="list__table-cell">{{notes}}</div>{{/if}}
|
||||
{{#if columns.groupName}}<div class="list__table-cell">{{groupName}}</div>{{/if}}
|
||||
{{#if columns.fileName}}<div class="list__table-cell">{{fileName}}</div>{{/if}}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<div class="list__list list__items-container">
|
||||
{{{itemsHtml}}}
|
||||
</div>
|
|
@ -0,0 +1,11 @@
|
|||
<div class="list__table">
|
||||
<div class="list__table-head list__table-row">
|
||||
<div class="list__table-cell"><i class="fa fa-bars muted-color list__table-options"></i></div>
|
||||
{{#each columns as |col|}}
|
||||
{{#if col.enabled}}<div class="list__table-cell">{{Res col.name}}</div>{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="list__table-body list__items-container">
|
||||
{{{itemsHtml}}}
|
||||
</div>
|
||||
</div>
|
|
@ -1,13 +0,0 @@
|
|||
<table class="list__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><i class="fa fa-bars muted-color list__table-options"></i></th>
|
||||
{{#each columns as |col|}}
|
||||
{{#if col.enabled}}<th>{{Res col.name}}</th>{{/if}}
|
||||
{{/each}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{{itemsHtml}}}
|
||||
</tbody>
|
||||
</table>
|
Loading…
Reference in New Issue