react-native-sortables
Version:
Powerful Sortable Components for Flexible Content Reordering in React Native
191 lines (183 loc) • 5.99 kB
JavaScript
;
import { useCallback, useRef } from 'react';
import { useAnimatedReaction } from 'react-native-reanimated';
import { IS_WEB } from '../../../constants';
import { useDebugContext } from '../../../debug';
import { setAnimatedTimeout, useMutableValue } from '../../../integrations/reanimated';
import { useAutoScrollContext, useCommonValuesContext, useItemsCount, useMeasurementsContext } from '../../shared';
import { createProvider } from '../../utils';
import { useAutoOffsetAdjustmentContext } from '../AutoOffsetAdjustmentProvider';
import { calculateLayout, shouldUpdateContainerDimensions } from './utils';
const DEBUG_COLORS = {
backgroundColor: '#ffa500',
borderColor: '#825500'
};
const {
GridLayoutProvider,
useGridLayoutContext
} = createProvider('GridLayout')(({
columnGap,
isVertical,
numGroups,
rowGap,
rowHeight
}) => {
const {
containerHeight,
containerWidth,
indexToKey,
itemHeights,
itemPositions,
itemWidths,
overriddenCellDimensions,
shouldAnimateLayout
} = useCommonValuesContext();
const {
applyControlledContainerDimensions
} = useMeasurementsContext();
const {
adaptLayoutProps
} = useAutoOffsetAdjustmentContext() ?? {};
const {
contentBounds
} = useAutoScrollContext() ?? {};
const debugContext = useDebugContext();
let debugCrossGapRects;
let debugMainGapRects;
if (__DEV__) {
const itemsCount = useItemsCount();
debugMainGapRects = debugContext?.useDebugRects(numGroups - 1);
debugCrossGapRects = debugContext?.useDebugRects(Math.ceil(itemsCount / numGroups) - 1);
}
const mainGap = isVertical ? columnGap : rowGap;
const crossGap = isVertical ? rowGap : columnGap;
const mainGroupSize = useMutableValue(null);
const layoutRequestId = useMutableValue(0);
let updateShouldAnimateWeb = null;
if (IS_WEB) {
const shouldAnimateTimeoutRef = useRef(null);
updateShouldAnimateWeb = useCallback((props, prevProps) => {
// On the web, animate layout only if parent container is not resized
// (e.g. skip animation when the browser window is resized)
const shouldAnimate = !prevProps?.itemHeights || !prevProps?.itemWidths || (isVertical ? props.itemWidths === prevProps?.itemWidths : props.itemHeights === prevProps?.itemHeights);
if (shouldAnimateTimeoutRef.current !== null) {
clearTimeout(shouldAnimateTimeoutRef.current);
}
if (!shouldAnimate) {
shouldAnimateLayout.value = false;
}
// Enable after timeout when the user stops resizing the browser window
shouldAnimateTimeoutRef.current = setTimeout(() => {
shouldAnimateLayout.value = true;
}, 100);
}, [shouldAnimateLayout, isVertical]);
}
// MAIN GROUP SIZE UPDATER
useAnimatedReaction(() => {
if (!isVertical) {
// TODO - maybe don't require specifying rowHeight (and instead
// occupy the entire height of the container)
return rowHeight ?? null;
}
return containerWidth.value ? (containerWidth.value + mainGap.value) / numGroups - mainGap.value : null;
}, value => {
if (!value) {
return;
}
if (isVertical) {
itemWidths.value = value;
overriddenCellDimensions.value = {
width: value + (IS_WEB ? 0 : mainGap.value)
};
} else {
itemHeights.value = value;
}
mainGroupSize.value = value;
// DEBUG ONLY
if (debugMainGapRects) {
const gap = mainGap.value;
for (let i = 0; i < numGroups - 1; i++) {
const pos = value * (i + 1) + gap * i;
debugMainGapRects[i]?.set({
...DEBUG_COLORS,
...(isVertical ? {
width: gap,
x: pos
} : {
height: gap,
y: pos
})
});
}
}
});
// GRID LAYOUT UPDATER
useAnimatedReaction(() => ({
gaps: {
cross: crossGap.value,
main: mainGap.value
},
indexToKey: indexToKey.value,
isVertical,
itemHeights: itemHeights.value,
itemWidths: itemWidths.value,
numGroups,
requestId: layoutRequestId.value // Helper to force layout re-calculation
}), (props, prevProps) => {
const adaptedProps = adaptLayoutProps?.(props, prevProps) ?? props;
const layout = calculateLayout(adaptedProps);
if (!layout) {
return;
}
// Update item positions
itemPositions.value = layout.itemPositions;
// Update controlled container dimensions
if (shouldUpdateContainerDimensions(isVertical ? containerHeight.value : containerWidth.value, layout.containerCrossSize, adaptedProps.startCrossOffset !== undefined)) {
applyControlledContainerDimensions({
[isVertical ? 'height' : 'width']: layout.containerCrossSize
});
}
// Update content bounds
if (contentBounds) contentBounds.value = layout.contentBounds;
if (adaptedProps.shouldAnimateLayout !== undefined) {
shouldAnimateLayout.value = adaptedProps.shouldAnimateLayout;
} else if (IS_WEB && props.requestId === prevProps?.requestId) {
updateShouldAnimateWeb?.(adaptedProps, prevProps);
} else {
shouldAnimateLayout.value = true;
}
if (adaptedProps.requestNextLayout) {
setAnimatedTimeout(() => {
layoutRequestId.value++;
});
}
// DEBUG ONLY
if (debugCrossGapRects) {
for (let i = 0; i < layout.crossAxisOffsets.length - 1; i++) {
const size = crossGap.value;
const pos = layout.crossAxisOffsets[i + 1] - crossGap.value;
debugCrossGapRects[i]?.set({
...DEBUG_COLORS,
...(isVertical ? {
height: size,
y: pos
} : {
width: size,
x: pos
})
});
}
}
});
return {
value: {
crossGap,
isVertical,
mainGap,
mainGroupSize,
numGroups
}
};
});
export { GridLayoutProvider, useGridLayoutContext };
//# sourceMappingURL=GridLayoutProvider.js.map