|
|
|
/**
|
|
|
|
* GridView
|
|
|
|
*
|
|
|
|
* Created by GUY on 2016/1/11.
|
|
|
|
* @class BI.GridView
|
|
|
|
* @extends BI.Widget
|
|
|
|
*/
|
|
|
|
import { Widget, shortcut } from "../../core";
|
|
|
|
@shortcut()
|
|
|
|
export default class GridView extends Widget {
|
|
|
|
_defaultConfig() {
|
|
|
|
return BI.extend(super._defaultConfig(arguments), {
|
|
|
|
baseCls: "bi-grid-view",
|
|
|
|
// width: 400, //必设
|
|
|
|
// height: 300, //必设
|
|
|
|
scrollable: true,
|
|
|
|
scrollx: false,
|
|
|
|
scrolly: false,
|
|
|
|
overflowX: true,
|
|
|
|
overflowY: true,
|
|
|
|
el: {
|
|
|
|
type: "bi.vertical"
|
|
|
|
},
|
|
|
|
overscanColumnCount: 0,
|
|
|
|
overscanRowCount: 0,
|
|
|
|
rowHeightGetter: BI.emptyFn, // number类型或function类型
|
|
|
|
columnWidthGetter: BI.emptyFn, // number类型或function类型
|
|
|
|
// estimatedColumnSize: 100, //columnWidthGetter为function时必设
|
|
|
|
// estimatedRowSize: 30, //rowHeightGetter为function时必设
|
|
|
|
scrollLeft: 0,
|
|
|
|
scrollTop: 0,
|
|
|
|
items: [],
|
|
|
|
itemFormatter: (item, row, col) => {
|
|
|
|
return item;
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
static xtype = "bi.grid_view";
|
|
|
|
static EVENT_SCROLL = "EVENT_SCROLL";
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const o = this.options;
|
|
|
|
const { overflowX, overflowY, el } = this.options;
|
|
|
|
this.renderedCells = [];
|
|
|
|
this.renderedKeys = [];
|
|
|
|
this.renderRange = {};
|
|
|
|
this._scrollLock = false;
|
|
|
|
this._debounceRelease = BI.debounce(() => {
|
|
|
|
this._scrollLock = false;
|
|
|
|
}, 1000 / 60);
|
|
|
|
this.container = BI._lazyCreateWidget({
|
|
|
|
type: "bi.absolute",
|
|
|
|
});
|
|
|
|
this.element.scroll(() => {
|
|
|
|
if (this._scrollLock === true) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
o.scrollLeft = this.element.scrollLeft();
|
|
|
|
o.scrollTop = this.element.scrollTop();
|
|
|
|
this._calculateChildrenToRender();
|
|
|
|
this.fireEvent(GridView.EVENT_SCROLL, {
|
|
|
|
scrollLeft: o.scrollLeft,
|
|
|
|
scrollTop: o.scrollTop,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
// 兼容一下
|
|
|
|
let scrollable = o.scrollable, scrollx = o.scrollx, scrolly = o.scrolly;
|
|
|
|
if (overflowX === false) {
|
|
|
|
if (overflowY === false) {
|
|
|
|
scrollable = false;
|
|
|
|
} else {
|
|
|
|
scrollable = "y";
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (overflowY === false) {
|
|
|
|
scrollable = "x";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
BI._lazyCreateWidget(el, {
|
|
|
|
type: "bi.vertical",
|
|
|
|
element: this,
|
|
|
|
scrollable: scrollable,
|
|
|
|
scrolly: scrolly,
|
|
|
|
scrollx: scrollx,
|
|
|
|
items: [this.container],
|
|
|
|
});
|
|
|
|
o.items = BI.isFunction(o.items) ? this.__watch(o.items, (context, newValue) => {
|
|
|
|
this.populate(newValue);
|
|
|
|
}) : o.items;
|
|
|
|
if (o.items.length > 0) {
|
|
|
|
this._calculateSizeAndPositionData();
|
|
|
|
this._populate();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// mounted之后绑定事件
|
|
|
|
mounted() {
|
|
|
|
const { scrollLeft, scrollTop } = this.options;
|
|
|
|
if (scrollLeft !== 0 || scrollTop !== 0) {
|
|
|
|
this.element.scrollTop(scrollTop);
|
|
|
|
this.element.scrollLeft(scrollLeft);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
destroyed() {
|
|
|
|
BI.each(this.renderedCells, (i, cell) => {
|
|
|
|
cell.el._destroy();
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
_calculateSizeAndPositionData() {
|
|
|
|
const { columnCount, items, rowCount, columnWidthGetter, estimatedColumnSize, rowHeightGetter, estimatedRowSize } = this.options;
|
|
|
|
this.rowCount = 0;
|
|
|
|
this.columnCount = 0;
|
|
|
|
if (BI.isNumber(columnCount)) {
|
|
|
|
this.columnCount = columnCount;
|
|
|
|
} else if (items.length > 0) {
|
|
|
|
this.columnCount = items[0].length;
|
|
|
|
}
|
|
|
|
if (BI.isNumber(rowCount)) {
|
|
|
|
this.rowCount = rowCount;
|
|
|
|
} else {
|
|
|
|
this.rowCount = items.length;
|
|
|
|
}
|
|
|
|
this._columnSizeAndPositionManager = new BI.ScalingCellSizeAndPositionManager(this.columnCount, columnWidthGetter, estimatedColumnSize);
|
|
|
|
this._rowSizeAndPositionManager = new BI.ScalingCellSizeAndPositionManager(this.rowCount, rowHeightGetter, estimatedRowSize);
|
|
|
|
}
|
|
|
|
|
|
|
|
_getOverscanIndices(cellCount, overscanCellsCount, startIndex, stopIndex) {
|
|
|
|
return {
|
|
|
|
overscanStartIndex: Math.max(0, startIndex - overscanCellsCount),
|
|
|
|
overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
_calculateChildrenToRender() {
|
|
|
|
const o = this.options;
|
|
|
|
|
|
|
|
const { itemFormatter, items } = this.options;
|
|
|
|
|
|
|
|
const width = o.width, height = o.height, scrollLeft = BI.clamp(o.scrollLeft, 0, this._getMaxScrollLeft()),
|
|
|
|
scrollTop = BI.clamp(o.scrollTop, 0, this._getMaxScrollTop()),
|
|
|
|
overscanColumnCount = o.overscanColumnCount, overscanRowCount = o.overscanRowCount;
|
|
|
|
|
|
|
|
if (height > 0 && width > 0) {
|
|
|
|
const visibleColumnIndices = this._columnSizeAndPositionManager.getVisibleCellRange(width, scrollLeft);
|
|
|
|
const visibleRowIndices = this._rowSizeAndPositionManager.getVisibleCellRange(height, scrollTop);
|
|
|
|
|
|
|
|
const renderedCells = [], renderedKeys = {}, renderedWidgets = {};
|
|
|
|
let minX = this._getMaxScrollLeft(), minY = this._getMaxScrollTop(), maxX = 0, maxY = 0;
|
|
|
|
// 没有可见的单元格就干掉所有渲染过的
|
|
|
|
if (!BI.isEmpty(visibleColumnIndices) && !BI.isEmpty(visibleRowIndices)) {
|
|
|
|
const horizontalOffsetAdjustment = this._columnSizeAndPositionManager.getOffsetAdjustment(width, scrollLeft);
|
|
|
|
const verticalOffsetAdjustment = this._rowSizeAndPositionManager.getOffsetAdjustment(height, scrollTop);
|
|
|
|
|
|
|
|
this._renderedColumnStartIndex = visibleColumnIndices.start;
|
|
|
|
this._renderedColumnStopIndex = visibleColumnIndices.stop;
|
|
|
|
this._renderedRowStartIndex = visibleRowIndices.start;
|
|
|
|
this._renderedRowStopIndex = visibleRowIndices.stop;
|
|
|
|
|
|
|
|
const overscanColumnIndices = this._getOverscanIndices(this.columnCount, overscanColumnCount, this._renderedColumnStartIndex, this._renderedColumnStopIndex);
|
|
|
|
|
|
|
|
const overscanRowIndices = this._getOverscanIndices(this.rowCount, overscanRowCount, this._renderedRowStartIndex, this._renderedRowStopIndex);
|
|
|
|
|
|
|
|
const columnStartIndex = overscanColumnIndices.overscanStartIndex;
|
|
|
|
const columnStopIndex = overscanColumnIndices.overscanStopIndex;
|
|
|
|
const rowStartIndex = overscanRowIndices.overscanStartIndex;
|
|
|
|
const rowStopIndex = overscanRowIndices.overscanStopIndex;
|
|
|
|
|
|
|
|
// 算区间size
|
|
|
|
const minRowDatum = this._rowSizeAndPositionManager.getSizeAndPositionOfCell(rowStartIndex);
|
|
|
|
const minColumnDatum = this._columnSizeAndPositionManager.getSizeAndPositionOfCell(columnStartIndex);
|
|
|
|
const maxRowDatum = this._rowSizeAndPositionManager.getSizeAndPositionOfCell(rowStopIndex);
|
|
|
|
const maxColumnDatum = this._columnSizeAndPositionManager.getSizeAndPositionOfCell(columnStopIndex);
|
|
|
|
const top = minRowDatum.offset + verticalOffsetAdjustment;
|
|
|
|
const left = minColumnDatum.offset + horizontalOffsetAdjustment;
|
|
|
|
const bottom = maxRowDatum.offset + verticalOffsetAdjustment + maxRowDatum.size;
|
|
|
|
const right = maxColumnDatum.offset + horizontalOffsetAdjustment + maxColumnDatum.size;
|
|
|
|
// 如果滚动的区间并没有超出渲染的范围
|
|
|
|
if (top >= this.renderRange.minY && bottom <= this.renderRange.maxY && left >= this.renderRange.minX && right <= this.renderRange.maxX) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let count = 0;
|
|
|
|
for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
|
|
|
|
const rowDatum = this._rowSizeAndPositionManager.getSizeAndPositionOfCell(rowIndex);
|
|
|
|
|
|
|
|
for (let columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) {
|
|
|
|
const key = rowIndex + "-" + columnIndex;
|
|
|
|
const columnDatum = this._columnSizeAndPositionManager.getSizeAndPositionOfCell(columnIndex);
|
|
|
|
|
|
|
|
const index = this.renderedKeys[key] && this.renderedKeys[key][2];
|
|
|
|
let child;
|
|
|
|
if (index >= 0) {
|
|
|
|
this.renderedCells[index].el.setWidth(columnDatum.size);
|
|
|
|
this.renderedCells[index].el.setHeight(rowDatum.size);
|
|
|
|
// 这里只使用px
|
|
|
|
this.renderedCells[index].el.element.css("left", columnDatum.offset + horizontalOffsetAdjustment + "px");
|
|
|
|
this.renderedCells[index].el.element.css("top", rowDatum.offset + verticalOffsetAdjustment + "px");
|
|
|
|
child = this.renderedCells[index].el;
|
|
|
|
renderedCells.push(this.renderedCells[index]);
|
|
|
|
} else {
|
|
|
|
const item = itemFormatter(items[rowIndex][columnIndex], rowIndex, columnIndex);
|
|
|
|
child = BI._lazyCreateWidget(BI.extend({
|
|
|
|
type: "bi.label",
|
|
|
|
width: columnDatum.size,
|
|
|
|
height: rowDatum.size,
|
|
|
|
}, item, {
|
|
|
|
cls: (item.cls || "") + " grid-cell" + (rowIndex === 0 ? " first-row" : "") + (columnIndex === 0 ? " first-col" : ""),
|
|
|
|
_rowIndex: rowIndex,
|
|
|
|
_columnIndex: columnIndex,
|
|
|
|
_left: columnDatum.offset + horizontalOffsetAdjustment,
|
|
|
|
_top: rowDatum.offset + verticalOffsetAdjustment,
|
|
|
|
}), this);
|
|
|
|
renderedCells.push({
|
|
|
|
el: child,
|
|
|
|
left: columnDatum.offset + horizontalOffsetAdjustment + "px",
|
|
|
|
top: rowDatum.offset + verticalOffsetAdjustment + "px",
|
|
|
|
_left: columnDatum.offset + horizontalOffsetAdjustment,
|
|
|
|
_top: rowDatum.offset + verticalOffsetAdjustment,
|
|
|
|
// _width: columnDatum.size,
|
|
|
|
// _height: rowDatum.size
|
|
|
|
});
|
|
|
|
}
|
|
|
|
minX = Math.min(minX, columnDatum.offset + horizontalOffsetAdjustment);
|
|
|
|
maxX = Math.max(maxX, columnDatum.offset + horizontalOffsetAdjustment + columnDatum.size);
|
|
|
|
minY = Math.min(minY, rowDatum.offset + verticalOffsetAdjustment);
|
|
|
|
maxY = Math.max(maxY, rowDatum.offset + verticalOffsetAdjustment + rowDatum.size);
|
|
|
|
renderedKeys[key] = [rowIndex, columnIndex, count];
|
|
|
|
renderedWidgets[count] = child;
|
|
|
|
count++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 已存在的, 需要添加的和需要删除的
|
|
|
|
const existSet = {}, addSet = {}, deleteArray = [];
|
|
|
|
BI.each(renderedKeys, (i, key) => {
|
|
|
|
if (this.renderedKeys[i]) {
|
|
|
|
existSet[i] = key;
|
|
|
|
} else {
|
|
|
|
addSet[i] = key;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
BI.each(this.renderedKeys, (i, key) => {
|
|
|
|
if (existSet[i]) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (addSet[i]) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
deleteArray.push(key[2]);
|
|
|
|
});
|
|
|
|
BI.each(deleteArray, (i, index) => {
|
|
|
|
// 性能优化,不调用destroy方法防止触发destroy事件
|
|
|
|
this.renderedCells[index].el._destroy();
|
|
|
|
});
|
|
|
|
const addedItems = [];
|
|
|
|
BI.each(addSet, (index, key) => {
|
|
|
|
addedItems.push(renderedCells[key[2]]);
|
|
|
|
});
|
|
|
|
// 与listview一样, 给上下文
|
|
|
|
this.container.addItems(addedItems, this);
|
|
|
|
// 拦截父子级关系
|
|
|
|
this.container._children = renderedWidgets;
|
|
|
|
this.container.attr("items", renderedCells);
|
|
|
|
this.renderedCells = renderedCells;
|
|
|
|
this.renderedKeys = renderedKeys;
|
|
|
|
this.renderRange = { minX: minX, minY: minY, maxX: maxX, maxY: maxY };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_isOverflowX() {
|
|
|
|
const { scrollable, scrollx, overflowX } = this.options;
|
|
|
|
// 兼容一下
|
|
|
|
if (overflowX === false) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (scrollx) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (scrollable === true || scrollable === "xy" || scrollable === "x") {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
_isOverflowY() {
|
|
|
|
const { scrollable, scrolly, overflowX } = this.options;
|
|
|
|
// 兼容一下
|
|
|
|
// var scrollable = o.scrollable, scrolly = o.scrolly;
|
|
|
|
if (overflowX === false) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (scrolly) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (scrollable === true || scrollable === "xy" || scrollable === "y") {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
_getMaxScrollLeft() {
|
|
|
|
return Math.max(0, this._getContainerWidth() - this.options.width + (this._isOverflowX() ? BI.DOM.getScrollWidth() : 0));
|
|
|
|
}
|
|
|
|
|
|
|
|
_getMaxScrollTop() {
|
|
|
|
return Math.max(0, this._getContainerHeight() - this.options.height + (this._isOverflowY() ? BI.DOM.getScrollWidth() : 0));
|
|
|
|
}
|
|
|
|
|
|
|
|
_getContainerWidth() {
|
|
|
|
return this.columnCount * this.options.estimatedColumnSize;
|
|
|
|
}
|
|
|
|
|
|
|
|
_getContainerHeight() {
|
|
|
|
return this.rowCount * this.options.estimatedRowSize;
|
|
|
|
}
|
|
|
|
|
|
|
|
_populate(items) {
|
|
|
|
const { scrollTop, scrollLeft } = this.options;
|
|
|
|
this._reRange();
|
|
|
|
if (items && items !== this.options.items) {
|
|
|
|
this.options.items = items;
|
|
|
|
this._calculateSizeAndPositionData();
|
|
|
|
}
|
|
|
|
this.container.setWidth(this._getContainerWidth());
|
|
|
|
this.container.setHeight(this._getContainerHeight());
|
|
|
|
|
|
|
|
// 元素未挂载时不能设置scrollTop
|
|
|
|
this._debounceRelease();
|
|
|
|
try {
|
|
|
|
this.element.scrollTop(scrollTop);
|
|
|
|
this.element.scrollLeft(scrollLeft);
|
|
|
|
} catch (e) {
|
|
|
|
}
|
|
|
|
this._calculateChildrenToRender();
|
|
|
|
}
|
|
|
|
|
|
|
|
setScrollLeft(scrollLeft) {
|
|
|
|
if (this.options.scrollLeft === scrollLeft) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this._scrollLock = true;
|
|
|
|
this.options.scrollLeft = BI.clamp(scrollLeft || 0, 0, this._getMaxScrollLeft());
|
|
|
|
this._debounceRelease();
|
|
|
|
this.element.scrollLeft(this.options.scrollLeft);
|
|
|
|
this._calculateChildrenToRender();
|
|
|
|
}
|
|
|
|
|
|
|
|
setScrollTop(scrollTop) {
|
|
|
|
if (this.options.scrollTop === scrollTop) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this._scrollLock = true;
|
|
|
|
this.options.scrollTop = BI.clamp(scrollTop || 0, 0, this._getMaxScrollTop());
|
|
|
|
this._debounceRelease();
|
|
|
|
this.element.scrollTop(this.options.scrollTop);
|
|
|
|
this._calculateChildrenToRender();
|
|
|
|
}
|
|
|
|
|
|
|
|
setColumnCount(columnCount) {
|
|
|
|
this.options.columnCount = columnCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
setRowCount(rowCount) {
|
|
|
|
this.options.rowCount = rowCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
setOverflowX(b) {
|
|
|
|
if (this.options.overflowX !== !!b) {
|
|
|
|
this.options.overflowX = !!b;
|
|
|
|
BI.nextTick(() => {
|
|
|
|
this.element.css({ overflowX: b ? "auto" : "hidden" });
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
setOverflowY(b) {
|
|
|
|
if (this.options.overflowY !== !!b) {
|
|
|
|
this.options.overflowY = !!b;
|
|
|
|
BI.nextTick(() => {
|
|
|
|
this.element.css({ overflowY: b ? "auto" : "hidden" });
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getScrollLeft() {
|
|
|
|
return this.options.scrollLeft;
|
|
|
|
}
|
|
|
|
getScrollTop() {
|
|
|
|
return this.options.scrollTop;
|
|
|
|
}
|
|
|
|
|
|
|
|
getMaxScrollLeft() {
|
|
|
|
return this._getMaxScrollLeft();
|
|
|
|
}
|
|
|
|
|
|
|
|
getMaxScrollTop() {
|
|
|
|
return this._getMaxScrollTop();
|
|
|
|
}
|
|
|
|
|
|
|
|
setEstimatedColumnSize(width) {
|
|
|
|
this.options.estimatedColumnSize = width;
|
|
|
|
}
|
|
|
|
|
|
|
|
setEstimatedRowSize(height) {
|
|
|
|
this.options.estimatedRowSize = height;
|
|
|
|
}
|
|
|
|
// 重新计算children
|
|
|
|
_reRange() {
|
|
|
|
this.renderRange = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
_clearChildren() {
|
|
|
|
this.container._children = {};
|
|
|
|
this.container.attr("items", []);
|
|
|
|
}
|
|
|
|
|
|
|
|
restore() {
|
|
|
|
BI.each(this.renderedCells, (i, cell) => {
|
|
|
|
cell.el._destroy();
|
|
|
|
});
|
|
|
|
this._clearChildren();
|
|
|
|
this.renderedCells = [];
|
|
|
|
this.renderedKeys = [];
|
|
|
|
this.renderRange = {};
|
|
|
|
this._scrollLock = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
populate(items) {
|
|
|
|
if (items && items !== this.options.items) {
|
|
|
|
this.restore();
|
|
|
|
}
|
|
|
|
this._populate(items);
|
|
|
|
}
|
|
|
|
}
|