Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Virtual insertions via Adapter.insert API #27

Merged
merged 21 commits into from
Oct 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
font-size: small;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/vscroll@1.3.4/dist/bundles/vscroll.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vscroll/dist/bundles/vscroll.umd.js"></script>
</head>
<body>

Expand Down
1,332 changes: 109 additions & 1,223 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vscroll",
"version": "1.3.4",
"version": "1.4.0",
"description": "Virtual scroll engine",
"main": "dist/bundles/vscroll.umd.js",
"module": "dist/bundles/vscroll.esm5.js",
Expand Down Expand Up @@ -42,10 +42,10 @@
"@types/jest": "^27.0.1",
"@typescript-eslint/eslint-plugin": "^4.12.0",
"@typescript-eslint/parser": "^4.12.0",
"babel-jest": "^27.2.0",
"babel-jest": "^27.2.1",
"chalk": "^4.1.0",
"eslint": "^7.17.0",
"jest": "^27.2.0",
"jest": "^27.2.1",
"rollup": "^2.35.1",
"rollup-plugin-license": "^2.2.0",
"rollup-plugin-sourcemaps": "^0.6.3",
Expand Down
59 changes: 45 additions & 14 deletions src/classes/buffer.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Cache } from './buffer/cache';
import { CheckBufferCall } from './buffer/checkCall';
import { Item } from './item';
import { Settings } from './settings';
import { Logger } from './logger';
import { Reactive } from './reactive';
import { Direction } from '../inputs/index';
import { OnDataChanged, BufferUpdater } from '../interfaces/index';
import { OnDataChanged, BufferUpdater, ItemsPredicate } from '../interfaces/index';

