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,73 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { DragZone } from './DragZone';
3
+ import { DropZone } from './DropZone';
4
+
5
+ /**
6
+ * Reorderer —— 保真复刻 Ext.ux.Reorderer(UX:通用列表重排)。
7
+ *
8
+ * Reorderer 把「DragZone + DropZone + 插入指示线 + 状态管理」封装为一个开箱
9
+ * 即用的可重排列表组件:传入 items 与 renderItem,组件内部维护顺序状态,
10
+ * 拖拽时显示插入指示线,松手后按索引重排并回调 onChange。等价于 Ext 示例中
11
+ * 给一个容器挂上 Reorderer 后即可上下拖拽排序。
12
+ *
13
+ * 底层复用 DragZone / DropZone(即 DndManager 引擎),不重复实现拖放内核。
14
+ */
15
+ export interface ReordererProps<T> {
16
+ /** 受控项列表(顺序即视觉顺序) */
17
+ items: T[];
18
+ /** 取每项唯一 key(等价 Reorderer 的 itemId) */
19
+ getKey: (item: T) => string;
20
+ /** 渲染每项内容 */
21
+ renderItem: (item: T) => React.ReactNode;
22
+ ddGroup?: string;
23
+ className?: string;
24
+ itemClassName?: string;
25
+ /** 重排后回调(内部顺序已更新时调用) */
26
+ onChange?: (items: T[]) => void;
27
+ }
28
+
29
+ export function Reorderer<T>(props: ReordererProps<T>): React.ReactElement {
30
+ const [order, setOrder] = useState<T[]>(props.items);
31
+ const itemsRef = useRef(props.items);
32
+ itemsRef.current = props.items;
33
+
34
+ // 受控同步:外部 items 变化时跟随(但若内部正在拖拽则以后者为准)
35
+ useEffect(() => {
36
+ setOrder(props.items);
37
+ }, [props.items]);
38
+
39
+ const itemIds = order.map((i) => props.getKey(i));
40
+
41
+ const onDropItem = (data: any, index: number) => {
42
+ const key = data?.item?.id as string | undefined;
43
+ if (!key) return;
44
+ const next = [...order];
45
+ const from = next.findIndex((i) => props.getKey(i) === key);
46
+ if (from === -1) return;
47
+ const [moved] = next.splice(from, 1);
48
+ let to = index;
49
+ if (from < index) to = index - 1;
50
+ next.splice(to, 0, moved);
51
+ setOrder(next);
52
+ props.onChange?.(next);
53
+ };
54
+
55
+ return (
56
+ <DropZone
57
+ ddGroup={props.ddGroup ?? 'reorder'}
58
+ className={props.className}
59
+ overClass="x-dd-drop-over"
60
+ itemIds={itemIds}
61
+ onDropItem={onDropItem}
62
+ >
63
+ <DragZone
64
+ ddGroup={props.ddGroup ?? 'reorder'}
65
+ items={order.map((i) => ({ id: props.getKey(i), content: props.renderItem(i) }))}
66
+ getDragData={(it) => ({
67
+ item: { id: it.id, value: order.find((x) => props.getKey(x) === it.id) },
68
+ })}
69
+ itemClassName={props.itemClassName}
70
+ />
71
+ </DropZone>
72
+ );
73
+ }
@@ -0,0 +1,226 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
+ import { Button, type ButtonProps } from '../Button';
3
+ import { Toolbar, type ToolbarAlign, type ToolbarItem } from '../Toolbar';
4
+ import { Draggable } from './DragSource';
5
+ import { dndManager, useDndState, type DropTargetReg } from './DndContext';
6
+
7
+ /**
8
+ * ToolbarReorderer —— 保真复刻 Ext.ux.ToolbarReorderer + Ext.ux.ToolbarDroppable
9
+ * (examples/ux/ToolbarReorderer.js、ToolbarDroppable.js)。
10
+ *
11
+ * 对应 Ext 官方 canonical 示例 `toolbar-ds.html`:同一工具栏同时挂两个插件——
12
+ * 1) ToolbarReorderer:工具栏内每个按钮(reorderable !== false)变拖拽源,
13
+ * 水平拖拽时按指针 X 与各按钮中点比较,越过中点即实时换位(等价原 onMovedLeft/Right)。
14
+ * 2) ToolbarDroppable:工具栏作为一个 DropTarget,从外部 palette 拖入的项经
15
+ * createItem(data) 转换为工具栏按钮,按落点 X 算插入位(等价 calculateEntryIndex)。
16
+ *
17
+ * 与 Ext 的差异(React 化):
18
+ * - Ext 用 Ext.dd.DD 直接移动真实按钮节点并动画归位;这里复刻为「拖出 ghost + 落点
19
+ * 插入指示线 + 松手改数组」的受控模式,与本项目 Portal/Reorderer 一致。
20
+ * - 横向插入位置由 useDndState() 暴露的 proxy x 实时计算(等价 calculateEntryIndex)。
21
+ */
22
+
23
+ let TB_SEQ = 0;
24
+
25
+ export interface ToolbarReorderItem {
26
+ /** 稳定 id,用于重排定位 */
27
+ id: string;
28
+ /** 按钮配置(透传给 Ext.Button 保真组件) */
29
+ config: ButtonProps;
30
+ /** 是否可重排,默认 true;false 的按钮固定不可拖(等价 reorderable:false) */
31
+ reorderable?: boolean;
32
+ }
33
+
34
+ export interface ToolbarReordererProps {
35
+ /** 初始按钮集合(仅作为种子,运行时重排/拖入均为内部状态) */
36
+ items: ToolbarReorderItem[];
37
+ /** 拖放组(内部重排与外部 palette 拖入共用,需同组才能交互) */
38
+ ddGroup?: string;
39
+ buttonAlign?: ToolbarAlign;
40
+ width?: number | string;
41
+ className?: string;
42
+ /** 测试 id,挂在根容器上 */
43
+ testId?: string;
44
+ /**
45
+ * 接收外部(如 palette)拖入时把拖拽数据转换为工具栏项。
46
+ * 不提供则拒绝外部拖入(仅保留内部重排能力,等价单独的 ToolbarReorderer)。
47
+ */
48
+ createItem?: (data: any) => ToolbarReorderItem;
49
+ /** 内部重排完成后回调,参数为最新顺序的 items */
50
+ onReordered?: (items: ToolbarReorderItem[]) => void;
51
+ /** 外部项拖入后回调,参数为新项与插入索引 */
52
+ onItemDropped?: (item: ToolbarReorderItem, index: number) => void;
53
+ }
54
+
55
+ /** 按指针 X 与各 [data-tool-id] 单元格中点比较,计算插入索引(等价 calculateEntryIndex) */
56
+ function computeIndex(container: HTMLElement | null, px: number): number {
57
+ if (!container) return 0;
58
+ const cells = Array.from(
59
+ container.querySelectorAll<HTMLElement>('[data-tool-id]'),
60
+ );
61
+ for (let i = 0; i < cells.length; i++) {
62
+ const r = cells[i].getBoundingClientRect();
63
+ if (px < r.left + r.width / 2) return i;
64
+ }
65
+ return cells.length;
66
+ }
67
+
68
+ /** 竖向插入指示线(等价 Ext 的插入指示,方向改为横向工具栏) */
69
+ const ToolbarIndicator: React.FC<{
70
+ containerRef: React.RefObject<HTMLDivElement>;
71
+ index: number;
72
+ }> = ({ containerRef, index }) => {
73
+ const [style, setStyle] = useState<React.CSSProperties>({});
74
+ useEffect(() => {
75
+ const c = containerRef.current;
76
+ if (!c) return;
77
+ const cr = c.getBoundingClientRect();
78
+ const cells = Array.from(
79
+ c.querySelectorAll<HTMLElement>('[data-tool-id]'),
80
+ );
81
+ let left: number;
82
+ if (cells.length === 0) left = 8;
83
+ else if (index >= cells.length) {
84
+ const r = cells[cells.length - 1].getBoundingClientRect();
85
+ left = r.right - cr.left;
86
+ } else {
87
+ const r = cells[index].getBoundingClientRect();
88
+ left = r.left - cr.left;
89
+ }
90
+ setStyle({ left, top: 4, height: Math.max(12, cr.height - 8) });
91
+ }, [index, containerRef]);
92
+ return <div className="x-toolbar-reorder-indicator" style={style} />;
93
+ };
94
+
95
+ export const ToolbarReorderer: React.FC<ToolbarReordererProps> = (props) => {
96
+ const {
97
+ items,
98
+ ddGroup = 'toolbar',
99
+ buttonAlign = 'left',
100
+ width,
101
+ className,
102
+ testId,
103
+ createItem,
104
+ onReordered,
105
+ onItemDropped,
106
+ } = props;
107
+
108
+ const containerRef = useRef<HTMLDivElement>(null);
109
+ const toolbarId = useRef(`tbrt-${(TB_SEQ += 1)}`).current;
110
+
111
+ // 内部项状态(种子来自 props,运行期重排/拖入均改此状态)
112
+ const [list, setList] = useState<ToolbarReorderItem[]>(() => items.slice());
113
+ const listRef = useRef(list);
114
+ listRef.current = list;
115
+
116
+ // 回调 ref,避免注册闭包过期
117
+ const onReorderedRef = useRef(onReordered);
118
+ onReorderedRef.current = onReordered;
119
+ const onItemDroppedRef = useRef(onItemDropped);
120
+ onItemDroppedRef.current = onItemDropped;
121
+
122
+ const dnd = useDndState();
123
+ const [insertIndex, setInsertIndex] = useState<number | null>(null);
124
+
125
+ // 拖拽悬停本工具栏时,实时计算横向插入位置
126
+ useEffect(() => {
127
+ if (dnd.overId === toolbarId && dnd.visible && containerRef.current) {
128
+ setInsertIndex(computeIndex(containerRef.current, dnd.x + 12));
129
+ } else {
130
+ setInsertIndex(null);
131
+ }
132
+ }, [dnd.overId, dnd.x, dnd.visible, toolbarId]);
133
+
134
+ // 手动注册 DropTarget,掌控 overId 匹配与 onDrop 逻辑(等价 ToolbarDroppable.createDropTarget)
135
+ const dropRef = useRef<(data: any, e: PointerEvent) => boolean>(() => false);
136
+ dropRef.current = (data, e) => {
137
+ const c = containerRef.current;
138
+ if (!c) return false;
139
+ const idx = computeIndex(c, e.clientX);
140
+ const prev = listRef.current;
141
+
142
+ // 内部重排:拖拽数据为 { __reorderId }
143
+ if (data && data.__reorderId) {
144
+ const from = prev.findIndex((x) => x.id === data.__reorderId);
145
+ if (from < 0) return false;
146
+ const next = prev.slice();
147
+ const [m] = next.splice(from, 1);
148
+ const to = idx > from ? idx - 1 : idx;
149
+ next.splice(to, 0, m);
150
+ listRef.current = next;
151
+ setList(next);
152
+ onReorderedRef.current?.(next);
153
+ return true;
154
+ }
155
+
156
+ // 外部拖入:经 createItem 转换为工具栏按钮
157
+ if (createItem) {
158
+ const ni = createItem(data);
159
+ if (!ni.id) ni.id = `tbi-${(TB_SEQ += 1)}`;
160
+ const next = prev.slice();
161
+ next.splice(idx, 0, ni);
162
+ listRef.current = next;
163
+ setList(next);
164
+ onItemDroppedRef.current?.(ni, idx);
165
+ return true;
166
+ }
167
+
168
+ return false;
169
+ };
170
+
171
+ useEffect(() => {
172
+ const reg: DropTargetReg = {
173
+ id: toolbarId,
174
+ groups: [ddGroup],
175
+ el: containerRef.current as HTMLElement,
176
+ validate: () => true,
177
+ onDrop: (data, e) => dropRef.current(data, e),
178
+ };
179
+ dndManager.registerDrop(reg);
180
+ return () => dndManager.unregisterDrop(toolbarId);
181
+ }, [toolbarId, ddGroup]);
182
+
183
+ // 把内部项渲染为工具栏项:可重排项包 Draggable,否则直接 Button
184
+ const toolbarItems: ToolbarItem[] = useMemo(
185
+ () =>
186
+ list.map((it) => {
187
+ const btn = <Button {...it.config} />;
188
+ if (it.reorderable === false) return btn;
189
+ return (
190
+ <Draggable
191
+ ddGroup={ddGroup}
192
+ getDragData={() => ({ __reorderId: it.id })}
193
+ renderGhost={() => <Button {...it.config} />}
194
+ >
195
+ {btn}
196
+ </Draggable>
197
+ );
198
+ }),
199
+ [list, ddGroup],
200
+ );
201
+
202
+ const over = dnd.overId === toolbarId;
203
+ const rootCls = [
204
+ 'x-toolbar-reorderer',
205
+ over ? 'x-toolbar-drop-over' : '',
206
+ className,
207
+ ]
208
+ .filter(Boolean)
209
+ .join(' ');
210
+
211
+ return (
212
+ <div
213
+ ref={containerRef}
214
+ className={rootCls}
215
+ data-testid={testId}
216
+ style={{ position: 'relative' }}
217
+ >
218
+ <Toolbar items={toolbarItems} buttonAlign={buttonAlign} width={width} />
219
+ {insertIndex !== null && (
220
+ <ToolbarIndicator containerRef={containerRef} index={insertIndex} />
221
+ )}
222
+ </div>
223
+ );
224
+ };
225
+
226
+ export default ToolbarReorderer;