@shopify/flash-list
Version:
FlashList is a more performant FlatList replacement
252 lines • 14.2 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.MasonryFlashList = void 0;
var tslib_1 = require("tslib");
var react_1 = tslib_1.__importStar(require("react"));
var react_native_1 = require("react-native");
var CustomError_1 = tslib_1.__importDefault(require("./errors/CustomError"));
var ExceptionList_1 = tslib_1.__importDefault(require("./errors/ExceptionList"));
var FlashList_1 = tslib_1.__importDefault(require("./FlashList"));
var ContentContainerUtils_1 = require("./utils/ContentContainerUtils");
var defaultEstimatedItemSize = 100;
/**
* FlashList variant that enables rendering of masonry layouts.
* If you want `MasonryFlashList` to optimize item arrangement, enable `optimizeItemArrangement` and pass a valid `overrideItemLayout` function.
*/
var MasonryFlashListComponent = react_1.default.forwardRef(function (
/**
* Forward Ref will force cast generic parament T to unknown. Export has a explicit cast to solve this.
*/
props, forwardRef) {
var _a, _b, _c, _d, _e;
var columnCount = props.numColumns || 1;
var drawDistance = props.drawDistance;
var estimatedListSize = (_b = (_a = props.estimatedListSize) !== null && _a !== void 0 ? _a : react_native_1.Dimensions.get("window")) !== null && _b !== void 0 ? _b : { height: 500, width: 500 };
if (props.optimizeItemArrangement && !props.overrideItemLayout) {
throw new CustomError_1.default(ExceptionList_1.default.overrideItemLayoutRequiredForMasonryOptimization);
}
var dataSet = useDataSet(columnCount, Boolean(props.optimizeItemArrangement), props.data, props.overrideItemLayout, props.extraData);
var totalColumnFlex = useTotalColumnFlex(dataSet, props);
var propsRef = (0, react_1.useRef)(props);
propsRef.current = props;
var onScrollRef = (0, react_1.useRef)([]);
var emptyScrollEvent = (0, react_1.useRef)(getEmptyScrollEvent())
.current;
var ScrollComponent = (0, react_1.useRef)(getFlashListScrollView(onScrollRef, function () {
var _a;
return (((_a = getListRenderedSize(parentFlashList)) === null || _a === void 0 ? void 0 : _a.height) ||
estimatedListSize.height);
})).current;
var onScrollProxy = (0, react_1.useRef)(function (scrollEvent) {
var _a, _b, _c, _d, _e;
emptyScrollEvent.nativeEvent.contentOffset.y =
scrollEvent.nativeEvent.contentOffset.y -
((_b = (_a = parentFlashList.current) === null || _a === void 0 ? void 0 : _a.firstItemOffset) !== null && _b !== void 0 ? _b : 0);
(_c = onScrollRef.current) === null || _c === void 0 ? void 0 : _c.forEach(function (onScrollCallback) {
onScrollCallback === null || onScrollCallback === void 0 ? void 0 : onScrollCallback(emptyScrollEvent);
});
if (!scrollEvent.nativeEvent.doNotPropagate) {
(_e = (_d = propsRef.current).onScroll) === null || _e === void 0 ? void 0 : _e.call(_d, scrollEvent);
}
}).current;
/**
* We're triggering an onScroll on internal lists so that they register the correct offset which is offset - header size.
* This will make sure viewability callbacks are triggered correctly.
* 32 ms is equal to two frames at 60 fps. Faster framerates will not cause any problems.
*/
var onLoadForNestedLists = (0, react_1.useRef)(function (args) {
var _a, _b;
setTimeout(function () {
emptyScrollEvent.nativeEvent.doNotPropagate = true;
onScrollProxy === null || onScrollProxy === void 0 ? void 0 : onScrollProxy(emptyScrollEvent);
emptyScrollEvent.nativeEvent.doNotPropagate = false;
}, 32);
(_b = (_a = propsRef.current).onLoad) === null || _b === void 0 ? void 0 : _b.call(_a, args);
}).current;
var _f = tslib_1.__read(useRefWithForwardRef(forwardRef), 2), parentFlashList = _f[0], getFlashList = _f[1];
var renderItem = props.renderItem, getItemType = props.getItemType, getColumnFlex = props.getColumnFlex, overrideItemLayout = props.overrideItemLayout, viewabilityConfig = props.viewabilityConfig, keyExtractor = props.keyExtractor, onLoad = props.onLoad, onViewableItemsChanged = props.onViewableItemsChanged, data = props.data, stickyHeaderIndices = props.stickyHeaderIndices, CellRendererComponent = props.CellRendererComponent, ItemSeparatorComponent = props.ItemSeparatorComponent, remainingProps = tslib_1.__rest(props, ["renderItem", "getItemType", "getColumnFlex", "overrideItemLayout", "viewabilityConfig", "keyExtractor", "onLoad", "onViewableItemsChanged", "data", "stickyHeaderIndices", "CellRendererComponent", "ItemSeparatorComponent"]);
var firstColumnHeight = ((_d = (_c = dataSet[0]) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) *
((_e = props.estimatedItemSize) !== null && _e !== void 0 ? _e : defaultEstimatedItemSize);
var insetForLayoutManager = (0, ContentContainerUtils_1.applyContentContainerInsetForLayoutManager)({ height: 0, width: 0 }, props.contentContainerStyle, false);
return (react_1.default.createElement(FlashList_1.default, tslib_1.__assign({ ref: getFlashList }, remainingProps, { horizontal: false, numColumns: columnCount, data: dataSet, onScroll: onScrollProxy, estimatedItemSize: firstColumnHeight || estimatedListSize.height, renderItem: function (args) {
var _a, _b;
return (react_1.default.createElement(FlashList_1.default, { renderScrollComponent: ScrollComponent, estimatedItemSize: props.estimatedItemSize, data: args.item, onLoad: args.index === 0 ? onLoadForNestedLists : undefined, renderItem: function (innerArgs) {
var _a;
return ((_a = renderItem === null || renderItem === void 0 ? void 0 : renderItem(tslib_1.__assign(tslib_1.__assign({}, innerArgs), { item: innerArgs.item.originalItem, index: innerArgs.item.originalIndex, columnSpan: 1, columnIndex: args.index }))) !== null && _a !== void 0 ? _a : null);
}, keyExtractor: keyExtractor
? function (item, _) {
return keyExtractor === null || keyExtractor === void 0 ? void 0 : keyExtractor(item.originalItem, item.originalIndex);
}
: undefined, getItemType: getItemType
? function (item, _, extraData) {
return getItemType === null || getItemType === void 0 ? void 0 : getItemType(item.originalItem, item.originalIndex, extraData);
}
: undefined, drawDistance: drawDistance, estimatedListSize: {
height: estimatedListSize.height,
width: (((((_a = getListRenderedSize(parentFlashList)) === null || _a === void 0 ? void 0 : _a.width) ||
estimatedListSize.width) +
insetForLayoutManager.width) /
totalColumnFlex) *
((_b = getColumnFlex === null || getColumnFlex === void 0 ? void 0 : getColumnFlex(args.item, args.index, columnCount, props.extraData)) !== null && _b !== void 0 ? _b : 1),
}, extraData: props.extraData, CellRendererComponent: CellRendererComponent, ItemSeparatorComponent: ItemSeparatorComponent, viewabilityConfig: viewabilityConfig, onViewableItemsChanged: onViewableItemsChanged
? function (info) {
updateViewTokens(info.viewableItems);
updateViewTokens(info.changed);
onViewableItemsChanged === null || onViewableItemsChanged === void 0 ? void 0 : onViewableItemsChanged(info);
}
: undefined, overrideItemLayout: overrideItemLayout
? function (layout, item, _, __, extraData) {
overrideItemLayout === null || overrideItemLayout === void 0 ? void 0 : overrideItemLayout(layout, item.originalItem, item.originalIndex, columnCount, extraData);
layout.span = undefined;
}
: undefined }));
}, overrideItemLayout: getColumnFlex
? function (layout, item, index, maxColumns, extraData) {
layout.span =
(columnCount *
getColumnFlex(item, index, maxColumns, extraData)) /
totalColumnFlex;
}
: undefined })));
});
/**
* Splits data for each column's FlashList
*/
var useDataSet = function (columnCount, optimizeItemArrangement, sourceData, overrideItemLayout, extraData) {
return (0, react_1.useMemo)(function () {
var _a;
if (!sourceData || sourceData.length === 0) {
return [];
}
var columnHeightTracker = new Array(columnCount).fill(0);
var layoutObject = { size: undefined };
var dataSet = new Array(columnCount);
var dataSize = sourceData.length;
for (var i = 0; i < columnCount; i++) {
dataSet[i] = [];
}
for (var i = 0; i < dataSize; i++) {
var nextColumnIndex = i % columnCount;
if (optimizeItemArrangement) {
for (var j = 0; j < columnCount; j++) {
if (columnHeightTracker[j] < columnHeightTracker[nextColumnIndex]) {
nextColumnIndex = j;
}
}
// update height of column
layoutObject.size = undefined;
overrideItemLayout(layoutObject, sourceData[i], i, columnCount, extraData);
columnHeightTracker[nextColumnIndex] +=
(_a = layoutObject.size) !== null && _a !== void 0 ? _a : defaultEstimatedItemSize;
}
dataSet[nextColumnIndex].push({
originalItem: sourceData[i],
originalIndex: i,
});
}
return dataSet;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sourceData, columnCount, optimizeItemArrangement, extraData]);
};
var useTotalColumnFlex = function (dataSet, props) {
return (0, react_1.useMemo)(function () {
var columnCount = props.numColumns || 1;
if (!props.getColumnFlex) {
return columnCount;
}
var totalFlexSum = 0;
var dataSize = dataSet.length;
for (var i = 0; i < dataSize; i++) {
totalFlexSum += props.getColumnFlex(dataSet[i], i, columnCount, props.extraData);
}
return totalFlexSum;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataSet, props.getColumnFlex, props.extraData]);
};
/**
* Handle both function refs and refs with current property
*/
var useRefWithForwardRef = function (forwardRef) {
var ref = (0, react_1.useRef)(null);
return [
ref,
(0, react_1.useCallback)(function (instance) {
ref.current = instance;
if (typeof forwardRef === "function") {
forwardRef(instance);
}
else if (forwardRef) {
forwardRef.current = instance;
}
}, [forwardRef]),
];
};
/**
* This ScrollView is actually just a view mimicking a scrollview. We block the onScroll event from being passed to the parent list directly.
* We manually drive onScroll from the parent and thus, achieve recycling.
*/
var getFlashListScrollView = function (onScrollRef, getParentHeight) {
var FlashListScrollView = react_1.default.forwardRef(function (props, ref) {
var onLayout = props.onLayout, onScroll = props.onScroll, rest = tslib_1.__rest(props, ["onLayout", "onScroll"]);
var onLayoutProxy = (0, react_1.useCallback)(function (layoutEvent) {
onLayout === null || onLayout === void 0 ? void 0 : onLayout({
nativeEvent: {
layout: {
height: getParentHeight(),
width: layoutEvent.nativeEvent.layout.width,
},
},
});
}, [onLayout]);
(0, react_1.useEffect)(function () {
var _a;
if (onScroll) {
(_a = onScrollRef.current) === null || _a === void 0 ? void 0 : _a.push(onScroll);
}
return function () {
if (!onScrollRef.current || !onScroll) {
return;
}
var indexToDelete = onScrollRef.current.indexOf(onScroll);
if (indexToDelete > -1) {
onScrollRef.current.splice(indexToDelete, 1);
}
};
}, [onScroll]);
return react_1.default.createElement(react_native_1.View, tslib_1.__assign({ ref: ref }, rest, { onLayout: onLayoutProxy }));
});
FlashListScrollView.displayName = "FlashListScrollView";
return FlashListScrollView;
};
var updateViewTokens = function (tokens) {
var length = tokens.length;
for (var i = 0; i < length; i++) {
var token = tokens[i];
if (token.index !== null && token.index !== undefined) {
if (token.item) {
token.index = token.item.originalIndex;
token.item = token.item.originalItem;
}
else {
token.index = null;
token.item = undefined;
}
}
}
};
var getEmptyScrollEvent = function () {
return {
nativeEvent: { contentOffset: { y: 0, x: 0 } },
};
};
var getListRenderedSize = function (parentFlashList) {
var _a, _b;
return (_b = (_a = parentFlashList === null || parentFlashList === void 0 ? void 0 : parentFlashList.current) === null || _a === void 0 ? void 0 : _a.recyclerlistview_unsafe) === null || _b === void 0 ? void 0 : _b.getRenderedSize();
};
MasonryFlashListComponent.displayName = "MasonryFlashList";
/**
* FlashList variant that enables rendering of masonry layouts.
* If you want `MasonryFlashList` to optimize item arrangement, enable `optimizeItemArrangement` and pass a valid `overrideItemLayout` function.
*/
exports.MasonryFlashList = MasonryFlashListComponent;
//# sourceMappingURL=MasonryFlashList.js.map
;