export class Buffer<Data> {

Expand All @@ -22,6 +23,7 @@ export class Buffer<Data> {

private pristine: boolean;
private cache: Cache<Data>;
private checkCall: CheckBufferCall<Data>;
private readonly logger: Logger;

constructor(settings: Settings<Data>, onDataChanged: OnDataChanged<Data>, logger: Logger) {
Expand All @@ -30,6 +32,7 @@ export class Buffer<Data> {
this.bof = new Reactive<boolean>(false);
this.eof = new Reactive<boolean>(false);
this.cache = new Cache<Data>(settings, logger);
this.checkCall = new CheckBufferCall(this, logger);
this.startIndexUser = settings.startIndex;
this.minIndexUser = settings.minIndex;
this.maxIndexUser = settings.maxIndex;
Expand Down Expand Up @@ -188,6 +191,10 @@ export class Buffer<Data> {
this.items = this.items.filter(({ toRemove }) => !toRemove);
}

getIndexToInsert(predicate?: ItemsPredicate, before?: number, after?: number): number {
return this.checkCall.insertInBuffer(predicate, before, after);
}

private shiftExtremum(amount: number, fixRight: boolean) {
if (!fixRight) {
this.absMaxIndex += amount;
Expand All @@ -202,22 +209,23 @@ export class Buffer<Data> {
}
}

appendVirtually(count: number, fixRight: boolean): void {
if (fixRight) {
this.items.forEach(item => item.updateIndex(item.$index - count));
this.cache.shiftIndexes(-count);
this.items = [...this.items];
insertVirtually(items: Data[], index: number, direction: Direction, fixRight: boolean): boolean {
if (!this.checkCall.insertVirtual(items, index, direction)) {
return false;
}
this.shiftExtremum(count, fixRight);
}

prependVirtually(count: number, fixRight: boolean): void {
if (!fixRight) {
this.items.forEach(item => item.updateIndex(item.$index + count));
this.cache.shiftIndexes(count);
let shift = 0;
if (index <= this.firstIndex && !fixRight) {
shift = items.length;
} else if (index >= this.lastIndex && fixRight) {
shift = -items.length;
}
if (shift) {
this.items.forEach(item => item.updateIndex(item.$index + shift));
this.cache.insertItems(items, index, direction, fixRight);
this.items = [...this.items];
}
this.shiftExtremum(count, fixRight);
this.shiftExtremum(items.length, fixRight);
return true;
}

removeVirtually(indexes: number[], fixRight: boolean): void {
Expand All @@ -243,6 +251,29 @@ export class Buffer<Data> {
this.cache.removeItems(indexes, fixRight);
}

fillEmpty(
items: Data[], beforeIndex: number | undefined, afterIndex: number | undefined, fixRight: boolean,
generator: (index: number, data: Data) => Item<Data>,
): boolean {
if (!this.checkCall.fillEmpty(items, beforeIndex, afterIndex)) {
return false;
}
const before = Number.isInteger(beforeIndex);
const index = (before ? beforeIndex : afterIndex) as number;
const shift = (fixRight ? items.length : (before ? 1 : 0));
this.items = items.map((data, i) =>
generator(index + i + (!before ? 1 : 0) - shift, data)
);
this._absMinIndex = this.items[0].$index;
this._absMaxIndex = this.items[this.size - 1].$index;
if (this.startIndex <= this.absMinIndex) {
this.startIndex = this.absMinIndex;
} else if (this.startIndex > this.absMaxIndex) {
this.startIndex = this.absMaxIndex;
}
return true;
}

updateItems(
predicate: BufferUpdater<Data>,
generator: (index: number, data: Data) => Item<Data>,
Expand Down
86 changes: 71 additions & 15 deletions src/classes/buffer/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { DefaultSize } from './defaultSize';
import { Item } from '../item';
import { Settings } from '../settings';
import { Logger } from '../logger';
import { SizeStrategy } from '../../inputs/index';
import { SizeStrategy, Direction } from '../../inputs/index';

interface ItemToCache<Data> {
$index: number;
data: Data;
size?: number;
}

interface ItemUpdate {
$index: number;
Expand All @@ -12,21 +18,18 @@ interface ItemUpdate {

export class ItemCache<Data = unknown> {
$index: number;
nodeId: string;
data: Data | null;
size: number;
size?: number;
position: number;

constructor(item: Item<Data>, saveData: boolean) {
constructor(item: ItemToCache<Data>, saveData: boolean) {
this.$index = item.$index;
this.nodeId = item.nodeId;
this.data = saveData ? item.data : null;
this.size = item.size;
}

changeIndex(value: number): void {
this.$index = value;
this.nodeId = String(value);
}
}

Expand Down Expand Up @@ -73,7 +76,7 @@ export class Cache<Data = unknown> {

getSizeByIndex(index: number): number {
const item = this.get(index);
return item ? item.size : this.defaultSize.get();
return item && item.size || this.defaultSize.get();
}

getDefaultSize(): number {
Expand Down Expand Up @@ -102,18 +105,18 @@ export class Cache<Data = unknown> {
if (this.saveData) {
itemCache.data = item.data;
}
if (itemCache.size !== item.size) { // size changes
if (!isNaN(itemCache.size)) {
this.defaultSize.setExisted(itemCache, item);
if (itemCache.size !== item.size) {
if (itemCache.size) {
this.defaultSize.setExisted(itemCache.size, item.size);
} else {
this.defaultSize.setNew(item);
this.defaultSize.setNew(item.size);
}
itemCache.size = item.size;
}
} else {
itemCache = new ItemCache<Data>(item, this.saveData);
this.items.set(item.$index, itemCache);
this.defaultSize.setNew(itemCache);
this.defaultSize.setNew(item.size);
}
if (item.$index < this.minIndex) {
this.minIndex = item.$index;
Expand All @@ -124,6 +127,59 @@ export class Cache<Data = unknown> {
return itemCache;
}

/**
* Inserts items to Set, shifts $indexes of items that remain.
* Replaces current Set with a new one with new regular $indexes.
* Maintains min/max indexes.
*
* @param {Data[]} toInsert List of non-indexed items to be inserted.
* @param {number} index The index before/after which the insertion is performed.
* @param {Direction} direction Determines the direction of insertion.
* @param {boolean} fixRight Defines indexes shifting strategy.
* If false, indexes that are greater than the inserted ones are increased.
* If true, indexes that are less than than the inserted ones are decreased.
*/
insertItems(toInsert: Data[], index: number, direction: Direction, fixRight: boolean): void {
const items = new Map<number, ItemCache<Data>>();
const length = toInsert.length;
let min = Infinity, max = -Infinity;
const set = (item: ItemCache<Data>) => {
items.set(item.$index, item);
min = item.$index < min ? item.$index : min;
max = item.$index > max ? item.$index : max;
};
this.items.forEach(item => {
let shift = 0;
if (direction === Direction.backward) {
if (item.$index < index && fixRight) {
shift = -length;
} else if (item.$index >= index && !fixRight) {
shift = length;
}
} else if (direction === Direction.forward) {
if (item.$index <= index && fixRight) {
shift = -length;
} else if (item.$index > index && !fixRight) {
shift = length;
}
}
if (shift) {
item.changeIndex(item.$index + shift);
}
set(item);
});
if (this.saveData) { // persist data with no sizes
toInsert.forEach((data, i) => {
const $index = index + i - (fixRight ? length : 0) + (direction === Direction.forward ? 1 : 0);
const item = new ItemCache<Data>({ $index, data }, this.saveData);
set(item);
});
}
this.items = items;
this.minIndex = min;
this.maxIndex = max;
}

/**
* Removes items from Set, shifts $indexes of items that remain.
* Replaces current Set with a new one with new regular $indexes.
Expand All @@ -139,8 +195,8 @@ export class Cache<Data = unknown> {
let min = Infinity, max = -Infinity;
this.items.forEach(item => {
if (toRemove.some(index => index === item.$index)) {
if (!isNaN(item.size)) {
this.defaultSize.setRemoved(item);
if (item.size) {
this.defaultSize.setRemoved(item.size);
}
return;
}
Expand Down Expand Up @@ -200,7 +256,7 @@ export class Cache<Data = unknown> {
);
before // to maintain default size on remove
.filter(item => item.toRemove)
.forEach(item => this.defaultSize.setRemoved(item));
.forEach(item => this.defaultSize.setRemoved(item.size));
this.minIndex += leftDiff;
this.maxIndex += rightDiff;
this.items = items;
Expand Down
65 changes: 65 additions & 0 deletions src/classes/buffer/checkCall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Buffer } from '../buffer';
import { Logger } from '../logger';
import { Direction } from '../../inputs/index';
import { ItemsPredicate } from '../../interfaces/index';

export class CheckBufferCall<Data> {
private context: Buffer<Data>;
private logger: Logger;

constructor(context: Buffer<Data>, logger: Logger) {
this.context = context;
this.logger = logger;
}

fillEmpty(items: Data[], before?: number, after?: number): boolean {
if (!items.length) {
this.logger.log('no items to fill the buffer; empty list');
return false;
}
if (!Number.isInteger(before) && !Number.isInteger(after)) {
this.logger.log('no items to fill the buffer; wrong indexes');
return false;
}
this.logger.log(() => `going to fill the buffer with ${items.length} item(s)`);
return true;
}

insertInBuffer(predicate?: ItemsPredicate, before?: number, after?: number): number {
const index = Number.isInteger(before) ? before : (Number.isInteger(after) ? after : NaN);
const found = this.context.items.find(item =>
(predicate && predicate(item.get())) ||
(Number.isInteger(index) && index === item.$index)
);
if (!found) {
this.logger.log('no items to insert in buffer; empty predicate\'s result');
return NaN;
}
return found.$index;
}

insertVirtual(items: Data[], index: number, direction: Direction): boolean {
if (!items.length) {
this.logger.log('no items to insert virtually; empty list');
return false;
}
const { firstIndex, lastIndex, finiteAbsMinIndex, finiteAbsMaxIndex } = this.context;
if (index < finiteAbsMinIndex || index > finiteAbsMaxIndex) {
this.logger.log(() =>
'no items to insert virtually; ' +
`selected index (${index}) does not match virtual area [${finiteAbsMinIndex}..${finiteAbsMaxIndex}]`
);
return false;
}
const before = direction === Direction.backward;
if (!(index < firstIndex + (before ? 1 : 0) || index > lastIndex - (before ? 0 : 1))) {
this.logger.log(() =>
`no items to insert virtually; selected index (${index}) belongs Buffer [${firstIndex}..${lastIndex}]`
);
return false;
}
this.logger.log(() => `going to insert ${items.length} item(s) virtually`);
return true;
}

}
20 changes: 8 additions & 12 deletions src/classes/buffer/defaultSize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,32 +127,28 @@ export class DefaultSize {
return this.get() !== oldValue;
}

setExisted(oldItem: ItemSize, newItem: ItemSize): void {
setExisted(oldSize: number, newSize: number): void {
if (this.sizeStrategy !== SizeStrategy.Constant) {
this.recalculation.oldItems.push({
size: oldItem.size,
newSize: newItem.size
size: oldSize,
newSize
});
}
}

setNew(newItem: ItemSize): void {
setNew(size: number): void {
if (this.sizeStrategy !== SizeStrategy.Constant) {
this.recalculation.newItems.push({
size: newItem.size
});
this.recalculation.newItems.push({ size });
} else {
if (!this.constantSize) {
this.constantSize = newItem.size;
this.constantSize = size;
}
}
}

setRemoved(oldItem: ItemSize): void {
setRemoved(size: number): void {
if (this.sizeStrategy !== SizeStrategy.Constant) {
this.recalculation.removed.push({
size: oldItem.size
});
this.recalculation.removed.push({ size });
}
}
}
Loading