fineui是帆软报表和BI产品线所使用的前端框架。
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

217 lines
7.5 KiB

import { VerticalLayout, Layout, Widget, shortcut, extend, isFunction, isNumber, PrefixIntervalTree, ResizeDetector } from "@/core";
import { VirtualGroup } from "../combination";
/**
* 同时用于virtualGroup和virtualList特性的虚拟列表
*
* Created by GUY on 2017/5/22.
* @class VirtualList
* @extends Widget
*/
@shortcut()
export class VirtualGroupList extends Widget {
props() {
return {
baseCls: "bi-virtual-group-list",
overscanHeight: 100,
blockSize: 10,
scrollTop: 0,
rowHeight: "auto",
items: [],
el: {},
itemFormatter: (item, index) => item,
};
}
init() {
this.renderedIndex = -1;
}
static xtype = "bi.virtual_group_list";
render() {
const { rowHeight, items, el } = this.options;
return {
type: VerticalLayout.xtype,
items: [
{
type: Layout.xtype,
ref: (ref) => {
this.topBlank = ref;
},
},
{
type: VirtualGroup.xtype,
height: rowHeight * items.length,
ref: (ref) => {
this.container = ref;
},
layouts: [
extend(
{
type: VerticalLayout.xtype,
scrolly: false,
},
el
)
],
},
{
type: Layout.xtype,
ref: (ref) => {
this.bottomBlank = ref;
},
}
],
element: this,
};
}
// mounted之后绑定事件
mounted() {
// 这里无法进行结构,因为存在赋值操作,如果使用结构则this.options的值不会跟随变化
const o = this.options;
o.items = isFunction(o.items)
? this.__watch(o.items, (context, newValue) => {
this.populate(newValue);
})
: o.items;
this._populate();
this.ticking = false;
this.element.scroll(() => {
o.scrollTop = this.element.scrollTop();
if (!this.ticking) {
requestAnimationFrame(() => {
this._calculateBlocksToRender();
this.ticking = false;
});
this.ticking = true;
}
});
ResizeDetector.addResizeListener(this, () => {
if (this.element.is(":visible")) {
this._calculateBlocksToRender();
}
});
}
_isAutoHeight() {
return !isNumber(this.options.rowHeight);
}
_renderMoreIf() {
const { scrollTop, overscanHeight, blockSize, items, itemFormatter } = this.options;
const height = this.element.height();
const minContentHeight = scrollTop + height + overscanHeight;
let index = (this.renderedIndex + 1) * blockSize,
cnt = this.renderedIndex + 1;
let lastHeight;
const getElementHeight = () =>
this.container.element.height() + this.topBlank.element.height() + this.bottomBlank.element.height();
lastHeight = this.renderedIndex === -1 ? 0 : getElementHeight();
while (lastHeight < minContentHeight && index < items.length) {
const itemsArr = items.slice(index, index + blockSize);
this.container[this.renderedIndex === -1 ? "populate" : "addItems"](
// eslint-disable-next-line no-loop-func
itemsArr.map((item, i) => itemFormatter(item, index + i)),
this
);
const elementHeight = getElementHeight();
const addedHeight = elementHeight - lastHeight;
this.tree.set(cnt, addedHeight);
this.renderedIndex = cnt;
cnt++;
index += blockSize;
lastHeight = this.renderedIndex === -1 ? 0 : elementHeight;
}
}
_calculateBlocksToRender() {
// BI-115750 不可见状态下依赖元素实际尺寸构造的线段树会分段错误,所以不进行后续计算和线段树的初始化。
// 这样从不可见状态变为可见状态能够重新触发线段树初始化
if (!this.element.is(":visible")) {
return;
}
const { scrollTop, overscanHeight, blockSize, items, itemFormatter, rowHeight } = this.options;
this._isAutoHeight() && this._renderMoreIf();
const height = this.element.height();
const minContentHeightFrom = scrollTop - overscanHeight;
const minContentHeightTo = scrollTop + height + overscanHeight;
const start = this.tree.greatestLowerBound(minContentHeightFrom);
const end = this.tree.leastUpperBound(minContentHeightTo);
const itemsArr = [];
const topHeight = this.tree.sumTo(Math.max(-1, start - 1));
this.topBlank.setHeight(`${topHeight}px`);
if (this._isAutoHeight()) {
for (let i = start < 0 ? 0 : start; i <= end && i <= this.renderedIndex; i++) {
const index = i * blockSize;
for (let j = index; j < index + blockSize && j < items.length; j++) {
itemsArr.push(items[j]);
}
}
this.bottomBlank.setHeight(
`${this.tree.sumTo(this.renderedIndex) - this.tree.sumTo(Math.min(end, this.renderedIndex))}px`
);
this.container.populate(
itemsArr.map((item, i) => itemFormatter(item, (start < 0 ? 0 : start) * blockSize + i))
);
} else {
for (let i = start < 0 ? 0 : start; i <= end; i++) {
const index = i * blockSize;
for (let j = index; j < index + blockSize && j < items.length; j++) {
itemsArr.push(items[j]);
}
}
this.container.element.height(rowHeight * items.length - topHeight);
this.container.populate(
itemsArr.map((item, i) => itemFormatter(item, (start < 0 ? 0 : start) * blockSize + i))
);
}
}
_populate(items) {
const { blockSize, rowHeight, scrollTop } = this.options;
if (items && this.options.items !== items) {
// 重新populate一组items,需要重新对线段树分块
this.options.items = items;
this._restore();
}
this.tree = PrefixIntervalTree.uniform(
Math.ceil(this.options.items.length / blockSize),
this._isAutoHeight() ? 0 : rowHeight * blockSize
);
this._calculateBlocksToRender();
try {
this.element.scrollTop(scrollTop);
} catch (e) {}
}
_restore() {
this.renderedIndex = -1;
// 依赖于cache的占位元素也要初始化
this.topBlank.setHeight(0);
this.bottomBlank.setHeight(0);
}
// 暂时只支持固定行高的场景
scrollTo(scrollTop) {
this.options.scrollTop = scrollTop;
this._calculateBlocksToRender();
this.element.scrollTop(scrollTop);
}
restore() {
this.options.scrollTop = 0;
this._restore();
}
populate(items) {
this._populate(items);
}
beforeDestroy() {
ResizeDetector.removeResizeListener(this);
this.restore();
}
}