@atlaskit/editor-plugin-layout
Version:
Layout plugin for @atlaskit/editor-core
146 lines (144 loc) • 5.82 kB
JavaScript
const roundLayoutColumnWidth = width => Number(width.toFixed(2));
const sumWidths = widths => widths.reduce((sum, width) => sum + width, 0);
const normaliseWidthsTotal = (widths, totalWidth, minWidth) => {
const roundedWidths = widths.map(roundLayoutColumnWidth);
const remainder = roundLayoutColumnWidth(totalWidth - sumWidths(roundedWidths));
if (remainder === 0 || roundedWidths.length === 0) {
return roundedWidths;
}
let adjustmentIndex = 0;
roundedWidths.forEach((width, index) => {
if (width > roundedWidths[adjustmentIndex]) {
adjustmentIndex = index;
}
});
const adjustedWidth = roundLayoutColumnWidth(roundedWidths[adjustmentIndex] + remainder);
if (adjustedWidth < minWidth) {
return roundedWidths;
}
return roundedWidths.map((width, index) => index === adjustmentIndex ? adjustedWidth : width);
};
const isValidWidth = width => Number.isFinite(width) && width > 0;
const redistributeWithMinimumWidth = ({
minWidth,
totalWidth,
weights
}) => {
if (weights.length * minWidth > totalWidth) {
return;
}
const widths = Array(weights.length).fill(0);
const clampedIndexes = new Set();
let remainingWidth = totalWidth;
let remainingWeight = sumWidths(weights);
while (clampedIndexes.size < weights.length) {
const remainingWidthForPass = remainingWidth;
const remainingWeightForPass = remainingWeight;
const indexesToClamp = [];
weights.forEach((weight, index) => {
if (clampedIndexes.has(index)) {
return;
}
const proportionalWidth = remainingWeightForPass > 0 ? weight / remainingWeightForPass * remainingWidthForPass : 0;
if (proportionalWidth < minWidth) {
indexesToClamp.push(index);
}
});
if (indexesToClamp.length === 0) {
break;
}
indexesToClamp.forEach(index => {
widths[index] = minWidth;
clampedIndexes.add(index);
remainingWidth -= minWidth;
remainingWeight -= weights[index];
});
}
weights.forEach((weight, index) => {
if (!clampedIndexes.has(index)) {
widths[index] = remainingWeight > 0 ? weight / remainingWeight * remainingWidth : minWidth;
}
});
return widths;
};
/**
* Returns true when the given selected columns already reflect the distribution that
* `distributeLayoutColumns` would produce — i.e. the first N-1 cols each hold
* `equalWidth` (rounded to 2 dp) and the last col absorbs the rounding remainder.
*
* This mirrors the action's "last col absorbs remainder" logic so that the UI can
* disable the option when it would be a no-op, avoiding spurious undo entries.
*/
export const calculateDistribution = selectedWidths => {
const count = selectedWidths.length;
if (count < 2) {
return undefined;
}
const selectedTotal = sumWidths(selectedWidths);
const equalWidth = roundLayoutColumnWidth(selectedTotal / count);
return {
selectedTotal,
equalWidth
};
};
export function isDistributedUniformly(selectedWidths, distribution = calculateDistribution(selectedWidths)) {
if (!distribution || selectedWidths.length < 2) {
return false;
}
const {
selectedTotal,
equalWidth
} = distribution;
const lastColWidth = roundLayoutColumnWidth(selectedTotal - equalWidth * (selectedWidths.length - 1));
return selectedWidths.slice(0, -1).every(width => width === equalWidth) && selectedWidths[selectedWidths.length - 1] === lastColWidth;
}
export const redistributeAfterDeletion = (currentWidths, removeIndex, minWidth) => {
if (currentWidths.length === 0 || removeIndex < 0 || removeIndex >= currentWidths.length || !isValidWidth(minWidth)) {
return currentWidths;
}
if (currentWidths.some(width => !isValidWidth(width))) {
return currentWidths.filter((_, i) => i !== removeIndex);
}
const remainingWidths = currentWidths.filter((_, i) => i !== removeIndex);
if (remainingWidths.length === 0) {
return remainingWidths;
}
const currentTotalWidth = sumWidths(currentWidths);
const targetTotalWidth = Math.round(currentTotalWidth) === 100 ? 100 : currentTotalWidth;
const redistributed = redistributeWithMinimumWidth({
weights: remainingWidths,
totalWidth: targetTotalWidth,
minWidth
});
if (!redistributed) {
const equalWidth = roundLayoutColumnWidth(targetTotalWidth / remainingWidths.length);
return normaliseWidthsTotal(Array(remainingWidths.length).fill(equalWidth), targetTotalWidth, minWidth);
}
return normaliseWidthsTotal(redistributed, targetTotalWidth, minWidth);
};
export const redistributeProportionally = (currentWidths, insertIndex, maxColumns, minWidth) => {
if (currentWidths.length === 0 || !Number.isInteger(maxColumns) || maxColumns <= 0 || currentWidths.length >= maxColumns || insertIndex < 0 || insertIndex > currentWidths.length || !isValidWidth(minWidth) || currentWidths.some(width => !isValidWidth(width))) {
return currentWidths;
}
const currentTotalWidth = sumWidths(currentWidths);
if (!isValidWidth(currentTotalWidth)) {
return currentWidths;
}
const targetTotalWidth = Math.round(currentTotalWidth) === 100 ? 100 : currentTotalWidth;
const newColumnWidth = Math.max(minWidth, roundLayoutColumnWidth(targetTotalWidth / (currentWidths.length + 1)));
const existingColumnsTotalWidth = targetTotalWidth - newColumnWidth;
if (existingColumnsTotalWidth < currentWidths.length * minWidth) {
return currentWidths;
}
const redistributedExistingWidths = redistributeWithMinimumWidth({
weights: currentWidths,
totalWidth: existingColumnsTotalWidth,
minWidth
});
if (!redistributedExistingWidths) {
return currentWidths;
}
const nextWidths = [...redistributedExistingWidths];
nextWidths.splice(insertIndex, 0, newColumnWidth);
return normaliseWidthsTotal(nextWidths, targetTotalWidth, minWidth);
};