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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +36 -0
  3. package/src/components/AbsoluteLayout.tsx +206 -0
  4. package/src/components/Accordion.tsx +266 -0
  5. package/src/components/AnchorLayout.tsx +131 -0
  6. package/src/components/BorderLayout.tsx +512 -0
  7. package/src/components/Box.tsx +289 -0
  8. package/src/components/Button.tsx +300 -0
  9. package/src/components/ButtonGroup.tsx +170 -0
  10. package/src/components/CalendarPicker.tsx +431 -0
  11. package/src/components/CardLayout.tsx +122 -0
  12. package/src/components/CenterLayout.tsx +95 -0
  13. package/src/components/CheckboxGroup.tsx +200 -0
  14. package/src/components/ColorMenu.tsx +195 -0
  15. package/src/components/ColorPalette.tsx +126 -0
  16. package/src/components/ColumnLayout.tsx +183 -0
  17. package/src/components/ColumnTree.tsx +383 -0
  18. package/src/components/CycleButton.tsx +302 -0
  19. package/src/components/DataView.tsx +244 -0
  20. package/src/components/DataViewMore.tsx +337 -0
  21. package/src/components/DataViewTransition.tsx +235 -0
  22. package/src/components/DateMenu.tsx +163 -0
  23. package/src/components/DatePicker.tsx +387 -0
  24. package/src/components/DisplayField.tsx +73 -0
  25. package/src/components/Editor.tsx +214 -0
  26. package/src/components/EditorGrid.tsx +361 -0
  27. package/src/components/FieldLabeler.tsx +81 -0
  28. package/src/components/FieldReplicator.tsx +135 -0
  29. package/src/components/FieldSet.tsx +149 -0
  30. package/src/components/FitLayout.tsx +76 -0
  31. package/src/components/Form.tsx +3484 -0
  32. package/src/components/Grid.tsx +2173 -0
  33. package/src/components/GridFilters.tsx +332 -0
  34. package/src/components/GroupTab.tsx +274 -0
  35. package/src/components/HtmlEditor.tsx +391 -0
  36. package/src/components/ImageOrganizer.tsx +984 -0
  37. package/src/components/ImageOrganizerService.ts +361 -0
  38. package/src/components/ListView.tsx +221 -0
  39. package/src/components/LoadMask.tsx +74 -0
  40. package/src/components/Menu.tsx +227 -0
  41. package/src/components/MessageBox.tsx +138 -0
  42. package/src/components/MultiSlider.tsx +369 -0
  43. package/src/components/Organizer.tsx +403 -0
  44. package/src/components/PagingToolbar.tsx +255 -0
  45. package/src/components/Panel.tsx +451 -0
  46. package/src/components/ProgressBar.tsx +127 -0
  47. package/src/components/PropertyGrid.tsx +142 -0
  48. package/src/components/QuickTip.tsx +174 -0
  49. package/src/components/Resizable.tsx +241 -0
  50. package/src/components/RowLayout.tsx +126 -0
  51. package/src/components/Slider.tsx +353 -0
  52. package/src/components/SliderField.tsx +206 -0
  53. package/src/components/SpinnerField.tsx +141 -0
  54. package/src/components/SplitBar.tsx +215 -0
  55. package/src/components/Spotlight.tsx +262 -0
  56. package/src/components/StatusBar.tsx +118 -0
  57. package/src/components/TabPanel.tsx +540 -0
  58. package/src/components/TableLayout.tsx +132 -0
  59. package/src/components/ThemeSwitcher.tsx +167 -0
  60. package/src/components/ToolTip.tsx +226 -0
  61. package/src/components/Toolbar.tsx +321 -0
  62. package/src/components/Tree.tsx +809 -0
  63. package/src/components/TreeGrid.tsx +418 -0
  64. package/src/components/Viewport.tsx +218 -0
  65. package/src/components/Window.tsx +441 -0
  66. package/src/components/Writer.tsx +303 -0
  67. package/src/components/XmlTreeLoader.tsx +233 -0
  68. package/src/components/dnd/DndContext.tsx +328 -0
  69. package/src/components/dnd/DragSource.tsx +202 -0
  70. package/src/components/dnd/DragZone.tsx +66 -0
  71. package/src/components/dnd/DropTarget.tsx +86 -0
  72. package/src/components/dnd/DropZone.tsx +166 -0
  73. package/src/components/dnd/PanelProxy.tsx +81 -0
  74. package/src/components/dnd/PanelResizer.tsx +143 -0
  75. package/src/components/dnd/Portal.tsx +254 -0
  76. package/src/components/dnd/Reorderer.tsx +73 -0
  77. package/src/components/dnd/ToolbarReorderer.tsx +226 -0
  78. package/src/main.tsx +2649 -0
  79. package/src/styles/ext-theme.css +889 -0
  80. package/src/util/theme.ts +95 -0
