flash-section-list
Version:
SectionList base on FlashList
290 lines (289 loc) • 11.7 kB
JavaScript
import { FlashList } from '@shopify/flash-list';
import { useImperativeHandle, useMemo, useRef, useState } from 'react';
import { binarySearchClosestIndex, findFirstProp, lcm, omit } from "./utils.js";
import React from 'react';
import { View } from 'react-native';
import { useDummy } from "./useDummy.js";
import Dummy from "./Dummy.js";
import LayoutManager from "./LayoutManager.js";
import { jsx as _jsx } from "react/jsx-runtime";
const methodNames = ['prepareForLayoutAnimationRender', 'recordInteraction', 'recomputeViewableItems', 'scrollToEnd', 'scrollToIndex', 'scrollToItem', 'scrollToOffset'];
const isElementSection = section => {
return /*#__PURE__*/React.isValidElement(section.element);
};
const convertDataSectionFrom = section => {
return {
data: [0],
renderItem: () => {
return section.element;
},
stickyHeaderIndices: section.sticky ? [0] : undefined,
type: section.type
};
};
const flex1 = {
flex: 1
};
const omitProps = ['data', 'renderItem', 'getItemType', 'getItemLayout', 'overrideItemLayout', 'stickyHeaderIndices', 'ListHeaderComponent', 'ListHeaderComponentStyle', 'ListFooterComponent', 'ListFooterComponentStyle', 'numOfColumns'];
export function FlashSectionListBuilder() {
const buildProps = {
FlashListComponent: FlashList,
DummyClass: Dummy,
LayoutManagerClass: LayoutManager
};
return {
build: () => {
const FlashListComponent = buildProps.FlashListComponent;
const DummyClass = buildProps.DummyClass;
const LayoutManagerClass = buildProps.LayoutManagerClass;
function FlashSectionList(propsOrigin, ref) {
const flashlist = useRef(null);
const [containerWidth, setContainerWidth] = useState(undefined);
let {
sections,
...props
} = propsOrigin;
props = omit(propsOrigin, omitProps, false);
const contentContainerPaddingHorizontal = useMemo(() => {
const paddingHorizontal = findFirstProp(props.contentContainerStyle, ['paddingHorizontal', 'paddingLeft']);
return typeof paddingHorizontal === 'number' ? paddingHorizontal * 2 : 0;
}, [props.contentContainerStyle]);
const Dummy = useDummy({
horizontal: props.horizontal,
DummyClass,
LayoutManagerClass
});
const {
dataSections,
sectionStartIndices,
serializedData,
stickyHeaderIndices,
numOfColumns
} = useMemo(() => {
const dataSections = [];
const numOfColumnArray = [];
const stickyHeaderIndices = [];
let index = 0;
const sectionStartIndices = [];
const serializedData = sections.reduce((acc, cur) => {
const section = isElementSection(cur) ? convertDataSectionFrom(cur) : cur;
dataSections.push(section);
const {
data,
header,
footer,
stickyHeaderIndices: stickyHeaderIndicesOfSection,
numOfColumns = 1
} = section;
let length = data.length;
sectionStartIndices.push(index);
numOfColumnArray.push(numOfColumns);
if (header) {
if (header.sticky) {
stickyHeaderIndices.push(index);
}
length += 1;
acc.push(header);
}
acc.push(...data);
if (stickyHeaderIndicesOfSection) {
stickyHeaderIndices.push(...stickyHeaderIndicesOfSection.map(indexWithinSection => indexWithinSection + index + (header ? 1 : 0)));
}
const dummyCount = data.length % numOfColumns !== 0 ? numOfColumns - data.length % numOfColumns : 0;
length += dummyCount;
section.dummyCount = dummyCount;
for (let i = 0; i < dummyCount; i++) {
acc.push(Dummy);
}
if (footer) {
if (footer.sticky) {
stickyHeaderIndices.push(index + length);
}
length += 1;
acc.push(footer);
}
index += length;
return acc;
}, []);
return {
dataSections,
sectionStartIndices,
serializedData,
stickyHeaderIndices,
numOfColumns: lcm(numOfColumnArray)
};
}, [Dummy, sections]);
useImperativeHandle(ref, () => {
const mothods = methodNames.reduce((acc, cur) => {
acc[cur] = (...props) => {
flashlist.current?.[cur](...props);
};
return acc;
}, {});
return {
...flashlist.current,
...mothods,
scrollToSection: params => {
if (!Array.isArray(sectionStartIndices) || sectionStartIndices.length === 0) {
return;
}
const index = sectionStartIndices[params.sectionIndex];
if (index === undefined) {
return;
}
flashlist.current?.scrollToIndex?.({
...params,
index
});
}
};
}, [sectionStartIndices]);
return /*#__PURE__*/_jsx(FlashListComponent, {
...props,
ref: flashlist,
onLayout: !props.horizontal ? e => {
setContainerWidth(e.nativeEvent?.layout?.width);
props.onLayout?.(e);
} : props.onLayout,
data: serializedData,
stickyHeaderIndices: stickyHeaderIndices,
numColumns: numOfColumns,
renderItem: ({
index,
item,
...etc
}) => {
const sectionIndex = binarySearchClosestIndex(sectionStartIndices, index);
const section = dataSections[sectionIndex];
const sectionStartIndex = sectionStartIndices[sectionIndex];
if (!section || sectionStartIndex === undefined) {
return null;
}
const headerOffset = section.header ? 1 : 0;
const dataLastIndex = section.data.length - 1;
const localIndex = index - sectionStartIndex - headerOffset;
if (item === Dummy) {
const isLastDummy = localIndex === dataLastIndex + (section.dummyCount ?? 0);
return /*#__PURE__*/_jsx(Dummy.View, {
sectionIndex: sectionIndex,
disabled: !isLastDummy
});
}
const isHeader = section.header && index === sectionStartIndex;
if (isHeader) {
return /*#__PURE__*/_jsx(View, {
style: flex1,
children: section.header.element
});
}
const isFooter = section.footer && index === sectionStartIndex + section.data.length + headerOffset + (section.dummyCount ?? 0);
if (isFooter) {
return /*#__PURE__*/_jsx(View, {
style: flex1,
children: section.footer.element
});
}
let style;
if (section.gap && containerWidth) {
const sectionNumOfColumns = section.numOfColumns ?? 1;
const numOfRows = Math.floor((section.data.length - 1) / (section.numOfColumns ?? 1));
const includeEdge = !(typeof section.gap === 'number') && !!section.gap.includeEdge;
const gap = typeof section.gap === 'number' ? section.gap : section.gap.size;
const numOfGaps = includeEdge ? sectionNumOfColumns + 1 : sectionNumOfColumns - 1;
const itemWidth = (containerWidth - contentContainerPaddingHorizontal - numOfGaps * gap) / sectionNumOfColumns;
style = {
width: itemWidth
};
const indexInRow = localIndex % sectionNumOfColumns;
style.marginLeft = includeEdge ? gap - gap * indexInRow / sectionNumOfColumns : gap * indexInRow / sectionNumOfColumns;
if (numOfRows > 0) {
const isLastRow = Math.floor(localIndex / sectionNumOfColumns) === numOfRows;
if (!isLastRow) {
style.marginBottom = gap;
}
}
} else if (numOfColumns > 1) {
style = flex1;
}
return /*#__PURE__*/_jsx(View, {
onLayout: e => {
const layout = e.nativeEvent?.layout;
if (!layout) return;
const {
width,
height
} = layout;
if (!width && !height) return;
Dummy.emitSize(sectionIndex, localIndex, {
width,
height
});
},
style: style,
children: section.renderItem({
index: localIndex,
item,
...etc
})
});
},
getItemType: (item, index) => {
const sectionIndex = binarySearchClosestIndex(sectionStartIndices, index);
const section = dataSections[sectionIndex];
const sectionStartIndex = sectionStartIndices[sectionIndex];
if (!section || sectionStartIndex === undefined) {
return -1;
}
if (item === Dummy) {
return Dummy.type;
}
const headerOffset = section.header ? 1 : 0;
const isHeader = section.header && index === sectionStartIndex;
if (isHeader) {
return section.header.type ?? `header-${sectionIndex}`;
}
const isFooter = section.footer && index === sectionStartIndex + section.data.length + headerOffset + (section.dummyCount ?? 0);
if (isFooter) {
return section.footer.type ?? `footer-${sectionIndex}`;
}
return section.type ?? sectionIndex;
},
overrideItemLayout: (layout, _, index) => {
const sectionIndex = binarySearchClosestIndex(sectionStartIndices, index);
const section = dataSections[sectionIndex];
const sectionStartIndex = sectionStartIndices[sectionIndex];
if (!section || sectionStartIndex === undefined) {
return;
}
const headerOffset = section.header ? 1 : 0;
const isHeader = section.header && index === sectionStartIndex;
const isFooter = section.footer && index === sectionStartIndex + section.data.length + headerOffset + (section.dummyCount ?? 0);
if (isHeader) {
layout.span = numOfColumns;
layout.size = section.header.size;
} else if (isFooter) {
layout.span = numOfColumns;
layout.size = section.footer.size;
} else {
layout.span = section.numOfColumns ? numOfColumns / section.numOfColumns : numOfColumns;
layout.size = section.itemSize;
}
}
});
}
return /*#__PURE__*/React.forwardRef(FlashSectionList);
},
setFlashList: FlashListComponent => {
buildProps.FlashListComponent = FlashListComponent;
},
setDummy: DummyClass => {
buildProps.DummyClass = DummyClass;
},
setLayoutManager: LayoutManagerClass => {
buildProps.LayoutManagerClass = LayoutManagerClass;
}
};
}
export default FlashSectionListBuilder().build();
//# sourceMappingURL=FlashSectionList.js.map
;