react-extjs 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +36 -0
- package/src/components/AbsoluteLayout.tsx +206 -0
- package/src/components/Accordion.tsx +266 -0
- package/src/components/AnchorLayout.tsx +131 -0
- package/src/components/BorderLayout.tsx +512 -0
- package/src/components/Box.tsx +289 -0
- package/src/components/Button.tsx +300 -0
- package/src/components/ButtonGroup.tsx +170 -0
- package/src/components/CalendarPicker.tsx +431 -0
- package/src/components/CardLayout.tsx +122 -0
- package/src/components/CenterLayout.tsx +95 -0
- package/src/components/CheckboxGroup.tsx +200 -0
- package/src/components/ColorMenu.tsx +195 -0
- package/src/components/ColorPalette.tsx +126 -0
- package/src/components/ColumnLayout.tsx +183 -0
- package/src/components/ColumnTree.tsx +383 -0
- package/src/components/CycleButton.tsx +302 -0
- package/src/components/DataView.tsx +244 -0
- package/src/components/DataViewMore.tsx +337 -0
- package/src/components/DataViewTransition.tsx +235 -0
- package/src/components/DateMenu.tsx +163 -0
- package/src/components/DatePicker.tsx +387 -0
- package/src/components/DisplayField.tsx +73 -0
- package/src/components/Editor.tsx +214 -0
- package/src/components/EditorGrid.tsx +361 -0
- package/src/components/FieldLabeler.tsx +81 -0
- package/src/components/FieldReplicator.tsx +135 -0
- package/src/components/FieldSet.tsx +149 -0
- package/src/components/FitLayout.tsx +76 -0
- package/src/components/Form.tsx +3484 -0
- package/src/components/Grid.tsx +2173 -0
- package/src/components/GridFilters.tsx +332 -0
- package/src/components/GroupTab.tsx +274 -0
- package/src/components/HtmlEditor.tsx +391 -0
- package/src/components/ImageOrganizer.tsx +984 -0
- package/src/components/ImageOrganizerService.ts +361 -0
- package/src/components/ListView.tsx +221 -0
- package/src/components/LoadMask.tsx +74 -0
- package/src/components/Menu.tsx +227 -0
- package/src/components/MessageBox.tsx +138 -0
- package/src/components/MultiSlider.tsx +369 -0
- package/src/components/Organizer.tsx +403 -0
- package/src/components/PagingToolbar.tsx +255 -0
- package/src/components/Panel.tsx +451 -0
- package/src/components/ProgressBar.tsx +127 -0
- package/src/components/PropertyGrid.tsx +142 -0
- package/src/components/QuickTip.tsx +174 -0
- package/src/components/Resizable.tsx +241 -0
- package/src/components/RowLayout.tsx +126 -0
- package/src/components/Slider.tsx +353 -0
- package/src/components/SliderField.tsx +206 -0
- package/src/components/SpinnerField.tsx +141 -0
- package/src/components/SplitBar.tsx +215 -0
- package/src/components/Spotlight.tsx +262 -0
- package/src/components/StatusBar.tsx +118 -0
- package/src/components/TabPanel.tsx +540 -0
- package/src/components/TableLayout.tsx +132 -0
- package/src/components/ThemeSwitcher.tsx +167 -0
- package/src/components/ToolTip.tsx +226 -0
- package/src/components/Toolbar.tsx +321 -0
- package/src/components/Tree.tsx +809 -0
- package/src/components/TreeGrid.tsx +418 -0
- package/src/components/Viewport.tsx +218 -0
- package/src/components/Window.tsx +441 -0
- package/src/components/Writer.tsx +303 -0
- package/src/components/XmlTreeLoader.tsx +233 -0
- package/src/components/dnd/DndContext.tsx +328 -0
- package/src/components/dnd/DragSource.tsx +202 -0
- package/src/components/dnd/DragZone.tsx +66 -0
- package/src/components/dnd/DropTarget.tsx +86 -0
- package/src/components/dnd/DropZone.tsx +166 -0
- package/src/components/dnd/PanelProxy.tsx +81 -0
- package/src/components/dnd/PanelResizer.tsx +143 -0
- package/src/components/dnd/Portal.tsx +254 -0
- package/src/components/dnd/Reorderer.tsx +73 -0
- package/src/components/dnd/ToolbarReorderer.tsx +226 -0
- package/src/main.tsx +2649 -0
- package/src/styles/ext-theme.css +889 -0
- package/src/util/theme.ts +95 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import React, { useState, useRef } from 'react';
|
|
2
|
+
import { Panel } from './Panel';
|
|
3
|
+
|
|
4
|
+
export type RegionName = 'north' | 'south' | 'east' | 'west' | 'center';
|
|
5
|
+
|
|
6
|
+
export interface RegionConfig {
|
|
7
|
+
region: RegionName;
|
|
8
|
+
/** east/west 区域宽度 */
|
|
9
|
+
width?: number;
|
|
10
|
+
/** north/south 区域高度 */
|
|
11
|
+
height?: number;
|
|
12
|
+
/** 区域面板标题(有标题则渲染头部,header 高 26px) */
|
|
13
|
+
title?: string;
|
|
14
|
+
/** body 内容(优先于 html) */
|
|
15
|
+
children?: React.ReactNode;
|
|
16
|
+
/** 纯文本/HTML 字符串内容 */
|
|
17
|
+
html?: string;
|
|
18
|
+
/** 顶部工具条内容(同 Ext.Panel.tbar,对齐 region 面板的 tbar;如 west region 在标题下挂工具栏) */
|
|
19
|
+
tbar?: React.ReactNode;
|
|
20
|
+
/** 追加到 body 的 inline style(会被布局计算的 width/height 合并) */
|
|
21
|
+
bodyStyle?: React.CSSProperties;
|
|
22
|
+
/** 是否显示边框(默认 true) */
|
|
23
|
+
border?: boolean;
|
|
24
|
+
// ===== 对齐 Ext.layout.BorderLayout.Region 的可折叠能力 =====
|
|
25
|
+
/** 是否允许折叠(默认 false;true 时 header 渲染折叠工具,或 collapseMode:'mini' 时渲染迷你工具) */
|
|
26
|
+
collapsible?: boolean;
|
|
27
|
+
/** 初始折叠态(默认 false) */
|
|
28
|
+
collapsed?: boolean;
|
|
29
|
+
/** 折叠为迷你条(默认 false;true 时折叠为极细条并在分隔条上渲染迷你工具) */
|
|
30
|
+
collapseMode?: 'normal' | 'mini';
|
|
31
|
+
/** 折叠时隐藏 header 中的折叠工具(默认 false,会渲染 x-tool) */
|
|
32
|
+
hideCollapseTool?: boolean;
|
|
33
|
+
/** 点击标题栏任意位置即可折叠(对齐 Ext titleCollapse,默认 false 仅工具可折叠) */
|
|
34
|
+
titleCollapse?: boolean;
|
|
35
|
+
/** 折叠动画(对齐 Ext animCollapse,默认 true) */
|
|
36
|
+
animCollapse?: boolean;
|
|
37
|
+
// ===== 分隔条(拖拽改变尺寸) =====
|
|
38
|
+
/** 是否渲染可拖拽分隔条(默认 false;center 区域忽略此配置) */
|
|
39
|
+
split?: boolean;
|
|
40
|
+
/** 拖拽最小值(px,对齐 Ext minSize,默认 50) */
|
|
41
|
+
minSize?: number;
|
|
42
|
+
/** 拖拽最大值(px,对齐 Ext maxSize,默认 10000) */
|
|
43
|
+
maxSize?: number;
|
|
44
|
+
// ===== 浮动(仅 west/east 折叠后生效) =====
|
|
45
|
+
/** 折叠后点击折叠条是否浮动显示(对齐 Ext floatable,默认 true) */
|
|
46
|
+
floatable?: boolean;
|
|
47
|
+
// ===== 间距 =====
|
|
48
|
+
/** 展开态外边距(对齐 Ext margins,字符串 "t r b l" 或 {top,right,bottom,left},默认 0) */
|
|
49
|
+
margins?: string | { top?: number; right?: number; bottom?: number; left?: number };
|
|
50
|
+
/** 折叠态外边距(对齐 Ext cmargins,折叠时生效) */
|
|
51
|
+
cmargins?: string | { top?: number; right?: number; bottom?: number; left?: number };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface BorderLayoutProps {
|
|
55
|
+
/** 容器宽(外层 .x-panel 宽度,含 1px 边框,内布局区 = width-2) */
|
|
56
|
+
width: number;
|
|
57
|
+
/** 容器高(内布局区 = height-2) */
|
|
58
|
+
height: number;
|
|
59
|
+
regions: RegionConfig[];
|
|
60
|
+
/** 挂在 .x-border-layout-ct 上的 testId(供保真截图/行为测试定位) */
|
|
61
|
+
testId?: string;
|
|
62
|
+
/** 追加到根 .x-panel 的 class */
|
|
63
|
+
className?: string;
|
|
64
|
+
/** 透传到根 .x-panel 的内联 style(与 width 合并,调用方覆盖项优先生效) */
|
|
65
|
+
style?: React.CSSProperties;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 保真常量(与主题/Ext 默认值一致):
|
|
69
|
+
const BODY_H_PAD = 2; // 左右边框各 1
|
|
70
|
+
const SPLIT = 3; // 分隔条厚度(原版 ExtJS 为 5px;本仓库区域边框修复为单侧 1px 后,5px 显得过粗,改为 3px 与边框视觉协调)
|
|
71
|
+
const COLLAPSED = 20; // 折叠条厚度(.x-layout-collapsed 默认 20/22)
|
|
72
|
+
const COLLAPSED_MINI = 5; // 迷你折叠条厚度
|
|
73
|
+
const DEFAULT_MIN = 50;
|
|
74
|
+
const DEFAULT_MAX = 10000;
|
|
75
|
+
|
|
76
|
+
function parseMargins(m?: string | { top?: number; right?: number; bottom?: number; left?: number }): {
|
|
77
|
+
top: number; right: number; bottom: number; left: number;
|
|
78
|
+
} {
|
|
79
|
+
const z = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
80
|
+
if (!m) return z;
|
|
81
|
+
if (typeof m === 'string') {
|
|
82
|
+
const p = m.trim().split(/\s+/).map(Number);
|
|
83
|
+
if (p.length === 1) return { top: p[0], right: p[0], bottom: p[0], left: p[0] };
|
|
84
|
+
if (p.length === 2) return { top: p[0], right: p[1], bottom: p[0], left: p[1] };
|
|
85
|
+
if (p.length === 3) return { top: p[0], right: p[1], bottom: p[2], left: p[1] };
|
|
86
|
+
return { top: p[0], right: p[1], bottom: p[2], left: p[3] };
|
|
87
|
+
}
|
|
88
|
+
return { top: m.top ?? 0, right: m.right ?? 0, bottom: m.bottom ?? 0, left: m.left ?? 0 };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function findRegion(regions: RegionConfig[], r: RegionName): RegionConfig | undefined {
|
|
92
|
+
return regions.find((x) => x.region === r);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface Box { left: number; top: number; w: number; h: number; }
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 保真映射(来源 src/widgets/layout/BorderLayout.js + Region/SplitRegion):
|
|
99
|
+
* - 外层 .x-panel > .x-panel-bwrap > .x-panel-body.x-panel-body-noheader.x-border-layout-ct
|
|
100
|
+
* - 每个区域渲染为 .x-panel.x-border-panel,绝对定位,内联 left/top/width/height
|
|
101
|
+
* - 区域 body 高度由布局算法显式设定(boxH-27),panel 自身不内联 height,自然撑高
|
|
102
|
+
* - 分隔条:.x-layout-split.x-layout-split-{pos}(.x-layout-split-h|v),5px,可拖拽改尺寸
|
|
103
|
+
* - 折叠:.x-layout-collapsed.x-layout-collapsed-{pos} 折叠条(含 expand 工具 / mini 工具)
|
|
104
|
+
* 展开态 header 渲染 x-tool-collapse-{pos} 折叠工具
|
|
105
|
+
* - 浮动:west/east 折叠后点击折叠条浮动显示面板(x-panel-floating,覆盖 center)
|
|
106
|
+
* 定位数学(对齐 Ext onLayout):
|
|
107
|
+
* ctW=width-2, ctH=height-2
|
|
108
|
+
* north : left0 top0 w=ctW h=nH(-split)
|
|
109
|
+
* south : left0 top=ctH-sH(+split) h=sH(-split)
|
|
110
|
+
* west : left0 top=nH w=wW(-split) h=midH
|
|
111
|
+
* east : left=ctW-eW(+split) top=nH w=eW(-split) h=midH
|
|
112
|
+
* center: left=wW top=nH w=ctW-wW-eW h=midH
|
|
113
|
+
* 折叠方向区域的占位缩减为 COLLAPSED,邻居延后/提前。
|
|
114
|
+
*/
|
|
115
|
+
export const BorderLayout: React.FC<BorderLayoutProps> = ({ width, height, regions, testId, className, style }) => {
|
|
116
|
+
const ctW = width - 2;
|
|
117
|
+
const ctH = height - 2;
|
|
118
|
+
|
|
119
|
+
const N = findRegion(regions, 'north');
|
|
120
|
+
const S = findRegion(regions, 'south');
|
|
121
|
+
const W = findRegion(regions, 'west');
|
|
122
|
+
const E = findRegion(regions, 'east');
|
|
123
|
+
const C = findRegion(regions, 'center');
|
|
124
|
+
|
|
125
|
+
// 折叠态:受控由 region.collapsed 驱动,非受控用内部状态
|
|
126
|
+
const [collapsedMap, setCollapsedMap] = useState<Record<string, boolean>>(() => {
|
|
127
|
+
const m: Record<string, boolean> = {};
|
|
128
|
+
regions.forEach((r) => { if (r.region !== 'center') m[r.region] = !!r.collapsed; });
|
|
129
|
+
return m;
|
|
130
|
+
});
|
|
131
|
+
// 分隔条拖拽尺寸:记录 north/south 的 height 与 west/east 的 width
|
|
132
|
+
const [sizeMap, setSizeMap] = useState<Record<string, number>>(() => {
|
|
133
|
+
const s: Record<string, number> = {};
|
|
134
|
+
regions.forEach((r) => {
|
|
135
|
+
if (r.region === 'north' || r.region === 'south') s[r.region] = r.height ?? 0;
|
|
136
|
+
else if (r.region === 'west' || r.region === 'east') s[r.region] = r.width ?? 0;
|
|
137
|
+
});
|
|
138
|
+
return s;
|
|
139
|
+
});
|
|
140
|
+
const [floating, setFloating] = useState<RegionName | null>(null);
|
|
141
|
+
const ctRef = useRef<HTMLDivElement>(null);
|
|
142
|
+
const dragRef = useRef<{
|
|
143
|
+
region: RegionName; start: number; origin: number; min: number; max: number; axis: 'w' | 'h';
|
|
144
|
+
} | null>(null);
|
|
145
|
+
|
|
146
|
+
const isCollapsed = (r: RegionName) => r !== 'center' && !!collapsedMap[r];
|
|
147
|
+
|
|
148
|
+
const toggleCollapse = (r: RegionName) => {
|
|
149
|
+
if (r === 'center') return;
|
|
150
|
+
setCollapsedMap((m) => ({ ...m, [r]: !m[r] }));
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// ---- 分隔条拖拽 ----
|
|
154
|
+
const onSplitMouseDown = (e: React.MouseEvent, region: RegionName) => {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
const cfg = findRegion(regions, region)!;
|
|
157
|
+
const axis = region === 'north' || region === 'south' ? 'h' : 'w';
|
|
158
|
+
const origin = sizeMap[region] ?? 0;
|
|
159
|
+
dragRef.current = {
|
|
160
|
+
region,
|
|
161
|
+
start: axis === 'h' ? e.clientY : e.clientX,
|
|
162
|
+
origin,
|
|
163
|
+
min: cfg.minSize ?? DEFAULT_MIN,
|
|
164
|
+
max: cfg.maxSize ?? DEFAULT_MAX,
|
|
165
|
+
axis,
|
|
166
|
+
};
|
|
167
|
+
const move = (ev: MouseEvent) => {
|
|
168
|
+
const d = dragRef.current;
|
|
169
|
+
if (!d) return;
|
|
170
|
+
const cur = d.axis === 'h' ? ev.clientY : ev.clientX;
|
|
171
|
+
let delta = cur - d.start;
|
|
172
|
+
// east/south 拖拽方向与 north/west 相反
|
|
173
|
+
if (d.region === 'east' || d.region === 'south') delta = -delta;
|
|
174
|
+
let next = Math.round(d.origin + delta);
|
|
175
|
+
next = Math.max(d.min, Math.min(d.max, next));
|
|
176
|
+
setSizeMap((s) => ({ ...s, [d.region]: next }));
|
|
177
|
+
};
|
|
178
|
+
const up = () => {
|
|
179
|
+
dragRef.current = null;
|
|
180
|
+
document.removeEventListener('mousemove', move);
|
|
181
|
+
document.removeEventListener('mouseup', up);
|
|
182
|
+
};
|
|
183
|
+
document.addEventListener('mousemove', move);
|
|
184
|
+
document.addEventListener('mouseup', up);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// ---- 计算各区域盒 ----
|
|
188
|
+
// forceExpand=true 时忽略折叠态,算出"完全展开"布局(供 floatable 浮动面板定位用)
|
|
189
|
+
const compute = (forceExpand = false): {
|
|
190
|
+
N?: { box: Box; split?: Box };
|
|
191
|
+
S?: { box: Box; split?: Box };
|
|
192
|
+
W?: { box: Box; split?: Box };
|
|
193
|
+
E?: { box: Box; split?: Box };
|
|
194
|
+
C: Box;
|
|
195
|
+
} => {
|
|
196
|
+
const isCol = (r: RegionName) => r !== 'center' && !forceExpand && !!collapsedMap[r];
|
|
197
|
+
let topEdge = 0;
|
|
198
|
+
let bottomEdge = ctH;
|
|
199
|
+
let leftEdge = 0;
|
|
200
|
+
let rightEdge = ctW;
|
|
201
|
+
const res: any = { C: { left: 0, top: 0, w: ctW, h: ctH } };
|
|
202
|
+
|
|
203
|
+
// 分隔条紧贴区域边缘(panel 占 dim-s,split 占 s,二者无缝衔接,无遗留 5px 间隙);
|
|
204
|
+
// edge 为区域在对应轴上占用的完整尺寸(panel + split = dim),供相邻区域延后/提前。
|
|
205
|
+
const regionBox = (r: RegionConfig, split: boolean): { box: Box; split?: Box; edge: number } => {
|
|
206
|
+
const m = parseMargins(r.collapsed && r.cmargins ? r.cmargins : r.margins);
|
|
207
|
+
const dim = (r.region === 'north' || r.region === 'south'
|
|
208
|
+
? sizeMap[r.region] ?? r.height ?? 0
|
|
209
|
+
: sizeMap[r.region] ?? r.width ?? 0);
|
|
210
|
+
const s = split && r.region !== 'center' ? SPLIT : 0;
|
|
211
|
+
switch (r.region) {
|
|
212
|
+
case 'north': {
|
|
213
|
+
const box = { left: m.left, top: topEdge + m.top, w: ctW - m.left - m.right, h: dim - s };
|
|
214
|
+
// 分隔条同样套用左右 margins,避免贴着外框左右(原版上下留间距、左右亦留)
|
|
215
|
+
const sp = s ? { left: m.left, top: topEdge + m.top + dim - s, w: ctW - m.left - m.right, h: s } : undefined;
|
|
216
|
+
// edge 取区域(含 margin+split)占用的真实下边界,供下一区域顺延;此前漏加 m.top 会让
|
|
217
|
+
// 下方 center 上移、压到分隔条与 north 上,导致边框错位/被盖(同 west/east 的 m.left 问题)
|
|
218
|
+
return { box, split: sp, edge: topEdge + m.top + dim };
|
|
219
|
+
}
|
|
220
|
+
case 'south': {
|
|
221
|
+
// 邻居为上方 center:分隔条贴区域顶端(top=bottomEdge-dim),面板底部对齐(top=bottomEdge-dim+s)。
|
|
222
|
+
const box = { left: m.left, top: bottomEdge - dim + s + m.top, w: ctW - m.left - m.right, h: dim - s };
|
|
223
|
+
const sp = s ? { left: m.left, top: bottomEdge - dim + m.top, w: ctW - m.left - m.right, h: s } : undefined;
|
|
224
|
+
// 取区域占用的真实上边界(含 bottom margin),供上方区域顶到正确位置
|
|
225
|
+
return { box, split: sp, edge: bottomEdge - m.bottom - dim };
|
|
226
|
+
}
|
|
227
|
+
case 'west': {
|
|
228
|
+
const box = { left: leftEdge + m.left, top: topEdge + m.top, w: dim - s, h: bottomEdge - topEdge - m.top - m.bottom };
|
|
229
|
+
// 分隔条同样套用上下 margins,不顶天立地贴外框上下(原版中间分隔条上下各留 5px 间距)
|
|
230
|
+
const sp = s ? { left: leftEdge + m.left + dim - s, top: topEdge + m.top, w: s, h: bottomEdge - topEdge - m.top - m.bottom } : undefined;
|
|
231
|
+
// edge 取区域(含 left margin+split)占用的真实右边界:此前漏加 m.left,使右侧 center 左移 5px
|
|
232
|
+
// 压在分隔条与 west 上,其中心左边框叠进 west 并被 zIndex:10 的分隔条盖住 → 看似“无左边框”。
|
|
233
|
+
return { box, split: sp, edge: leftEdge + m.left + dim };
|
|
234
|
+
}
|
|
235
|
+
case 'east': {
|
|
236
|
+
// 邻居为左侧 center:分隔条贴区域左端(left=rightEdge-dim),面板右对齐(left=rightEdge-dim+s)。
|
|
237
|
+
const box = { left: rightEdge - dim + s + m.left, top: topEdge + m.top, w: dim - s, h: bottomEdge - topEdge - m.top - m.bottom };
|
|
238
|
+
const sp = s ? { left: rightEdge - dim + m.left, top: topEdge + m.top, w: s, h: bottomEdge - topEdge - m.top - m.bottom } : undefined;
|
|
239
|
+
// 取区域占用的真实左边界(含 right margin),供左侧 center 右到正确位置
|
|
240
|
+
return { box, split: sp, edge: rightEdge - m.right - dim };
|
|
241
|
+
}
|
|
242
|
+
default:
|
|
243
|
+
return { box: { left: 0, top: 0, w: ctW, h: ctH }, edge: 0 };
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (N) {
|
|
248
|
+
if (isCol('north')) {
|
|
249
|
+
const h = N.collapseMode === 'mini' ? COLLAPSED_MINI : COLLAPSED;
|
|
250
|
+
res.N = { box: { left: 0, top: 0, w: ctW, h }, edge: h };
|
|
251
|
+
topEdge = h;
|
|
252
|
+
} else {
|
|
253
|
+
res.N = regionBox(N, !!N.split);
|
|
254
|
+
topEdge = res.N.edge;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (S) {
|
|
258
|
+
if (isCol('south')) {
|
|
259
|
+
const h = S.collapseMode === 'mini' ? COLLAPSED_MINI : COLLAPSED;
|
|
260
|
+
res.S = { box: { left: 0, top: ctH - h, w: ctW, h }, edge: ctH - h };
|
|
261
|
+
bottomEdge = ctH - h;
|
|
262
|
+
} else {
|
|
263
|
+
res.S = regionBox(S, !!S.split);
|
|
264
|
+
bottomEdge = res.S.edge;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const midH = bottomEdge - topEdge;
|
|
268
|
+
if (W) {
|
|
269
|
+
if (isCol('west')) {
|
|
270
|
+
const w = W.collapseMode === 'mini' ? COLLAPSED_MINI : COLLAPSED;
|
|
271
|
+
res.W = { box: { left: 0, top: topEdge, w, h: midH }, edge: w };
|
|
272
|
+
leftEdge = w;
|
|
273
|
+
} else {
|
|
274
|
+
res.W = regionBox(W, !!W.split);
|
|
275
|
+
leftEdge = res.W.edge;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (E) {
|
|
279
|
+
if (isCol('east')) {
|
|
280
|
+
const w = E.collapseMode === 'mini' ? COLLAPSED_MINI : COLLAPSED;
|
|
281
|
+
res.E = { box: { left: ctW - w, top: topEdge, w, h: midH }, edge: ctW - w };
|
|
282
|
+
rightEdge = ctW - w;
|
|
283
|
+
} else {
|
|
284
|
+
res.E = regionBox(E, !!E.split);
|
|
285
|
+
rightEdge = res.E.edge;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// center region 同样应尊重其 margins(对齐 ExtJS:center 的 '5 5 5 0' 使其向内缩进,
|
|
289
|
+
// 否则会顶满外框、与原版 region 视觉尺寸不符)。原版 onLayout 对 center 的 left/top/width/height
|
|
290
|
+
// 同样是 edge 减去 margins 后得出。
|
|
291
|
+
const cM = C ? parseMargins(C.margins) : { top: 0, right: 0, bottom: 0, left: 0 };
|
|
292
|
+
res.C = {
|
|
293
|
+
left: leftEdge + cM.left,
|
|
294
|
+
top: topEdge + cM.top,
|
|
295
|
+
w: rightEdge - leftEdge - cM.left - cM.right,
|
|
296
|
+
h: midH - cM.top - cM.bottom,
|
|
297
|
+
};
|
|
298
|
+
return res;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const layout = compute();
|
|
302
|
+
// 完全展开布局:供 floatable 浮动面板定位(浮动时面板应回到原始展开尺寸)
|
|
303
|
+
const layoutExpanded = compute(true);
|
|
304
|
+
|
|
305
|
+
const renderRegionPanel = (cfg: RegionConfig, box: Box, opts: { floating?: boolean } = {}) => {
|
|
306
|
+
const hasHeader = !!cfg.title;
|
|
307
|
+
const bodyW = box.w - BODY_H_PAD;
|
|
308
|
+
const collapsed = isCollapsed(cfg.region);
|
|
309
|
+
const tools = cfg.collapsible && !collapsed
|
|
310
|
+
? [{ id: `collapse-${cfg.region}`, handler: () => toggleCollapse(cfg.region), tooltip: `Collapse ${cfg.region}` }]
|
|
311
|
+
: undefined;
|
|
312
|
+
// 严格对齐原生 ExtJS 的 border 布局:每个 region 渲染为「完整 1px 边框的 .x-panel」
|
|
313
|
+
// (.x-panel-header 四向 + .x-panel-body 左/右/下,顶边由相邻 header 提供以免双线),边框
|
|
314
|
+
// 颜色完全交给 @import 进来的 xtheme-*.css(blue #99bbe8 / gray #d0d0d0),默认主题不写死
|
|
315
|
+
// 任何颜色、切换主题时整圈边框自动跟随。这正是原生观感——相邻 region 直接相接时各自 1px
|
|
316
|
+
// 边框相接成 2px(原生无 split 的默认五区演示即是如此,非 bug);被 splitter 或 margins
|
|
317
|
+
// 隔开的 region(如 Organizer 的 west/center)则互不相邻、各自完整框、无重叠。
|
|
318
|
+
// .x-border-panel 类仅作布局定位钩子(真实 Ext 中只负责 position:absolute,本身无边框),
|
|
319
|
+
// 不再承载任何颜色规则。box-sizing:border-box 使 1px 边框不撑破布局算出的 box.w/box.h。
|
|
320
|
+
// 浮动面板是独立浮层,同样保留完整面板边框。是否带框尊重 cfg.border(默认 true);
|
|
321
|
+
// border:false 的区域不挂 x-border-panel 类、保持无框,不影响其它布局的 borderless 配置。
|
|
322
|
+
const bordered = cfg.border !== false;
|
|
323
|
+
const regionBorder: React.CSSProperties = {
|
|
324
|
+
boxSizing: 'border-box',
|
|
325
|
+
borderRadius: 0,
|
|
326
|
+
boxShadow: 'none',
|
|
327
|
+
};
|
|
328
|
+
return (
|
|
329
|
+
<Panel
|
|
330
|
+
key={cfg.region}
|
|
331
|
+
cls={[bordered ? 'x-border-panel' : '', opts.floating ? 'x-panel-floating' : ''].filter(Boolean).join(' ')}
|
|
332
|
+
width={box.w}
|
|
333
|
+
// 区域面板固定为 box.h(对齐 ExtJS region 被布局锁定为 box.h);bodyFlex 使内部
|
|
334
|
+
// flex 排列,header 因长标题换行变高时 body 自动收缩,绝不突破 box.h 覆盖邻居。
|
|
335
|
+
height={box.h}
|
|
336
|
+
bodyFlex
|
|
337
|
+
header={hasHeader ? true : false}
|
|
338
|
+
title={cfg.title}
|
|
339
|
+
border={bordered}
|
|
340
|
+
collapsible={cfg.collapsible}
|
|
341
|
+
hideCollapseTool={cfg.hideCollapseTool}
|
|
342
|
+
titleCollapse={cfg.titleCollapse}
|
|
343
|
+
tools={tools}
|
|
344
|
+
tbar={cfg.tbar}
|
|
345
|
+
onHeaderClick={cfg.titleCollapse ? () => toggleCollapse(cfg.region) : undefined}
|
|
346
|
+
style={{
|
|
347
|
+
position: 'absolute',
|
|
348
|
+
left: box.left,
|
|
349
|
+
top: box.top,
|
|
350
|
+
zIndex: opts.floating ? 100 : undefined,
|
|
351
|
+
...regionBorder,
|
|
352
|
+
}}
|
|
353
|
+
// 保真注意:body 宽度公式(box.w-2)按 content-box 设计,对齐 golden(Ext 默认
|
|
354
|
+
// content-box)。文档站有全局 border-box 重置,须显式声明 content-box,否则 body
|
|
355
|
+
// 会比贴边的 header 缩进 2px,导致相邻 region body 间隙、左右边框错位(accordion 同理)。
|
|
356
|
+
// 高度不再内联:交由 bodyFlex 的 flex:1 填满剩余,header 变高时 body 收缩。
|
|
357
|
+
bodyStyle={{ boxSizing: 'content-box', width: bodyW, ...(cfg.bodyStyle ? { ...cfg.bodyStyle, height: undefined } : {}) }}
|
|
358
|
+
>
|
|
359
|
+
{cfg.children ?? cfg.html}
|
|
360
|
+
</Panel>
|
|
361
|
+
);
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const renderSplit = (box: Box, region: RegionName) => (
|
|
365
|
+
<div
|
|
366
|
+
key={`split-${region}`}
|
|
367
|
+
className={`x-layout-split x-layout-split-${region} ${region === 'north' || region === 'south' ? 'x-layout-split-h' : 'x-layout-split-v'}`}
|
|
368
|
+
style={{
|
|
369
|
+
left: box.left,
|
|
370
|
+
top: box.top,
|
|
371
|
+
width: box.w,
|
|
372
|
+
height: box.h,
|
|
373
|
+
zIndex: 10,
|
|
374
|
+
cursor: region === 'north' || region === 'south' ? 'row-resize' : 'col-resize',
|
|
375
|
+
// 原版 .x-layout-split 背景透明(仅 s.gif 1×1 透明占位),在区域边框修复后
|
|
376
|
+
// 5px 透明间隙与相邻面板的 1px 边框组合成"看起来很宽的拖拽条"。
|
|
377
|
+
// 此处用 3px 厚度 + 极淡底色使分隔条呈现为微妙的拖拽手柄(零改 CSS,纯内联覆盖)。
|
|
378
|
+
backgroundColor: '#f0f4fa',
|
|
379
|
+
}}
|
|
380
|
+
onMouseDown={(e) => onSplitMouseDown(e, region)}
|
|
381
|
+
/>
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
const renderCollapsedBar = (cfg: RegionConfig, box: Box) => {
|
|
385
|
+
const pos = cfg.region;
|
|
386
|
+
const mini = cfg.collapseMode === 'mini';
|
|
387
|
+
if (mini) {
|
|
388
|
+
return (
|
|
389
|
+
<div
|
|
390
|
+
key={`collapsed-${pos}`}
|
|
391
|
+
className={`x-layout-collapsed x-layout-collapsed-${pos} x-layout-cmini-${pos}`}
|
|
392
|
+
style={{ left: box.left, top: box.top, width: box.w, height: box.h, visibility: 'visible', zIndex: 20 }}
|
|
393
|
+
>
|
|
394
|
+
<div className={`x-layout-mini x-layout-mini-${pos}`} onClick={() => toggleCollapse(pos)}> </div>
|
|
395
|
+
</div>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
// 对齐 ExtJS 3.2.1 (resources/css/structure/layout.css:80):
|
|
399
|
+
// .x-tool-expand-north, .x-tool-expand-south { float:right; margin:3px; }
|
|
400
|
+
// .x-tool-expand-east, .x-tool-expand-west { float:none; margin:3px auto; }
|
|
401
|
+
// 即 South/North 折叠条为水平条、工具靠右;West/East 为竖直条、工具水平居中。
|
|
402
|
+
// 注意:文档站加载了上述 ExtJS CSS,会使 .x-tool-expand-south 带 float:right,
|
|
403
|
+
// 与 flex 定位互相干扰(West 无 float 故正常、South 异常)。因此不依赖 flex/float,
|
|
404
|
+
// 改用绝对定位把 icon 钉死在正确位置(也对齐 ExtJS 原生做法):
|
|
405
|
+
// 水平条(South/North) → 靠右(right:3) + 垂直居中(top:50%, translateY(-50%))
|
|
406
|
+
// 竖直条(West/East) → 水平居中(left:50%, translateX(-50%)) + 顶部(top:3)
|
|
407
|
+
const horizontal = pos === 'north' || pos === 'south';
|
|
408
|
+
const toolStyle: React.CSSProperties = horizontal
|
|
409
|
+
? { position: 'absolute', right: 3, top: '50%', transform: 'translateY(-50%)', margin: 0 }
|
|
410
|
+
: { position: 'absolute', left: '50%', top: 3, transform: 'translateX(-50%)', margin: 0 };
|
|
411
|
+
return (
|
|
412
|
+
<div
|
|
413
|
+
key={`collapsed-${pos}`}
|
|
414
|
+
className={`x-layout-collapsed x-layout-collapsed-${pos}`}
|
|
415
|
+
style={{
|
|
416
|
+
left: box.left,
|
|
417
|
+
top: box.top,
|
|
418
|
+
width: box.w,
|
|
419
|
+
height: box.h,
|
|
420
|
+
visibility: 'visible',
|
|
421
|
+
zIndex: 20,
|
|
422
|
+
}}
|
|
423
|
+
onClick={() => {
|
|
424
|
+
if (cfg.floatable !== false && (pos === 'west' || pos === 'east')) setFloating(pos);
|
|
425
|
+
else toggleCollapse(pos);
|
|
426
|
+
}}
|
|
427
|
+
>
|
|
428
|
+
{cfg.collapsible !== false && !cfg.hideCollapseTool && (
|
|
429
|
+
<div
|
|
430
|
+
className={`x-tool x-tool-expand-${pos}`}
|
|
431
|
+
style={toolStyle}
|
|
432
|
+
onClick={(e) => { e.stopPropagation(); toggleCollapse(pos); }}
|
|
433
|
+
> </div>
|
|
434
|
+
)}
|
|
435
|
+
</div>
|
|
436
|
+
);
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
return (
|
|
440
|
+
<div className={['x-panel', className].filter(Boolean).join(' ')} style={{ width, ...style }}>
|
|
441
|
+
<div className="x-panel-bwrap">
|
|
442
|
+
<div
|
|
443
|
+
ref={ctRef}
|
|
444
|
+
className="x-panel-body x-panel-body-noheader x-border-layout-ct"
|
|
445
|
+
data-testid={testId}
|
|
446
|
+
// 对齐真实 Ext 3.2.1:.x-border-layout-ct 同时是面板 body(class 即 x-panel-body
|
|
447
|
+
// x-panel-body-noheader),其整圈边框由 ext-theme.css 的 .x-panel-body + .x-panel-body-noheader
|
|
448
|
+
// 提供(1px,颜色由 xtheme-*.css 的 border-color 主题化:blue #99bbe8 / gray #d0d0d0)。
|
|
449
|
+
// 因此**不在此写死任何颜色**——切换主题时外框随 xtheme 自动跟随,与真实 Ext 完全一致。
|
|
450
|
+
// 根 .x-panel 为 border:0(真实 Ext 实测亦为 0),故无“根 + ct”叠加成 2px 问题。
|
|
451
|
+
// 几何:content-box,width=ctW/height=ctH,外框总尺寸 = ctW + 2(实测真实 Ext ct:
|
|
452
|
+
// content 518 + border 2 = 520,与 root 一致)。
|
|
453
|
+
style={{ width: ctW, height: ctH, position: 'relative', boxSizing: 'content-box', padding: 0 }}
|
|
454
|
+
>
|
|
455
|
+
{/* 折叠条(展开面板渲染在下方,避免 z-index 覆盖) */}
|
|
456
|
+
{N && isCollapsed('north') && renderCollapsedBar(N, layout.N!.box)}
|
|
457
|
+
{S && isCollapsed('south') && renderCollapsedBar(S, layout.S!.box)}
|
|
458
|
+
{W && isCollapsed('west') && renderCollapsedBar(W, layout.W!.box)}
|
|
459
|
+
{E && isCollapsed('east') && renderCollapsedBar(E, layout.E!.box)}
|
|
460
|
+
|
|
461
|
+
{/* 浮动面板(覆盖 center):west/east 折叠后点击折叠条浮起,回到原始展开尺寸 */}
|
|
462
|
+
{floating && isCollapsed(floating) && (() => {
|
|
463
|
+
const cfg = floating === 'west' ? W! : E!;
|
|
464
|
+
const exp = floating === 'west' ? layoutExpanded.W : layoutExpanded.E;
|
|
465
|
+
if (!cfg || !exp) return null;
|
|
466
|
+
const box = exp.box;
|
|
467
|
+
return (
|
|
468
|
+
<>
|
|
469
|
+
<div
|
|
470
|
+
key="floating-overlay"
|
|
471
|
+
data-testid="floating-overlay"
|
|
472
|
+
onClick={() => setFloating(null)}
|
|
473
|
+
style={{ position: 'absolute', inset: 0, zIndex: 99 }}
|
|
474
|
+
/>
|
|
475
|
+
{renderRegionPanel(cfg, box, { floating: true })}
|
|
476
|
+
</>
|
|
477
|
+
);
|
|
478
|
+
})()}
|
|
479
|
+
|
|
480
|
+
{/* 展开面板 + 分隔条 */}
|
|
481
|
+
{N && !isCollapsed('north') && (
|
|
482
|
+
<>
|
|
483
|
+
{renderRegionPanel(N, layout.N!.box, {})}
|
|
484
|
+
{layout.N!.split && renderSplit(layout.N!.split!, 'north')}
|
|
485
|
+
</>
|
|
486
|
+
)}
|
|
487
|
+
{S && !isCollapsed('south') && (
|
|
488
|
+
<>
|
|
489
|
+
{renderRegionPanel(S, layout.S!.box, {})}
|
|
490
|
+
{layout.S!.split && renderSplit(layout.S!.split!, 'south')}
|
|
491
|
+
</>
|
|
492
|
+
)}
|
|
493
|
+
{W && !isCollapsed('west') && (
|
|
494
|
+
<>
|
|
495
|
+
{renderRegionPanel(W, layout.W!.box, {})}
|
|
496
|
+
{layout.W!.split && renderSplit(layout.W!.split!, 'west')}
|
|
497
|
+
</>
|
|
498
|
+
)}
|
|
499
|
+
{E && !isCollapsed('east') && (
|
|
500
|
+
<>
|
|
501
|
+
{renderRegionPanel(E, layout.E!.box, {})}
|
|
502
|
+
{layout.E!.split && renderSplit(layout.E!.split!, 'east')}
|
|
503
|
+
</>
|
|
504
|
+
)}
|
|
505
|
+
{C && renderRegionPanel(C, layout.C)}
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
);
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
export default BorderLayout;
|