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.
488 lines
16 KiB
488 lines
16 KiB
import { |
|
VerticalLayout, |
|
AbsoluteLayout, |
|
Widget, |
|
shortcut, |
|
extend, |
|
emptyFn, |
|
debounce, |
|
_lazyCreateWidget, |
|
isFunction, |
|
SectionManager, |
|
isNull, |
|
each, |
|
clamp, |
|
toArray, |
|
invert, |
|
min, |
|
max, |
|
nextTick, |
|
DOM |
|
} from "@/core"; |
|
import { Label } from "../single"; |
|
|
|
/** |
|
* CollectionView |
|
* |
|
* Created by GUY on 2016/1/15. |
|
* @class CollectionView |
|
* @extends Widget |
|
*/ |
|
|
|
@shortcut() |
|
export class CollectionView extends Widget { |
|
_defaultConfig() { |
|
return extend(super._defaultConfig(...arguments), { |
|
baseCls: "bi-collection", |
|
// width: 400, //必设 |
|
// height: 300, //必设 |
|
scrollable: true, |
|
scrollx: false, |
|
scrolly: false, |
|
overflowX: true, |
|
overflowY: true, |
|
el: { |
|
type: VerticalLayout.xtype, |
|
}, |
|
cellSizeAndPositionGetter: emptyFn, |
|
horizontalOverscanSize: 0, |
|
verticalOverscanSize: 0, |
|
scrollLeft: 0, |
|
scrollTop: 0, |
|
items: [], |
|
itemFormatter: (item, index) => item, |
|
}); |
|
} |
|
|
|
static EVENT_SCROLL = "EVENT_SCROLL"; |
|
static xtype = "bi.collection_view"; |
|
|
|
render() { |
|
const o = this.options; |
|
const { overflowX, overflowY, el } = this.options; |
|
this.renderedCells = []; |
|
this.renderedKeys = []; |
|
this.renderRange = {}; |
|
this._scrollLock = false; |
|
this._debounceRelease = debounce(() => { |
|
this._scrollLock = false; |
|
}, 1000 / 60); |
|
this.container = _lazyCreateWidget({ |
|
type: AbsoluteLayout.xtype, |
|
}); |
|
this.element.scroll(() => { |
|
if (this._scrollLock === true) { |
|
return; |
|
} |
|
o.scrollLeft = this.element.scrollLeft(); |
|
o.scrollTop = this.element.scrollTop(); |
|
this._calculateChildrenToRender(); |
|
this.fireEvent(CollectionView.EVENT_SCROLL, { |
|
scrollLeft: o.scrollLeft, |
|
scrollTop: o.scrollTop, |
|
}); |
|
}); |
|
// 兼容一下 |
|
let scrollable = o.scrollable; |
|
const scrollx = o.scrollx, |
|
scrolly = o.scrolly; |
|
if (overflowX === false) { |
|
if (overflowY === false) { |
|
scrollable = false; |
|
} else { |
|
scrollable = "y"; |
|
} |
|
} else { |
|
if (overflowY === false) { |
|
scrollable = "x"; |
|
} |
|
} |
|
_lazyCreateWidget(el, { |
|
type: VerticalLayout.xtype, |
|
element: this, |
|
scrollable, |
|
scrolly, |
|
scrollx, |
|
items: [this.container], |
|
}); |
|
o.items = 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); |
|
} |
|
} |
|
|
|
_calculateSizeAndPositionData() { |
|
const { items, cellSizeAndPositionGetter } = this.options; |
|
const cellMetadata = []; |
|
const sectionManager = new SectionManager(); |
|
let height = 0; |
|
let width = 0; |
|
|
|
for (let index = 0, len = items.length; index < len; index++) { |
|
const cellMetadatum = cellSizeAndPositionGetter(index); |
|
|
|
if ( |
|
isNull(cellMetadatum.height) || |
|
isNaN(cellMetadatum.height) || |
|
isNull(cellMetadatum.width) || |
|
isNaN(cellMetadatum.width) || |
|
isNull(cellMetadatum.x) || |
|
isNaN(cellMetadatum.x) || |
|
isNull(cellMetadatum.y) || |
|
isNaN(cellMetadatum.y) |
|
) { |
|
throw Error(); |
|
} |
|
|
|
height = Math.max(height, cellMetadatum.y + cellMetadatum.height); |
|
width = Math.max(width, cellMetadatum.x + cellMetadatum.width); |
|
|
|
cellMetadatum.index = index; |
|
cellMetadata[index] = cellMetadatum; |
|
sectionManager.registerCell(cellMetadatum, index); |
|
} |
|
|
|
this._cellMetadata = cellMetadata; |
|
this._sectionManager = sectionManager; |
|
this._height = height; |
|
this._width = width; |
|
} |
|
|
|
_cellRenderers(height, width, x, y) { |
|
this._lastRenderedCellIndices = this._sectionManager.getCellIndices(height, width, x, y); |
|
|
|
return this._cellGroupRenderer(); |
|
} |
|
|
|
_cellGroupRenderer() { |
|
const rendered = []; |
|
each(this._lastRenderedCellIndices, (i, index) => { |
|
const cellMetadata = this._sectionManager.getCellMetadata(index); |
|
rendered.push(cellMetadata); |
|
}); |
|
|
|
return rendered; |
|
} |
|
|
|
_calculateChildrenToRender() { |
|
const o = this.options; |
|
const { horizontalOverscanSize, verticalOverscanSize, width, height, itemFormatter, items } = this.options; |
|
const scrollLeft = clamp(o.scrollLeft, 0, this._getMaxScrollLeft()); |
|
const scrollTop = clamp(o.scrollTop, 0, this._getMaxScrollTop()); |
|
const left = Math.max(0, scrollLeft - horizontalOverscanSize); |
|
const top = Math.max(0, scrollTop - verticalOverscanSize); |
|
const right = Math.min(this._width, scrollLeft + width + horizontalOverscanSize); |
|
const bottom = Math.min(this._height, scrollTop + height + verticalOverscanSize); |
|
if (right > 0 && bottom > 0) { |
|
// 如果滚动的区间并没有超出渲染的范围 |
|
if ( |
|
top >= this.renderRange.minY && |
|
bottom <= this.renderRange.maxY && |
|
left >= this.renderRange.minX && |
|
right <= this.renderRange.maxX |
|
) { |
|
return; |
|
} |
|
const childrenToDisplay = this._cellRenderers(bottom - top, right - left, left, top); |
|
const renderedCells = [], |
|
renderedKeys = {}, |
|
renderedWidgets = {}; |
|
// 存储所有的left和top |
|
let lefts = {}, |
|
tops = {}; |
|
for (let i = 0, len = childrenToDisplay.length; i < len; i++) { |
|
const datum = childrenToDisplay[i]; |
|
lefts[datum.x] = datum.x; |
|
lefts[datum.x + datum.width] = datum.x + datum.width; |
|
tops[datum.y] = datum.y; |
|
tops[datum.y + datum.height] = datum.y + datum.height; |
|
} |
|
lefts = toArray(lefts); |
|
tops = toArray(tops); |
|
const leftMap = invert(lefts); |
|
const topMap = invert(tops); |
|
// 存储上下左右四个边界 |
|
const leftBorder = {}, |
|
rightBorder = {}, |
|
topBorder = {}, |
|
bottomBorder = {}; |
|
function assertMinBorder(border, offset) { |
|
if (isNull(border[offset])) { |
|
border[offset] = Number.MAX_VALUE; |
|
} |
|
} |
|
function assertMaxBorder(border, offset) { |
|
if (isNull(border[offset])) { |
|
border[offset] = 0; |
|
} |
|
} |
|
for (let i = 0, len = childrenToDisplay.length; i < len; i++) { |
|
const datum = childrenToDisplay[i]; |
|
const index = this.renderedKeys[datum.index] && this.renderedKeys[datum.index][1]; |
|
let child; |
|
if (index >= 0) { |
|
this.renderedCells[index].el.setWidth(datum.width); |
|
this.renderedCells[index].el.setHeight(datum.height); |
|
// 这里只使用px |
|
this.renderedCells[index].el.element.css("left", `${datum.x}px`); |
|
this.renderedCells[index].el.element.css("top", `${datum.y}px`); |
|
renderedCells.push((child = this.renderedCells[index])); |
|
} else { |
|
const item = itemFormatter(items[datum.index], datum.index); |
|
child = _lazyCreateWidget( |
|
extend( |
|
{ |
|
type: Label.xtype, |
|
width: datum.width, |
|
height: datum.height, |
|
}, |
|
item, |
|
{ |
|
cls: `${item.cls || ""} collection-cell${datum.y === 0 ? " first-row" : ""}${ |
|
datum.x === 0 ? " first-col" : "" |
|
}`, |
|
_left: datum.x, |
|
_top: datum.y, |
|
} |
|
) |
|
); |
|
renderedCells.push({ |
|
el: child, |
|
left: `${datum.x}px`, |
|
top: `${datum.y}px`, |
|
_left: datum.x, |
|
_top: datum.y, |
|
// _width: datum.width, |
|
// _height: datum.height |
|
}); |
|
} |
|
const startTopIndex = topMap[datum.y] | 0; |
|
const endTopIndex = topMap[datum.y + datum.height] | 0; |
|
for (let k = startTopIndex; k <= endTopIndex; k++) { |
|
const t = tops[k]; |
|
assertMinBorder(leftBorder, t); |
|
assertMaxBorder(rightBorder, t); |
|
leftBorder[t] = Math.min(leftBorder[t], datum.x); |
|
rightBorder[t] = Math.max(rightBorder[t], datum.x + datum.width); |
|
} |
|
const startLeftIndex = leftMap[datum.x] | 0; |
|
const endLeftIndex = leftMap[datum.x + datum.width] | 0; |
|
for (let k = startLeftIndex; k <= endLeftIndex; k++) { |
|
const l = lefts[k]; |
|
assertMinBorder(topBorder, l); |
|
assertMaxBorder(bottomBorder, l); |
|
topBorder[l] = Math.min(topBorder[l], datum.y); |
|
bottomBorder[l] = Math.max(bottomBorder[l], datum.y + datum.height); |
|
} |
|
|
|
renderedKeys[datum.index] = [datum.index, i]; |
|
renderedWidgets[i] = child; |
|
} |
|
// 已存在的, 需要添加的和需要删除的 |
|
const existSet = {}, |
|
addSet = {}, |
|
deleteArray = []; |
|
each(renderedKeys, (i, key) => { |
|
if (this.renderedKeys[i]) { |
|
existSet[i] = key; |
|
} else { |
|
addSet[i] = key; |
|
} |
|
}); |
|
each(this.renderedKeys, (i, key) => { |
|
if (existSet[i]) { |
|
return; |
|
} |
|
if (addSet[i]) { |
|
return; |
|
} |
|
deleteArray.push(key[1]); |
|
}); |
|
each(deleteArray, (i, index) => { |
|
// 性能优化,不调用destroy方法防止触发destroy事件 |
|
this.renderedCells[index].el._destroy(); |
|
}); |
|
const addedItems = []; |
|
each(addSet, (index, key) => { |
|
addedItems.push(renderedCells[key[1]]); |
|
}); |
|
this.container.addItems(addedItems); |
|
// 拦截父子级关系 |
|
this.container._children = renderedWidgets; |
|
this.container.attr("items", renderedCells); |
|
this.renderedCells = renderedCells; |
|
this.renderedKeys = renderedKeys; |
|
|
|
// Todo 左右比较特殊 |
|
const minX = min(leftBorder); |
|
const maxX = max(rightBorder); |
|
|
|
const minY = max(topBorder); |
|
const maxY = min(bottomBorder); |
|
|
|
this.renderRange = { minX, minY, maxX, maxY }; |
|
} |
|
} |
|
|
|
_isOverflowX() { |
|
const o = this.options; |
|
const { overflowX } = this.options; |
|
// 兼容一下 |
|
const scrollable = o.scrollable, |
|
scrollx = o.scrollx; |
|
if (overflowX === false) { |
|
return false; |
|
} |
|
if (scrollx) { |
|
return true; |
|
} |
|
if (scrollable === true || scrollable === "xy" || scrollable === "x") { |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
_isOverflowY() { |
|
const o = this.options; |
|
const { overflowX } = this.options; |
|
// 兼容一下 |
|
const 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._width - this.options.width + (this._isOverflowX() ? DOM.getScrollWidth() : 0)); |
|
} |
|
|
|
_getMaxScrollTop() { |
|
return Math.max(0, this._height - this.options.height + (this._isOverflowY() ? DOM.getScrollWidth() : 0)); |
|
} |
|
|
|
_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._width); |
|
this.container.setHeight(this._height); |
|
|
|
this._debounceRelease(); |
|
// 元素未挂载时不能设置scrollTop |
|
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 = 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 = clamp(scrollTop || 0, 0, this._getMaxScrollTop()); |
|
this._debounceRelease(); |
|
this.element.scrollTop(this.options.scrollTop); |
|
this._calculateChildrenToRender(); |
|
} |
|
|
|
setOverflowX(b) { |
|
if (this.options.overflowX !== !!b) { |
|
this.options.overflowX = !!b; |
|
nextTick(() => { |
|
this.element.css({ overflowX: b ? "auto" : "hidden" }); |
|
}); |
|
} |
|
} |
|
|
|
setOverflowY(b) { |
|
if (this.options.overflowY !== !!b) { |
|
this.options.overflowY = !!b; |
|
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(); |
|
} |
|
|
|
// 重新计算children |
|
_reRange() { |
|
this.renderRange = {}; |
|
} |
|
|
|
_clearChildren() { |
|
this.container._children = {}; |
|
this.container.attr("items", []); |
|
} |
|
|
|
restore() { |
|
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); |
|
} |
|
}
|
|
|