feat: Initial clean release of CHM
一些检查失败了
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
一些检查失败了
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
这个提交包含在:
@@ -0,0 +1,154 @@
|
||||
// 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;
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户