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,302 @@
1
+ import React, { useState, useRef, useCallback, useEffect, forwardRef, useImperativeHandle } from 'react';
2
+ import { Menu, type MenuItem } from './Menu';
3
+
4
+ export interface CycleButtonHandle {
5
+ /** 追加/插入选项(index 省略则追加到末尾) */
6
+ addItem: (item: CycleItem, index?: number) => void;
7
+ /** 删除指定索引选项,激活索引自动收紧 */
8
+ removeItem: (index: number) => void;
9
+ /** 局部更新指定索引选项 */
10
+ updateItem: (index: number, partial: Partial<CycleItem>) => void;
11
+ /** 设置激活项(等价 Ext.setActiveItem) */
12
+ setActive: (index: number) => void;
13
+ /** 取当前激活索引 */
14
+ getActive: () => number;
15
+ /** 取当前选项克隆数组 */
16
+ getItems: () => CycleItem[];
17
+ }
18
+
19
+ export interface CycleItem {
20
+ text?: string;
21
+ iconCls?: string;
22
+ disabled?: boolean;
23
+ /** 初始选中项(仅首项生效,多个 checked 取第一个) */
24
+ checked?: boolean;
25
+ }
26
+
27
+ export interface CycleButtonProps {
28
+ /** 按钮文字(showText=false 时固定显示此文字;默认使用首选项 text 同款占位) */
29
+ text?: string;
30
+ /** True 时把当前激活项的 text(含 prependText)显示为按钮文字(等同 Ext.CycleButton.showText) */
31
+ showText?: boolean;
32
+ /** 显示在激活项文字前的静态前缀(仅 showText=true 生效) */
33
+ prependText?: string;
34
+ /** 固定图标 class,始终显示,不随激活项变化(等同 Ext.CycleButton.forceIcon) */
35
+ forceIcon?: string;
36
+ /** 选项列表(等价 Ext items:渲染为 x-cycle-menu 内 CheckItem 单组) */
37
+ items: CycleItem[];
38
+ scale?: 'small' | 'medium' | 'large';
39
+ iconAlign?: 'top' | 'right' | 'bottom' | 'left';
40
+ type?: 'submit' | 'reset' | 'button';
41
+ cls?: string;
42
+ disabled?: boolean;
43
+ arrowAlign?: 'right' | 'bottom';
44
+ width?: number;
45
+ minWidth?: number;
46
+ /** 激活项变化回调(等价 Ext change / changeHandler:fn(activeIndex, item)) */
47
+ onChange?: (activeIndex: number, item: CycleItem) => void;
48
+ }
49
+
50
+ /**
51
+ * CycleButton —— 保真复刻 Ext.CycleButton(src/widgets/CycleButton.js,继承 Ext.SplitButton)。
52
+ *
53
+ * 保真契约(DOM/class 严格对齐源码):
54
+ * - 根容器与 Ext.SplitButton 完全一致:<table class="x-btn x-btn-split ..."> 两层 table 模板;
55
+ * em 承载 x-btn-split(箭头区 s-arrow.gif),点击箭头 = 展开菜单。
56
+ * - 菜单:<Menu rootClassName="x-cycle-menu">,每个 item 为 Ext.menu.CheckItem(单 group 互斥),
57
+ * 激活项 <li class="x-menu-item-checked">(背景 #def8f6 + 勾选 checked.gif)。
58
+ *
59
+ * 行为契约(对齐 CycleButton.js):
60
+ * - 主体点击 -> 在 items 间循环到下一项(toggleSelected):skip disabled,到末尾回卷到首项;
61
+ * 同时 setActiveItem -> 更新显示文本/图标 + 触发 onChange(等同 change 事件)。
62
+ * - 箭头区点击 -> 展开菜单(与 SplitButton 一致),点菜单项 -> 直接 setActiveItem 到该项。
63
+ * - showText=true 时按钮文字 = prependText + 激活项 text;forceIcon 时图标固定不随项变化。
64
+ */
65
+ export const CycleButton = forwardRef<CycleButtonHandle, CycleButtonProps>(
66
+ (
67
+ {
68
+ text = '',
69
+ showText = false,
70
+ prependText = '',
71
+ forceIcon,
72
+ items,
73
+ scale = 'small',
74
+ iconAlign = 'left',
75
+ type = 'button',
76
+ cls = '',
77
+ disabled = false,
78
+ arrowAlign = 'right',
79
+ width,
80
+ minWidth,
81
+ onChange,
82
+ },
83
+ ref,
84
+ ) => {
85
+ // 单源数组法:dynItems 为唯一真相,句柄运行时改写;useEffect([items]) 同步 prop 变化。
86
+ const [dynItems, setDynItems] = useState<CycleItem[]>(() => items);
87
+ const itemsRef = useRef<CycleItem[]>(dynItems);
88
+ const [activeIndex, setActiveIndex] = useState(() => {
89
+ const fc = items.findIndex((it) => it.checked);
90
+ return fc >= 0 ? fc : 0;
91
+ });
92
+ const activeIndexRef = useRef(activeIndex);
93
+ useEffect(() => {
94
+ itemsRef.current = dynItems;
95
+ }, [dynItems]);
96
+ useEffect(() => {
97
+ activeIndexRef.current = activeIndex;
98
+ }, [activeIndex]);
99
+ useEffect(() => {
100
+ setDynItems(items);
101
+ }, [items]);
102
+
103
+ useImperativeHandle(
104
+ ref,
105
+ (): CycleButtonHandle => ({
106
+ addItem: (item, index) => {
107
+ setDynItems((prev) => {
108
+ const next = [...prev];
109
+ if (index == null || index >= next.length) next.push(item);
110
+ else next.splice(index, 0, item);
111
+ return next;
112
+ });
113
+ },
114
+ removeItem: (index) => {
115
+ const curLen = itemsRef.current.length;
116
+ setDynItems((prev) => prev.filter((_, i) => i !== index));
117
+ setActiveIndex((prev) => {
118
+ if (curLen <= 1) return 0;
119
+ const newLen = curLen - 1;
120
+ if (index < prev) return prev - 1;
121
+ if (index === prev) return Math.min(prev, newLen - 1);
122
+ return Math.min(prev, newLen - 1);
123
+ });
124
+ },
125
+ updateItem: (index, partial) => {
126
+ setDynItems((prev) => prev.map((it, i) => (i === index ? { ...it, ...partial } : it)));
127
+ },
128
+ setActive: (i) => {
129
+ setActiveIndex(i);
130
+ const it = itemsRef.current[i];
131
+ if (it) onChange?.(i, it);
132
+ activeIndexRef.current = i;
133
+ },
134
+ getActive: () => activeIndexRef.current,
135
+ getItems: () => itemsRef.current.map((it) => ({ ...it })),
136
+ }),
137
+ [onChange],
138
+ );
139
+
140
+ const [hovered, setHovered] = useState(false);
141
+ const [focused, setFocused] = useState(false);
142
+ const [clicked, setClicked] = useState(false);
143
+ const [arrowHovered, setArrowHovered] = useState(false);
144
+ const [menuOpen, setMenuOpen] = useState(false);
145
+ const wrapRef = useRef<HTMLSpanElement>(null);
146
+
147
+ // 外部点击 / Esc 关闭菜单(等同 Ext 的 document 级监听)
148
+ useEffect(() => {
149
+ if (!menuOpen) return;
150
+ const onDocDown = (e: MouseEvent) => {
151
+ if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) {
152
+ setMenuOpen(false);
153
+ }
154
+ };
155
+ const onKey = (e: KeyboardEvent) => {
156
+ if (e.key === 'Escape') setMenuOpen(false);
157
+ };
158
+ document.addEventListener('mousedown', onDocDown);
159
+ document.addEventListener('keydown', onKey);
160
+ return () => {
161
+ document.removeEventListener('mousedown', onDocDown);
162
+ document.removeEventListener('keydown', onKey);
163
+ };
164
+ }, [menuOpen]);
165
+
166
+ const activeItem = dynItems[activeIndex] ?? dynItems[0];
167
+
168
+ const displayText = showText ? `${prependText}${activeItem?.text ?? ''}` : text;
169
+ const displayIconCls = forceIcon || activeItem?.iconCls || '';
170
+
171
+ // 与 Ext.Button 一致:图标 + 文字 -> x-btn-text-icon;仅图标 -> x-btn-icon;无 -> x-btn-noicon
172
+ const iconStateClass = displayIconCls
173
+ ? displayText
174
+ ? 'x-btn-text-icon'
175
+ : 'x-btn-icon'
176
+ : 'x-btn-noicon';
177
+
178
+ const emClass =
179
+ arrowAlign === 'bottom' ? 'x-btn-split-bottom' : 'x-btn-split';
180
+
181
+ const tableClass = [
182
+ 'x-btn',
183
+ cls,
184
+ iconStateClass,
185
+ disabled ? 'x-item-disabled' : '',
186
+ hovered ? 'x-btn-over' : '',
187
+ focused ? 'x-btn-focus' : '',
188
+ clicked ? 'x-btn-click' : '',
189
+ arrowHovered ? 'x-btn-menu-active' : '',
190
+ ]
191
+ .filter(Boolean)
192
+ .join(' ');
193
+
194
+ // 菜单项:激活项 checked(单组互斥,等同 CheckItem),点击 -> 选中并关闭
195
+ const menuItems: MenuItem[] = dynItems.map((it, i) => ({
196
+ text: it.text,
197
+ iconCls: it.iconCls,
198
+ disabled: it.disabled,
199
+ checked: i === activeIndex,
200
+ handler: () => {
201
+ setActiveIndex(i);
202
+ onChange?.(i, it);
203
+ setMenuOpen(false);
204
+ },
205
+ }));
206
+
207
+ // 主体点击:循环到下一项(toggleSelected),跳过 disabled,末尾回卷首项
208
+ const handleBodyClick = useCallback(
209
+ (e: React.MouseEvent<HTMLButtonElement>) => {
210
+ if (disabled) return;
211
+ e.preventDefault();
212
+ for (let step = 1; step < dynItems.length; step++) {
213
+ const idx = (activeIndex + step) % dynItems.length;
214
+ if (!dynItems[idx].disabled) {
215
+ setActiveIndex(idx);
216
+ onChange?.(idx, dynItems[idx]);
217
+ break;
218
+ }
219
+ }
220
+ },
221
+ [disabled, dynItems, activeIndex, onChange],
222
+ );
223
+
224
+ // 箭头区点击(target 为 EM 即箭头区)-> 展开菜单
225
+ const handleEmClick = useCallback(
226
+ (e: React.MouseEvent<HTMLElement>) => {
227
+ if (disabled) return;
228
+ if ((e.target as HTMLElement).tagName !== 'EM') return; // 主体点击交给 handleBodyClick
229
+ e.preventDefault();
230
+ setMenuOpen((o) => !o);
231
+ },
232
+ [disabled],
233
+ );
234
+
235
+ return (
236
+ <span ref={wrapRef} style={{ position: 'relative', display: 'block' }}>
237
+ <table
238
+ cellSpacing={0}
239
+ className={tableClass}
240
+ style={{ ...(minWidth ? { minWidth } : {}), ...(width ? { width: `${width}px` } : {}) }}
241
+ >
242
+ <tbody className={`x-btn-${scale} x-btn-icon-${scale}-${iconAlign}`}>
243
+ <tr>
244
+ <td className="x-btn-tl"><i>&#160;</i></td>
245
+ <td className="x-btn-tc"></td>
246
+ <td className="x-btn-tr"><i>&#160;</i></td>
247
+ </tr>
248
+ <tr>
249
+ <td className="x-btn-ml"><i>&#160;</i></td>
250
+ <td className="x-btn-mc">
251
+ <em
252
+ className={emClass}
253
+ unselectable="on"
254
+ onClick={handleEmClick}
255
+ onMouseEnter={() => !disabled && setArrowHovered(true)}
256
+ onMouseLeave={() => !disabled && setArrowHovered(false)}
257
+ >
258
+ <button
259
+ type={type}
260
+ className={['x-btn-text', displayIconCls].filter(Boolean).join(' ')}
261
+ style={{
262
+ ...(displayIconCls ? {} : { padding: 0 }),
263
+ }}
264
+ onClick={handleBodyClick}
265
+ onMouseEnter={() => !disabled && setHovered(true)}
266
+ onMouseLeave={() => !disabled && setHovered(false)}
267
+ onFocus={() => !disabled && setFocused(true)}
268
+ onBlur={() => !disabled && setFocused(false)}
269
+ onMouseDown={() => !disabled && setClicked(true)}
270
+ onMouseUp={() => !disabled && setClicked(false)}
271
+ >{displayText || ' '}</button>
272
+ </em>
273
+ </td>
274
+ <td className="x-btn-mr"><i>&#160;</i></td>
275
+ </tr>
276
+ <tr>
277
+ <td className="x-btn-bl"><i>&#160;</i></td>
278
+ <td className="x-btn-bc"></td>
279
+ <td className="x-btn-br"><i>&#160;</i></td>
280
+ </tr>
281
+ </tbody>
282
+ </table>
283
+ {menuOpen && (
284
+ <div style={{ position: 'absolute', top: '100%', left: 0, zIndex: 15000 }}>
285
+ <Menu
286
+ rootClassName="x-cycle-menu"
287
+ items={menuItems}
288
+ onItemClick={(_item, index) => {
289
+ setActiveIndex(index);
290
+ onChange?.(index, dynItems[index]);
291
+ setMenuOpen(false);
292
+ }}
293
+ />
294
+ </div>
295
+ )}
296
+ </span>
297
+ );
298
+ });
299
+
300
+ CycleButton.displayName = 'CycleButton';
301
+
302
+ export default CycleButton;
@@ -0,0 +1,244 @@
1
+ import React, { useState, useRef, useCallback, forwardRef, useImperativeHandle } from 'react';
2
+
3
+ export interface DataViewItem {
4
+ text: string;
5
+ value?: string;
6
+ }
7
+
8
+ export interface DataViewProps {
9
+ /** 数据项(等价 Ext DataView 的 store 数据,displayField='text') */
10
+ data: DataViewItem[];
11
+ /** 初始选中项索引(可多值,开启 multiSelect 时) */
12
+ selected?: number | number[];
13
+ /** 允许多选(点击累加 / 拖拽框选多值),默认单选 */
14
+ multiSelect?: boolean;
15
+ width?: number;
16
+ height?: number;
17
+ /** 选中变化回调(等价 Ext selectionchange / click:fn(selectedIndexes)) */
18
+ onSelectionChange?: (indexes: number[]) => void;
19
+ /** 空数据提示文本(对齐 Ext.DataView.emptyText;data 为空时渲染,复用既有 DOM 结构)。 */
20
+ emptyText?: string;
21
+ }
22
+
23
+ /**
24
+ * DataView —— 保真复刻 Ext.DataView(src/widgets/DataView.js)。
25
+ *
26
+ * 保真契约(DOM/class 严格对齐源码):
27
+ * - 容器 <div class="x-data-view">(position:relative,承载拖拽选择框),内部按 itemTpl 渲染
28
+ * N×<div class="x-view-item">(itemSelector 默认 div.x-view-item)。
29
+ * - 选中项追加 x-list-selected(复用 Ext.ListView 原生选中类,xtheme-blue/gray 中均有主题化规则:
30
+ * blue #dfe8f6 / gray #f0f0f0;零自定义 CSS,蓝/灰原生主题化),
31
+ * 悬停项追加 x-view-over(Ext 默认 overClass;原版 DataView 悬停无主题高亮,保持一致)。
32
+ * 注:原版 Ext.DataView 的 selectedClass 为 'x-view-selected',但框架(ext-all/xtheme)对其
33
+ * 无 base 规则,选中视觉本由示例 CSS 提供;此处为"零新增 CSS"改用原生 .x-list-selected。
34
+ * - 拖拽框选渲染 <div class="x-view-selector">(ext-all.css 唯一有规则的 x-view 类:
35
+ * #c3daf9 背景 + #3399bb 边框,CSS 2706/5958)。
36
+ *
37
+ * 行为契约(对齐 DataView.js onMouseDown / onClick / doSingleSelect / doMultiSelect):
38
+ * - 点击项 -> 单选(清空其他);multiSelect 下 Ctrl/点击累加切换。
39
+ * - 悬停项 -> x-view-over(mouseover/mouseout)。
40
+ * - 在容器空白处按下并拖拽 -> 绘制 x-view-selector,松手时选中与框相交的所有项(multiSelect 框选)。
41
+ */
42
+ export interface DataViewHandle {
43
+ /** 追加一项(动态数据源) */
44
+ addItem: (item: DataViewItem) => void;
45
+ /** 按下标删除一项(同时调整选中索引,前移其后的选中项) */
46
+ removeByIndex: (index: number) => void;
47
+ /** 读取当前全部项(含动态追加/删除后的结果) */
48
+ getItems: () => DataViewItem[];
49
+ /** 读取当前选中下标数组 */
50
+ getSelected: () => number[];
51
+ /** 程序化设置选中下标 */
52
+ setSelected: (indexes: number[]) => void;
53
+ /** 清空全部项 */
54
+ clear: () => void;
55
+ }
56
+
57
+ export const DataView = forwardRef<DataViewHandle, DataViewProps>(function DataView(
58
+ { data, selected, multiSelect = false, width, height, onSelectionChange, emptyText },
59
+ ref,
60
+ ) {
61
+ // 单源数组:默认渲染 = prop data(未调用句柄时与原 React.FC 行为一致);
62
+ // 句柄的 addItem/removeByIndex/clear 直接改写 items,作为「动态态」。
63
+ const [items, setItems] = useState<DataViewItem[]>(() => data);
64
+ const initial = selected == null ? [] : Array.isArray(selected) ? selected : [selected];
65
+ const [selectedIdx, setSelectedIdx] = useState<number[]>(initial);
66
+ const [overIdx, setOverIdx] = useState<number | null>(null);
67
+ const [drag, setDrag] = useState<{ x: number; y: number; w: number; h: number } | null>(null);
68
+ const containerRef = useRef<HTMLDivElement>(null);
69
+ const dragStart = useRef<{ x: number; y: number } | null>(null);
70
+ // 镜像最新值,供 getItems/getSelected 同步读取(句柄闭包在 commit 阶段执行,无 TDZ)
71
+ const itemsRef = useRef<DataViewItem[]>(items);
72
+ itemsRef.current = items;
73
+ const selectedRef = useRef<number[]>(selectedIdx);
74
+ selectedRef.current = selectedIdx;
75
+
76
+ const emit = useCallback(
77
+ (idx: number[]) => {
78
+ onSelectionChange?.(idx);
79
+ },
80
+ [onSelectionChange],
81
+ );
82
+
83
+ const selectSingle = useCallback(
84
+ (index: number, additive: boolean) => {
85
+ let next: number[];
86
+ if (additive && multiSelect) {
87
+ next = selectedIdx.includes(index)
88
+ ? selectedIdx.filter((i) => i !== index)
89
+ : [...selectedIdx, index];
90
+ } else {
91
+ next = [index];
92
+ }
93
+ setSelectedIdx(next);
94
+ emit(next);
95
+ },
96
+ [multiSelect, selectedIdx, emit],
97
+ );
98
+
99
+ // 命令式句柄:运行时动态增删数据源 / 读写选中态
100
+ useImperativeHandle(
101
+ ref,
102
+ () => ({
103
+ addItem: (item: DataViewItem) => setItems((prev) => [...prev, item]),
104
+ removeByIndex: (index: number) => {
105
+ if (index < 0) return;
106
+ setItems((prev) => prev.filter((_, i) => i !== index));
107
+ setSelectedIdx((prev) =>
108
+ prev.filter((s) => s !== index).map((s) => (s > index ? s - 1 : s)),
109
+ );
110
+ },
111
+ getItems: () => itemsRef.current,
112
+ getSelected: () => selectedRef.current,
113
+ setSelected: (indexes: number[]) => {
114
+ setSelectedIdx(indexes);
115
+ emit(indexes);
116
+ },
117
+ clear: () => {
118
+ setItems([]);
119
+ setSelectedIdx([]);
120
+ },
121
+ }),
122
+ [emit],
123
+ );
124
+
125
+ // 拖拽框选:mousedown 在容器空白(非 item)开始
126
+ const onMouseDown = useCallback(
127
+ (e: React.MouseEvent<HTMLDivElement>) => {
128
+ const target = e.target as HTMLElement;
129
+ if (target.classList.contains('x-view-item')) return; // 点在项上走 click 逻辑
130
+ if (!multiSelect) return;
131
+ const rect = containerRef.current!.getBoundingClientRect();
132
+ const x = e.clientX - rect.left;
133
+ const y = e.clientY - rect.top;
134
+ dragStart.current = { x, y };
135
+ setDrag({ x, y, w: 0, h: 0 });
136
+ },
137
+ [multiSelect],
138
+ );
139
+
140
+ const onMouseMove = useCallback(
141
+ (e: React.MouseEvent<HTMLDivElement>) => {
142
+ if (!dragStart.current) return;
143
+ const rect = containerRef.current!.getBoundingClientRect();
144
+ const x = e.clientX - rect.left;
145
+ const y = e.clientY - rect.top;
146
+ const sx = dragStart.current.x;
147
+ const sy = dragStart.current.y;
148
+ setDrag({
149
+ x: Math.min(sx, x),
150
+ y: Math.min(sy, y),
151
+ w: Math.abs(x - sx),
152
+ h: Math.abs(y - sy),
153
+ });
154
+ },
155
+ [],
156
+ );
157
+
158
+ const onMouseUp = useCallback(() => {
159
+ if (!dragStart.current || !drag) {
160
+ dragStart.current = null;
161
+ setDrag(null);
162
+ return;
163
+ }
164
+ // 选中与框相交的项
165
+ const container = containerRef.current!;
166
+ const cRect = container.getBoundingClientRect();
167
+ const selRect = {
168
+ left: drag.x,
169
+ top: drag.y,
170
+ right: drag.x + drag.w,
171
+ bottom: drag.y + drag.h,
172
+ };
173
+ const hit: number[] = [];
174
+ const items = container.querySelectorAll<HTMLElement>('.x-view-item');
175
+ items.forEach((el, i) => {
176
+ const r = el.getBoundingClientRect();
177
+ const rel = {
178
+ left: r.left - cRect.left,
179
+ top: r.top - cRect.top,
180
+ right: r.right - cRect.left,
181
+ bottom: r.bottom - cRect.top,
182
+ };
183
+ const intersects =
184
+ rel.right >= selRect.left && rel.left <= selRect.right &&
185
+ rel.bottom >= selRect.top && rel.top <= selRect.bottom;
186
+ if (intersects) hit.push(i);
187
+ });
188
+ const next = multiSelect ? Array.from(new Set([...selectedIdx, ...hit])) : hit;
189
+ setSelectedIdx(next);
190
+ emit(next);
191
+ dragStart.current = null;
192
+ setDrag(null);
193
+ }, [drag, multiSelect, selectedIdx, emit]);
194
+
195
+ return (
196
+ <div
197
+ ref={containerRef}
198
+ className="x-data-view"
199
+ style={{ position: 'relative', width, height, userSelect: 'none' }}
200
+ onMouseDown={onMouseDown}
201
+ onMouseMove={onMouseMove}
202
+ onMouseUp={onMouseUp}
203
+ >
204
+ {items.length === 0 && emptyText != null ? (
205
+ <div className="x-view-empty" style={{ padding: '10px 8px' }}>
206
+ {emptyText}
207
+ </div>
208
+ ) : (
209
+ items.map((item, i) => (
210
+ <div
211
+ key={i}
212
+ className={[
213
+ 'x-view-item',
214
+ selectedIdx.includes(i) ? 'x-list-selected' : '',
215
+ overIdx === i ? 'x-view-over' : '',
216
+ ]
217
+ .filter(Boolean)
218
+ .join(' ')}
219
+ onClick={(e) => selectSingle(i, (e as React.MouseEvent).ctrlKey)}
220
+ onMouseEnter={() => setOverIdx(i)}
221
+ onMouseLeave={() => setOverIdx((o) => (o === i ? null : o))}
222
+ >
223
+ {item.text}
224
+ </div>
225
+ )))}
226
+ {drag && (
227
+ <div
228
+ className="x-view-selector"
229
+ style={{
230
+ position: 'absolute',
231
+ left: drag.x,
232
+ top: drag.y,
233
+ width: drag.w,
234
+ height: drag.h,
235
+ }}
236
+ />
237
+ )}
238
+ </div>
239
+ );
240
+ });
241
+
242
+ (DataView as unknown as { displayName?: string }).displayName = 'DataView';
243
+
244
+ export default DataView;