@@ -0,0 +1,135 @@
1
+ import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
2
+ import { TextField } from './Form';
3
+
4
+ /**
5
+ * 保真 Ext.ux.FieldReplicator(一个挂载到字段上的 plugin)。
6
+ *
7
+ * Ext 行为(examples/ux/FieldReplicator.js):字段被填写(由空变非空)时,
8
+ * 在其后插入一个克隆字段;字段被清空(由非空变空)且并非唯一一个时,将其移除。
9
+ * 始终保持末尾有一个空字段供继续输入,最少 1 个。
10
+ *
11
+ * React 中以受控数组 values[] 等价表达:填满末尾空字段 → push 一个新空字段;
12
+ * 清空某个非末尾字段 → splice 移除该字段。TAB 按 DOM 自然顺序在字段间移动,
13
+ * 等价于 Ext 的 focusNext/focusPrev 拦截。
14
+ */
15
+
16
+ export interface FieldReplicatorHandle {
17
+ /** 读取所有非空当前值(汇总各克隆字段的 getValue) */
18
+ getValues: () => string[];
19
+ /** 重置为初始数量的空字段 */
20
+ reset: () => void;
21
+ }
22
+
23
+ export interface FieldReplicatorProps {
24
+ /** 被复制的字段组件,默认 TextField(对齐 Ext 任意 Field 可挂 plugin) */
25
+ as?: React.ComponentType<any>;
26
+ /** 传给每个克隆字段的共享 props(name / width / placeholder 等) */
27
+ fieldProps?: Record<string, any>;
28
+ /** 初始字段数量,默认 1 */
29
+ initialCount?: number;
30
+ /**
31
+ * 字段 label(可选)。对齐 Ext.ux.FieldReplicator 通过 cloneConfig() 复制 fieldLabel 的行为:
32
+ * 每个克隆字段都带同样的 label,渲染在各自行左侧(与 FormLayout 一致),而非整组顶部一个。
33
+ */
34
+ label?: string;
35
+ labelAlign?: 'left' | 'top' | 'right';
36
+ labelWidth?: number;
37
+ labelPad?: number;
38
+ labelSeparator?: string;
39
+ id?: string;
40
+ className?: string;
41
+ style?: React.CSSProperties;
42
+ }
43
+
44
+ export const FieldReplicator = forwardRef<FieldReplicatorHandle, FieldReplicatorProps>(
45
+ (
46
+ {
47
+ as: FieldComp = TextField,
48
+ fieldProps = {},
49
+ initialCount = 1,
50
+ label,
51
+ labelAlign = 'left',
52
+ labelWidth = 100,
53
+ labelPad = 5,
54
+ labelSeparator = ':',
55
+ id,
56
+ className,
57
+ style,
58
+ },
59
+ ref,
60
+ ) => {
61
+ const count = Math.max(1, initialCount);
62
+ const initValues = useRef<string[]>(Array.from({ length: count }, () => ''));
63
+ const [values, setValues] = useState<string[]>(initValues.current);
64
+
65
+ // 镜像最新值,供命令式句柄同步读取(避免 React setState 异步读旧值)
66
+ const valuesRef = useRef<string[]>(values);
67
+ valuesRef.current = values;
68
+
69
+ const handleChange = useCallback((index: number, v: string) => {
70
+ setValues((prev) => {
71
+ const next = [...prev];
72
+ next[index] = v;
73
+ if (v === '') {
74
+ // 清空某字段:若不是最后一个则移除(对齐 Ext onChange 的 blank→remove 分支)
75
+ if (index !== next.length - 1) {
76
+ next.splice(index, 1);
77
+ }
78
+ } else if (index === next.length - 1) {
79
+ // 填充满最后一个空字段:追加一个新的空字段(对齐 Ext insert clone)
80
+ next.push('');
81
+ }
82
+ return next;
83
+ });
84
+ }, []);
85
+
86
+ useImperativeHandle(
87
+ ref,
88
+ () => ({
89
+ getValues: () => valuesRef.current.filter((v) => v !== ''),
90
+ reset: () => setValues(Array.from({ length: count }, () => '')),
91
+ }),
92
+ [count],
93
+ );
94
+
95
+ return (
96
+ <div
97
+ id={id}
98
+ className={['x-ux-fieldreplicator', className].filter(Boolean).join(' ')}
99
+ style={style}
100
+ >
101
+ {values.map((v, i) => {
102
+ const isTop = labelAlign === 'top';
103
+ const labelStyle: React.CSSProperties = isTop
104
+ ? { width: 'auto' }
105
+ : { width: `${labelWidth}px` };
106
+ const elementStyle: React.CSSProperties = isTop
107
+ ? { paddingLeft: 0 }
108
+ : { paddingLeft: `${labelWidth + labelPad}px` };
109
+ const itemClass = ['x-form-item', `x-form-label-${labelAlign}`].filter(Boolean).join(' ');
110
+ return (
111
+ <div key={i} className={itemClass} style={{ marginBottom: 4 }}>
112
+ {label && (
113
+ <label style={labelStyle} className="x-form-item-label">
114
+ {label}
115
+ {labelSeparator}
116
+ </label>
117
+ )}
118
+ <div className="x-form-element" style={elementStyle}>
119
+ <FieldComp
120
+ {...fieldProps}
121
+ id={fieldProps.id ? `${fieldProps.id}-${i}` : undefined}
122
+ value={v}
123
+ onChange={(val: string) => handleChange(i, val)}
124
+ />
125
+ </div>
126
+ <div className="x-form-clear-left"></div>
127
+ </div>
128
+ );
129
+ })}
130
+ </div>
131
+ );
132
+ },
133
+ );
134
+
135
+ FieldReplicator.displayName = 'FieldReplicator';
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Ext.form.FieldSet 保真复刻(来源 src/widgets/form/FieldSet.js + ext-all.css .x-fieldset*)
3
+ *
4
+ * 保真契约:
5
+ * - onRender:fieldset.x-fieldset;当 title/header/checkboxToggle 时追加 legend.x-fieldset-header。
6
+ * - legend 内含折叠工具 div.x-tool.x-tool-toggle(collapsible)或 input(checkboxToggle),
7
+ * 以及标题 span.x-fieldset-header-text。
8
+ * - 主体 wrapper:div.x-fieldset-bwrap > div.x-fieldset-body(承载子项/字段)。
9
+ * - 内容宽度 = 总宽 - 左右 border(1+1) - 左右 padding(10+10) = 总宽 - 22(ext-all.css 用 content-box,
10
+ * golden 内联 style.width 即内容宽,offsetWidth = 总宽)。
11
+ * - 折叠态:fieldset 追加 x-panel-collapsed(非 x-fieldset-collapsed);CSS 隐藏 bwrap
12
+ * (visibility:hidden + 移出视口)并把工具图标切到展开位(.x-panel-collapsed .x-tool-toggle)。
13
+ * - 工具图标背景来自 .x-tool 的 tool-sprites.gif(position 0 -60px 折叠/0 -75px 展开),
14
+ * 与 Panel/Window 共用同一 sprite,复用 ext-all.css 像素一致。
15
+ * 仅重写折叠交互(useState + onClick),DOM/class 与 Ext JS 3.2.1 完全一致。
16
+ */
17
+ import {
18
+ useState,
19
+ useRef,
20
+ forwardRef,
21
+ useImperativeHandle,
22
+ useMemo,
23
+ Children,
24
+ Fragment,
25
+ type ReactNode,
26
+ type CSSProperties,
27
+ } from 'react';
28
+
29
+ export interface FieldSetProps {
30
+ /** 图例标题文本。 */
31
+ title?: ReactNode;
32
+ /** 是否可折叠(legend 渲染折叠工具)。默认 false。 */
33
+ collapsible?: boolean;
34
+ /** 初始折叠态。默认 false(展开)。 */
35
+ collapsed?: boolean;
36
+ /** legend 前渲染复选框控制展开/折叠(本期未实现复选框形态,保留接口)。 */
37
+ checkboxToggle?: boolean;
38
+ /** 总宽度(含 border/padding)。省略则自适应。 */
39
+ width?: number;
40
+ /** 附加类名。 */
41
+ className?: string;
42
+ style?: CSSProperties;
43
+ children?: ReactNode;
44
+ }
45
+
46
+ /**
47
+ * 命令式动态增删接口(对齐 Ext.form.FieldSet.add / remove)。
48
+ * add 返回该项的稳定 id,remove(id) 移除它;getItems 返回当前所有项 id。
49
+ */
50
+ export interface FieldSetHandle {
51
+ add: (node: ReactNode, id?: string) => string;
52
+ remove: (id: string) => void;
53
+ getItems: () => string[];
54
+ }
55
+
56
+ export const FieldSet = forwardRef<FieldSetHandle, FieldSetProps>(function FieldSet(
57
+ { title, collapsible = false, collapsed = false, checkboxToggle = false, width, className, style, children },
58
+ ref,
59
+ ) {
60
+ const [isCollapsed, setCollapsed] = useState(collapsed);
61
+ // 双轨:children prop 为静态项来源(受控——父级重渲染时其中受控字段能正常收到新 props),
62
+ // 运行时 add/remove 维护独立的 dynamicItems / removedSeeds 状态,绝不会被 prop 变更冲掉。
63
+ // 用 Fragment 带 key 渲染,避免额外包裹 DOM 节点破坏既有保真像素基线。
64
+ // 注:早期实现把 children 播种进 state 一次后只渲染该快照,导致 FieldSet 内受控字段
65
+ // (如 ComboBox 的 value)在父级重渲染时收不到新 props,下拉选择不回显。此处改为实时渲染 children。
66
+ const seedItems = useMemo<{ id: string; node: ReactNode }[]>(
67
+ () =>
68
+ Children.toArray(children)
69
+ // Children.toArray 把单个 Fragment 当成 1 个 opaque child(不二次展开其内部),
70
+ // 故需先把 Fragment 扁平为数组,否则外层 Fragment 内的多项会被当作 1 项播种,
71
+ // 导致「渲染出 N 项、getItems 却只返回 1 项」的不一致。
72
+ .flatMap((c) => (c && (c as any).type === Fragment ? Children.toArray((c as any).props.children) : [c]))
73
+ .map((c, i) => ({ id: `seed-${i}`, node: c })),
74
+ [children],
75
+ );
76
+ const [dynamicItems, setDynamicItems] = useState<{ id: string; node: ReactNode }[]>([]);
77
+ const [removedSeeds, setRemovedSeeds] = useState<Set<string>>(new Set());
78
+ const dynCounter = useRef(0);
79
+
80
+ useImperativeHandle(
81
+ ref,
82
+ () => ({
83
+ add: (node: ReactNode, id?: string) => {
84
+ const key = id ?? `dyn-${dynCounter.current++}`;
85
+ setDynamicItems((prev) => [...prev, { id: key, node }]);
86
+ return key;
87
+ },
88
+ remove: (id: string) => {
89
+ if (id.startsWith('dyn-')) {
90
+ setDynamicItems((prev) => prev.filter((it) => it.id !== id));
91
+ } else {
92
+ setRemovedSeeds((prev) => new Set(prev).add(id));
93
+ }
94
+ },
95
+ getItems: () => [
96
+ ...seedItems.filter((s) => !removedSeeds.has(s.id)).map((s) => s.id),
97
+ ...dynamicItems.map((it) => it.id),
98
+ ],
99
+ }),
100
+ [seedItems, dynamicItems, removedSeeds],
101
+ );
102
+
103
+ // 内容宽度 = 总宽 - 左右 border(1+1) - 左右 padding(10+10)
104
+ const contentWidth = width != null ? width - 22 : undefined;
105
+
106
+ const fsClass = ['x-fieldset', isCollapsed ? 'x-panel-collapsed' : '', className]
107
+ .filter(Boolean)
108
+ .join(' ');
109
+
110
+ const fsStyle: CSSProperties = {
111
+ ...(contentWidth != null ? { width: contentWidth } : {}),
112
+ ...style,
113
+ };
114
+
115
+ const hasHeader = title != null || collapsible || checkboxToggle;
116
+
117
+ return (
118
+ <fieldset className={fsClass} style={fsStyle}>
119
+ {hasHeader && (
120
+ <legend className="x-fieldset-header x-unselectable">
121
+ {collapsible && (
122
+ <div
123
+ className="x-tool x-tool-toggle"
124
+ onClick={() => setCollapsed((c) => !c)}
125
+ >
126
+ {' '}
127
+ </div>
128
+ )}
129
+ {title != null && <span className="x-fieldset-header-text">{title}</span>}
130
+ </legend>
131
+ )}
132
+ <div className="x-fieldset-bwrap">
133
+ <div
134
+ className="x-fieldset-body"
135
+ style={contentWidth != null ? { width: contentWidth } : undefined}
136
+ >
137
+ {seedItems
138
+ .filter((it) => !removedSeeds.has(it.id))
139
+ .map((it) => (
140
+ <Fragment key={it.id}>{it.node}</Fragment>
141
+ ))}
142
+ {dynamicItems.map((it) => (
143
+ <Fragment key={it.id}>{it.node}</Fragment>
144
+ ))}
145
+ </div>
146
+ </div>
147
+ </fieldset>
148
+ );
149
+ });
@@ -0,0 +1,76 @@
1
+ import React from 'react';
2
+
3
+ export interface FitItem {
4
+ /** 面板标题(渲染 x-panel-header)。省略则无 header、整高即 body。 */
5
+ title?: string;
6
+ /** 主体内容(纯文本/HTML 字符串);也可通过 children 渲染任意 React 节点。 */
7
+ html?: string;
8
+ children?: React.ReactNode;
9
+ /** 透传到 .x-panel-body 的 inline style */
10
+ bodyStyle?: React.CSSProperties;
11
+ }
12
+
13
+ export interface FitLayoutProps {
14
+ /** 容器总宽(px),对齐 Ext.Container width,也是 fit 子面板目标宽。 */
15
+ width: number;
16
+ /** 容器总高(px),对齐 Ext.Container height,也是 fit 子面板目标高。 */
17
+ height: number;
18
+ /** 唯一子面板配置(FitLayout 仅容纳一个子项)。 */
19
+ item: FitItem;
20
+ /** 数据测试 id,挂在根容器上。 */
21
+ testId?: string;
22
+ style?: React.CSSProperties;
23
+ className?: string;
24
+ }
25
+
26
+ // Ext 3.2.1 实测:x-panel 本身 border 为 0,header 高 26px,body 占剩余(含 1px 底边框,border-box)。
27
+ const HEADER_H = 26;
28
+
29
+ /**
30
+ * 复刻 Ext.layout.FitLayout(Ext 3.2.1)。
31
+ *
32
+ * DOM 对齐 ContainerLayout(fit) 实测真值(#fitlayout):
33
+ * - 容器根无专属 ct 类(ext-all.css 无 .x-fit-item 等);
34
+ * - 唯一的子面板直接作为容器子节点,setSize 到容器内容区(width × height),铺满整个容器。
35
+ *
36
+ * 定位数学:子面板整体宽高 = 容器内容区(无 padding),header 占 26px,body = height-26。
37
+ * 全部视觉来自 ext-all.css 既有规则(零新增 CSS)。
38
+ */
39
+ export function FitLayout({
40
+ width,
41
+ height,
42
+ item,
43
+ testId,
44
+ style,
45
+ className = '',
46
+ }: FitLayoutProps) {
47
+ const rootStyle: React.CSSProperties = { width, height, ...style };
48
+ const headerH = item.title ? HEADER_H : 0;
49
+ const bodyH = height - headerH;
50
+
51
+ return (
52
+ <div
53
+ data-testid={testId}
54
+ className={['fit-layout-root', className].filter(Boolean).join(' ')}
55
+ style={rootStyle}
56
+ >
57
+ <div className="x-panel" style={{ width, height }}>
58
+ {item.title && (
59
+ <div className="x-panel-header x-unselectable" unselectable="on">
60
+ <span className="x-panel-header-text">{item.title}</span>
61
+ </div>
62
+ )}
63
+ <div className="x-panel-bwrap">
64
+ <div
65
+ className={['x-panel-body', !item.title && 'x-panel-body-noheader'].filter(Boolean).join(' ')}
66
+ style={{ height: bodyH, ...item.bodyStyle }}
67
+ >
68
+ {item.html != null ? item.html : item.children}
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ );
74
+ }
75
+
76
+ export default FitLayout;