文件
chen 301194963b
一些检查失败了
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
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

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;
}