一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
155 行
6.5 KiB
TypeScript
155 行
6.5 KiB
TypeScript
// Pure helpers extracted from ResizableLayout so they can be unit-tested
|
|
// without a React harness. Everything here is state-in / state-out — no
|
|
// re-resizable, no DOM, no hooks.
|
|
|
|
import { EdgeFlags, itemMinWidth, itemMinHeight } from './useLayoutItemSizes';
|
|
import { LayoutItem, LayoutItemDirection } from './types';
|
|
|
|
export function isItemVisibleInRender(item: LayoutItem, moveMode: boolean): boolean {
|
|
if (moveMode) return true;
|
|
if (item.children && !item.children.length) return false;
|
|
return item.visible !== false;
|
|
}
|
|
|
|
// The subtree carries an absorber if any node in it is flagged flexible.
|
|
export function isSubtreeFlexible(item: LayoutItem): boolean {
|
|
if (item.flexible) return true;
|
|
if (!item.children) return false;
|
|
for (const child of item.children) {
|
|
if (isSubtreeFlexible(child)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// The first visible-in-render sibling after `item` in `parent.children`, or
|
|
// null when `item` is the last visible child (or has no parent).
|
|
export function nextVisibleSibling(item: LayoutItem, parent: LayoutItem | null, moveMode: boolean): LayoutItem | null {
|
|
if (!parent || !parent.children) return null;
|
|
const idx = parent.children.findIndex(c => c.key === item.key);
|
|
for (let i = idx + 1; i < parent.children.length; i++) {
|
|
if (isItemVisibleInRender(parent.children[i], moveMode)) return parent.children[i];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// The index of the last visible-in-render child of `item`, or -1.
|
|
export function lastVisibleChildIndex(item: LayoutItem, moveMode: boolean): number {
|
|
if (!item.children) return -1;
|
|
for (let i = item.children.length - 1; i >= 0; i--) {
|
|
if (isItemVisibleInRender(item.children[i], moveMode)) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
const zeroEdges: EdgeFlags = { ownRight: false, ownBottom: false, parentRight: false, parentBottom: false };
|
|
|
|
// Compute an item's edge flags from its parent's direction, its own position
|
|
// (is it the last visible child?), and the parent's own edges. Same-axis
|
|
// nesting is handled: a last-child of a row-in-row inherits both the outer's
|
|
// own right gap and the outer's inherited parentRight.
|
|
export function computeEdges(parent: LayoutItem | null, isLastChild: boolean, parentEdges: EdgeFlags = zeroEdges): EdgeFlags {
|
|
const parentDir = parent?.direction;
|
|
const ownRight = parentDir === LayoutItemDirection.Row && !isLastChild;
|
|
const ownBottom = parentDir === LayoutItemDirection.Column && !isLastChild;
|
|
const parentRight = parentDir === LayoutItemDirection.Column
|
|
? parentEdges.ownRight || parentEdges.parentRight
|
|
: (isLastChild && (parentEdges.ownRight || parentEdges.parentRight));
|
|
const parentBottom = parentDir === LayoutItemDirection.Row
|
|
? parentEdges.ownBottom || parentEdges.parentBottom
|
|
: (isLastChild && (parentEdges.ownBottom || parentEdges.parentBottom));
|
|
return { ownRight, ownBottom, parentRight, parentBottom };
|
|
}
|
|
|
|
export interface ResizeStartSnapshot {
|
|
key: string;
|
|
initialWidth: number;
|
|
initialHeight: number;
|
|
nextSiblingKey: string | null;
|
|
nextSiblingInitialWidth: number;
|
|
nextSiblingInitialHeight: number;
|
|
itemAbsorbsAlongAxis: { width: boolean; height: boolean };
|
|
nextAbsorbsAlongAxis: { width: boolean; height: boolean };
|
|
}
|
|
|
|
// Build the drag-start snapshot from the layout state. This captures which
|
|
// side of the divider counts as the absorber on each axis (only truly
|
|
// flexible subtrees do — an unsized panel that isn't flagged still gets
|
|
// deltas applied to it so drags on newly-shown panels behave normally).
|
|
export function buildResizeSnapshot(
|
|
item: LayoutItem,
|
|
parent: LayoutItem | null,
|
|
moveMode: boolean,
|
|
sizes: { [key: string]: { width: number; height: number } },
|
|
): ResizeStartSnapshot {
|
|
const next = nextVisibleSibling(item, parent, moveMode);
|
|
const itemFlex = isSubtreeFlexible(item);
|
|
const nextFlex = next ? isSubtreeFlexible(next) : false;
|
|
|
|
return {
|
|
key: item.key,
|
|
initialWidth: sizes[item.key].width,
|
|
initialHeight: sizes[item.key].height,
|
|
nextSiblingKey: next?.key ?? null,
|
|
nextSiblingInitialWidth: next ? sizes[next.key].width : 0,
|
|
nextSiblingInitialHeight: next ? sizes[next.key].height : 0,
|
|
itemAbsorbsAlongAxis: {
|
|
width: itemFlex && !('width' in item),
|
|
height: itemFlex && !('height' in item),
|
|
},
|
|
nextAbsorbsAlongAxis: {
|
|
width: nextFlex && next ? !('width' in next) : false,
|
|
height: nextFlex && next ? !('height' in next) : false,
|
|
},
|
|
};
|
|
}
|
|
|
|
export interface ResizeUpdate {
|
|
key: string;
|
|
props: { width?: number; height?: number };
|
|
}
|
|
|
|
// Given a drag snapshot, a direction, and re-resizable's cumulative delta,
|
|
// return the layout-state writes needed to move the divider. The dragged
|
|
// panel and its next visible sibling exchange space; either side can be
|
|
// skipped when it's an absorber so the layout system keeps it flexible.
|
|
// When both sides are absorbers (e.g. two unsized column-nested siblings),
|
|
// we write to the dragged panel so the divider still moves.
|
|
export function planResize(
|
|
snapshot: ResizeStartSnapshot,
|
|
direction: string,
|
|
delta: { width: number; height: number },
|
|
): ResizeUpdate[] {
|
|
const isHorizontal = direction !== 'bottom';
|
|
const minSize = isHorizontal ? itemMinWidth : itemMinHeight;
|
|
const rawDelta = isHorizontal ? delta.width : delta.height;
|
|
|
|
const itemIsAbsorber = isHorizontal ? snapshot.itemAbsorbsAlongAxis.width : snapshot.itemAbsorbsAlongAxis.height;
|
|
const nextIsAbsorber = isHorizontal ? snapshot.nextAbsorbsAlongAxis.width : snapshot.nextAbsorbsAlongAxis.height;
|
|
const bothAbsorb = itemIsAbsorber && nextIsAbsorber;
|
|
|
|
const writeItem = !itemIsAbsorber || bothAbsorb;
|
|
const writeNext = !nextIsAbsorber && !!snapshot.nextSiblingKey;
|
|
|
|
const itemInitial = isHorizontal ? snapshot.initialWidth : snapshot.initialHeight;
|
|
const nextInitial = isHorizontal ? snapshot.nextSiblingInitialWidth : snapshot.nextSiblingInitialHeight;
|
|
|
|
// When both panels are written, clamp a single shared delta against both
|
|
// minimums so their combined size is preserved. When only one side is
|
|
// written, the other is absorbing and we don't clamp against it.
|
|
let effectiveDelta = rawDelta;
|
|
if (writeItem) effectiveDelta = Math.max(effectiveDelta, minSize - itemInitial);
|
|
if (writeNext) effectiveDelta = Math.min(effectiveDelta, nextInitial - minSize);
|
|
|
|
const updates: ResizeUpdate[] = [];
|
|
if (writeItem) {
|
|
const newSize = itemInitial + effectiveDelta;
|
|
updates.push({ key: snapshot.key, props: isHorizontal ? { width: newSize } : { height: newSize } });
|
|
}
|
|
if (writeNext) {
|
|
const newSize = nextInitial - effectiveDelta;
|
|
updates.push({ key: snapshot.nextSiblingKey, props: isHorizontal ? { width: newSize } : { height: newSize } });
|
|
}
|
|
|
|
return updates;
|
|
}
|