一些检查失败了
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
168 行
5.4 KiB
TypeScript
168 行
5.4 KiB
TypeScript
import { produce } from 'immer';
|
|
import iterateItems from './iterateItems';
|
|
import { LayoutItem, LayoutItemDirection } from './types';
|
|
|
|
// Fallback size assigned to a non-absorber sibling that has none, so the
|
|
// absorber stays the only width/height-less child of its container.
|
|
const itemMinWidthFallback = 250;
|
|
const itemMinHeightFallback = 250;
|
|
|
|
function isLastVisible(itemIndex: number, item: LayoutItem, parent: LayoutItem) {
|
|
if (item.visible === false) return false;
|
|
|
|
for (let i = parent.children.length - 1; i >= 0; i--) {
|
|
const child = parent.children[i];
|
|
if (child && child.visible !== false) return i === itemIndex;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// A subtree counts as flexible if it carries the flag at any nested level, so
|
|
// the absorber role propagates up to the root container.
|
|
function containsFlexible(item: LayoutItem): boolean {
|
|
if (item.visible === false) return false;
|
|
if (item.flexible) return true;
|
|
if (!item.children) return false;
|
|
for (const child of item.children) {
|
|
if (containsFlexible(child)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function explicitAbsorberIndex(parent: LayoutItem) {
|
|
if (!parent.children) return -1;
|
|
for (let i = 0; i < parent.children.length; i++) {
|
|
const child = parent.children[i];
|
|
if (child.visible === false) continue;
|
|
if (containsFlexible(child)) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function updateItemSize(itemIndex: number, itemDraft: LayoutItem, parent: LayoutItem) {
|
|
if (!parent) return;
|
|
|
|
// If a container has only one child, this child should not
|
|
// have a width and height, and simply fill up the container
|
|
if (parent.children.length === 1) {
|
|
delete itemDraft.width;
|
|
delete itemDraft.height;
|
|
}
|
|
|
|
const explicitIdx = explicitAbsorberIndex(parent);
|
|
|
|
if (explicitIdx !== -1) {
|
|
// Strip the absorber's size and assign fallback sizes to any sibling
|
|
// that lacks one, so the absorber alone fills remaining space.
|
|
if (explicitIdx !== itemIndex || itemDraft.visible === false) return;
|
|
|
|
if (parent.direction === LayoutItemDirection.Row) {
|
|
delete itemDraft.width;
|
|
for (let i = 0; i < parent.children.length; i++) {
|
|
if (i === itemIndex) continue;
|
|
const child = parent.children[i];
|
|
if (child.visible === false) continue;
|
|
if (!('width' in child)) {
|
|
(child as { width?: number }).width = child.minWidth || itemMinWidthFallback;
|
|
}
|
|
}
|
|
} else {
|
|
delete itemDraft.height;
|
|
for (let i = 0; i < parent.children.length; i++) {
|
|
if (i === itemIndex) continue;
|
|
const child = parent.children[i];
|
|
if (child.visible === false) continue;
|
|
if (!('height' in child)) {
|
|
(child as { height?: number }).height = child.minHeight || itemMinHeightFallback;
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// No explicit absorber: legacy fallback makes the last visible child
|
|
// flexible when every other sibling is already sized.
|
|
if (isLastVisible(itemIndex, itemDraft, parent)) {
|
|
let allChildrenAreSized = true;
|
|
for (const child of parent.children) {
|
|
if (child.visible === false) continue;
|
|
|
|
if (parent.direction === LayoutItemDirection.Row) {
|
|
if (!child.width) { allChildrenAreSized = false; break; }
|
|
} else {
|
|
if (!child.height) { allChildrenAreSized = false; break; }
|
|
}
|
|
}
|
|
|
|
if (allChildrenAreSized) {
|
|
if (parent.direction === LayoutItemDirection.Row) {
|
|
delete itemDraft.width;
|
|
} else {
|
|
delete itemDraft.height;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// All items get a draggable trailing edge except the last visible child,
|
|
// whose trailing edge is the container's edge.
|
|
function updateResizeRules(itemIndex: number, itemDraft: LayoutItem, parent: LayoutItem) {
|
|
if (!parent) return;
|
|
const isLastVisibleChild = isLastVisible(itemIndex, itemDraft, parent);
|
|
itemDraft.resizableRight = parent.direction === LayoutItemDirection.Row && !isLastVisibleChild;
|
|
itemDraft.resizableBottom = parent.direction === LayoutItemDirection.Column && !isLastVisibleChild;
|
|
}
|
|
|
|
// Container direction should alternate between row (for the root) and
|
|
// columns, then rows again.
|
|
function updateDirection(_itemIndex: number, itemDraft: LayoutItem, parent: LayoutItem) {
|
|
if (!parent) {
|
|
itemDraft.direction = LayoutItemDirection.Row;
|
|
} else {
|
|
itemDraft.direction = parent.direction === LayoutItemDirection.Row ? LayoutItemDirection.Column : LayoutItemDirection.Row;
|
|
}
|
|
}
|
|
|
|
function itemShouldBeVisible(item: LayoutItem): boolean {
|
|
if (!item.children) return item.visible !== false;
|
|
|
|
let oneIsVisible = false;
|
|
|
|
for (const child of item.children) {
|
|
if (itemShouldBeVisible(child)) {
|
|
oneIsVisible = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return oneIsVisible;
|
|
}
|
|
|
|
// If all children of a container are hidden, the container should be
|
|
// hidden too. A container visibility cannot be changed by the user.
|
|
function updateContainerVisibility(_itemIndex: number, itemDraft: LayoutItem, _parent: LayoutItem) {
|
|
if (itemDraft.children) {
|
|
itemDraft.visible = itemShouldBeVisible(itemDraft);
|
|
} else {
|
|
itemDraft.visible = itemDraft.visible !== false;
|
|
}
|
|
}
|
|
|
|
export default function validateLayout(layout: LayoutItem): LayoutItem {
|
|
if (!layout) throw new Error('Layout is null');
|
|
if (!layout.children || !layout.children.length) throw new Error('Root does not have children');
|
|
|
|
return produce(layout, (draft: LayoutItem) => {
|
|
draft.isRoot = true;
|
|
|
|
iterateItems(draft, (itemIndex: number, itemDraft: LayoutItem, parent: LayoutItem) => {
|
|
updateItemSize(itemIndex, itemDraft, parent);
|
|
updateResizeRules(itemIndex, itemDraft, parent);
|
|
updateDirection(itemIndex, itemDraft, parent);
|
|
updateContainerVisibility(itemIndex, itemDraft, parent);
|
|
return true;
|
|
});
|
|
});
|
|
}
|