UNPKG

@fesjs/fes-design

Version:
353 lines (334 loc) 10.7 kB
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 };