react-native-sortables
Version:
Powerful Sortable Components for Flexible Content Reordering in React Native
183 lines (179 loc) • 6.69 kB
JavaScript
;
import { useCallback, useRef } from 'react';
import { runOnUI } from 'react-native-reanimated';
import { useStableCallback } from '../../hooks';
import { setAnimatedTimeout, useAnimatedDebounce, useMutableValue } from '../../integrations/reanimated';
import { areValuesDifferent, resolveDimension } from '../../utils';
import { createProvider } from '../utils';
import { useCommonValuesContext } from './CommonValuesProvider';
import { useItemsContext } from './ItemsProvider';
import { useMultiZoneContext } from './MultiZoneProvider';
const {
MeasurementsProvider,
useMeasurementsContext
} = createProvider('Measurements')(({
measureDebounceDelay
}) => {
const {
activeItemDimensions,
activeItemKey,
containerHeight,
containerWidth,
controlledContainerDimensions,
controlledItemDimensions,
itemHeights,
itemWidths,
usesAbsoluteLayout
} = useCommonValuesContext();
const {
activeItemDimensions: multiZoneActiveItemDimensions
} = useMultiZoneContext() ?? {};
const {
getKeys
} = useItemsContext();
const context = useMutableValue(null);
const previousItemDimensionsRef = useRef({});
const debounce = useAnimatedDebounce();
const handleItemMeasurement = useStableCallback((key, dimensions) => {
const prevDimensions = previousItemDimensionsRef.current[key];
const {
height: isHeightControlled,
width: isWidthControlled
} = controlledItemDimensions;
if (isWidthControlled && isHeightControlled) {
return;
}
const changedDimensions = {};
if (!isWidthControlled && areValuesDifferent(prevDimensions?.width, dimensions.width, 1)) {
changedDimensions.width = dimensions.width;
}
if (!isHeightControlled && areValuesDifferent(prevDimensions?.height, dimensions.height, 1)) {
changedDimensions.height = dimensions.height;
}
if (!Object.keys(changedDimensions).length) {
return;
}
previousItemDimensionsRef.current[key] = dimensions;
const itemsCount = getKeys().length;
runOnUI(() => {
context.value ??= {
measuredItemKeys: new Set(),
queuedMeasurements: new Map()
};
const ctx = context.value;
const isNewItem = !ctx.measuredItemKeys.has(key) && (resolveDimension(itemWidths.value, key) === null || resolveDimension(itemHeights.value, key) === null);
if (isNewItem) {
ctx.measuredItemKeys.add(key);
}
ctx.queuedMeasurements.set(key, dimensions);
if (activeItemKey.value === key) {
activeItemDimensions.value = dimensions;
if (multiZoneActiveItemDimensions) {
multiZoneActiveItemDimensions.value = dimensions;
}
}
// Update the array of item dimensions only after all items have been
// measured to reduce the number of times animated reactions are triggered
if (ctx.measuredItemKeys.size !== itemsCount) {
return;
}
const updateDimensions = () => {
const updateDimension = (dimension, sizes) => {
const newSizes = {
...sizes.value
};
for (const [k, dims] of ctx.queuedMeasurements.entries()) {
newSizes[k] = dims[dimension];
}
sizes.value = newSizes;
};
if (!isWidthControlled) {
updateDimension('width', itemWidths);
}
if (!isHeightControlled) {
updateDimension('height', itemHeights);
}
ctx.queuedMeasurements.clear();
debounce.cancel();
};
if (isNewItem || ctx.queuedMeasurements.size === itemsCount) {
// Update dimensions immediately to avoid unnecessary delays when:
// - measurements were triggered because of adding new items and all new items have been measured
// - all sortable container items' dimensions have changed (e.g. when someone creates collapsible
// items which change their height when the user starts dragging them)
updateDimensions();
} else {
// In all other cases, debounce the update to reduce the number of
// updates when dimensions change many times within a short period of time
debounce.schedule(updateDimensions, measureDebounceDelay);
}
})();
});
const removeItemMeasurements = useCallback(key => {
delete previousItemDimensionsRef.current[key];
const {
height: isHeightControlled,
width: isWidthControlled
} = controlledItemDimensions;
if (isWidthControlled && isHeightControlled) {
return;
}
runOnUI(() => {
if (itemWidths.value && typeof itemWidths.value === 'object') {
delete itemWidths.value[key];
}
if (itemHeights.value && typeof itemHeights.value === 'object') {
delete itemHeights.value[key];
}
context.value?.measuredItemKeys.delete(key);
})();
}, [controlledItemDimensions, itemHeights, itemWidths, context]);
const handleContainerMeasurement = useCallback((width, height) => {
'worklet';
if (!controlledContainerDimensions.width) {
containerWidth.value = width;
}
if (!controlledContainerDimensions.height) {
containerHeight.value = height;
}
}, [controlledContainerDimensions, containerHeight, containerWidth]);
const applyControlledContainerDimensions = useCallback(dimensions => {
'worklet';
if (controlledContainerDimensions.width && dimensions.width !== undefined) {
containerWidth.value = dimensions.width;
}
if (controlledContainerDimensions.height && dimensions.height !== undefined) {
containerHeight.value = dimensions.height;
}
if (!usesAbsoluteLayout.value) {
// Add timeout for safety, to prevent too many layout recalculations
// in a short period of time (this may cause issues on low-end devices)
setAnimatedTimeout(() => {
usesAbsoluteLayout.value = true;
}, 100);
}
}, [containerHeight, containerWidth, controlledContainerDimensions, usesAbsoluteLayout]);
const resetMeasurements = useCallback(() => {
previousItemDimensionsRef.current = {};
runOnUI(() => {
context.value = null;
if (typeof itemWidths.value === 'object') {
itemWidths.value = null;
}
if (typeof itemHeights.value === 'object') {
itemHeights.value = null;
}
})();
}, [itemHeights, itemWidths, context]);
return {
value: {
applyControlledContainerDimensions,
handleContainerMeasurement,
handleItemMeasurement,
removeItemMeasurements,
resetMeasurements
}
};
});
export { MeasurementsProvider, useMeasurementsContext };
//# sourceMappingURL=MeasurementsProvider.js.map