react-native-sortables
Version:
Powerful Sortable Components for Flexible Content Reordering in React Native
298 lines (290 loc) • 9.97 kB
JavaScript
"use strict";
import { IS_WEB } from '../../../../constants';
import { resolveDimension, reverseArray, sum } from '../../../../utils';
const createGroups = (indexToKey, mainItemSizes, crossItemSizes, gap, groupMainSizeLimit) => {
'worklet';
const groups = [];
const crossAxisGroupSizes = [];
let currentGroup = [];
let totalGroupItemsMainSize = 0;
let groupCrossSize = 0;
for (const key of indexToKey) {
const mainItemDimension = resolveDimension(mainItemSizes, key);
const crossItemDimension = resolveDimension(crossItemSizes, key);
if (mainItemDimension === null || crossItemDimension === null) {
return null;
}
const currentTotalSize = totalGroupItemsMainSize + currentGroup.length * gap + mainItemDimension;
if (currentTotalSize > groupMainSizeLimit + 0.1) {
groups.push(currentGroup);
crossAxisGroupSizes.push(groupCrossSize);
currentGroup = [];
totalGroupItemsMainSize = 0;
groupCrossSize = 0;
}
currentGroup.push(key);
totalGroupItemsMainSize += mainItemDimension;
if (crossItemDimension > groupCrossSize) {
groupCrossSize = crossItemDimension;
}
}
if (currentGroup.length > 0) {
groups.push(currentGroup);
crossAxisGroupSizes.push(groupCrossSize);
}
return {
crossAxisGroupSizes,
groups
};
};
const calculateAlignment = (align, sizes, minSize, maxSize, shouldWrap, providedGap = 0) => {
'worklet';
let startOffset = 0;
let adjustedGap = providedGap;
const getTotalSize = gap => sum(sizes) + gap * (sizes.length - 1);
const totalSize = getTotalSize(providedGap);
const clampedTotalSize = Math.min(Math.max(getTotalSize(providedGap), minSize), maxSize);
switch (align) {
case 'center':
startOffset = (clampedTotalSize - totalSize) / 2;
break;
case 'flex-end':
startOffset = clampedTotalSize - totalSize;
break;
case 'space-around':
if (sizes.length > 1 || shouldWrap) {
adjustedGap = Math.max((clampedTotalSize - sum(sizes) + providedGap) / sizes.length, providedGap);
if (adjustedGap > providedGap) {
startOffset = (clampedTotalSize - getTotalSize(adjustedGap)) / 2;
}
}
break;
case 'space-between':
if (sizes.length > 1 || shouldWrap) {
adjustedGap = Math.max((clampedTotalSize - sum(sizes)) / (sizes.length - 1), providedGap);
}
break;
case 'space-evenly':
if (sizes.length > 1 || shouldWrap) {
adjustedGap = Math.max((clampedTotalSize - sum(sizes) + 2 * providedGap) / (sizes.length + 1), providedGap);
if (adjustedGap > providedGap) {
startOffset = (clampedTotalSize - getTotalSize(adjustedGap)) / 2;
}
}
break;
}
const offsets = [startOffset];
for (let i = 0; i < sizes.length - 1; i++) {
offsets.push(startOffset += (sizes[i] ?? 0) + adjustedGap);
}
return {
adjustedGap,
offsets,
totalSize: clampedTotalSize
};
};
const handleLayoutCalculation = (groups, crossAxisGroupSizes, mainItemSizes, crossItemSizes, gaps, axisDirections, {
alignContent,
alignItems,
justifyContent
}, paddings, limits, isReverse, shouldWrap) => {
'worklet';
const isRow = axisDirections.main === 'row';
const expandMultiGroup = !IS_WEB && groups.length > 1; // expands to max height/width
const paddingHorizontal = paddings.left + paddings.right;
const paddingVertical = paddings.top + paddings.bottom;
let minMainContainerSize;
let maxMainContainerSize;
let minCrossContainerSize;
let maxCrossContainerSize;
if (isRow) {
minMainContainerSize = limits.minWidth - paddingHorizontal;
maxMainContainerSize = limits.maxWidth - paddingHorizontal;
minCrossContainerSize = limits.minHeight - paddingVertical;
maxCrossContainerSize = limits.maxHeight - paddingVertical;
} else {
minMainContainerSize = limits.minHeight - paddingVertical;
maxMainContainerSize = limits.maxHeight - paddingVertical;
minCrossContainerSize = limits.minWidth - paddingHorizontal;
maxCrossContainerSize = limits.maxWidth - paddingHorizontal;
}
// ALIGN CONTENT
// position groups on the cross axis
const contentAlignment = calculateAlignment(alignContent, crossAxisGroupSizes, minCrossContainerSize, maxCrossContainerSize, shouldWrap, gaps[axisDirections.main]);
let totalHeight = 0;
let totalWidth = 0;
if (isRow) {
totalHeight = contentAlignment.totalSize + paddingVertical;
totalWidth = expandMultiGroup ? limits.maxWidth : limits.minWidth;
} else {
totalHeight = expandMultiGroup ? limits.maxHeight : limits.minHeight;
totalWidth = contentAlignment.totalSize + paddingHorizontal;
}
const itemPositions = {};
for (let i = 0; i < groups.length; i++) {
// JUSTIFY CONTENT
// position items in groups on the main axis
const group = groups[i];
const groupCrossSize = crossAxisGroupSizes[i];
const groupCrossOffset = contentAlignment.offsets[i];
const mainAxisGroupItemSizes = [];
for (const key of group) {
const mainItemSize = resolveDimension(mainItemSizes, key);
if (mainItemSize === null) {
return null;
}
mainAxisGroupItemSizes.push(mainItemSize);
}
const contentJustification = calculateAlignment(justifyContent, mainAxisGroupItemSizes, expandMultiGroup ? maxMainContainerSize : minMainContainerSize, maxMainContainerSize, shouldWrap, gaps[axisDirections.cross]);
if (!expandMultiGroup) {
if (isRow) {
totalWidth = Math.max(totalWidth, contentJustification.totalSize + paddingHorizontal);
} else {
totalHeight = Math.max(totalHeight, contentJustification.totalSize + paddingVertical);
}
}
for (let j = 0; j < group.length; j++) {
// ALIGN ITEMS // TODO - override with alignSelf if specified for an item
// position items in groups on the cross axis
const key = group[j];
const crossItemSize = resolveDimension(crossItemSizes, key);
if (crossItemSize === null) {
return null;
}
const itemAlignment = calculateAlignment(alignItems, [crossItemSize], groupCrossSize, groupCrossSize, shouldWrap);
const crossAxisPosition = groupCrossOffset + itemAlignment.offsets[0];
const mainAxisPosition = contentJustification.offsets[j];
if (isRow && isReverse) {
// row-reverse
itemPositions[key] = {
x: totalWidth - mainAxisPosition - mainAxisGroupItemSizes[j] - paddings.right,
y: crossAxisPosition + paddings.top
};
} else if (isRow) {
// row
itemPositions[key] = {
x: mainAxisPosition + paddings.left,
y: crossAxisPosition + paddings.top
};
} else if (isReverse) {
// column-reverse
itemPositions[key] = {
x: crossAxisPosition + paddings.left,
y: totalHeight - mainAxisPosition - mainAxisGroupItemSizes[j] - paddings.bottom
};
} else {
// column
itemPositions[key] = {
x: crossAxisPosition + paddings.left,
y: mainAxisPosition + paddings.top
};
}
}
}
let additionalOffset = 0;
if (isRow && isReverse) {
// row-reverse
additionalOffset = paddings.bottom;
} else if (isRow) {
// row
additionalOffset = paddings.top;
} else if (isReverse) {
// column-reverse
additionalOffset = paddings.right;
} else {
// column
additionalOffset = paddings.left;
}
const crossAxisGroupOffsets = contentAlignment.offsets.map(offset => offset + additionalOffset);
return {
adjustedCrossGap: contentAlignment.adjustedGap,
crossAxisGroupOffsets,
itemPositions,
totalDimensions: {
height: totalHeight,
width: totalWidth
}
};
};
export const calculateLayout = ({
flexAlignments,
flexDirection,
flexWrap,
gaps,
indexToKey,
itemHeights,
itemWidths,
limits,
paddings
}) => {
'worklet';
if (!limits) {
return null;
}
// CREATE GROUPS
// Determine the direction of the main axis and the parallel dimension
const isRow = flexDirection.startsWith('row');
const axisDirections = isRow ? {
cross: 'column',
main: 'row'
} : {
cross: 'row',
main: 'column'
};
let crossItemSizes, mainItemSizes;
if (isRow) {
mainItemSizes = itemWidths;
crossItemSizes = itemHeights;
} else {
mainItemSizes = itemHeights;
crossItemSizes = itemWidths;
}
const shouldWrap = flexWrap !== 'nowrap';
let groupSizeLimit = Infinity;
if (shouldWrap) {
if (isRow) {
groupSizeLimit = limits.maxWidth - paddings.left - paddings.right;
} else {
groupSizeLimit = limits.maxHeight - paddings.top - paddings.bottom;
}
}
if (groupSizeLimit <= 0) {
return null;
}
const groupingResult = createGroups(indexToKey, mainItemSizes, crossItemSizes, gaps[axisDirections.cross], groupSizeLimit);
if (!groupingResult) {
return null;
}
const {
crossAxisGroupSizes,
groups
} = groupingResult;
if (flexWrap === 'wrap-reverse') {
reverseArray(groups);
reverseArray(crossAxisGroupSizes);
}
// CALCULATE LAYOUT
// based on item groups, gaps and alignment
const isReverse = flexDirection.endsWith('reverse');
const layoutResult = handleLayoutCalculation(groups, crossAxisGroupSizes, mainItemSizes, crossItemSizes, gaps, axisDirections, flexAlignments, paddings, limits, isReverse, shouldWrap);
if (!layoutResult) {
return null;
}
return {
adjustedCrossGap: layoutResult.adjustedCrossGap,
contentBounds: [{
x: 0,
y: 0
}, {
x: layoutResult.totalDimensions.width,
y: layoutResult.totalDimensions.height
}],
crossAxisGroupOffsets: layoutResult.crossAxisGroupOffsets,
crossAxisGroupSizes,
groupSizeLimit,
itemGroups: groups,
itemPositions: layoutResult.itemPositions,
totalDimensions: layoutResult.totalDimensions
};
};
//# sourceMappingURL=layout.js.map