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,142 @@
|
|
|
1
|
+
import React, { forwardRef, useImperativeHandle, useRef, useEffect, useState } from 'react';
|
|
2
|
+
import { EditorGrid, type EditorColumn } from './EditorGrid';
|
|
3
|
+
|
|
4
|
+
// 对齐 Ext.grid.PropertyStore.isEditableValue:仅原始类型(string/number/boolean)与 Date 显示,
|
|
5
|
+
// 对象/数组/function 被忽略(Ext 不渲染不可编辑值)。
|
|
6
|
+
function isEditableValue(val: unknown): boolean {
|
|
7
|
+
if (val === null || val === undefined) return false;
|
|
8
|
+
if (val instanceof Date) return true;
|
|
9
|
+
return typeof val !== 'object';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// 对齐 Ext.grid.PropertyColumnModel.renderCell:Date → 'm/j/Y'(月/日均不补零)、
|
|
13
|
+
// boolean → trueText/falseText('true'/'false')、其余 → 字符串(React 自动 htmlEncode)。
|
|
14
|
+
function formatValue(val: unknown): string {
|
|
15
|
+
if (val instanceof Date) {
|
|
16
|
+
return `${val.getMonth() + 1}/${val.getDate()}/${val.getFullYear()}`;
|
|
17
|
+
}
|
|
18
|
+
if (typeof val === 'boolean') return val ? 'true' : 'false';
|
|
19
|
+
if (val === null || val === undefined) return '';
|
|
20
|
+
return String(val);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PropertyGridProps {
|
|
24
|
+
/** 属性键值对(仅原始类型与 Date 会显示,对象/数组被忽略,对齐 Ext.isEditableValue) */
|
|
25
|
+
source: Record<string, unknown>;
|
|
26
|
+
width?: number;
|
|
27
|
+
height?: number;
|
|
28
|
+
title?: string;
|
|
29
|
+
/** 数据测试 id,挂在根 div(供保真截图/行为测试定位) */
|
|
30
|
+
testId?: string;
|
|
31
|
+
/** 属性名显示映射(仅影响 Name 列文本,不影响排序/数据键,对齐 Ext.propertyNames) */
|
|
32
|
+
propertyNames?: Record<string, string>;
|
|
33
|
+
className?: string;
|
|
34
|
+
style?: React.CSSProperties;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 保真映射(来源 src/widgets/grid/PropertyGrid.js + PropertyColumnModel.js + PropertyStore.js):
|
|
39
|
+
* - 继承 EditorGridPanel:根 GridPanel,内核即 Grid;两列 name/value,forceFit 平分宽,stripeRows=false(无斑马纹)。
|
|
40
|
+
* - 渲染时根元素加 x-props-grid 类(onRender: getGridEl().addClass('x-props-grid')),
|
|
41
|
+
* ext-all.css 据此给 name 列加特殊背景图(grid3-special-col-bg.gif) 与右分隔线(border-right:1px)。
|
|
42
|
+
* - 列定义:name(id=name, header='Name', sortable, menuDisabled) + value(id=value, header='Value', resizable:false, menuDisabled);
|
|
43
|
+
* enableHdMenu:false → 表头无 x-grid3-hd-btn(经 Grid Column.menuDisabled 实现)。
|
|
44
|
+
* - 值渲染:原始类型/Date,Date→'m/j/Y'、boolean→'true'/'false'(见 formatValue)。
|
|
45
|
+
* - 交互:value 列可编辑(clicksToEdit:1),name 列只读;复用 EditorGrid 的 x-grid-editor 覆盖层。
|
|
46
|
+
* 所有 DOM/class 来自同构 Grid/EditorGrid,x-props-grid 视觉差异完全由 ext-all.css 驱动,保证像素一致。
|
|
47
|
+
*/
|
|
48
|
+
export interface PropertyGridHandle {
|
|
49
|
+
/** 整体替换属性源(覆盖当前全部键值) */
|
|
50
|
+
setSource: (source: Record<string, unknown>) => void;
|
|
51
|
+
/** 读取当前属性源快照 */
|
|
52
|
+
getSource: () => Record<string, unknown>;
|
|
53
|
+
/** 新增或覆盖单个属性(不存在则添加,存在则更新) */
|
|
54
|
+
setProperty: (name: string, value: unknown) => void;
|
|
55
|
+
/** 删除单个属性(不存在则无副作用) */
|
|
56
|
+
removeProperty: (name: string) => void;
|
|
57
|
+
/** 新增单个属性(等价于 setProperty) */
|
|
58
|
+
addProperty: (name: string, value: unknown) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const PropertyGrid = forwardRef<PropertyGridHandle, PropertyGridProps>(function PropertyGrid(
|
|
62
|
+
{
|
|
63
|
+
source,
|
|
64
|
+
width = 300,
|
|
65
|
+
height,
|
|
66
|
+
title,
|
|
67
|
+
testId,
|
|
68
|
+
propertyNames,
|
|
69
|
+
className,
|
|
70
|
+
style,
|
|
71
|
+
},
|
|
72
|
+
ref,
|
|
73
|
+
) {
|
|
74
|
+
// 双轨:内部 dynSource 由 prop 初始化、句柄可运行时改写;useEffect 同步 prop 变化
|
|
75
|
+
const [dynSource, setDynSource] = useState<Record<string, unknown>>(source ?? {});
|
|
76
|
+
const sourceRef = useRef<Record<string, unknown>>(dynSource);
|
|
77
|
+
sourceRef.current = dynSource;
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
setDynSource(source ?? {});
|
|
80
|
+
}, [source]);
|
|
81
|
+
|
|
82
|
+
useImperativeHandle(ref, () => ({
|
|
83
|
+
setSource: (s) => setDynSource(s),
|
|
84
|
+
getSource: () => sourceRef.current,
|
|
85
|
+
setProperty: (name, value) => setDynSource((prev) => ({ ...prev, [name]: value })),
|
|
86
|
+
removeProperty: (name) =>
|
|
87
|
+
setDynSource((prev) => {
|
|
88
|
+
const next = { ...prev };
|
|
89
|
+
delete next[name];
|
|
90
|
+
return next;
|
|
91
|
+
}),
|
|
92
|
+
addProperty: (name, value) => setDynSource((prev) => ({ ...prev, [name]: value })),
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
// 过滤可编辑值,按原始 key 排序(对齐 Ext store.sort('name','ASC'),name 字段即原始 key)
|
|
96
|
+
const keys = Object.keys(dynSource)
|
|
97
|
+
.filter((k) => isEditableValue(dynSource[k]))
|
|
98
|
+
.sort((a, b) => a.toUpperCase().localeCompare(b.toUpperCase()));
|
|
99
|
+
const data = keys.map((k) => ({
|
|
100
|
+
name: propertyNames?.[k] ?? k,
|
|
101
|
+
value: formatValue(dynSource[k]),
|
|
102
|
+
}));
|
|
103
|
+
const colW = Math.floor(width / 2);
|
|
104
|
+
const columns: EditorColumn[] = [
|
|
105
|
+
{
|
|
106
|
+
dataIndex: 'name',
|
|
107
|
+
header: 'Name',
|
|
108
|
+
width: colW,
|
|
109
|
+
sortable: true,
|
|
110
|
+
resizable: false,
|
|
111
|
+
menuDisabled: true,
|
|
112
|
+
editor: false,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
dataIndex: 'value',
|
|
116
|
+
header: 'Value',
|
|
117
|
+
width: width - colW,
|
|
118
|
+
sortable: false,
|
|
119
|
+
resizable: false,
|
|
120
|
+
menuDisabled: true,
|
|
121
|
+
editor: true,
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
return (
|
|
125
|
+
<div data-testid={testId} className={className} style={{ width, ...style }}>
|
|
126
|
+
<EditorGrid
|
|
127
|
+
columns={columns}
|
|
128
|
+
data={data}
|
|
129
|
+
title={title}
|
|
130
|
+
width={width}
|
|
131
|
+
height={height}
|
|
132
|
+
stripeRows={false}
|
|
133
|
+
clicksToEdit={1}
|
|
134
|
+
className="x-props-grid"
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
PropertyGrid.displayName = 'PropertyGrid';
|
|
141
|
+
|
|
142
|
+
export default PropertyGrid;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState, useCallback, useImperativeHandle, forwardRef } from 'react';
|
|
2
|
+
import { ToolTip } from './ToolTip';
|
|
3
|
+
|
|
4
|
+
export interface QuickTipContent {
|
|
5
|
+
title?: string;
|
|
6
|
+
text?: string;
|
|
7
|
+
html?: string;
|
|
8
|
+
width?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface QuickTipProps {
|
|
12
|
+
/**
|
|
13
|
+
* hover 目标元素 CSS 选择器;提供后默认隐藏,鼠标进入显示、移出隐藏。
|
|
14
|
+
* 目标元素可携带 data-qtip / data-qtitle / data-qwidth 覆盖实例文本(对齐 Ext 的
|
|
15
|
+
* ext:qtip / ext:qtitle / ext:qwidth 属性),不携带则使用实例级 title/text。
|
|
16
|
+
*/
|
|
17
|
+
target?: string;
|
|
18
|
+
/** 实例级标题(对齐 Ext.QuickTips.register 的 title) */
|
|
19
|
+
title?: string;
|
|
20
|
+
/** 实例级正文文本(对齐 text) */
|
|
21
|
+
text?: string;
|
|
22
|
+
/** 实例级正文 HTML(对齐 html) */
|
|
23
|
+
html?: string;
|
|
24
|
+
/** tip 宽度(px)。未给时由内容撑开(对齐 Ext 默认 autoWidth) */
|
|
25
|
+
width?: number;
|
|
26
|
+
/** 显示延迟(ms)。Ext 默认 500;测试可设 0 加速 */
|
|
27
|
+
showDelay?: number;
|
|
28
|
+
/** 隐藏延迟(ms)。Ext 默认 200 */
|
|
29
|
+
hideDelay?: number;
|
|
30
|
+
/** 箭头方向 */
|
|
31
|
+
anchor?: 'top' | 'bottom' | 'left' | 'right';
|
|
32
|
+
/** 相对容器的 left(对齐 Ext showAt 的 x) */
|
|
33
|
+
x?: number;
|
|
34
|
+
/** 相对容器的 top(对齐 Ext showAt 的 y) */
|
|
35
|
+
y?: number;
|
|
36
|
+
/** 受控初始可见(保真固定显示用) */
|
|
37
|
+
visible?: boolean;
|
|
38
|
+
/** 保真裁剪 / 测试定位用根节点 testid(透传给内部 ToolTip) */
|
|
39
|
+
testId?: string;
|
|
40
|
+
/** 附加根 class(透传) */
|
|
41
|
+
className?: string;
|
|
42
|
+
/** 附加根 style(透传) */
|
|
43
|
+
style?: React.CSSProperties;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** QuickTip 命令式句柄:运行时改写内容 / 显隐(对齐 Ext.QuickTips 的 register/update/show/hide)。 */
|
|
47
|
+
export interface QuickTipHandle {
|
|
48
|
+
/** 显示 tip,可附带部分内容覆盖(title/text/html/width) */
|
|
49
|
+
show: (content?: Partial<QuickTipContent>) => void;
|
|
50
|
+
/** 隐藏 tip */
|
|
51
|
+
hide: () => void;
|
|
52
|
+
/** 当前是否可见 */
|
|
53
|
+
isVisible: () => boolean;
|
|
54
|
+
/** 读取当前内容配置 */
|
|
55
|
+
getContent: () => QuickTipContent;
|
|
56
|
+
/** 局部更新内容(合并 patch) */
|
|
57
|
+
setContent: (patch: Partial<QuickTipContent>) => void;
|
|
58
|
+
/** 仅改标题 */
|
|
59
|
+
setTitle: (title?: string) => void;
|
|
60
|
+
/** 仅改正文文本 */
|
|
61
|
+
setText: (text?: string) => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 保真映射(来源 src/widgets/tips/QuickTip.js + Ext.QuickTips 管理逻辑):
|
|
66
|
+
* - QuickTip 是全局 hover 管理器:监听 target 内任意带 ext:qtip 的元素,移入时显示
|
|
67
|
+
* 一个共享的 Tip(baseCls 'x-tip',9 宫格圆角 + 标题 + 正文),移出后隐藏。
|
|
68
|
+
* - 展示的 tip DOM 与 ToolTip 完全一致(QuickTip 内部即持有一个 Ext.Tip 实例),
|
|
69
|
+
* 故本组件直接复用 ToolTip 渲染,仅需实现 hover 触发与属性读取。
|
|
70
|
+
* - 文本优先级:目标元素 data-qtip/data-qtitle/data-qwidth > 实例级 props。
|
|
71
|
+
* React 仅重写触发逻辑,DOM 结构与 class 名与 Ext JS 3.2.1 完全一致。
|
|
72
|
+
*/
|
|
73
|
+
export const QuickTip = forwardRef<QuickTipHandle, QuickTipProps>(
|
|
74
|
+
({ target, title, text, html, width, showDelay = 500, hideDelay = 200, anchor, x = 0, y = 0, visible, testId, className, style }, ref) => {
|
|
75
|
+
const [shown, setShown] = useState(visible ?? false);
|
|
76
|
+
const [content, setContent] = useState<QuickTipContent>({ title, text, html, width });
|
|
77
|
+
const shownRef = useRef(shown);
|
|
78
|
+
const contentRef = useRef(content);
|
|
79
|
+
shownRef.current = shown;
|
|
80
|
+
contentRef.current = content;
|
|
81
|
+
const showTimer = useRef<number | undefined>(undefined);
|
|
82
|
+
const hideTimer = useRef<number | undefined>(undefined);
|
|
83
|
+
|
|
84
|
+
const show = useCallback(
|
|
85
|
+
(c?: Partial<QuickTipContent>) => {
|
|
86
|
+
if (c) setContent((prev) => ({ ...prev, ...c }));
|
|
87
|
+
setShown(true);
|
|
88
|
+
},
|
|
89
|
+
[],
|
|
90
|
+
);
|
|
91
|
+
const hide = useCallback(() => setShown(false), []);
|
|
92
|
+
const setContentPatch = useCallback((patch: Partial<QuickTipContent>) => {
|
|
93
|
+
setContent((prev) => ({ ...prev, ...patch }));
|
|
94
|
+
}, []);
|
|
95
|
+
const setTitle = useCallback((t?: string) => setContentPatch({ title: t }), [setContentPatch]);
|
|
96
|
+
const setText = useCallback((t?: string) => setContentPatch({ text: t }), [setContentPatch]);
|
|
97
|
+
const getContent = useCallback(() => contentRef.current, []);
|
|
98
|
+
const isVisible = useCallback(() => shownRef.current, []);
|
|
99
|
+
|
|
100
|
+
useImperativeHandle(
|
|
101
|
+
ref,
|
|
102
|
+
() => ({ show, hide, isVisible, getContent, setContent: setContentPatch, setTitle, setText }),
|
|
103
|
+
[show, hide, isVisible, getContent, setContentPatch, setTitle, setText],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!target) return;
|
|
108
|
+
setShown(false);
|
|
109
|
+
const el = document.querySelector(target);
|
|
110
|
+
if (!el) return;
|
|
111
|
+
|
|
112
|
+
const clearTimers = () => {
|
|
113
|
+
window.clearTimeout(showTimer.current);
|
|
114
|
+
window.clearTimeout(hideTimer.current);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const onOver = (e: Event) => {
|
|
118
|
+
const t = (e.target as HTMLElement | null)?.closest('[data-qtip]') as HTMLElement | null;
|
|
119
|
+
if (!t) return;
|
|
120
|
+
const qtip = t.getAttribute('data-qtip');
|
|
121
|
+
const qtitle = t.getAttribute('data-qtitle');
|
|
122
|
+
const qwidth = t.getAttribute('data-qwidth');
|
|
123
|
+
setContent({
|
|
124
|
+
title: qtitle ?? title,
|
|
125
|
+
text: qtip ?? text,
|
|
126
|
+
html: undefined,
|
|
127
|
+
width: qwidth ? Number(qwidth) : width,
|
|
128
|
+
});
|
|
129
|
+
clearTimers();
|
|
130
|
+
showTimer.current = window.setTimeout(() => setShown(true), showDelay);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const onOut = (e: Event) => {
|
|
134
|
+
const related = (e as MouseEvent).relatedTarget as HTMLElement | null;
|
|
135
|
+
// 移动到仍带 data-qtip 的子元素时不隐藏(对齐 Ext 的 within-target 判断)
|
|
136
|
+
if (related && (related.closest('[data-qtip]') || related.closest(target))) return;
|
|
137
|
+
clearTimers();
|
|
138
|
+
hideTimer.current = window.setTimeout(() => setShown(false), hideDelay);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
el.addEventListener('mouseover', onOver);
|
|
142
|
+
el.addEventListener('mouseout', onOut);
|
|
143
|
+
return () => {
|
|
144
|
+
el.removeEventListener('mouseover', onOver);
|
|
145
|
+
el.removeEventListener('mouseout', onOut);
|
|
146
|
+
clearTimers();
|
|
147
|
+
};
|
|
148
|
+
// 仅绑定一次(target/延迟变化极少,演示固定)
|
|
149
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
150
|
+
}, [target]);
|
|
151
|
+
|
|
152
|
+
// 受控可见性同步:visible prop 变化时以 prop 为准
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
setShown(visible ?? false);
|
|
155
|
+
}, [visible]);
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<ToolTip
|
|
159
|
+
visible={shown}
|
|
160
|
+
title={content.title}
|
|
161
|
+
html={content.html}
|
|
162
|
+
children={content.text}
|
|
163
|
+
width={content.width}
|
|
164
|
+
anchor={anchor}
|
|
165
|
+
x={x}
|
|
166
|
+
y={y}
|
|
167
|
+
testId={testId}
|
|
168
|
+
className={className}
|
|
169
|
+
style={style}
|
|
170
|
+
/>
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
export default QuickTip;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import React, { useRef, useState, type CSSProperties, type MouseEvent as ReactMouseEvent } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
|
|
4
|
+
// Ext.Resizable.positions:缩写 → 完整方位(句柄 class 后缀用完整名)
|
|
5
|
+
const SHORT_TO_FULL: Record<string, string> = {
|
|
6
|
+
n: 'north',
|
|
7
|
+
s: 'south',
|
|
8
|
+
e: 'east',
|
|
9
|
+
w: 'west',
|
|
10
|
+
ne: 'northeast',
|
|
11
|
+
nw: 'northwest',
|
|
12
|
+
se: 'southeast',
|
|
13
|
+
sw: 'southwest',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export interface ResizableProps {
|
|
17
|
+
/** 元素宽(px),对应 Ext 配置 width。 */
|
|
18
|
+
width?: number;
|
|
19
|
+
/** 元素高(px),对应 Ext 配置 height。 */
|
|
20
|
+
height?: number;
|
|
21
|
+
/** 句柄集合:'all' 或 'n s e w ne nw se sw'。缺省对齐 Ext 默认值 's,e,se'。 */
|
|
22
|
+
handles?: string;
|
|
23
|
+
/** 句柄常显(true)或仅 hover 显(false,默认)。 */
|
|
24
|
+
pinned?: boolean;
|
|
25
|
+
/** 拖拽时实时改尺寸(true)或使用 proxy 预览(false)。此处采用实时模式。 */
|
|
26
|
+
dynamic?: boolean;
|
|
27
|
+
/** 透明句柄(opacity:0),用于嵌入 Panel/Window 等容器的无痕缩放。 */
|
|
28
|
+
transparent?: boolean;
|
|
29
|
+
minWidth?: number;
|
|
30
|
+
minHeight?: number;
|
|
31
|
+
maxWidth?: number;
|
|
32
|
+
maxHeight?: number;
|
|
33
|
+
cls?: string;
|
|
34
|
+
style?: CSSProperties;
|
|
35
|
+
id?: string;
|
|
36
|
+
testId?: string;
|
|
37
|
+
children?: React.ReactNode;
|
|
38
|
+
onResize?: (width: number, height: number) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const clamp = (v: number, min: number, max: number) => Math.min(Math.max(min, v), max);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 复刻 Ext.Resizable(Ext 3.2.1)。
|
|
45
|
+
*
|
|
46
|
+
* DOM 结构对齐 Ext 真值(src/widgets/Resizable.js):
|
|
47
|
+
* - 根 el:调用方传入的 element 本身,挂 'x-resizable-pinned'(pinned 时);
|
|
48
|
+
* position 非 absolute/fixed 时设 'relative'(此处统一 relative,对齐默认静态元素)。
|
|
49
|
+
* - 8 个句柄作为 el 的子节点 append:模板
|
|
50
|
+
* `<div class="x-resizable-handle x-resizable-handle-{pos}">`(pos 为 north/south/...)。
|
|
51
|
+
* - proxy(x-resizable-proxy,id={el.id}-rzproxy)与 overlay(x-resizable-overlay,
|
|
52
|
+
* html=' ')均 createProxy 挂到 document.body;静态态 display:none,不参与 el 截图。
|
|
53
|
+
*
|
|
54
|
+
* 全部视觉来自 ext-all.css 的 37 条 .x-resizable* 规则,零新增 CSS 契约破坏。
|
|
55
|
+
* 拖拽缩放数学对齐 Ext.onMouseMove(含 min/max 钳制与 n/w 方位的 left/top 偏移)。
|
|
56
|
+
*/
|
|
57
|
+
export function Resizable(props: ResizableProps) {
|
|
58
|
+
const {
|
|
59
|
+
width,
|
|
60
|
+
height,
|
|
61
|
+
handles,
|
|
62
|
+
pinned = false,
|
|
63
|
+
transparent = false,
|
|
64
|
+
minWidth = 5,
|
|
65
|
+
minHeight = 5,
|
|
66
|
+
maxWidth = 10000,
|
|
67
|
+
maxHeight = 10000,
|
|
68
|
+
cls = '',
|
|
69
|
+
style,
|
|
70
|
+
id,
|
|
71
|
+
testId,
|
|
72
|
+
children,
|
|
73
|
+
onResize,
|
|
74
|
+
} = props;
|
|
75
|
+
|
|
76
|
+
const [box, setBox] = useState<{
|
|
77
|
+
w: number | undefined;
|
|
78
|
+
h: number | undefined;
|
|
79
|
+
left: number | undefined;
|
|
80
|
+
top: number | undefined;
|
|
81
|
+
}>({ w: width, h: height, left: undefined, top: undefined });
|
|
82
|
+
|
|
83
|
+
// 始终持有最新 box,供 mousedown 捕获拖拽起始偏移(避免闭包过期)。
|
|
84
|
+
const boxRef = useRef(box);
|
|
85
|
+
boxRef.current = box;
|
|
86
|
+
|
|
87
|
+
const [over, setOver] = useState(false);
|
|
88
|
+
const elRef = useRef<HTMLDivElement>(null);
|
|
89
|
+
const drag = useRef<{
|
|
90
|
+
pos: string;
|
|
91
|
+
startX: number;
|
|
92
|
+
startY: number;
|
|
93
|
+
startBox: { x: number; y: number; width: number; height: number };
|
|
94
|
+
} | null>(null);
|
|
95
|
+
|
|
96
|
+
// 解析句柄方位集合(对齐 Ext 默认 's,e,se' 与 'all' 展开)。
|
|
97
|
+
let hSpec = handles;
|
|
98
|
+
if (hSpec === undefined || hSpec === '') hSpec = 's,e,se';
|
|
99
|
+
if (hSpec === 'all') hSpec = 'n s e w ne nw se sw';
|
|
100
|
+
const positions = hSpec
|
|
101
|
+
.split(/[\s,;]+/)
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.map((p) => SHORT_TO_FULL[p])
|
|
104
|
+
.filter(Boolean) as string[];
|
|
105
|
+
|
|
106
|
+
const constrain = (v: number, diff: number, m: number, mx: number) => {
|
|
107
|
+
if (v - diff < m) diff = v - m;
|
|
108
|
+
else if (v - diff > mx) diff = v - mx;
|
|
109
|
+
return diff;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const onHandleDown = (pos: string) => (e: ReactMouseEvent) => {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
e.stopPropagation();
|
|
115
|
+
const el = elRef.current;
|
|
116
|
+
if (!el) return;
|
|
117
|
+
const sb = el.getBoundingClientRect();
|
|
118
|
+
const startBox = { x: sb.x, y: sb.y, width: sb.width, height: sb.height };
|
|
119
|
+
const startLeft = boxRef.current.left ?? 0;
|
|
120
|
+
const startTop = boxRef.current.top ?? 0;
|
|
121
|
+
drag.current = { pos, startX: e.clientX, startY: e.clientY, startBox };
|
|
122
|
+
|
|
123
|
+
const move = (ev: MouseEvent) => {
|
|
124
|
+
const d = drag.current;
|
|
125
|
+
if (!d) return;
|
|
126
|
+
const { width: sw, height: sh } = d.startBox;
|
|
127
|
+
let w = sw;
|
|
128
|
+
let h = sh;
|
|
129
|
+
let offLeft = startLeft;
|
|
130
|
+
let offTop = startTop;
|
|
131
|
+
const diffX = -(d.startX - Math.max(0, ev.clientX));
|
|
132
|
+
const diffY = -(d.startY - Math.max(0, ev.clientY));
|
|
133
|
+
|
|
134
|
+
switch (d.pos) {
|
|
135
|
+
case 'east':
|
|
136
|
+
w = clamp(sw + diffX, minWidth, maxWidth);
|
|
137
|
+
break;
|
|
138
|
+
case 'south':
|
|
139
|
+
h = clamp(sh + diffY, minHeight, maxHeight);
|
|
140
|
+
break;
|
|
141
|
+
case 'southeast':
|
|
142
|
+
w = clamp(sw + diffX, minWidth, maxWidth);
|
|
143
|
+
h = clamp(sh + diffY, minHeight, maxHeight);
|
|
144
|
+
break;
|
|
145
|
+
case 'north': {
|
|
146
|
+
const dy = constrain(sh, diffY, minHeight, maxHeight);
|
|
147
|
+
offTop = startTop + dy;
|
|
148
|
+
h = sh - dy;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case 'west': {
|
|
152
|
+
const dx = constrain(sw, diffX, minWidth, maxWidth);
|
|
153
|
+
offLeft = startLeft + dx;
|
|
154
|
+
w = sw - dx;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case 'northeast': {
|
|
158
|
+
w = clamp(sw + diffX, minWidth, maxWidth);
|
|
159
|
+
const dy = constrain(sh, diffY, minHeight, maxHeight);
|
|
160
|
+
offTop = startTop + dy;
|
|
161
|
+
h = sh - dy;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case 'northwest': {
|
|
165
|
+
const dx = constrain(sw, diffX, minWidth, maxWidth);
|
|
166
|
+
offLeft = startLeft + dx;
|
|
167
|
+
w = sw - dx;
|
|
168
|
+
const dy = constrain(sh, diffY, minHeight, maxHeight);
|
|
169
|
+
offTop = startTop + dy;
|
|
170
|
+
h = sh - dy;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case 'southwest': {
|
|
174
|
+
const dx = constrain(sw, diffX, minWidth, maxWidth);
|
|
175
|
+
offLeft = startLeft + dx;
|
|
176
|
+
w = sw - dx;
|
|
177
|
+
h = clamp(sh + diffY, minHeight, maxHeight);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
setBox({ w, h, left: offLeft, top: offTop });
|
|
182
|
+
onResize?.(Math.round(w), Math.round(h));
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const up = () => {
|
|
186
|
+
drag.current = null;
|
|
187
|
+
window.removeEventListener('mousemove', move);
|
|
188
|
+
window.removeEventListener('mouseup', up);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
window.addEventListener('mousemove', move);
|
|
192
|
+
window.addEventListener('mouseup', up);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const rootClass = [cls, pinned ? 'x-resizable-pinned' : '', over ? 'x-resizable-over' : '']
|
|
196
|
+
.filter(Boolean)
|
|
197
|
+
.join(' ');
|
|
198
|
+
|
|
199
|
+
const rootStyle: CSSProperties = {
|
|
200
|
+
position: 'relative',
|
|
201
|
+
...(box.w != null ? { width: box.w } : {}),
|
|
202
|
+
...(box.h != null ? { height: box.h } : {}),
|
|
203
|
+
...(box.left != null ? { left: box.left } : {}),
|
|
204
|
+
...(box.top != null ? { top: box.top } : {}),
|
|
205
|
+
...style,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<>
|
|
210
|
+
<div ref={elRef} id={id} data-testid={testId} className={rootClass} style={rootStyle}>
|
|
211
|
+
{children}
|
|
212
|
+
{positions.map((pos) => (
|
|
213
|
+
<div
|
|
214
|
+
key={pos}
|
|
215
|
+
className={`x-resizable-handle x-resizable-handle-${pos}`}
|
|
216
|
+
style={transparent ? { opacity: 0 } : undefined}
|
|
217
|
+
onMouseDown={onHandleDown(pos)}
|
|
218
|
+
onMouseEnter={() => setOver(true)}
|
|
219
|
+
onMouseLeave={() => setOver(false)}
|
|
220
|
+
/>
|
|
221
|
+
))}
|
|
222
|
+
</div>
|
|
223
|
+
{typeof document !== 'undefined' &&
|
|
224
|
+
createPortal(
|
|
225
|
+
<>
|
|
226
|
+
<div
|
|
227
|
+
id={id ? `${id}-rzproxy` : 'resizable-rzproxy'}
|
|
228
|
+
className="x-resizable-proxy"
|
|
229
|
+
style={{ display: 'none' }}
|
|
230
|
+
/>
|
|
231
|
+
<div
|
|
232
|
+
className="x-resizable-overlay"
|
|
233
|
+
dangerouslySetInnerHTML={{ __html: ' ' }}
|
|
234
|
+
style={{ display: 'none' }}
|
|
235
|
+
/>
|
|
236
|
+
</>,
|
|
237
|
+
document.body,
|
|
238
|
+
)}
|
|
239
|
+
</>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import React, { useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Panel } from './Panel';
|
|
3
|
+
|
|
4
|
+
export interface RowItem {
|
|
5
|
+
/** 面板标题(渲染 x-panel-header)。省略则无 header、整高即 body。 */
|
|
6
|
+
title?: string;
|
|
7
|
+
/** 主体内容(纯文本/HTML 字符串);也可通过 children 渲染任意 React 节点。 */
|
|
8
|
+
html?: string;
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
/** 透传到 .x-panel-body 的 inline style */
|
|
11
|
+
bodyStyle?: React.CSSProperties;
|
|
12
|
+
/**
|
|
13
|
+
* 固定高(px,>=1)。与 rowHeight 互斥;省略则 auto(按内容高自适应)。
|
|
14
|
+
* 对齐 Ext panel.height(始终按像素,且 >=1)。
|
|
15
|
+
*/
|
|
16
|
+
height?: number;
|
|
17
|
+
/**
|
|
18
|
+
* 百分比高(小数 0~1,如 .25)。所有 rowHeight 之和须 = 1。
|
|
19
|
+
* 这些行瓜分「容器高 − 所有固定/auto 行高」后的剩余高度(对齐 Ext:百分比行填充分配给固定/auto 行后的空间)。
|
|
20
|
+
*/
|
|
21
|
+
rowHeight?: number;
|
|
22
|
+
/**
|
|
23
|
+
* 行宽(px 或百分比字符串如 '50%' / 200)。省略则铺满容器宽(100%)。
|
|
24
|
+
* 对齐 Ext「standard panel widths are still supported」——百分比行也可指定宽度,仅影响水平尺寸、不影响垂直百分比分配。
|
|
25
|
+
*/
|
|
26
|
+
width?: number | string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RowLayoutProps {
|
|
30
|
+
/** 容器总宽(px),对齐 Ext.Container width。 */
|
|
31
|
+
width: number;
|
|
32
|
+
/** 容器总高(px),对齐 Ext.Container height,也是行高分配的总预算。 */
|
|
33
|
+
height: number;
|
|
34
|
+
/** 行面板数组(自上而下垂直堆叠)。 */
|
|
35
|
+
items: RowItem[];
|
|
36
|
+
/** 数据测试 id,挂在根容器上。 */
|
|
37
|
+
testId?: string;
|
|
38
|
+
style?: React.CSSProperties;
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 复刻 Ext.ux.layout.RowLayout(Ext 3.2.1,UX 扩展,继承 ContainerLayout)。
|
|
44
|
+
*
|
|
45
|
+
* DOM 对齐 RowLayout.renderAll/onLayout 实测真值:
|
|
46
|
+
* - 容器根无专属 ct 类;renderAll 创建 .x-column-inner(含一个 .x-clear 子节点,对齐源码 innerCt 结构);
|
|
47
|
+
* - 每行即一个 .x-panel(复用 Panel),垂直堆叠(block 流),行宽默认 100%(指定 width 时变窄、左对齐)。
|
|
48
|
+
*
|
|
49
|
+
* 高度算法逐行移植 onLayout(examples/ux/RowLayout.js):
|
|
50
|
+
* - 第一遍:遍历「非 rowHeight」行,从剩余高 ph 扣掉其高(固定行用 height;auto 行按内容实测高);
|
|
51
|
+
* - 第二遍:遍历「rowHeight」行,height = floor(rowHeight * ph)(ph=扣完固定/auto 后的剩余高);
|
|
52
|
+
* auto 行保持内容自适应高、不强制拉伸。
|
|
53
|
+
* 行默认无上下 margin(RowLayout 不暴露 margins),故 m=0,算法不涉及 margin 扣减。
|
|
54
|
+
*
|
|
55
|
+
* 说明:auto 行的内容高只能在渲染后实测,故用 useLayoutEffect + ref 做一次测量-重算(收敛后即停,无循环)。
|
|
56
|
+
* 全部视觉来自 ext-all.css 的 .x-panel(零新增 CSS)。
|
|
57
|
+
*/
|
|
58
|
+
export function RowLayout({
|
|
59
|
+
width,
|
|
60
|
+
height,
|
|
61
|
+
items,
|
|
62
|
+
testId,
|
|
63
|
+
style,
|
|
64
|
+
className = '',
|
|
65
|
+
}: RowLayoutProps) {
|
|
66
|
+
const rowRefs = useRef<(HTMLDivElement | null)[]>([]);
|
|
67
|
+
const [computed, setComputed] = useState<(number | undefined)[]>(() => items.map(() => undefined));
|
|
68
|
+
const lastKey = useRef('');
|
|
69
|
+
|
|
70
|
+
useLayoutEffect(() => {
|
|
71
|
+
// 两遍算法(对齐 RowLayout.onLayout)
|
|
72
|
+
let ph = height;
|
|
73
|
+
// 第一遍:从剩余高里扣掉所有「非 rowHeight」行(固定行取 height;auto 行取实测内容高)
|
|
74
|
+
items.forEach((it, i) => {
|
|
75
|
+
if (it.rowHeight == null) {
|
|
76
|
+
const wrap = rowRefs.current[i];
|
|
77
|
+
const rowH = it.height != null ? it.height : wrap ? wrap.getBoundingClientRect().height : 0;
|
|
78
|
+
ph -= rowH;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
ph = ph < 0 ? 0 : ph;
|
|
82
|
+
// 第二遍:rowHeight 行按剩余高比例分配
|
|
83
|
+
const hs = items.map((it) => {
|
|
84
|
+
if (it.rowHeight != null) {
|
|
85
|
+
return Math.floor(it.rowHeight * ph);
|
|
86
|
+
}
|
|
87
|
+
return it.height != null ? it.height : undefined;
|
|
88
|
+
});
|
|
89
|
+
const key = hs.map((h) => (h == null ? 'a' : Math.round(h))).join(',');
|
|
90
|
+
if (key !== lastKey.current) {
|
|
91
|
+
lastKey.current = key;
|
|
92
|
+
setComputed(hs);
|
|
93
|
+
}
|
|
94
|
+
}, [width, height, items]);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div
|
|
98
|
+
data-testid={testId}
|
|
99
|
+
className={['row-layout-root', className].filter(Boolean).join(' ')}
|
|
100
|
+
style={{ width, height, overflow: 'hidden', ...style }}
|
|
101
|
+
>
|
|
102
|
+
<div className="x-column-inner" style={{ width: '100%', height }}>
|
|
103
|
+
{items.map((it, i) => (
|
|
104
|
+
<div
|
|
105
|
+
key={i}
|
|
106
|
+
ref={(el) => {
|
|
107
|
+
rowRefs.current[i] = el;
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
<Panel
|
|
111
|
+
title={it.title}
|
|
112
|
+
width={it.width != null ? it.width : '100%'}
|
|
113
|
+
height={computed[i] != null ? computed[i] : it.height}
|
|
114
|
+
bodyStyle={it.bodyStyle}
|
|
115
|
+
>
|
|
116
|
+
{it.html != null ? it.html : it.children}
|
|
117
|
+
</Panel>
|
|
118
|
+
</div>
|
|
119
|
+
))}
|
|
120
|
+
<div className="x-clear" />
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export default RowLayout;
|