@fesjs/fes-design
Version:
fes-design for PC
353 lines (334 loc) • 10.7 kB
JavaScript
import { defineComponent, ref, watch, onActivated, onMounted, onBeforeUnmount, createVNode } from 'vue';
import { throttle, isNil } from 'lodash-es';
import FScrollbar from '../scrollbar/scrollbar.js';
import { TO_TOP_EVENT, TO_BOTTOM_EVENT, RESIZED_EVENT } from '../_util/constants';
import getPrefixCls from '../_util/getPrefixCls';
import Virtual from './virtual';
import { FVirtualListItem } from './listItem';
import { virtualProps } from './props';
import { ITME_RESIZE_UPDATE_SCROLL_BAR_TIMEOUT } from './const';
/**
* virtual list default component
* rewrite by uct8086
*/
var SLOT_TYPE = /*#__PURE__*/function (SLOT_TYPE) {
SLOT_TYPE["HEADER"] = "thead";
// string value also use for aria role attribute
SLOT_TYPE["FOOTER"] = "tfoot";
return SLOT_TYPE;
}(SLOT_TYPE || {});
const prefixCls = getPrefixCls('virtual-list');
var virtualList = defineComponent({
name: 'FVirtualList',
props: virtualProps,
emits: [TO_TOP_EVENT, TO_BOTTOM_EVENT, RESIZED_EVENT, 'scroll'],
setup(props, _ref) {
let {
emit,
slots
} = _ref;
const isHorizontal = props.direction === 'horizontal';
const directionKey = isHorizontal ? 'scrollLeft' : 'scrollTop';
const rootRef = ref();
// const shepherdRef = ref();
const rangeRef = ref(Object.create(null));
const scrollRef = ref();
let virtual = null;
const getUniqueIdFromDataSources = () => {
const {
dataKey
} = props;
return props.dataSources.map(dataSource => typeof dataKey === 'function' ? dataKey(dataSource) : dataSource[dataKey]);
};
const installVirtual = () => {
virtual = new Virtual({
slotHeaderSize: 0,
slotFooterSize: 0,
keeps: props.keeps,
estimateSize: props.estimateSize,
buffer: Math.round(props.keeps / 3),
// increase buffer size for smoother scrolling
uniqueIds: getUniqueIdFromDataSources()
}, range => {
rangeRef.value = range;
});
// sync initial range
rangeRef.value = virtual.getRange();
};
installVirtual();
// get item size by id
const getSize = id => virtual.sizes.get(id);
// get the total number of stored (rendered) items
const getSizes = () => virtual.sizes.size;
// return current scroll offset
const getOffset = () => {
const root = rootRef.value;
return root ? Math.ceil(root[directionKey]) : 0;
};
// return client viewport size
const getClientSize = () => {
const key = isHorizontal ? 'clientWidth' : 'clientHeight';
const root = rootRef.value;
return root ? Math.ceil(root[key]) : 0;
};
// return all scroll size
const getScrollSize = () => {
const key = isHorizontal ? 'scrollWidth' : 'scrollHeight';
const root = rootRef.value;
return root ? Math.ceil(root[key]) : 0;
};
// set current scroll position to a expectant offset
const scrollToOffset = offset => {
const root = rootRef.value;
if (root) {
if (isHorizontal) {
root.scrollBy(offset, 0);
} else {
root.scrollBy(0, offset); // 解决设置OffsetTop无效的问题
}
}
};
// set current scroll position to a expectant target
const scrollToTarget = position => {
const root = rootRef.value;
if (root) {
if (isHorizontal) {
root.scrollTo(position, 0);
} else {
root.scrollTo(0, position); // 解决设置OffsetTop无效的问题
}
}
};
// set current scroll position to bottom
const scrollToBottom = () => {
const root = rootRef.value;
if (root) {
const offset = root[isHorizontal ? 'scrollWidth' : 'scrollHeight'];
scrollToTarget(offset);
// check if it's really scrolled to the bottom
// maybe list doesn't render and calculate to last range
// so we need retry in next event loop until it really at bottom
setTimeout(() => {
if (getOffset() + getClientSize() < getScrollSize()) {
scrollToBottom();
}
}, ITME_RESIZE_UPDATE_SCROLL_BAR_TIMEOUT + 10);
}
};
// set current scroll position to a expectant index
const scrollToIndex = index => {
// scroll to bottom
if (index >= props.dataSources.length - 1) {
scrollToBottom();
} else {
const offset = virtual.getOffset(index);
scrollToTarget(offset);
}
};
// reset all state back to initial
const reset = () => {
virtual.destroy();
scrollToIndex(0);
installVirtual();
};
let lastSize = getSizes();
const updateScrollBar = throttle(() => {
const nowSize = getSizes();
if (nowSize !== lastSize) {
lastSize = nowSize;
if (scrollRef.value) {
var _scrollRef$value$upda, _scrollRef$value;
(_scrollRef$value$upda = (_scrollRef$value = scrollRef.value).update) === null || _scrollRef$value$upda === void 0 || _scrollRef$value$upda.call(_scrollRef$value);
}
}
}, ITME_RESIZE_UPDATE_SCROLL_BAR_TIMEOUT);
// event called when each item mounted or size changed
const onItemResized = (id, size) => {
const sizes = virtual.sizes;
const oldSize = sizes.get(id);
if (oldSize !== size) {
virtual.saveSize(id, size);
emit(RESIZED_EVENT, id, size);
updateScrollBar();
}
};
// event called when slot mounted or size changed
const onSlotResized = (type, size, hasInit) => {
if (slots.header() || slots.footer()) {
if (type === SLOT_TYPE.HEADER) {
virtual.updateParam('slotHeaderSize', size);
} else if (type === SLOT_TYPE.FOOTER) {
virtual.updateParam('slotFooterSize', size);
}
if (hasInit) {
virtual.handleSlotSizeChange();
}
}
};
// emit event in special position
const emitEvent = (offset, clientSize, scrollSize, evt) => {
emit('scroll', evt, virtual.getRange());
if (virtual.isFront() && !!props.dataSources.length && offset - props.topThreshold <= 0) {
emit(TO_TOP_EVENT);
} else if (virtual.isBehind() && offset + clientSize + props.bottomThreshold >= scrollSize) {
emit(TO_BOTTOM_EVENT);
}
};
const onScroll = evt => {
const offset = getOffset();
const clientSize = getClientSize();
const scrollSize = getScrollSize();
// iOS scroll-spring-back behavior will make direction mistake
if (offset < 0 || offset + clientSize > scrollSize + 1 || !scrollSize) {
return;
}
virtual.handleScroll(offset);
emitEvent(offset, clientSize, scrollSize, evt);
};
// get the real render slots based on range data
// in-place patch strategy will try to reuse components as possible
// so those components that are reused will not trigger lifecycle mounted
const getRenderItems = () => {
const itemVNodes = [];
const {
start,
end
} = rangeRef.value;
const {
dataSources,
dataKey
} = props;
for (let index = start; index <= end; index++) {
const dataSource = dataSources[index];
if (!isNil(dataSource)) {
const uniqueKey = typeof dataKey === 'function' ? dataKey(dataSource) : dataSource[dataKey];
if (typeof uniqueKey === 'string' || typeof uniqueKey === 'number') {
const tempNode = createVNode(FVirtualListItem, {
key: uniqueKey,
index,
horizontal: isHorizontal,
uniqueKey,
source: dataSource,
onItemResized,
observeResize: props.observeResize
}, {
default: slots.default
});
itemVNodes.push(tempNode);
} else {
console.warn(`Cannot get the data-key '${dataKey}' from data-sources.`);
}
} else {
console.warn(`Cannot get the index '${index}' from data-sources.`);
}
}
return itemVNodes;
};
watch(() => props.dataSources, () => {
virtual.updateParam('uniqueIds', getUniqueIdFromDataSources());
virtual.handleDataSourcesChange();
});
watch(() => props.keeps, newValue => {
virtual.updateParam('keeps', newValue);
virtual.handleSlotSizeChange();
});
watch(() => props.start, newValue => {
scrollToIndex(newValue);
});
watch(() => props.offset, newValue => {
scrollToOffset(newValue);
});
// set back offset when awake from keep-alive
onActivated(() => {
scrollToOffset(virtual.offset);
});
onMounted(() => {
// set position
if (props.start) {
scrollToIndex(props.start);
} else if (props.offset) {
scrollToOffset(props.offset);
}
});
onBeforeUnmount(() => {
virtual.destroy();
});
return {
reset,
scrollToBottom,
scrollToIndex,
scrollToOffset,
getSize,
getSizes,
getOffset,
getClientSize,
getScrollSize,
onScroll,
getRenderItems,
onItemResized,
onSlotResized,
isHorizontal,
rootRef,
rangeRef,
scrollRef
};
},
render() {
const {
padFront,
padBehind
} = this.rangeRef;
const {
isHorizontal,
wrapTag,
wrapClass,
wrapStyle,
onScroll,
renderItemList,
shadow,
height,
maxHeight,
native,
always,
minSize
} = this;
const wrapperStyle = Object.assign({}, wrapStyle || {}, isHorizontal ? {
display: 'flex',
flexDirection: 'row',
height: '100%',
padding: `0px ${padBehind}px 0px ${padFront}px`
} : {
width: '100%',
padding: `${padFront}px 0px ${padBehind}px`
});
const rootStyle = isHorizontal ? {
position: 'relative',
height: '100%'
} : {
position: 'relative',
width: '100%'
};
const wrapNode = createVNode(wrapTag, {
class: wrapClass,
style: wrapperStyle
}, renderItemList ? renderItemList(this.getRenderItems()) : this.getRenderItems());
return createVNode(FScrollbar, {
"ref": e => {
this.scrollRef = e;
this.rootRef = e === null || e === void 0 ? void 0 : e.containerRef;
},
"onScroll": onScroll,
"shadow": shadow,
"height": height,
"maxHeight": maxHeight,
"native": native,
"always": always,
"minSize": minSize,
"contentStyle": rootStyle,
"containerClass": `${prefixCls}-container`
}, {
default: () => [wrapNode]
});
}
});
export { virtualList as default };