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,391 @@
1
+ import React, { forwardRef, useImperativeHandle } from 'react';
2
+ import { Toolbar, type ToolbarItem, type ToolbarButtonConfig } from './Toolbar';
3
+ import { ColorMenu } from './ColorMenu';
4
+
5
+ /**
6
+ * HtmlEditor —— 保真复刻 Ext.form.HtmlEditor(src/widgets/form/HtmlEditor.js)。
7
+ *
8
+ * 保真契约(DOM/class 严格对齐源码):
9
+ * - onRender(423-448 行):textarea(el)被 .x-html-editor-wrap 包一层,并注入 cn:.x-html-editor-tb 作为首个子节点;
10
+ * createToolbar 把 Toolbar 渲染进 wrap.dom.firstChild(即 .x-html-editor-tb);
11
+ * createIFrame 末尾 append 一个 iframe(frameBorder=0、overflow=auto)。
12
+ * - 故 DOM 顺序为:.x-html-editor-tb(Toolbar) → textarea.x-form-textarea.x-form-field.x-hidden → iframe。
13
+ * - wrap:content-box + border:1px solid(ext-all.css 第 4458 行)。
14
+ * - 工具栏项(createToolbar,201-365 行):
15
+ * bold/italic/underline(toggle)、-、increase/decreasefontsize(toggle)、-、
16
+ * forecolor/backcolor(带 ColorMenu → 渲染下拉箭头,宽 32px)、-、
17
+ * justifyleft/center/right(toggle)、-、createlink(toggle)、-、
18
+ * insertorderedlist/insertunorderedlist(toggle)、-、sourceedit(toggle)。
19
+ * 共 14 按钮 + 7 分隔符;每个图标按钮 cls='x-btn-icon'、iconCls='x-edit-*'(tb-sprite.gif)。
20
+ * - 图标由 .x-html-editor-tb .x-edit-*(ext-all.css 第 4466-4533 行)定位到 tb-sprite.gif 对应 16px 位置。
21
+ * - 尺寸(实测契约):容器 configWidth×configHeight 时,wrap 渲染为 520×204(border 各 1px);
22
+ * 工具栏 27px,iframe 内联 518×171(内联元素底部 4px 间隙 → 内容 202 → 含边框 204)。
23
+ * React 侧令 wrap 宽=configWidth、border-box、不显式设高(由内容自然撑开,精确等于 golden);
24
+ * iframe 显式 width=configWidth-2、height=configHeight-27-2。
25
+ *
26
+ * 交互(行为契约,对应 Ext HtmlEditor + Toolbar + Button):
27
+ * - 工具栏图标按钮:toggle 类点击切换 x-btn-pressed;forecolor/backcolor 带下拉箭头(ColorMenu 代理)。
28
+ * - 源码编辑(sourceedit)按钮:点击在 design 模式(iframe,designMode 编辑区)与 source 模式
29
+ * (textarea 显示 HTML 源码)间切换;切换为 source 时其余工具栏按钮禁用(对齐 Ext toggleSourceEdit 的 disableItems)。
30
+ * 默认 design 模式,与 golden 静态态一致(fidelity:htmleditor 零回归)。
31
+ * - 实际编辑:iframe 开启 designMode,格式按钮(粗体/斜体/下划线/对齐/列表/链接)点击对编辑区执行
32
+ * document.execCommand,并据 queryCommandState 同步按钮 pressed 态(对齐 Ext onEditorEvent + btn.updateState)。
33
+ * - forecolor/backcolor 按钮:点击展开 ColorMenu(复用 ColorMenu + ColorPalette),选中颜色后
34
+ * 对编辑区执行 execCommand('forecolor'/'backcolor', color)(对齐 Ext 这两个按钮挂载 ColorMenu,
35
+ * 选中色调色板回调触发 document.execCommand 应用前景/背景色)。
36
+ */
37
+
38
+ /** iconCls → execCommand 命令映射(对齐 Ext HtmlEditor 工具栏命令)。 */
39
+ const FORMAT_CMD: Record<string, string> = {
40
+ 'x-edit-bold': 'bold',
41
+ 'x-edit-italic': 'italic',
42
+ 'x-edit-underline': 'underline',
43
+ 'x-edit-justifyleft': 'justifyleft',
44
+ 'x-edit-justifycenter': 'justifycenter',
45
+ 'x-edit-justifyright': 'justifyright',
46
+ 'x-edit-insertorderedlist': 'insertorderedlist',
47
+ 'x-edit-insertunorderedlist': 'insertunorderedlist',
48
+ 'x-edit-createlink': 'createlink',
49
+ };
50
+
51
+ /** 工具栏图标项:cls='x-btn-icon'、iconCls='x-edit-*';toggle 类 enableToggle,颜色类带箭头(menu=true)。 */
52
+ const EDITOR_TOOLBAR: ToolbarItem[] = [
53
+ // 字体下拉(enableFont 默认 true):Ext 用 Toolbar.Item 包裹 <select class="x-font-select">,
54
+ // 内含 5 个字体 option(defaultFont='tahoma' 选中)。
55
+ (() => (
56
+ <select className="x-font-select" defaultValue="tahoma">
57
+ {['Arial', 'Courier New', 'Tahoma', 'Times New Roman', 'Verdana'].map((f) => (
58
+ <option key={f} value={f.toLowerCase()} style={{ fontFamily: f }}>
59
+ {f}
60
+ </option>
61
+ ))}
62
+ </select>
63
+ ))(),
64
+ '-',
65
+ { xtype: 'button', cls: 'x-btn-icon', iconCls: 'x-edit-bold', enableToggle: true },
66
+ { xtype: 'button', cls: 'x-btn-icon', iconCls: 'x-edit-italic', enableToggle: true },
67
+ { xtype: 'button', cls: 'x-btn-icon', iconCls: 'x-edit-underline', enableToggle: true },
68
+ '-',
69
+ { xtype: 'button', cls: 'x-btn-icon', iconCls: 'x-edit-increasefontsize' },
70
+ { xtype: 'button', cls: 'x-btn-icon', iconCls: 'x-edit-decreasefontsize' },
71
+ '-',
72
+ { xtype: 'button', cls: 'x-btn-icon', iconCls: 'x-edit-forecolor', menu: true },
73
+ { xtype: 'button', cls: 'x-btn-icon', iconCls: 'x-edit-backcolor', menu: true },
74
+ '-',
75
+ { xtype: 'button', cls: 'x-btn-icon', iconCls: 'x-edit-justifyleft', enableToggle: true },
76
+ { xtype: 'button', cls: 'x-btn-icon', iconCls: 'x-edit-justifycenter', enableToggle: true },
77
+ { xtype: 'button', cls: 'x-btn-icon', iconCls: 'x-edit-justifyright', enableToggle: true },
78
+ '-',
79
+ { xtype: 'button', cls: 'x-btn-icon', iconCls: 'x-edit-createlink', enableToggle: true },
80
+ '-',
81
+ { xtype: 'button', cls: 'x-btn-icon', iconCls: 'x-edit-insertorderedlist', enableToggle: true },
82
+ { xtype: 'button', cls: 'x-btn-icon', iconCls: 'x-edit-insertunorderedlist', enableToggle: true },
83
+ '-',
84
+ { xtype: 'button', cls: 'x-btn-icon', iconCls: 'x-edit-sourceedit', enableToggle: true },
85
+ ];
86
+
87
+ export interface HtmlEditorProps {
88
+ /** 编辑器宽(含 1px 边框),等同 Ext width。默认 520 */
89
+ width?: number;
90
+ /** 编辑器高(含 1px 边框),等同 Ext height。默认 200 */
91
+ height?: number;
92
+ /** 初始 HTML 内容(golden 用 textarea 承载,React 仅作隐藏回显,编辑区由 iframe 呈现) */
93
+ value?: string;
94
+ /** 是否禁用(设计模式关闭 + 根加 x-item-disabled) */
95
+ disabled?: boolean;
96
+ /** 数据测试 id,挂在根 div 上 */
97
+ testId?: string;
98
+ className?: string;
99
+ }
100
+
101
+ /** HtmlEditor 句柄:对齐 Ext.form.HtmlEditor 公共 API 的运行时子集。 */
102
+ export interface HtmlEditorHandle {
103
+ /** 运行时写入 HTML 内容(重写 iframe 编辑区) */
104
+ setValue: (html: string) => void;
105
+ /** 读取当前编辑区 HTML */
106
+ getValue: () => string;
107
+ /** 设置禁用态 */
108
+ setDisabled: (b: boolean) => void;
109
+ /** 是否禁用 */
110
+ isDisabled: () => boolean;
111
+ /** 聚焦编辑区 */
112
+ focus: () => void;
113
+ /** 切换 design/source 源码模式 */
114
+ toggleSource: () => void;
115
+ /** 当前是否源码模式 */
116
+ isSource: () => boolean;
117
+ /** 运行时对编辑区执行 execCommand */
118
+ execCommand: (cmd: string, value?: string) => void;
119
+ }
120
+
121
+ export const HtmlEditor = forwardRef<HtmlEditorHandle, HtmlEditorProps>(function HtmlEditor(
122
+ { width = 520, height = 200, value = '', disabled, testId, className },
123
+ ref,
124
+ ) {
125
+ // 双轨:value/disabled 由 prop 初始化,句柄运行时改写,useEffect 同步 prop 变化。
126
+ const [dynValue, setDynValue] = React.useState(value ?? '');
127
+ const [dynDisabled, setDynDisabled] = React.useState(disabled ?? false);
128
+ React.useEffect(() => {
129
+ if (value !== undefined) setDynValue(value);
130
+ }, [value]);
131
+ React.useEffect(() => {
132
+ if (disabled !== undefined) setDynDisabled(disabled);
133
+ }, [disabled]);
134
+
135
+ // 工具栏实测高度 27px;iframe 高 = 配置高 - 工具栏 - 上下边框(各1px);宽 = 配置宽 - 左右边框。
136
+ const TOOLBAR_H = 27;
137
+ const iframeW = width - 2;
138
+ const iframeH = height - TOOLBAR_H - 2;
139
+ // 源码编辑态:false=design(iframe 编辑区可见),true=source(textarea 显示 HTML 源码)。
140
+ // 默认 design,与 golden 静态态一致。
141
+ const [sourceMode, setSourceMode] = React.useState(false);
142
+ // 格式按钮 pressed 态(对齐 Ext btn.updateState:依据编辑区 queryCommandState 同步)。
143
+ const [formatStates, setFormatStates] = React.useState<Record<string, boolean>>({});
144
+ const iframeRef = React.useRef<HTMLIFrameElement>(null);
145
+ // 编辑区选区快照(对齐 Ext 工具栏命令前保持选区:点击按钮 mousedown 会令 iframe 失焦、选区丢失,
146
+ // 故在 selectionchange 时缓存 Range,执行命令前恢复)。
147
+ const savedRange = React.useRef<Range | null>(null);
148
+ // forecolor/backcolor 调色板浮层(对齐 Ext 这两个按钮挂载的 ColorMenu):null 表示未展开。
149
+ const [colorMenu, setColorMenu] = React.useState<{ cmd: 'forecolor' | 'backcolor'; anchor: HTMLElement } | null>(null);
150
+ const colorAnchorRef = React.useRef<HTMLElement | null>(null);
151
+
152
+ // 初始化 iframe:写入初始 HTML 并开启 designMode(对齐 Ext createIFrame + syncValue)。
153
+ React.useEffect(() => {
154
+ const doc = iframeRef.current?.contentDocument;
155
+ if (!doc) return;
156
+ try {
157
+ doc.open();
158
+ doc.write(`<html><head></head><body>${dynValue}</body></html>`);
159
+ doc.close();
160
+ doc.designMode = dynDisabled ? 'off' : 'on';
161
+ const refresh = () => {
162
+ const next: Record<string, boolean> = {};
163
+ for (const cmd of Object.values(FORMAT_CMD)) {
164
+ try {
165
+ next[cmd] = doc.queryCommandState(cmd);
166
+ } catch {
167
+ next[cmd] = false;
168
+ }
169
+ }
170
+ setFormatStates(next);
171
+ // 缓存当前选区(仅限编辑区 body 内),供执行命令前恢复
172
+ try {
173
+ const sel = doc.defaultView?.getSelection?.();
174
+ if (sel && sel.rangeCount > 0) {
175
+ const r = sel.getRangeAt(0);
176
+ if (doc.body.contains(r.commonAncestorContainer)) savedRange.current = r.cloneRange();
177
+ }
178
+ } catch {
179
+ /* 选区快照失败可忽略 */
180
+ }
181
+ };
182
+ doc.addEventListener('keyup', refresh);
183
+ doc.addEventListener('mouseup', refresh);
184
+ doc.addEventListener('selectionchange', refresh);
185
+ } catch {
186
+ /* designMode 初始化失败(极旧环境)时静默降级,不影响静态保真 */
187
+ }
188
+ }, [dynValue, dynDisabled]);
189
+
190
+ // 对编辑区执行 execCommand(对齐 Ext 工具栏命令):focus 编辑区后运行命令,并刷新按钮态。
191
+ // value 可选(如 fontname 传字体名、createlink 传 URL)。
192
+ const runCommand = React.useCallback((cmd: string, value?: string) => {
193
+ const doc = iframeRef.current?.contentDocument;
194
+ if (!doc) return;
195
+ try {
196
+ iframeRef.current?.focus();
197
+ const sel = doc.defaultView?.getSelection?.();
198
+ if (sel) {
199
+ sel.removeAllRanges();
200
+ if (savedRange.current) sel.addRange(savedRange.current);
201
+ else {
202
+ const r = doc.createRange();
203
+ r.selectNodeContents(doc.body);
204
+ sel.addRange(r);
205
+ }
206
+ }
207
+ const cmdValue = value !== undefined ? value : cmd === 'createlink' ? 'http://example.com' : undefined;
208
+ doc.execCommand(cmd, false, cmdValue);
209
+ const next: Record<string, boolean> = {};
210
+ for (const c of Object.values(FORMAT_CMD)) {
211
+ try {
212
+ next[c] = doc.queryCommandState(c);
213
+ } catch {
214
+ next[c] = false;
215
+ }
216
+ }
217
+ setFormatStates(next);
218
+ } catch {
219
+ /* 命令不可用时静默降级 */
220
+ }
221
+ }, []);
222
+
223
+ // 应用颜色命令(forecolor/backcolor):对齐 Ext 调色板选中后 document.execCommand(cmd, false, color)。
224
+ const runColorCommand = React.useCallback((cmd: 'forecolor' | 'backcolor', color: string) => {
225
+ const doc = iframeRef.current?.contentDocument;
226
+ if (!doc) return;
227
+ try {
228
+ iframeRef.current?.focus();
229
+ const sel = doc.defaultView?.getSelection?.();
230
+ if (sel) {
231
+ sel.removeAllRanges();
232
+ if (savedRange.current) sel.addRange(savedRange.current);
233
+ else {
234
+ const r = doc.createRange();
235
+ r.selectNodeContents(doc.body);
236
+ sel.addRange(r);
237
+ }
238
+ }
239
+ const hex = color.startsWith('#') ? color : `#${color}`;
240
+ doc.execCommand(cmd, false, hex);
241
+ } catch {
242
+ /* 命令不可用时静默降级 */
243
+ }
244
+ }, []);
245
+
246
+ // 句柄:运行时 setValue/getValue/setDisabled/聚焦/切换源码/执行命令。
247
+ // 须置于 runCommand 定义之后,避免依赖数组访问时触发 TDZ。
248
+ useImperativeHandle(
249
+ ref,
250
+ (): HtmlEditorHandle => ({
251
+ setValue: (html) => setDynValue(html),
252
+ getValue: () => {
253
+ const doc = iframeRef.current?.contentDocument;
254
+ return doc ? doc.body.innerHTML : '';
255
+ },
256
+ setDisabled: (b) => setDynDisabled(b),
257
+ isDisabled: () => dynDisabled,
258
+ focus: () => iframeRef.current?.focus(),
259
+ toggleSource: () => setSourceMode((v) => !v),
260
+ isSource: () => sourceMode,
261
+ execCommand: (cmd, val) => runCommand(cmd, val),
262
+ }),
263
+ [dynDisabled, sourceMode, runCommand],
264
+ );
265
+
266
+ // 字号增减(对齐 Ext onFontIncrease/onFontDecrease → adjustFont):维护 1..7 的绝对字号档,
267
+ // 每次点击在选区上 execCommand('fontsize', N) 放大/缩小一档。Chromium 不支持原生的
268
+ // increasefontsize/decreasefontsize 命令,故用绝对字号方式达到等价行为。
269
+ const [fontSize, setFontSize] = React.useState(3);
270
+ const runFontSize = React.useCallback(
271
+ (delta: number) => {
272
+ const next = Math.min(7, Math.max(1, fontSize + delta));
273
+ setFontSize(next);
274
+ runCommand('fontsize', String(next));
275
+ },
276
+ [fontSize, runCommand],
277
+ );
278
+
279
+ // 插入链接(createlink):对齐 Ext 默认 createLink 行为——点击按钮弹出 prompt 让用户输入 URL,
280
+ // 再对当前选区 execCommand('createlink', url)。空输入则不操作。
281
+ const onLink = React.useCallback(() => {
282
+ const url = window.prompt('链接地址:', 'http://');
283
+ if (url) runCommand('createlink', url);
284
+ }, [runCommand]);
285
+
286
+ // 工具栏项:源码编辑按钮带 toggle 态与切换 handler;源码模式下其余按钮禁用
287
+ // (对齐 Ext toggleSourceEdit 的 disableItems)。格式按钮注入 execCommand handler 与 pressed 态。
288
+ const items: ToolbarItem[] = React.useMemo(() => {
289
+ return EDITOR_TOOLBAR.map((it) => {
290
+ // 字体下拉(x-font-select):选中后执行 fontname 命令;源码模式下禁用
291
+ if (React.isValidElement(it) && (it.props as { className?: string }).className === 'x-font-select') {
292
+ return React.cloneElement(it as React.ReactElement<React.SelectHTMLAttributes<HTMLSelectElement>>, {
293
+ disabled: sourceMode,
294
+ onChange: (e: React.ChangeEvent<HTMLSelectElement>) => runCommand('fontname', e.target.value),
295
+ });
296
+ }
297
+ if (React.isValidElement(it) || typeof it !== 'object') return it;
298
+ const o = it as ToolbarButtonConfig;
299
+ if (o.iconCls === 'x-edit-sourceedit') {
300
+ return { ...o, pressed: sourceMode, handler: () => setSourceMode((v) => !v) };
301
+ }
302
+ // forecolor/backcolor:点击展开 ColorMenu(调色板),选中颜色后应用命令
303
+ if (o.iconCls === 'x-edit-forecolor' || o.iconCls === 'x-edit-backcolor') {
304
+ const cmd = o.iconCls === 'x-edit-forecolor' ? 'forecolor' : 'backcolor';
305
+ return {
306
+ ...o,
307
+ onClick: (e: React.MouseEvent<HTMLButtonElement>) => {
308
+ const anchor = e.currentTarget as HTMLElement;
309
+ colorAnchorRef.current = anchor;
310
+ setColorMenu((prev) => (prev?.cmd === cmd ? null : { cmd, anchor }));
311
+ },
312
+ };
313
+ }
314
+ if (sourceMode) return { ...o, disabled: true };
315
+ // 字号增减:点击放大/缩小一档(非 toggle,走 runFontSize 绝对字号)
316
+ if (o.iconCls === 'x-edit-increasefontsize') {
317
+ return { ...o, enableToggle: false, handler: () => runFontSize(1) };
318
+ }
319
+ if (o.iconCls === 'x-edit-decreasefontsize') {
320
+ return { ...o, enableToggle: false, handler: () => runFontSize(-1) };
321
+ }
322
+ // 插入链接:点击弹出 prompt 取 URL(对齐 Ext createLink)
323
+ if (o.iconCls === 'x-edit-createlink') {
324
+ return { ...o, handler: onLink };
325
+ }
326
+ const cmd = o.iconCls ? FORMAT_CMD[o.iconCls] : undefined;
327
+ if (cmd) {
328
+ return { ...o, pressed: !!formatStates[cmd], handler: () => runCommand(cmd) };
329
+ }
330
+ return it;
331
+ });
332
+ }, [sourceMode, formatStates, runCommand, runFontSize, onLink]);
333
+
334
+ // 源码模式下 textarea 显示 HTML 源码;design 模式为空且隐藏。
335
+ const srcValue = sourceMode ? dynValue : '';
336
+
337
+ return (
338
+ <div
339
+ className={['x-form-field-wrap', 'x-html-editor-wrap', className, dynDisabled ? 'x-item-disabled' : '']
340
+ .filter(Boolean)
341
+ .join(' ')}
342
+ data-testid={testId}
343
+ style={{ width, height, boxSizing: 'border-box', position: 'relative' }}
344
+ >
345
+ <div className="x-html-editor-tb">
346
+ <Toolbar items={items} />
347
+ </div>
348
+ <textarea
349
+ className={`x-form-textarea x-form-field${sourceMode ? '' : ' x-hidden'}`}
350
+ value={srcValue}
351
+ readOnly={!sourceMode}
352
+ style={
353
+ sourceMode
354
+ ? {
355
+ width: iframeW,
356
+ height: iframeH,
357
+ position: 'absolute',
358
+ top: TOOLBAR_H,
359
+ left: 1,
360
+ boxSizing: 'border-box',
361
+ overflow: 'auto',
362
+ resize: 'none',
363
+ }
364
+ : { position: 'absolute', top: -10000, left: -10000, visibility: 'hidden' }
365
+ }
366
+ />
367
+ <iframe
368
+ ref={iframeRef}
369
+ title="htmleditor"
370
+ frameBorder={0}
371
+ style={{ width: iframeW, height: iframeH, overflow: 'auto', display: sourceMode ? 'none' : undefined }}
372
+ />
373
+ {colorMenu && (
374
+ <ColorMenu
375
+ open={!!colorMenu}
376
+ anchorRef={colorAnchorRef}
377
+ onSelect={(c) => {
378
+ const hex = c.startsWith('#') ? c : `#${c}`;
379
+ runColorCommand(colorMenu.cmd, hex);
380
+ setColorMenu(null);
381
+ }}
382
+ onClose={() => setColorMenu(null)}
383
+ />
384
+ )}
385
+ </div>
386
+ );
387
+ });
388
+
389
+ (HtmlEditor as React.FC).displayName = 'HtmlEditor';
390
+
391
+ export default HtmlEditor;