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,167 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { ComboBox, type ComboItem } from './Form';
3
+ import { ensureDefaultTheme, resolveHref } from '../util/theme';
4
+
5
+ /* ThemeSwitcher —— 用保真复刻的 ComboBox 驱动 Ext JS 3.2.1 的原生主题切换。
6
+ *
7
+ * 机制(对齐 Ext.util.CSS.swapStyleSheet,src/util/CSS.js:68):
8
+ * Ext 的「主题」= 整篇替换 <head> 中主题样式表 <link> 指向的 xtheme-*.css。
9
+ * 本组件按固定 id 复用/新建一条 <link> 追加到 <head> 末尾;相等特异性下后加载者
10
+ * (即本主题表)优先,从而覆盖 base 结构表(ext-all.css)之上的默认蓝主题。
11
+ * 等价于删旧 link + 新建 link,不改动任何组件 DOM 与既有 CSS。
12
+ *
13
+ * 用法:
14
+ * import { ThemeSwitcher } from 'react-extjs/components/ThemeSwitcher';
15
+ * <ThemeSwitcher /> // 默认 4 套 Ext 主题 + localStorage 持久化
16
+ * <ThemeSwitcher themes={[...]} storageKey={false} /> // 自定义主题、关闭持久化
17
+ */
18
+ export type ThemeOption = ComboItem;
19
+
20
+ /* 本组件只按 Ext.util.CSS.swapStyleSheet 机制整篇替换主题 <link>:换文件即换色。
21
+ * 主题色完全由被加载的 xtheme-*.css(原版 ExtJS 主题表)本身决定,切换器不含任何样式逻辑;
22
+ * 各组件亦直接套用原版 ExtJS 类名(.x-panel/.x-panel-header/.x-splitbar-h 等)由 ext-all.css /
23
+ * xtheme 主题化,不新增任何自定义 CSS 变量或规则。 */
24
+ export const EXT_THEMES: ThemeOption[] = [
25
+ { value: 'xtheme-blue', text: 'Blue(默认)' },
26
+ { value: 'xtheme-gray', text: 'Gray' },
27
+ { value: 'xtheme-access', text: 'Accessibility' },
28
+ { value: 'xtheme-brand', text: 'Brand(品牌绿)' },
29
+ ];
30
+
31
+ export interface ThemeSwitcherProps {
32
+ /** 可选主题列表;默认 EXT_THEMES(Blue / Gray / Accessibility / Brand 品牌绿)。
33
+ * value 为主题 CSS 基名(如 'xtheme-gray',组件自动拼 cssBase + '.css');
34
+ * 也可直接传完整 href(含 '/' 或以 '.css' 结尾或 http(s):),用于自定义主题路径。 */
35
+ themes?: ThemeOption[];
36
+ /** 受控当前主题 value;不传则由组件内部管理(优先读 storageKey,否则 defaultValue / 首项)。 */
37
+ value?: string;
38
+ /** 非受控初始主题 value。 */
39
+ defaultValue?: string;
40
+ /** 切换回调:(value, option) => void。 */
41
+ onChange?: (value: string, option: ThemeOption) => void;
42
+ /** 左侧标签文字;传空字符串可隐藏标签。默认 '主题'。 */
43
+ label?: string;
44
+ /** ComboBox 宽度。默认 170。 */
45
+ width?: number;
46
+ /** localStorage 持久化 key;传 false 关闭持久化。默认 'react-extjs-theme'。 */
47
+ storageKey?: string | false;
48
+ /** 动态主题 <link> 的 id(同页多个切换器时用不同 id 隔离)。默认 'extjs-dynamic-theme'。 */
49
+ linkId?: string;
50
+ /** 主题 CSS 目录前缀(用于拼基名 href)。默认 '/extjs-assets/css/'。 */
51
+ cssBase?: string;
52
+ /** 是否允许输入过滤。默认 false(纯选择)。 */
53
+ editable?: boolean;
54
+ /** 内部 ComboBox 的 id(不传则自动生成,保证同页多实例不冲突)。 */
55
+ id?: string;
56
+ /** 根节点附加 class(根节点已含 x-theme-switcher)。 */
57
+ className?: string;
58
+ /** 根节点 inline style。 */
59
+ style?: React.CSSProperties;
60
+ }
61
+
62
+ /** 把主题 value 解析为完整 href(实现见 src/util/theme.ts 的 resolveHref,本组件直接复用)。 */
63
+
64
+ /** 等价 Ext.util.CSS.swapStyleSheet:按 id 复用/新建一条主题 <link>。 */
65
+ function applyTheme(value: string, cssBase: string, linkId: string) {
66
+ const href = resolveHref(value, cssBase);
67
+ let link = document.getElementById(linkId) as HTMLLinkElement | null;
68
+ if (!link) {
69
+ link = document.createElement('link');
70
+ link.id = linkId;
71
+ link.rel = 'stylesheet';
72
+ link.type = 'text/css';
73
+ document.head.appendChild(link);
74
+ }
75
+ if (link.getAttribute('href') !== href) {
76
+ link.href = href;
77
+ }
78
+ }
79
+
80
+ export const ThemeSwitcher: React.FC<ThemeSwitcherProps> = (props) => {
81
+ const {
82
+ themes = EXT_THEMES,
83
+ value,
84
+ defaultValue,
85
+ onChange,
86
+ label = '主题',
87
+ width = 170,
88
+ storageKey = 'react-extjs-theme',
89
+ linkId = 'extjs-dynamic-theme',
90
+ cssBase = '/extjs-assets/css/',
91
+ editable = false,
92
+ id,
93
+ className,
94
+ style,
95
+ } = props;
96
+
97
+ const controlled = value !== undefined;
98
+ const reactId = React.useId();
99
+ const comboId = id ?? `x-theme-switcher-${reactId}`;
100
+
101
+ const [internal, setInternal] = useState<string>(() => {
102
+ if (controlled) return value as string;
103
+ const stored = storageKey ? localStorage.getItem(storageKey) : null;
104
+ return stored || defaultValue || (themes[0] ? themes[0].value : '');
105
+ });
106
+ const current = controlled ? (value as string) : internal;
107
+
108
+ // 每次 current 变化即应用主题(含挂载初值与 storage 恢复)。
109
+ useEffect(() => {
110
+ // 保证默认主题 <link> 存在(无 ThemeSwitcher 时由入口 ensureDefaultTheme 建;
111
+ // 此处再幂等兜底,使仅引入本组件也必有默认主题)。
112
+ ensureDefaultTheme({ linkId, cssBase });
113
+ applyTheme(current, cssBase, linkId);
114
+ }, [current, cssBase, linkId]);
115
+
116
+ // 卸载清理:仅「非持久化」实例移除其主题 <link>,避免离开演示页后残留全局样式表。
117
+ // 持久化实例(含默认 storageKey)的 link 保留,作为站点级主题来源。
118
+ useEffect(() => {
119
+ return () => {
120
+ if (storageKey === false) {
121
+ const l = document.getElementById(linkId);
122
+ if (l && l.parentNode) l.parentNode.removeChild(l);
123
+ }
124
+ };
125
+ }, [linkId, storageKey]);
126
+
127
+ const handleChange = (v: string) => {
128
+ if (!controlled) setInternal(v);
129
+ if (storageKey) {
130
+ try {
131
+ localStorage.setItem(storageKey, v);
132
+ } catch {
133
+ /* localStorage 不可用时忽略,仅本次生效 */
134
+ }
135
+ }
136
+ const opt = themes.find((t) => t.value === v);
137
+ onChange?.(v, opt as ThemeOption);
138
+ };
139
+
140
+ return (
141
+ <div
142
+ className={className ? `x-theme-switcher ${className}` : 'x-theme-switcher'}
143
+ style={{ display: 'inline-flex', alignItems: 'center', gap: 8, ...style }}
144
+ >
145
+ {label ? (
146
+ <label
147
+ className="x-theme-switcher-label"
148
+ htmlFor={comboId}
149
+ style={{ whiteSpace: 'nowrap' }}
150
+ >
151
+ {label}
152
+ </label>
153
+ ) : null}
154
+ <ComboBox
155
+ id={comboId}
156
+ name="theme"
157
+ data={themes}
158
+ value={current}
159
+ onChange={handleChange}
160
+ width={width}
161
+ editable={editable}
162
+ />
163
+ </div>
164
+ );
165
+ };
166
+
167
+ export default ThemeSwitcher;
@@ -0,0 +1,226 @@
1
+ import React, {
2
+ forwardRef,
3
+ useEffect,
4
+ useRef,
5
+ useState,
6
+ useCallback,
7
+ useImperativeHandle,
8
+ } from 'react';
9
+
10
+ export type ToolTipAnchor = 'top' | 'bottom' | 'left' | 'right';
11
+
12
+ export interface ToolTipProps {
13
+ /** 标题(渲染 .x-tip-header > .x-tip-header-text) */
14
+ title?: string;
15
+ /** body 内容(HTML 字符串,渲染进 .x-tip-body) */
16
+ html?: string;
17
+ /** body 内容(React 节点,与 html 二选一) */
18
+ children?: React.ReactNode;
19
+ /** tip 宽度(px)。未给时由内容撑开(对齐 Ext 默认 autoWidth) */
20
+ width?: number;
21
+ /** 是否渲染关闭按钮(.x-tip-close) */
22
+ closable?: boolean;
23
+ /** 箭头方向(在对应边中央绘制 .x-tip-anchor) */
24
+ anchor?: ToolTipAnchor;
25
+ /** 阴影(Ext 默认 'sides',此处仅占位,保真时关闭以免半透明阴影干扰像素对比) */
26
+ shadow?: boolean;
27
+ /** 相对容器的 left(对齐 Ext showAt 的 x) */
28
+ x?: number;
29
+ /** 相对容器的 top(对齐 Ext showAt 的 y) */
30
+ y?: number;
31
+ /** hover 目标元素 CSS 选择器;提供后默认隐藏,鼠标进入显示、移出隐藏 */
32
+ target?: string;
33
+ /** 显示延迟(ms)。Ext 默认 500;测试可设 0 加速 */
34
+ showDelay?: number;
35
+ /** 隐藏延迟(ms)。Ext 默认 200 */
36
+ hideDelay?: number;
37
+ /** 受控初始可见(保真固定显示用) */
38
+ visible?: boolean;
39
+ onClose?: () => void;
40
+ /** 保真裁剪/测试定位用根节点 testid */
41
+ testId?: string;
42
+ /** 附加根 class */
43
+ className?: string;
44
+ /** 附加根 style(覆盖/补充定位) */
45
+ style?: React.CSSProperties;
46
+ }
47
+
48
+ /** 命令式句柄:运行时控制显示/隐藏与内容 */
49
+ export interface ToolTipHandle {
50
+ /** 显示 tip */
51
+ show: () => void;
52
+ /** 隐藏 tip */
53
+ hide: () => void;
54
+ /** 当前是否可见 */
55
+ isVisible: () => boolean;
56
+ /** 运行时改写标题 */
57
+ setTitle: (title: string) => void;
58
+ /** 运行时改写 body(HTML 字符串) */
59
+ setBody: (html: string) => void;
60
+ }
61
+
62
+ /**
63
+ * 保真映射(来源 src/widgets/tips/Tip.js + ext-all.css .x-tip 规则):
64
+ * - Tip 继承 Panel,baseCls='x-tip',frame:true → 复用 9 宫格圆角结构,
65
+ * 但 class 前缀为 x-tip-(非 x-panel-):tl/tr/tc/ml/mr/mc/bl/br/bc。
66
+ * - header 落于 .x-tip-tc 内(class x-tip-header > span.x-tip-header-text);
67
+ * 可选 closable 渲染 .x-tip-close(CSS 背景 qtip/close.gif)。
68
+ * - body 落于 .x-tip-mc 内(class x-tip-body)。
69
+ * - anchor 箭头:.x-tip-anchor(绝对定位,width 9px)+ x-tip-anchor-{top|bottom|left|right}。
70
+ * - 根 .x-tip position:absolute(CSS),inline left/top 由 showAt 注入。
71
+ * React 仅重写行为与定位,DOM 结构与 class 名与 Ext JS 3.2.1 完全一致。
72
+ */
73
+ export const ToolTip = forwardRef<ToolTipHandle, ToolTipProps>(function ToolTip(
74
+ {
75
+ title,
76
+ html,
77
+ children,
78
+ width,
79
+ closable = false,
80
+ anchor,
81
+ x = 0,
82
+ y = 0,
83
+ target,
84
+ showDelay = 500,
85
+ hideDelay = 200,
86
+ visible,
87
+ onClose,
88
+ testId,
89
+ className,
90
+ style,
91
+ },
92
+ ref,
93
+ ) {
94
+ // 双轨:prop 作为初始值,句柄可运行时改写
95
+ const [dynTitle, setDynTitle] = useState(title);
96
+ const [dynHtml, setDynHtml] = useState(html);
97
+ const [dynVisible, setDynVisible] = useState(visible ?? true);
98
+ // visible / title / html prop 受控:变化时同步到双轨 state
99
+ useEffect(() => {
100
+ setDynVisible(visible ?? true);
101
+ }, [visible]);
102
+ useEffect(() => {
103
+ setDynTitle(title);
104
+ }, [title]);
105
+ useEffect(() => {
106
+ setDynHtml(html);
107
+ }, [html]);
108
+
109
+ const shown = dynVisible;
110
+ const showTimer = useRef<number | undefined>(undefined);
111
+ const hideTimer = useRef<number | undefined>(undefined);
112
+
113
+ const show = useCallback(() => setDynVisible(true), []);
114
+ const hide = useCallback(() => setDynVisible(false), []);
115
+ const isVisible = useCallback(() => dynVisible, [dynVisible]);
116
+ const setTitle = useCallback((t: string) => setDynTitle(t), []);
117
+ const setBody = useCallback((h: string) => setDynHtml(h), []);
118
+
119
+ useImperativeHandle(
120
+ ref,
121
+ () => ({ show, hide, isVisible, setTitle, setBody }),
122
+ [show, hide, isVisible, setTitle, setBody],
123
+ );
124
+
125
+ const showHeader = !!dynTitle || closable;
126
+
127
+ // hover 触发显示/隐藏(target 存在时启用)
128
+ useEffect(() => {
129
+ if (!target) return;
130
+ setDynVisible(false);
131
+ const el = document.querySelector(target);
132
+ if (!el) return;
133
+
134
+ const onOver = () => {
135
+ window.clearTimeout(hideTimer.current);
136
+ showTimer.current = window.setTimeout(() => setDynVisible(true), showDelay);
137
+ };
138
+ const onOut = () => {
139
+ window.clearTimeout(showTimer.current);
140
+ hideTimer.current = window.setTimeout(() => setDynVisible(false), hideDelay);
141
+ };
142
+
143
+ el.addEventListener('mouseover', onOver);
144
+ el.addEventListener('mouseout', onOut);
145
+ return () => {
146
+ el.removeEventListener('mouseover', onOver);
147
+ el.removeEventListener('mouseout', onOut);
148
+ window.clearTimeout(showTimer.current);
149
+ window.clearTimeout(hideTimer.current);
150
+ };
151
+ // 仅绑定一次(target/延迟变化极少,演示固定)
152
+ // eslint-disable-next-line react-hooks/exhaustive-deps
153
+ }, [target]);
154
+
155
+ const rootStyle: React.CSSProperties = {
156
+ position: 'absolute',
157
+ left: x,
158
+ top: y,
159
+ visibility: shown ? 'visible' : 'hidden',
160
+ ...(width !== undefined ? { width } : {}),
161
+ };
162
+
163
+ // anchor 默认定位(与 Ext.syncAnchor 近似:贴边中央偏左位置由 CSS sprite 决定视觉,
164
+ // 这里仅给一个合理的几何落点,保证截图时箭头可见且不溢出 tip bounding box 太多)
165
+ const anchorStyle: React.CSSProperties =
166
+ anchor === 'top'
167
+ ? { position: 'absolute', top: -8, left: 12 }
168
+ : anchor === 'bottom'
169
+ ? { position: 'absolute', bottom: -8, left: 12 }
170
+ : anchor === 'left'
171
+ ? { position: 'absolute', left: -9, top: 8 }
172
+ : anchor === 'right'
173
+ ? { position: 'absolute', right: -10, top: 8 }
174
+ : {};
175
+
176
+ return (
177
+ <div
178
+ className={['x-tip', shown ? '' : 'x-hidden', className].filter(Boolean).join(' ')}
179
+ data-testid={testId}
180
+ style={{ ...rootStyle, ...style }}
181
+ >
182
+ <div className="x-tip-tl">
183
+ <div className="x-tip-tr">
184
+ <div className="x-tip-tc">
185
+ {showHeader && (
186
+ <div className="x-tip-header" unselectable="on">
187
+ {dynTitle && <span className="x-tip-header-text">{dynTitle}</span>}
188
+ {closable && (
189
+ <div
190
+ className="x-tip-close"
191
+ unselectable="on"
192
+ onClick={onClose}
193
+ style={{ display: 'block' }}
194
+ >
195
+ &#160;
196
+ </div>
197
+ )}
198
+ </div>
199
+ )}
200
+ </div>
201
+ </div>
202
+ </div>
203
+ <div className="x-tip-bwrap">
204
+ <div className="x-tip-ml">
205
+ <div className="x-tip-mr">
206
+ <div className="x-tip-mc">
207
+ {dynHtml !== undefined ? (
208
+ <div className="x-tip-body" dangerouslySetInnerHTML={{ __html: dynHtml }} />
209
+ ) : (
210
+ <div className="x-tip-body">{children}</div>
211
+ )}
212
+ </div>
213
+ </div>
214
+ </div>
215
+ <div className="x-tip-bl" style={{ height: 3 }}>
216
+ <div className="x-tip-br">
217
+ <div className="x-tip-bc" style={{ height: 3, fontSize: 0, lineHeight: 0 }} />
218
+ </div>
219
+ </div>
220
+ </div>
221
+ {anchor && <div className={`x-tip-anchor x-tip-anchor-${anchor}`} style={anchorStyle} />}
222
+ </div>
223
+ );
224
+ });
225
+
226
+ export default ToolTip;