@tntd/monaco-editor 1.0.1 → 1.0.3

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.
@@ -0,0 +1,503 @@
1
+ import { findNearestLeftParenthesisIndex, getParentItem, getChildList } from '../../utils';
2
+ import React from 'react';
3
+ import ReactDOM from 'react-dom';
4
+ import Cascader from './Cascader';
5
+
6
+ export class CascaderPlugin {
7
+ constructor(options = {}) {
8
+ this.name = 'cascader';
9
+ this.options = options;
10
+ this.state = {
11
+ visible: false,
12
+ position: { left: 0, top: 0 },
13
+ items: [],
14
+ type: null
15
+ };
16
+ this.disposables = new Set();
17
+ this.baseEditor = null;
18
+ this.currentEditor = null;
19
+ this.monaco = null;
20
+
21
+ // 从 options 中获取必要的数据
22
+ const { fieldList = [], methodList = [], fieldRegExp, methodRegExp, regExpState } = options;
23
+ this.fieldList = fieldList;
24
+ this.methodList = methodList;
25
+ this.fieldRegExp = fieldRegExp;
26
+ this.methodRegExp = methodRegExp;
27
+ this.regExpState = regExpState;
28
+
29
+ this.parentContainer = options.parentContainer;
30
+ document.addEventListener('click', this.hideCascaderByDocumentClick.bind(this));
31
+ }
32
+
33
+ apply(baseEditor) {
34
+ this.baseEditor = baseEditor;
35
+ this.monaco = baseEditor.monaco;
36
+
37
+ // 处理 DiffEditor 的情况
38
+ if (this.options.isDiff) {
39
+ // 监听原始编辑器的光标变化
40
+ const originalEditor = baseEditor.editor.getOriginalEditor();
41
+ this.disposables.add(
42
+ originalEditor.onDidChangeCursorPosition((e) => {
43
+ this.currentEditor = originalEditor;
44
+ if (e?.source !== 'api') {
45
+ this.handleCursorChange(e);
46
+ }
47
+ })
48
+ );
49
+
50
+ // 监听修改后编辑器的光标变化
51
+ const modifiedEditor = baseEditor.editor.getModifiedEditor();
52
+ this.disposables.add(
53
+ modifiedEditor.onDidChangeCursorPosition((e) => {
54
+ this.currentEditor = modifiedEditor;
55
+ if (e?.source !== 'api') {
56
+ this.handleCursorChange(e);
57
+ }
58
+ })
59
+ );
60
+
61
+ // 初始设置为 modified editor
62
+ this.currentEditor = modifiedEditor;
63
+ } else {
64
+ // 普通编辑器的情况
65
+ this.currentEditor = baseEditor.editor;
66
+ this.disposables.add(
67
+ baseEditor.editor.onDidChangeCursorPosition((e) => {
68
+ if (e?.source !== 'api') {
69
+ this.handleCursorChange(e);
70
+ }
71
+ })
72
+ );
73
+ }
74
+ }
75
+
76
+ hideCascaderByDocumentClick = (e) => {
77
+ const targetDropdown =
78
+ e.target.closest('.tntd-monaco-editor-cascader') ||
79
+ (this.parentContainer && this.parentContainer.parentNode.contains(e.target));
80
+ if (!targetDropdown) {
81
+ if (this.state.visible) {
82
+ this.options.editor.setPosition({
83
+ lineNumber: 1,
84
+ column: 1
85
+ });
86
+ }
87
+ this.hideCascader();
88
+ }
89
+ };
90
+
91
+ //输入字段&函数排序先匹配最长的
92
+ sortBy = (a, b) => {
93
+ if (a.length > b.length) {
94
+ return -1;
95
+ }
96
+ if (a.length < b.length) {
97
+ return 1;
98
+ }
99
+ return 0;
100
+ };
101
+
102
+ // 匹配[]
103
+ transformContent = (content) => {
104
+ if (content) {
105
+ const regex = /^([^[]+\【[^】]+\】)/;
106
+ const match = content.match(regex);
107
+ content = match ? match[1] : content;
108
+ }
109
+ return content;
110
+ };
111
+
112
+ // 如果当前匹配对象后面跟随一个. 则需要匹配后面的
113
+ getEndIndex = ({ originStr, str, regExpRef, endIndex }) => {
114
+ const match = str.match(regExpRef);
115
+ if (match && match.index === 0) {
116
+ const firstMatchLen = match[0].length;
117
+ endIndex = endIndex + firstMatchLen;
118
+ if (originStr.substring(endIndex, endIndex + 1) === '.') {
119
+ endIndex = endIndex + 1;
120
+ endIndex = this.getEndIndex({
121
+ originStr,
122
+ str: originStr.substring(endIndex),
123
+ regExpRef,
124
+ endIndex
125
+ });
126
+ }
127
+ }
128
+
129
+ return endIndex;
130
+ };
131
+
132
+ // 点击自定义级联数据
133
+ handleClick = (item, type) => {
134
+ const { isEndMark } = this.options || {};
135
+ const position = this.currentEditor.getPosition();
136
+ const model = this.currentEditor.getModel();
137
+ if (!position || !model) return;
138
+
139
+ const lineContent = model.getLineContent(position.lineNumber);
140
+ const adjustedColumn = Math.max(1, Math.min(position.column, lineContent.length + 1)) - 1;
141
+
142
+ const cursorBeforeOneChar = lineContent.substring(0, adjustedColumn); // 起始到当前焦点数据
143
+ const lastIndex = cursorBeforeOneChar.lastIndexOf(type, adjustedColumn); // 最后一个标记位置
144
+ const cursorAfterOneChar = lineContent.substring(lastIndex, lineContent.length); // 最后一个标记到结尾数据
145
+ let endIndex = adjustedColumn;
146
+ if (type === '@' || type === '#' || type === '.') {
147
+ const regExpRef = type === '@' || type === '.' ? this.fieldRegExp : this.methodRegExp;
148
+ const match = cursorAfterOneChar.match(regExpRef);
149
+ if (match && match.index === 0) {
150
+ endIndex = this.getEndIndex({ originStr: cursorAfterOneChar, str: cursorAfterOneChar, regExpRef, endIndex: 0 });
151
+ endIndex = lastIndex + endIndex;
152
+ }
153
+ if (type === '@' || type === '.') {
154
+ let lastCursorAfterOneChar = cursorAfterOneChar.slice(1);
155
+ if (lastCursorAfterOneChar?.startsWith('.')) {
156
+ lastCursorAfterOneChar = lastCursorAfterOneChar?.slice(1);
157
+ }
158
+ let restDot = lastCursorAfterOneChar.match(/^((@?)[^-+*=,;&|/\s\)]+(?:\.[@][^-+*=,;&|/\s\)]+)*)/);
159
+ if (restDot?.index === 0) {
160
+ restDot = restDot?.[0];
161
+ if (restDot?.length) {
162
+ endIndex = restDot?.length + lastIndex + 1;
163
+ }
164
+ }
165
+ }
166
+
167
+ // 使用保存的 monaco 引用创建 Selection
168
+ const curSelection = new this.monaco.Selection(position.lineNumber, lastIndex + 2, position.lineNumber, endIndex + 1);
169
+ this.currentEditor.setSelection(curSelection);
170
+ }
171
+
172
+ let content = '';
173
+ if (type === '@') {
174
+ const valueKey = this.options?.fieldNames?.value || 'value';
175
+ const curField = this.fieldList.find((_item) => _item?.[valueKey] === item?.[valueKey]);
176
+ if (item?.value?.startsWith('customfunction_')) {
177
+ // 判断是否是自定义函数字段
178
+ if (curField && curField.data.length > 0) {
179
+ let str = '';
180
+ for (const fieldItem of curField.data) {
181
+ str = str + `,'${fieldItem.dName}'`;
182
+ }
183
+ content = `${item.name}${str}`;
184
+ } else {
185
+ content = item.name + (isEndMark ? '@' : '');
186
+ }
187
+ } else {
188
+ content = item.name + (isEndMark ? '@' : '');
189
+ }
190
+ // if (!notSupportArrayData && curField && (curField.type === 'ARRAY' || curField.array)) {
191
+ // content = content + '[]';
192
+ // }
193
+ } else if (type === '#' && lineContent.length > endIndex) {
194
+ content = item.name;
195
+ } else if (type === '.') {
196
+ // TODO 增加.的判断
197
+ // const curField = this.fieldList.find((_item) => _item.value === item.value);
198
+ content = '@' + item.name;
199
+ // if (!notSupportArrayData && curField && (curField.type === 'ARRAY' || curField.array)) {
200
+ // content += '[]';
201
+ // }
202
+ } else {
203
+ content = item.value;
204
+ }
205
+
206
+ const selections = this.currentEditor.getSelections();
207
+ if (selections) {
208
+ const edits = selections.map((selection) => ({
209
+ range: selection,
210
+ text: content,
211
+ forceMoveMarkers: true
212
+ }));
213
+
214
+ this.currentEditor.executeEdits('edit-selection', edits);
215
+ }
216
+
217
+ this.currentEditor.focus();
218
+ this.hideCascader();
219
+ };
220
+
221
+ // 搜索方法
222
+ search = (val, type, methodParamsInfo, filterList, prevParentItem) => {
223
+ let searchList = type === '@' ? this.fieldList : this.methodList;
224
+
225
+ if (Array.isArray(filterList) && filterList?.length) {
226
+ searchList = filterList;
227
+ }
228
+ // 如果有自定义搜索回调,使用它
229
+ if (this.options.searchCb) {
230
+ searchList = this.options.searchCb({
231
+ field: val,
232
+ type,
233
+ methodParamsInfo,
234
+ fieldList: this.fieldList,
235
+ methodList: this.methodList,
236
+ searchList,
237
+ prevParentItem
238
+ });
239
+ }
240
+
241
+ // 更新状态中的 items
242
+ this.updateCascaderState({
243
+ ...this.state,
244
+ items: searchList
245
+ });
246
+ };
247
+
248
+ setupCascader() {
249
+ // 创建容器
250
+ if (!this.container) {
251
+ this.container = document.createElement('div');
252
+ document.body.appendChild(this.container);
253
+ }
254
+ }
255
+
256
+ handleCursorChange() {
257
+ // 如果是只读的原始编辑器,不显示级联
258
+ if (this.options.isDiff && this.isOriginalEditor() && !this.options.originalEditable) {
259
+ this.hideCascader();
260
+ return;
261
+ }
262
+
263
+ this.hideCascader();
264
+ const position = this.currentEditor.getPosition();
265
+ const model = this.currentEditor.getModel();
266
+ const lineContent = model.getLineContent(position.lineNumber);
267
+
268
+ // 获取光标前的所有字符(注意:column是从1开始计数)
269
+ const adjustedColumn = Math.max(1, Math.min(position.column, lineContent.length + 1)) - 1;
270
+
271
+ const cursorBeforeOneChar = lineContent.substring(0, adjustedColumn);
272
+ const fieldIndex = cursorBeforeOneChar.lastIndexOf('@', adjustedColumn);
273
+ const methodIndex = cursorBeforeOneChar.lastIndexOf('#', adjustedColumn);
274
+
275
+ let methodParamsInfo = '';
276
+ if (methodIndex > -1) {
277
+ methodParamsInfo = lineContent.substring(methodIndex, adjustedColumn);
278
+ }
279
+
280
+ const pos = this.currentEditor.getScrolledVisiblePosition(position);
281
+
282
+ // 计算相对于视口的坐标
283
+ const rect = this.currentEditor.getDomNode().getBoundingClientRect();
284
+ const viewportX = rect.left + pos.left;
285
+ const viewportY = rect.top + pos.top;
286
+ let popupPlacement = 'bottomLeft';
287
+ if (rect?.height - pos.top < 244) {
288
+ popupPlacement = 'topLeft';
289
+ }
290
+
291
+ if (this.fieldList && this.fieldList.length > 0 && fieldIndex !== -1 && fieldIndex > methodIndex) {
292
+ // 监测@
293
+ // .@xx 场景
294
+ // 判断当前是否存在.
295
+ const prevIsDotObj = cursorBeforeOneChar.substring(0, fieldIndex);
296
+ // 已知@a.@ 这里默认获取前面 a 父元素
297
+ let { prevParentItem, preNearItem } = getParentItem({ prevIsDotObj, fieldIndex, cursorBeforeOneChar }) || {};
298
+ let content = cursorBeforeOneChar.substring(fieldIndex + 1, adjustedColumn);
299
+ let originContent = content;
300
+ // 特殊场景 直接.
301
+ let lastDot;
302
+ if (adjustedColumn > 1) {
303
+ lastDot = cursorBeforeOneChar.substring(adjustedColumn - 1, adjustedColumn) === '.';
304
+ }
305
+ if (lastDot) {
306
+ if (cursorBeforeOneChar.endsWith('].')) {
307
+ let leftQuoteIndex = findNearestLeftParenthesisIndex(cursorBeforeOneChar, adjustedColumn - 2);
308
+ if (fieldIndex < leftQuoteIndex) {
309
+ content = cursorBeforeOneChar.substring(fieldIndex + 1, leftQuoteIndex);
310
+ } else {
311
+ content = cursorBeforeOneChar.substring(1, leftQuoteIndex);
312
+ content = content.substring(content.lastIndexOf('@') + 1);
313
+ }
314
+ }
315
+ ({ prevParentItem, preNearItem } =
316
+ getParentItem({ prevIsDotObj: originContent, fieldIndex: adjustedColumn, cursorBeforeOneChar }) || {});
317
+ }
318
+
319
+ if (prevParentItem) {
320
+ prevParentItem = this.transformContent(prevParentItem);
321
+ }
322
+ let filterList = this.fieldList;
323
+ if (prevParentItem) {
324
+ filterList = getChildList({ prevParentItem, preNearItem, fieldList: filterList, lastDot })?.children;
325
+ }
326
+ if (content && !lastDot && filterList?.length) {
327
+ filterList = filterList.filter((item) => item.name.includes(content));
328
+ }
329
+ // 前提条件是复杂对象被拍平
330
+ if (filterList?.length) {
331
+ this.updateCascaderState({
332
+ visible: true,
333
+ position: {
334
+ popupPlacement,
335
+ left: viewportX,
336
+ top: viewportY + 8
337
+ },
338
+ type: lastDot ? '.' : '@'
339
+ });
340
+ this.search(lastDot ? '' : content, '@', methodParamsInfo, filterList, prevParentItem);
341
+ } else {
342
+ this.hideCascader();
343
+ }
344
+ }
345
+ if (this.methodList?.length && methodIndex !== -1 && methodIndex > fieldIndex) {
346
+ // 监测#
347
+ const content = cursorBeforeOneChar.substring(methodIndex + 1, adjustedColumn);
348
+ const findObj = this.methodList.find((item) => item.name.includes(content));
349
+ if (findObj) {
350
+ this.options?.triggerSuggest?.(content ? content : undefined);
351
+ }
352
+ }
353
+ if (!cursorBeforeOneChar.includes('@') && !cursorBeforeOneChar.includes('#')) {
354
+ this.hideCascader();
355
+ }
356
+ }
357
+
358
+ getItemsForWord(word) {
359
+ const { fieldList, methodList } = this.options;
360
+ const wordStr = word.word;
361
+
362
+ if (wordStr.startsWith('@')) {
363
+ return fieldList || [];
364
+ }
365
+ if (wordStr.startsWith('#')) {
366
+ return methodList || [];
367
+ }
368
+ return [];
369
+ }
370
+
371
+ getTypeForWord(word) {
372
+ return word.word.startsWith('@') ? 'field' : 'method';
373
+ }
374
+
375
+ updateCascaderState(newState) {
376
+ this.state = { ...this.state, ...newState };
377
+ this.renderCascader();
378
+ }
379
+
380
+ hideCascader() {
381
+ this.options?.clearSuggest?.();
382
+ this.updateCascaderState({
383
+ visible: false,
384
+ type: null,
385
+ items: []
386
+ });
387
+ }
388
+
389
+ handleSelect = (item, type) => {
390
+ const position = this.currentEditor.getPosition();
391
+ const model = this.currentEditor.getModel();
392
+ const word = model.getWordAtPosition(position);
393
+
394
+ if (!word) return;
395
+
396
+ // 替换文本
397
+ const range = {
398
+ startLineNumber: position.lineNumber,
399
+ endLineNumber: position.lineNumber,
400
+ startColumn: word.startColumn,
401
+ endColumn: word.endColumn
402
+ };
403
+
404
+ const prefix = type === 'field' ? '@' : '#';
405
+ this.currentEditor.executeEdits('cascader', [
406
+ {
407
+ range,
408
+ text: `${prefix}${item.name}`
409
+ }
410
+ ]);
411
+
412
+ this.hideCascader();
413
+ };
414
+
415
+ renderCascader() {
416
+ const { fieldNames } = this.options;
417
+ const { visible, position, items, type } = this.state;
418
+
419
+ if (!this.container) {
420
+ this.setupCascader();
421
+ }
422
+
423
+ if (!visible && this.container) {
424
+ ReactDOM.unmountComponentAtNode(this.container);
425
+ return;
426
+ }
427
+
428
+ // 调整位置,考虑 diff 编辑器的布局
429
+ let adjustedPosition = { ...position };
430
+ if (this.options.isDiff) {
431
+ const editorType = this.getCurrentEditorType();
432
+ if (editorType === 'original') {
433
+ // 原始编辑器在左侧,不需要调整
434
+ } else if (editorType === 'modified') {
435
+ // 修改后的编辑器在右侧,需要考虑 renderSideBySide 选项
436
+ if (this.options.renderSideBySide) {
437
+ // 如果是并排显示,需要考虑原始编辑器的宽度
438
+ const originalEditor = this.baseEditor.editor.getOriginalEditor();
439
+ const originalWidth = originalEditor.getLayoutInfo().contentWidth;
440
+ adjustedPosition.left += originalWidth;
441
+ }
442
+ }
443
+ }
444
+
445
+ ReactDOM.render(
446
+ <Cascader
447
+ {...this.options}
448
+ dropdownClassName={`${this.options?.dropdownClassName || ''} tntd-monaco-editor-cascader`}
449
+ selectChange={(item) => this.handleClick(item, this.state.type)}
450
+ visible={visible}
451
+ dropList={items}
452
+ fieldNames={fieldNames}
453
+ placement={adjustedPosition.popupPlacement}
454
+ style={{
455
+ position: 'fixed',
456
+ left: adjustedPosition.left,
457
+ top: adjustedPosition.top - (adjustedPosition.popupPlacement === 'topLeft' ? 24 : 0),
458
+ zIndex: 1000
459
+ }}
460
+ onSelect={(item) => this.handleSelect(item, type)}
461
+ />,
462
+ this.container
463
+ );
464
+ }
465
+
466
+ dispose() {
467
+ if (this.triggerSuugestTime) {
468
+ clearTimeout(this.triggerSuugestTime);
469
+ this.triggerSuugestTime = null;
470
+ }
471
+
472
+ document.removeEventListener('click', this.hideCascaderByDocumentClick);
473
+ // 清理事件监听
474
+ this.disposables.forEach((disposable) => disposable.dispose());
475
+ this.disposables.clear();
476
+
477
+ // 移除 DOM
478
+ if (this.container) {
479
+ ReactDOM.unmountComponentAtNode(this.container);
480
+ this.container.remove();
481
+ this.container = null;
482
+ }
483
+
484
+ this.dom = null;
485
+ }
486
+
487
+ // 添加辅助方法来判断当前编辑器类型
488
+ isOriginalEditor() {
489
+ if (!this.options.isDiff) return false;
490
+ return this.currentEditor === this.baseEditor.editor.getOriginalEditor();
491
+ }
492
+
493
+ isModifiedEditor() {
494
+ if (!this.options.isDiff) return false;
495
+ return this.currentEditor === this.baseEditor.editor.getModifiedEditor();
496
+ }
497
+
498
+ // 添加获取当前编辑器类型的方法
499
+ getCurrentEditorType() {
500
+ if (!this.options.isDiff) return 'single';
501
+ return this.isOriginalEditor() ? 'original' : 'modified';
502
+ }
503
+ }