forked from mystiq/hydrogen-web
WIP
This commit is contained in:
parent
d625d57aa4
commit
4a64d0ee17
7 changed files with 450 additions and 134 deletions
|
@ -1,115 +0,0 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {createEnum} from "../../../../utils/enum.js";
|
||||
|
||||
export const ScrollDirection = createEnum("upwards", "downwards");
|
||||
|
||||
export class ItemRange {
|
||||
constructor(topCount, renderCount, bottomCount) {
|
||||
this.topCount = topCount;
|
||||
this.renderCount = renderCount;
|
||||
this.bottomCount = bottomCount;
|
||||
}
|
||||
|
||||
contains(range) {
|
||||
// don't contain empty ranges
|
||||
// as it will prevent clearing the list
|
||||
// once it is scrolled far enough out of view
|
||||
if (!range.renderCount && this.renderCount) {
|
||||
return false;
|
||||
}
|
||||
return range.topCount >= this.topCount &&
|
||||
(range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
|
||||
}
|
||||
|
||||
containsIndex(idx) {
|
||||
return idx >= this.topCount && idx <= this.lastIndex;
|
||||
}
|
||||
|
||||
expand(amount) {
|
||||
// don't expand ranges that won't render anything
|
||||
if (this.renderCount === 0) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const topGrow = Math.min(amount, this.topCount);
|
||||
const bottomGrow = Math.min(amount, this.bottomCount);
|
||||
return new ItemRange(
|
||||
this.topCount - topGrow,
|
||||
this.renderCount + topGrow + bottomGrow,
|
||||
this.bottomCount - bottomGrow,
|
||||
);
|
||||
}
|
||||
|
||||
get lastIndex() {
|
||||
return this.topCount + this.renderCount - 1;
|
||||
}
|
||||
|
||||
totalSize() {
|
||||
return this.topCount + this.renderCount + this.bottomCount;
|
||||
}
|
||||
|
||||
normalize(idx) {
|
||||
/*
|
||||
map index from list to index in rendered range
|
||||
eg: if the index range of this._list is [0, 200] and we have rendered
|
||||
elements in range [50, 60] then index 50 in list must map to index 0
|
||||
in DOM tree/childInstance array.
|
||||
*/
|
||||
return idx - this.topCount;
|
||||
}
|
||||
|
||||
scrollDirectionTo(range) {
|
||||
return range.bottomCount < this.bottomCount ? ScrollDirection.downwards : ScrollDirection.upwards;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this range intersects with another range
|
||||
* @param {ItemRange} range The range to check against
|
||||
* @param {ScrollDirection} scrollDirection
|
||||
* @returns {boolean}
|
||||
*/
|
||||
intersects(range) {
|
||||
return !!Math.max(0, Math.min(this.lastIndex, range.lastIndex) - Math.max(this.topCount, range.topCount));
|
||||
}
|
||||
|
||||
diff(range) {
|
||||
/**
|
||||
* Range-1
|
||||
* |----------------------|
|
||||
* Range-2
|
||||
* |---------------------|
|
||||
* <-------><------------><------->
|
||||
* bisect-1 intersection bisect-2
|
||||
*/
|
||||
const scrollDirection = this.scrollDirectionTo(range);
|
||||
if (!this.intersects(range)) {
|
||||
// There is no intersection between the ranges; which can happen if you scroll really fast
|
||||
// In this case, we need to do full render of the new range
|
||||
const toRemove = { start: this.topCount, end: this.lastIndex };
|
||||
const toAdd = { start: range.topCount, end: range.lastIndex };
|
||||
return {toRemove, toAdd, scrollDirection};
|
||||
}
|
||||
const bisection1 = {start: Math.min(this.topCount, range.topCount), end: Math.max(this.topCount, range.topCount) - 1};
|
||||
const bisection2 = {start: Math.min(this.lastIndex, range.lastIndex) + 1, end: Math.max(this.lastIndex, range.lastIndex)};
|
||||
// When scrolling down, bisection1 needs to be removed and bisection2 needs to be added
|
||||
// When scrolling up, vice versa
|
||||
const toRemove = scrollDirection === ScrollDirection.downwards ? bisection1 : bisection2;
|
||||
const toAdd = scrollDirection === ScrollDirection.downwards ? bisection2 : bisection1;
|
||||
return {toAdd, toRemove, scrollDirection};
|
||||
}
|
||||
}
|
210
src/platform/web/ui/general/ItemRange.ts
Normal file
210
src/platform/web/ui/general/ItemRange.ts
Normal file
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// start is included in the range,
|
||||
// end is excluded,
|
||||
// so [2, 2[ means an empty range
|
||||
class Range {
|
||||
constructor(
|
||||
public readonly start: number,
|
||||
public readonly end: number
|
||||
) {}
|
||||
|
||||
get length() {
|
||||
return this.end - this.start;
|
||||
}
|
||||
|
||||
contains(range: Range): boolean {
|
||||
return range.start >= this.start && range.end <= this.end;
|
||||
}
|
||||
|
||||
containsIndex(idx: number): boolean {
|
||||
return idx >= this.start && idx < this.end;
|
||||
}
|
||||
|
||||
intersects(range: Range): boolean {
|
||||
return range.start < this.end && this.start < range.end;
|
||||
}
|
||||
|
||||
forEach(callback: ((i: number) => void)) {
|
||||
for (let i = this.start; i < this.end; i += 1) {
|
||||
callback(i);
|
||||
}
|
||||
}
|
||||
|
||||
forEachInIterator<T>(it: IterableIterator<T>, callback: ((T, i: number) => void)) {
|
||||
let i = 0;
|
||||
for (i = 0; i < this.start; i += 1) {
|
||||
it.next();
|
||||
}
|
||||
for (i = 0; i < this.length; i += 1) {
|
||||
const result = it.next();
|
||||
if (result.done) {
|
||||
break;
|
||||
} else {
|
||||
callback(result.value, this.start + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.iterator](): Iterator<number> {
|
||||
return new RangeIterator(this);
|
||||
}
|
||||
}
|
||||
|
||||
class RangeIterator implements Iterator<number> {
|
||||
private idx: number;
|
||||
constructor(private readonly range: Range) {
|
||||
this.idx = range.start - 1;
|
||||
}
|
||||
|
||||
next(): IteratorResult<number> {
|
||||
if (this.idx < (this.range.end - 1)) {
|
||||
this.idx += 1;
|
||||
return {value: this.idx, done: false};
|
||||
} else {
|
||||
return {value: undefined, done: true};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function tests() {
|
||||
return {
|
||||
"length": assert => {
|
||||
const a = new Range(2, 5);
|
||||
assert.equal(a.length, 3);
|
||||
},
|
||||
"iterator": assert => {
|
||||
assert.deepEqual(Array.from(new Range(2, 5)), [2, 3, 4]);
|
||||
},
|
||||
"containsIndex": assert => {
|
||||
const a = new Range(2, 5);
|
||||
assert.equal(a.containsIndex(0), false);
|
||||
assert.equal(a.containsIndex(1), false);
|
||||
assert.equal(a.containsIndex(2), true);
|
||||
assert.equal(a.containsIndex(3), true);
|
||||
assert.equal(a.containsIndex(4), true);
|
||||
assert.equal(a.containsIndex(5), false);
|
||||
assert.equal(a.containsIndex(6), false);
|
||||
},
|
||||
"intersects returns false for touching ranges": assert => {
|
||||
const a = new Range(2, 5);
|
||||
const b = new Range(5, 10);
|
||||
assert.equal(a.intersects(b), false);
|
||||
assert.equal(b.intersects(a), false);
|
||||
},
|
||||
"intersects returns false": assert => {
|
||||
const a = new Range(2, 5);
|
||||
const b = new Range(50, 100);
|
||||
assert.equal(a.intersects(b), false);
|
||||
assert.equal(b.intersects(a), false);
|
||||
},
|
||||
"intersects returns true for 1 overlapping item": assert => {
|
||||
const a = new Range(2, 5);
|
||||
const b = new Range(4, 10);
|
||||
assert.equal(a.intersects(b), true);
|
||||
assert.equal(b.intersects(a), true);
|
||||
},
|
||||
"contains beyond left edge": assert => {
|
||||
const a = new Range(2, 5);
|
||||
const b = new Range(1, 3);
|
||||
assert.equal(a.contains(b), false);
|
||||
},
|
||||
"contains at left edge": assert => {
|
||||
const a = new Range(2, 5);
|
||||
const b = new Range(2, 3);
|
||||
assert.equal(a.contains(b), true);
|
||||
},
|
||||
"contains between edges": assert => {
|
||||
const a = new Range(2, 5);
|
||||
const b = new Range(3, 4);
|
||||
assert.equal(a.contains(b), true);
|
||||
},
|
||||
"contains at right edge": assert => {
|
||||
const a = new Range(2, 5);
|
||||
const b = new Range(3, 5);
|
||||
assert.equal(a.contains(b), true);
|
||||
},
|
||||
"contains beyond right edge": assert => {
|
||||
const a = new Range(2, 5);
|
||||
const b = new Range(4, 6);
|
||||
assert.equal(a.contains(b), false);
|
||||
},
|
||||
"contains for non-intersecting ranges": assert => {
|
||||
const a = new Range(2, 5);
|
||||
const b = new Range(5, 6);
|
||||
assert.equal(a.contains(b), false);
|
||||
},
|
||||
"forEachInIterator with more values available": assert => {
|
||||
const callbackValues: {v: string, i: number}[] = [];
|
||||
const values = ["a", "b", "c", "d", "e", "f"];
|
||||
const it = values[Symbol.iterator]();
|
||||
new Range(2, 5).forEachInIterator(it, (v, i) => callbackValues.push({v, i}));
|
||||
assert.deepEqual(callbackValues, [
|
||||
{v: "c", i: 2},
|
||||
{v: "d", i: 3},
|
||||
{v: "e", i: 4},
|
||||
]);
|
||||
},
|
||||
"forEachInIterator with fewer values available": assert => {
|
||||
const callbackValues: {v: string, i: number}[] = [];
|
||||
const values = ["a", "b", "c"];
|
||||
const it = values[Symbol.iterator]();
|
||||
new Range(2, 5).forEachInIterator(it, (v, i) => callbackValues.push({v, i}));
|
||||
assert.deepEqual(callbackValues, [
|
||||
{v: "c", i: 2},
|
||||
]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export class ItemRange extends Range {
|
||||
constructor(
|
||||
start: number,
|
||||
end: number,
|
||||
public readonly totalLength: number
|
||||
) {
|
||||
super(start, end);
|
||||
}
|
||||
|
||||
|
||||
expand(amount: number): ItemRange {
|
||||
// don't expand ranges that won't render anything
|
||||
if (this.length === 0) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const topGrow = Math.min(amount, this.start);
|
||||
const bottomGrow = Math.min(amount, this.totalLength - this.end);
|
||||
return new ItemRange(
|
||||
this.start - topGrow,
|
||||
this.end + topGrow + bottomGrow,
|
||||
this.totalLength,
|
||||
);
|
||||
}
|
||||
|
||||
static fromViewport(listLength: number, itemHeight: number, listHeight: number, scrollTop: number) {
|
||||
const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), listLength);
|
||||
const itemsAfterTop = listLength - topCount;
|
||||
const visibleItems = listHeight !== 0 ? Math.ceil(listHeight / itemHeight) : 0;
|
||||
const renderCount = Math.min(visibleItems, itemsAfterTop);
|
||||
return new ItemRange(topCount, topCount + renderCount, listLength);
|
||||
}
|
||||
|
||||
missingFrom() {
|
||||
|
||||
}
|
||||
}
|
200
src/platform/web/ui/general/LazyListView.ts
Normal file
200
src/platform/web/ui/general/LazyListView.ts
Normal file
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {tag} from "./html";
|
||||
import {removeChildren, mountView} from "./utils";
|
||||
import {ItemRange} from "./ItemRange";
|
||||
import {ListView, IOptions as IParentOptions} from "./ListView";
|
||||
import {IView} from "./types";
|
||||
|
||||
export interface IOptions<T, V> extends IParentOptions<T, V> {
|
||||
itemHeight: number;
|
||||
overflowMargin?: number;
|
||||
overflowItems?: number;
|
||||
}
|
||||
|
||||
export class LazyListView<T, V extends IView> extends ListView<T, V> {
|
||||
private renderRange?: ItemRange;
|
||||
private height?: number;
|
||||
private itemHeight: number;
|
||||
private overflowItems: number;
|
||||
private scrollContainer?: Element;
|
||||
|
||||
constructor(
|
||||
{itemHeight, overflowMargin = 5, overflowItems = 20,...options}: IOptions<T, V>,
|
||||
childCreator: (value: T) => V
|
||||
) {
|
||||
super(options, childCreator);
|
||||
this.itemHeight = itemHeight;
|
||||
this.overflowItems = overflowItems;
|
||||
// TODO: this.overflowMargin = overflowMargin;
|
||||
}
|
||||
|
||||
handleEvent(e: Event) {
|
||||
if (e.type === "scroll") {
|
||||
this.handleScroll();
|
||||
} else {
|
||||
super.handleEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll() {
|
||||
const visibleRange = this._getVisibleRange();
|
||||
// don't contain empty ranges
|
||||
// as it will prevent clearing the list
|
||||
// once it is scrolled far enough out of view
|
||||
if (visibleRange.length !== 0 && !this.renderRange!.contains(visibleRange)) {
|
||||
const prevRenderRange = this.renderRange!;
|
||||
this.renderRange = visibleRange.expand(this.overflowItems);
|
||||
this.renderUpdate(prevRenderRange, this.renderRange);
|
||||
}
|
||||
}
|
||||
|
||||
override async loadList() {
|
||||
/*
|
||||
Wait two frames for the return from mount() to be inserted into DOM.
|
||||
This should be enough, but if this gives us trouble we can always use
|
||||
MutationObserver.
|
||||
*/
|
||||
await new Promise(r => requestAnimationFrame(r));
|
||||
await new Promise(r => requestAnimationFrame(r));
|
||||
|
||||
if (!this._list) {
|
||||
return;
|
||||
}
|
||||
const visibleRange = this._getVisibleRange();
|
||||
this.renderRange = visibleRange.expand(this.overflowItems);
|
||||
this._childInstances = [];
|
||||
this._subscription = this._list.subscribe(this);
|
||||
this.reRenderFullRange(this.renderRange);
|
||||
}
|
||||
|
||||
private _getVisibleRange() {
|
||||
const {clientHeight, scrollTop} = this.root()!;
|
||||
if (clientHeight === 0) {
|
||||
throw new Error("LazyListView height is 0");
|
||||
}
|
||||
return ItemRange.fromViewport(this._list.length, this.itemHeight, clientHeight, scrollTop);
|
||||
}
|
||||
|
||||
private reRenderFullRange(range: ItemRange) {
|
||||
removeChildren(this._listElement!);
|
||||
const fragment = document.createDocumentFragment();
|
||||
const it = this._list[Symbol.iterator]();
|
||||
this._childInstances!.length = 0;
|
||||
range.forEachInIterator(it, item => {
|
||||
const child = this._childCreator(item);
|
||||
this._childInstances!.push(child);
|
||||
fragment.appendChild(mountView(child, this._mountArgs));
|
||||
});
|
||||
this._listElement!.appendChild(fragment);
|
||||
this.adjustPadding(range);
|
||||
}
|
||||
|
||||
private renderUpdate(prevRange: ItemRange, newRange: ItemRange) {
|
||||
if (newRange.intersects(prevRange)) {
|
||||
for (const idxInList of prevRange) {
|
||||
// TODO: we need to make sure we keep childInstances in order so the indices lign up.
|
||||
// Perhaps we should join both ranges and see in which range it appears and either add or remove?
|
||||
if (!newRange.containsIndex(idxInList)) {
|
||||
const localIdx = idxInList - prevRange.start;
|
||||
this.removeChild(localIdx);
|
||||
}
|
||||
}
|
||||
const addedRange = newRange.missingFrom(prevRange);
|
||||
addedRange.forEachInIterator(this._list[Symbol.iterator](), (item, idxInList) => {
|
||||
const localIdx = idxInList - newRange.start;
|
||||
this.addChild(localIdx, item);
|
||||
});
|
||||
this.adjustPadding(newRange);
|
||||
} else {
|
||||
this.reRenderFullRange(newRange);
|
||||
}
|
||||
}
|
||||
|
||||
private adjustPadding(range: ItemRange) {
|
||||
const paddingTop = range.start * this.itemHeight;
|
||||
const paddingBottom = (range.totalLength - range.end) * this.itemHeight;
|
||||
const style = this.scrollContainer!.style;
|
||||
style.paddingTop = `${paddingTop}px`;
|
||||
style.paddingBottom = `${paddingBottom}px`;
|
||||
}
|
||||
|
||||
mount() {
|
||||
const listElement = super.mount();
|
||||
this.scrollContainer = tag.div({className: "LazyListParent"}, listElement);
|
||||
/*
|
||||
Hooking to scroll events can be expensive.
|
||||
Do we need to do more (like event throttling)?
|
||||
*/
|
||||
this.scrollContainer.addEventListener("scroll", this);
|
||||
return this.scrollContainer;
|
||||
}
|
||||
|
||||
unmount() {
|
||||
this.root()!.removeEventListener("scroll", this);
|
||||
this.scrollContainer = undefined;
|
||||
super.unmount();
|
||||
}
|
||||
|
||||
root(): Element | undefined {
|
||||
return this.scrollContainer;
|
||||
}
|
||||
|
||||
private get _listElement(): Element | undefined {
|
||||
return super.root();
|
||||
}
|
||||
|
||||
onAdd(idx: number, value: T) {
|
||||
// TODO: update totalLength in renderRange
|
||||
const result = this.renderRange!.queryAdd(idx);
|
||||
if (result.addIdx !== -1) {
|
||||
this.addChild(result.addIdx, value);
|
||||
}
|
||||
if (result.removeIdx !== -1) {
|
||||
this.removeChild(result.removeIdx);
|
||||
}
|
||||
}
|
||||
|
||||
onRemove(idx: number, value: T) {
|
||||
// TODO: update totalLength in renderRange
|
||||
const result = this.renderRange!.queryRemove(idx);
|
||||
if (result.removeIdx !== -1) {
|
||||
this.removeChild(result.removeIdx);
|
||||
}
|
||||
if (result.addIdx !== -1) {
|
||||
this.addChild(result.addIdx, value);
|
||||
}
|
||||
}
|
||||
|
||||
onMove(fromIdx: number, toIdx: number, value: T) {
|
||||
const result = this.renderRange!.queryMove(fromIdx, toIdx);
|
||||
if (result.moveFromIdx !== -1 && result.moveToIdx !== -1) {
|
||||
this.moveChild(result.moveFromIdx, result.moveToIdx);
|
||||
} else if (result.removeIdx !== -1) {
|
||||
this.removeChild(result.removeIdx);
|
||||
} else if (result.addIdx !== -1) {
|
||||
this.addChild(result.addIdx, value);
|
||||
}
|
||||
}
|
||||
|
||||
onUpdate(i: number, value: T, params: any) {
|
||||
const updateIdx = this.renderRange!.queryUpdate(i);
|
||||
if (updateIdx !== -1) {
|
||||
this.updateChild(updateIdx, value, params);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,10 +17,10 @@ limitations under the License.
|
|||
import {el} from "./html";
|
||||
import {mountView, insertAt} from "./utils";
|
||||
import {SubscriptionHandle} from "../../../../observable/BaseObservable";
|
||||
import {BaseObservableList as ObservableList} from "../../../../observable/list/BaseObservableList";
|
||||
import {BaseObservableList as ObservableList, IListObserver} from "../../../../observable/list/BaseObservableList";
|
||||
import {IView, IMountArgs} from "./types";
|
||||
|
||||
interface IOptions<T, V> {
|
||||
export interface IOptions<T, V> {
|
||||
list: ObservableList<T>,
|
||||
onItemClick?: (childView: V, evt: UIEvent) => void,
|
||||
className?: string,
|
||||
|
@ -28,17 +28,17 @@ interface IOptions<T, V> {
|
|||
parentProvidesUpdates?: boolean
|
||||
}
|
||||
|
||||
export class ListView<T, V extends IView> implements IView {
|
||||
export class ListView<T, V extends IView> implements IView, IListObserver<T> {
|
||||
|
||||
private _onItemClick?: (childView: V, evt: UIEvent) => void;
|
||||
private _list: ObservableList<T>;
|
||||
private _className?: string;
|
||||
private _tagName: string;
|
||||
private _root?: Element;
|
||||
private _subscription?: SubscriptionHandle;
|
||||
private _childCreator: (value: T) => V;
|
||||
private _childInstances?: V[];
|
||||
private _mountArgs: IMountArgs;
|
||||
protected _subscription?: SubscriptionHandle;
|
||||
protected _childCreator: (value: T) => V;
|
||||
protected _mountArgs: IMountArgs;
|
||||
protected _list: ObservableList<T>;
|
||||
protected _childInstances?: V[];
|
||||
|
||||
constructor(
|
||||
{list, onItemClick, className, tagName = "ul", parentProvidesUpdates = true}: IOptions<T, V>,
|
||||
|
@ -145,31 +145,48 @@ export class ListView<T, V extends IView> implements IView {
|
|||
}
|
||||
|
||||
onAdd(idx: number, value: T) {
|
||||
const child = this._childCreator(value);
|
||||
this._childInstances!.splice(idx, 0, child);
|
||||
insertAt(this._root!, idx, mountView(child, this._mountArgs));
|
||||
this.addChild(idx, value);
|
||||
}
|
||||
|
||||
onRemove(idx: number, value: T) {
|
||||
const [child] = this._childInstances!.splice(idx, 1);
|
||||
this.removeChild(idx);
|
||||
}
|
||||
|
||||
onMove(fromIdx: number, toIdx: number, value: T) {
|
||||
this.moveChild(fromIdx, toIdx);
|
||||
}
|
||||
|
||||
onUpdate(i: number, value: T, params: any) {
|
||||
this.updateChild(i, value, params);
|
||||
}
|
||||
|
||||
protected addChild(childIdx: number, value: T) {
|
||||
const child = this._childCreator(value);
|
||||
this._childInstances!.splice(childIdx, 0, child);
|
||||
insertAt(this._root!, childIdx, mountView(child, this._mountArgs));
|
||||
}
|
||||
|
||||
protected removeChild(childIdx: number) {
|
||||
const [child] = this._childInstances!.splice(childIdx, 1);
|
||||
child.root()!.remove();
|
||||
child.unmount();
|
||||
}
|
||||
|
||||
onMove(fromIdx: number, toIdx: number, value: T) {
|
||||
const [child] = this._childInstances!.splice(fromIdx, 1);
|
||||
this._childInstances!.splice(toIdx, 0, child);
|
||||
protected moveChild(fromChildIdx: number, toChildIdx: number) {
|
||||
const [child] = this._childInstances!.splice(fromChildIdx, 1);
|
||||
this._childInstances!.splice(toChildIdx, 0, child);
|
||||
child.root()!.remove();
|
||||
insertAt(this._root!, toIdx, child.root()! as Element);
|
||||
insertAt(this._root!, toChildIdx, child.root()! as Element);
|
||||
}
|
||||
|
||||
onUpdate(i: number, value: T, params: any) {
|
||||
protected updateChild(childIdx: number, value: T, params: any) {
|
||||
if (this._childInstances) {
|
||||
const instance = this._childInstances![i];
|
||||
const instance = this._childInstances![childIdx];
|
||||
instance && instance.update(value, params);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: is this the list or view index?
|
||||
protected recreateItem(index: number, value: T) {
|
||||
if (this._childInstances) {
|
||||
const child = this._childCreator(value);
|
||||
|
|
|
@ -50,3 +50,7 @@ export function insertAt(parentNode: Element, idx: number, childNode: Node): voi
|
|||
parentNode.insertBefore(childNode, nextDomNode);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeChildren(parentNode: Element): void {
|
||||
parentNode.innerHTML = '';
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {LazyListView} from "../../general/LazyListView.js";
|
||||
import {LazyListView} from "../../general/LazyListView";
|
||||
import {MemberTileView} from "./MemberTileView.js";
|
||||
|
||||
export class MemberListView extends LazyListView{
|
||||
|
|
Loading…
Reference in a new issue