ciyidogan commited on
Commit
fcc1d97
·
verified ·
1 Parent(s): 2766662

Update flare-ui/src/app/shared/json-editor/json-editor.component.ts

Browse files
flare-ui/src/app/shared/json-editor/json-editor.component.ts CHANGED
@@ -1,451 +1,451 @@
1
- import { Component, Input, forwardRef, OnInit, ViewChild, ElementRef } from '@angular/core';
2
- import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl } from '@angular/forms';
3
- import { CommonModule } from '@angular/common';
4
- import { FormsModule, ReactiveFormsModule } from '@angular/forms';
5
- import { MatFormFieldModule } from '@angular/material/form-field';
6
- import { MatInputModule } from '@angular/material/input';
7
- import { MatIconModule } from '@angular/material/icon';
8
- import { MatChipsModule } from '@angular/material/chips';
9
- import { MatExpansionModule } from '@angular/material/expansion';
10
-
11
- @Component({
12
- selector: 'app-json-editor',
13
- standalone: true,
14
- imports: [
15
- CommonModule,
16
- FormsModule,
17
- ReactiveFormsModule,
18
- MatFormFieldModule,
19
- MatInputModule,
20
- MatIconModule,
21
- MatChipsModule,
22
- MatExpansionModule
23
- ],
24
- providers: [
25
- {
26
- provide: NG_VALUE_ACCESSOR,
27
- useExisting: forwardRef(() => JsonEditorComponent),
28
- multi: true
29
- }
30
- ],
31
- template: `
32
- <mat-form-field appearance="outline"
33
- class="full-width"
34
- [class.json-valid]="isValidJson()"
35
- [class.json-invalid]="!isValidJson() && value">
36
- <mat-label>{{ label }}</mat-label>
37
- <textarea matInput
38
- #textareaRef
39
- [(ngModel)]="value"
40
- [rows]="rows"
41
- [placeholder]="placeholder"
42
- (keydown)="handleKeydown($event)"
43
- (click)="handleCursorMove($event)"
44
- (keyup)="handleCursorMove($event)"
45
- (blur)="onTouched()"
46
- class="code-editor"></textarea>
47
- <mat-hint>{{ hint }}</mat-hint>
48
- @if (!isValidJson() && value) {
49
- <mat-error>Invalid JSON format</mat-error>
50
- }
51
- </mat-form-field>
52
-
53
- <!-- JSON Validation Indicator -->
54
- <div class="json-validation-status">
55
- @if (isValidJson()) {
56
- <mat-icon class="valid">check_circle</mat-icon>
57
- <span class="valid">Valid JSON</span>
58
- } @else if (value) {
59
- <mat-icon class="invalid">error</mat-icon>
60
- <span class="invalid">Invalid JSON</span>
61
- }
62
- </div>
63
-
64
- <!-- Collapsible Variables Panel -->
65
- @if (availableVariables && availableVariables.length > 0) {
66
- <mat-expansion-panel class="variables-panel">
67
- <mat-expansion-panel-header>
68
- <mat-panel-title>
69
- <mat-icon>code</mat-icon>
70
- Available Variables
71
- </mat-panel-title>
72
- <mat-panel-description>
73
- Click to insert template variables
74
- </mat-panel-description>
75
- </mat-expansion-panel-header>
76
-
77
- <mat-chip-set>
78
- @for (variable of availableVariables; track variable) {
79
- <mat-chip (click)="insertVariable(variable)">
80
- {{ variable }}
81
- </mat-chip>
82
- }
83
- </mat-chip-set>
84
- </mat-expansion-panel>
85
- }
86
- `,
87
- styles: [`
88
- :host {
89
- display: block;
90
- margin-bottom: 16px;
91
- }
92
-
93
- .full-width {
94
- width: 100%;
95
- }
96
-
97
- .code-editor {
98
- font-family: 'Consolas', 'Monaco', 'Courier New', monospace !important;
99
- font-size: 13px;
100
- line-height: 1.5;
101
- tab-size: 2;
102
- -moz-tab-size: 2;
103
- white-space: pre;
104
- }
105
-
106
- .json-validation-status {
107
- display: flex;
108
- align-items: center;
109
- gap: 4px;
110
- margin-top: -12px;
111
- margin-bottom: 16px;
112
- font-size: 12px;
113
-
114
- mat-icon {
115
- font-size: 16px;
116
- width: 16px;
117
- height: 16px;
118
-
119
- &.valid {
120
- color: #4caf50;
121
- }
122
-
123
- &.invalid {
124
- color: #f44336;
125
- }
126
- }
127
-
128
- span {
129
- &.valid {
130
- color: #4caf50;
131
- }
132
-
133
- &.invalid {
134
- color: #f44336;
135
- }
136
- }
137
- }
138
-
139
- .variables-panel {
140
- margin-bottom: 16px;
141
- box-shadow: none;
142
- border: 1px solid rgba(0, 0, 0, 0.12);
143
-
144
- .mat-expansion-panel-header {
145
- padding: 0 16px;
146
- height: 40px;
147
-
148
- .mat-panel-title {
149
- display: flex;
150
- align-items: center;
151
- gap: 8px;
152
- font-size: 13px;
153
- color: #666;
154
-
155
- mat-icon {
156
- font-size: 18px;
157
- width: 18px;
158
- height: 18px;
159
- }
160
- }
161
-
162
- .mat-panel-description {
163
- font-size: 12px;
164
- color: #999;
165
- }
166
- }
167
-
168
- .mat-expansion-panel-body {
169
- padding: 8px 16px 16px;
170
- }
171
-
172
- mat-chip-set {
173
- mat-chip {
174
- font-size: 12px;
175
- min-height: 24px;
176
- padding: 4px 8px;
177
- cursor: pointer;
178
- transition: all 0.2s;
179
-
180
- &:hover {
181
- background-color: #e3f2fd;
182
- color: #1976d2;
183
- }
184
- }
185
- }
186
- }
187
-
188
- // JSON field background colors
189
- .json-valid {
190
- textarea {
191
- background-color: rgba(76, 175, 80, 0.05) !important;
192
- }
193
- }
194
-
195
- .json-invalid {
196
- textarea {
197
- background-color: rgba(244, 67, 54, 0.05) !important;
198
- }
199
- }
200
- `]
201
- })
202
- export class JsonEditorComponent implements ControlValueAccessor, OnInit {
203
- @ViewChild('textareaRef') textareaRef!: ElementRef<HTMLTextAreaElement>;
204
-
205
- @Input() label = 'JSON Editor';
206
- @Input() placeholder = '{}';
207
- @Input() hint = 'Enter valid JSON';
208
- @Input() rows = 8;
209
- @Input() availableVariables: string[] = [];
210
- @Input() variableReplacer?: (json: string) => string;
211
-
212
- value = '';
213
- onChange: (value: string) => void = () => {};
214
- onTouched: () => void = () => {};
215
-
216
- private bracketPairs: { [key: string]: string } = {
217
- '{': '}',
218
- '[': ']',
219
- '(': ')'
220
- };
221
-
222
- private cursorPosition = 0;
223
-
224
- ngOnInit() {}
225
-
226
- writeValue(value: string): void {
227
- this.value = value || '';
228
- }
229
-
230
- registerOnChange(fn: (value: string) => void): void {
231
- this.onChange = fn;
232
- }
233
-
234
- registerOnTouched(fn: () => void): void {
235
- this.onTouched = fn;
236
- }
237
-
238
- setDisabledState?(isDisabled: boolean): void {
239
- // Handle disabled state if needed
240
- }
241
-
242
- isValidJson(): boolean {
243
- if (!this.value || !this.value.trim()) return true;
244
-
245
- try {
246
- let jsonStr = this.value;
247
-
248
- // If variableReplacer is provided, use it to replace variables for validation
249
- if (this.variableReplacer) {
250
- jsonStr = this.variableReplacer(jsonStr);
251
- } else {
252
- // Default variable replacement for validation
253
- jsonStr = this.replaceVariablesForValidation(jsonStr);
254
- }
255
-
256
- JSON.parse(jsonStr);
257
- return true;
258
- } catch {
259
- return false;
260
- }
261
- }
262
-
263
- private replaceVariablesForValidation(jsonStr: string): string {
264
- let processed = jsonStr;
265
-
266
- processed = processed.replace(/\{\{([^}]+)\}\}/g, (match, variablePath) => {
267
- if (variablePath.includes('variables.')) {
268
- const varName = variablePath.split('.').pop()?.toLowerCase() || '';
269
-
270
- const numericVars = ['count', 'passenger_count', 'timeout_seconds', 'retry_count', 'amount', 'price', 'quantity', 'age', 'id'];
271
- const booleanVars = ['enabled', 'published', 'is_active', 'confirmed', 'canceled', 'deleted', 'required'];
272
-
273
- if (numericVars.some(v => varName.includes(v))) {
274
- return '1';
275
- } else if (booleanVars.some(v => varName.includes(v))) {
276
- return 'true';
277
- } else {
278
- return '"placeholder"';
279
- }
280
- }
281
-
282
- return '"placeholder"';
283
- });
284
-
285
- return processed;
286
- }
287
-
288
- handleKeydown(event: KeyboardEvent): void {
289
- if (event.key === 'Tab') {
290
- this.handleTabKey(event);
291
- } else if (event.key === 'Enter') {
292
- this.handleEnterKey(event);
293
- } else if (event.key in this.bracketPairs || Object.values(this.bracketPairs).includes(event.key)) {
294
- this.handleBracketInput(event);
295
- }
296
- }
297
-
298
- handleTabKey(event: KeyboardEvent): void {
299
- event.preventDefault();
300
-
301
- const textarea = event.target as HTMLTextAreaElement;
302
- const start = textarea.selectionStart;
303
- const end = textarea.selectionEnd;
304
-
305
- if (start !== end) {
306
- this.handleBlockIndent(textarea, !event.shiftKey);
307
- } else {
308
- const newValue = this.value.substring(0, start) + '\t' + this.value.substring(end);
309
- this.updateValue(newValue);
310
-
311
- setTimeout(() => {
312
- textarea.selectionStart = textarea.selectionEnd = start + 1;
313
- textarea.focus();
314
- }, 0);
315
- }
316
- }
317
-
318
- private handleBlockIndent(textarea: HTMLTextAreaElement, indent: boolean): void {
319
- const start = textarea.selectionStart;
320
- const end = textarea.selectionEnd;
321
- const value = this.value;
322
-
323
- const lineStart = value.lastIndexOf('\n', start - 1) + 1;
324
- const lineEnd = value.indexOf('\n', end);
325
- const actualEnd = lineEnd === -1 ? value.length : lineEnd;
326
-
327
- const selectedLines = value.substring(lineStart, actualEnd);
328
- const lines = selectedLines.split('\n');
329
-
330
- let newLines: string[];
331
- if (indent) {
332
- newLines = lines.map(line => '\t' + line);
333
- } else {
334
- newLines = lines.map(line => line.startsWith('\t') ? line.substring(1) : line);
335
- }
336
-
337
- const newText = newLines.join('\n');
338
- const newValue = value.substring(0, lineStart) + newText + value.substring(actualEnd);
339
-
340
- this.updateValue(newValue);
341
-
342
- setTimeout(() => {
343
- const lengthDiff = newText.length - selectedLines.length;
344
- textarea.selectionStart = lineStart;
345
- textarea.selectionEnd = actualEnd + lengthDiff;
346
- textarea.focus();
347
- }, 0);
348
- }
349
-
350
- handleEnterKey(event: KeyboardEvent): void {
351
- event.preventDefault();
352
-
353
- const textarea = event.target as HTMLTextAreaElement;
354
- const start = textarea.selectionStart;
355
- const value = this.value;
356
-
357
- const lineStart = value.lastIndexOf('\n', start - 1) + 1;
358
- const currentLine = value.substring(lineStart, start);
359
- const indent = currentLine.match(/^[\t ]*/)?.[0] || '';
360
-
361
- const prevChar = value[start - 1];
362
- const nextChar = value[start];
363
-
364
- let newLineContent = '\n' + indent;
365
- let cursorOffset = newLineContent.length;
366
-
367
- if (prevChar in this.bracketPairs) {
368
- newLineContent = '\n' + indent + '\t';
369
- cursorOffset = newLineContent.length;
370
-
371
- if (nextChar === this.bracketPairs[prevChar]) {
372
- newLineContent += '\n' + indent;
373
- }
374
- }
375
-
376
- const newValue = value.substring(0, start) + newLineContent + value.substring(start);
377
- this.updateValue(newValue);
378
-
379
- setTimeout(() => {
380
- textarea.selectionStart = textarea.selectionEnd = start + cursorOffset;
381
- textarea.focus();
382
- }, 0);
383
- }
384
-
385
- handleBracketInput(event: KeyboardEvent): void {
386
- const textarea = event.target as HTMLTextAreaElement;
387
- const char = event.key;
388
-
389
- if (char in this.bracketPairs) {
390
- event.preventDefault();
391
-
392
- const start = textarea.selectionStart;
393
- const end = textarea.selectionEnd;
394
- const value = this.value;
395
-
396
- const selectedText = value.substring(start, end);
397
- const closingBracket = this.bracketPairs[char];
398
-
399
- let newValue: string;
400
- let cursorPos: number;
401
-
402
- if (selectedText) {
403
- newValue = value.substring(0, start) + char + selectedText + closingBracket + value.substring(end);
404
- cursorPos = start + 1 + selectedText.length;
405
- } else {
406
- newValue = value.substring(0, start) + char + closingBracket + value.substring(end);
407
- cursorPos = start + 1;
408
- }
409
-
410
- this.updateValue(newValue);
411
-
412
- setTimeout(() => {
413
- textarea.selectionStart = textarea.selectionEnd = cursorPos;
414
- textarea.focus();
415
- }, 0);
416
- } else if (Object.values(this.bracketPairs).includes(char)) {
417
- const start = textarea.selectionStart;
418
- const value = this.value;
419
- const nextChar = value[start];
420
-
421
- if (nextChar === char) {
422
- event.preventDefault();
423
- textarea.selectionStart = textarea.selectionEnd = start + 1;
424
- }
425
- }
426
- }
427
-
428
- handleCursorMove(event: any): void {
429
- this.cursorPosition = event.target.selectionStart;
430
- }
431
-
432
- insertVariable(variable: string): void {
433
- const textarea = this.textareaRef.nativeElement;
434
- const start = this.cursorPosition;
435
- const variableText = `{{${variable}}}`;
436
-
437
- const newValue = this.value.substring(0, start) + variableText + this.value.substring(start);
438
- this.updateValue(newValue);
439
-
440
- setTimeout(() => {
441
- const newPos = start + variableText.length;
442
- textarea.selectionStart = textarea.selectionEnd = newPos;
443
- textarea.focus();
444
- }, 0);
445
- }
446
-
447
- private updateValue(newValue: string): void {
448
- this.value = newValue;
449
- this.onChange(newValue);
450
- }
451
  }
 
1
+ import { Component, Input, forwardRef, OnInit, ViewChild, ElementRef } from '@angular/core';
2
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl } from '@angular/forms';
3
+ import { CommonModule } from '@angular/common';
4
+ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
5
+ import { MatFormFieldModule } from '@angular/material/form-field';
6
+ import { MatInputModule } from '@angular/material/input';
7
+ import { MatIconModule } from '@angular/material/icon';
8
+ import { MatChipsModule } from '@angular/material/chips';
9
+ import { MatExpansionModule } from '@angular/material/expansion';
10
+
11
+ @Component({
12
+ selector: 'app-json-editor',
13
+ standalone: true,
14
+ imports: [
15
+ CommonModule,
16
+ FormsModule,
17
+ ReactiveFormsModule,
18
+ MatFormFieldModule,
19
+ MatInputModule,
20
+ MatIconModule,
21
+ MatChipsModule,
22
+ MatExpansionModule
23
+ ],
24
+ providers: [
25
+ {
26
+ provide: NG_VALUE_ACCESSOR,
27
+ useExisting: forwardRef(() => JsonEditorComponent),
28
+ multi: true
29
+ }
30
+ ],
31
+ template: `
32
+ <mat-form-field appearance="outline"
33
+ class="full-width"
34
+ [class.json-valid]="isValidJson()"
35
+ [class.json-invalid]="!isValidJson() && value">
36
+ <mat-label>{{ label }}</mat-label>
37
+ <textarea matInput
38
+ #textareaRef
39
+ [(ngModel)]="value"
40
+ [rows]="rows"
41
+ [placeholder]="placeholder"
42
+ (keydown)="handleKeydown($event)"
43
+ (click)="handleCursorMove($event)"
44
+ (keyup)="handleCursorMove($event)"
45
+ (blur)="onTouched()"
46
+ class="code-editor"></textarea>
47
+ <mat-hint>{{ hint }}</mat-hint>
48
+ @if (!isValidJson() && value) {
49
+ <mat-error>Invalid JSON format</mat-error>
50
+ }
51
+ </mat-form-field>
52
+
53
+ <!-- JSON Validation Indicator -->
54
+ <div class="json-validation-status">
55
+ @if (isValidJson()) {
56
+ <mat-icon class="valid">check_circle</mat-icon>
57
+ <span class="valid">Valid JSON</span>
58
+ } @else if (value) {
59
+ <mat-icon class="invalid">error</mat-icon>
60
+ <span class="invalid">Invalid JSON</span>
61
+ }
62
+ </div>
63
+
64
+ <!-- Collapsible Variables Panel -->
65
+ @if (availableVariables && availableVariables.length > 0) {
66
+ <mat-expansion-panel class="variables-panel">
67
+ <mat-expansion-panel-header>
68
+ <mat-panel-title>
69
+ <mat-icon>code</mat-icon>
70
+ Available Variables
71
+ </mat-panel-title>
72
+ <mat-panel-description>
73
+ Click to insert template variables
74
+ </mat-panel-description>
75
+ </mat-expansion-panel-header>
76
+
77
+ <mat-chip-set>
78
+ @for (variable of availableVariables; track variable) {
79
+ <mat-chip (click)="insertVariable(variable)">
80
+ {{ variable }}
81
+ </mat-chip>
82
+ }
83
+ </mat-chip-set>
84
+ </mat-expansion-panel>
85
+ }
86
+ `,
87
+ styles: [`
88
+ :host {
89
+ display: block;
90
+ margin-bottom: 16px;
91
+ }
92
+
93
+ .full-width {
94
+ width: 100%;
95
+ }
96
+
97
+ .code-editor {
98
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace !important;
99
+ font-size: 13px;
100
+ line-height: 1.5;
101
+ tab-size: 2;
102
+ -moz-tab-size: 2;
103
+ white-space: pre;
104
+ }
105
+
106
+ .json-validation-status {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 4px;
110
+ margin-top: -6px;
111
+ margin-bottom: 16px;
112
+ font-size: 12px;
113
+
114
+ mat-icon {
115
+ font-size: 16px;
116
+ width: 16px;
117
+ height: 20px;
118
+
119
+ &.valid {
120
+ color: #4caf50;
121
+ }
122
+
123
+ &.invalid {
124
+ color: #f44336;
125
+ }
126
+ }
127
+
128
+ span {
129
+ &.valid {
130
+ color: #4caf50;
131
+ }
132
+
133
+ &.invalid {
134
+ color: #f44336;
135
+ }
136
+ }
137
+ }
138
+
139
+ .variables-panel {
140
+ margin-bottom: 16px;
141
+ box-shadow: none;
142
+ border: 1px solid rgba(0, 0, 0, 0.12);
143
+
144
+ .mat-expansion-panel-header {
145
+ padding: 0 16px;
146
+ height: 40px;
147
+
148
+ .mat-panel-title {
149
+ display: flex;
150
+ align-items: center;
151
+ gap: 8px;
152
+ font-size: 13px;
153
+ color: #666;
154
+
155
+ mat-icon {
156
+ font-size: 18px;
157
+ width: 18px;
158
+ height: 18px;
159
+ }
160
+ }
161
+
162
+ .mat-panel-description {
163
+ font-size: 12px;
164
+ color: #999;
165
+ }
166
+ }
167
+
168
+ .mat-expansion-panel-body {
169
+ padding: 8px 16px 16px;
170
+ }
171
+
172
+ mat-chip-set {
173
+ mat-chip {
174
+ font-size: 12px;
175
+ min-height: 24px;
176
+ padding: 4px 8px;
177
+ cursor: pointer;
178
+ transition: all 0.2s;
179
+
180
+ &:hover {
181
+ background-color: #e3f2fd;
182
+ color: #1976d2;
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ // JSON field background colors
189
+ .json-valid {
190
+ textarea {
191
+ background-color: rgba(76, 175, 80, 0.05) !important;
192
+ }
193
+ }
194
+
195
+ .json-invalid {
196
+ textarea {
197
+ background-color: rgba(244, 67, 54, 0.05) !important;
198
+ }
199
+ }
200
+ `]
201
+ })
202
+ export class JsonEditorComponent implements ControlValueAccessor, OnInit {
203
+ @ViewChild('textareaRef') textareaRef!: ElementRef<HTMLTextAreaElement>;
204
+
205
+ @Input() label = 'JSON Editor';
206
+ @Input() placeholder = '{}';
207
+ @Input() hint = 'Enter valid JSON';
208
+ @Input() rows = 8;
209
+ @Input() availableVariables: string[] = [];
210
+ @Input() variableReplacer?: (json: string) => string;
211
+
212
+ value = '';
213
+ onChange: (value: string) => void = () => {};
214
+ onTouched: () => void = () => {};
215
+
216
+ private bracketPairs: { [key: string]: string } = {
217
+ '{': '}',
218
+ '[': ']',
219
+ '(': ')'
220
+ };
221
+
222
+ private cursorPosition = 0;
223
+
224
+ ngOnInit() {}
225
+
226
+ writeValue(value: string): void {
227
+ this.value = value || '';
228
+ }
229
+
230
+ registerOnChange(fn: (value: string) => void): void {
231
+ this.onChange = fn;
232
+ }
233
+
234
+ registerOnTouched(fn: () => void): void {
235
+ this.onTouched = fn;
236
+ }
237
+
238
+ setDisabledState?(isDisabled: boolean): void {
239
+ // Handle disabled state if needed
240
+ }
241
+
242
+ isValidJson(): boolean {
243
+ if (!this.value || !this.value.trim()) return true;
244
+
245
+ try {
246
+ let jsonStr = this.value;
247
+
248
+ // If variableReplacer is provided, use it to replace variables for validation
249
+ if (this.variableReplacer) {
250
+ jsonStr = this.variableReplacer(jsonStr);
251
+ } else {
252
+ // Default variable replacement for validation
253
+ jsonStr = this.replaceVariablesForValidation(jsonStr);
254
+ }
255
+
256
+ JSON.parse(jsonStr);
257
+ return true;
258
+ } catch {
259
+ return false;
260
+ }
261
+ }
262
+
263
+ private replaceVariablesForValidation(jsonStr: string): string {
264
+ let processed = jsonStr;
265
+
266
+ processed = processed.replace(/\{\{([^}]+)\}\}/g, (match, variablePath) => {
267
+ if (variablePath.includes('variables.')) {
268
+ const varName = variablePath.split('.').pop()?.toLowerCase() || '';
269
+
270
+ const numericVars = ['count', 'passenger_count', 'timeout_seconds', 'retry_count', 'amount', 'price', 'quantity', 'age', 'id'];
271
+ const booleanVars = ['enabled', 'published', 'is_active', 'confirmed', 'canceled', 'deleted', 'required'];
272
+
273
+ if (numericVars.some(v => varName.includes(v))) {
274
+ return '1';
275
+ } else if (booleanVars.some(v => varName.includes(v))) {
276
+ return 'true';
277
+ } else {
278
+ return '"placeholder"';
279
+ }
280
+ }
281
+
282
+ return '"placeholder"';
283
+ });
284
+
285
+ return processed;
286
+ }
287
+
288
+ handleKeydown(event: KeyboardEvent): void {
289
+ if (event.key === 'Tab') {
290
+ this.handleTabKey(event);
291
+ } else if (event.key === 'Enter') {
292
+ this.handleEnterKey(event);
293
+ } else if (event.key in this.bracketPairs || Object.values(this.bracketPairs).includes(event.key)) {
294
+ this.handleBracketInput(event);
295
+ }
296
+ }
297
+
298
+ handleTabKey(event: KeyboardEvent): void {
299
+ event.preventDefault();
300
+
301
+ const textarea = event.target as HTMLTextAreaElement;
302
+ const start = textarea.selectionStart;
303
+ const end = textarea.selectionEnd;
304
+
305
+ if (start !== end) {
306
+ this.handleBlockIndent(textarea, !event.shiftKey);
307
+ } else {
308
+ const newValue = this.value.substring(0, start) + '\t' + this.value.substring(end);
309
+ this.updateValue(newValue);
310
+
311
+ setTimeout(() => {
312
+ textarea.selectionStart = textarea.selectionEnd = start + 1;
313
+ textarea.focus();
314
+ }, 0);
315
+ }
316
+ }
317
+
318
+ private handleBlockIndent(textarea: HTMLTextAreaElement, indent: boolean): void {
319
+ const start = textarea.selectionStart;
320
+ const end = textarea.selectionEnd;
321
+ const value = this.value;
322
+
323
+ const lineStart = value.lastIndexOf('\n', start - 1) + 1;
324
+ const lineEnd = value.indexOf('\n', end);
325
+ const actualEnd = lineEnd === -1 ? value.length : lineEnd;
326
+
327
+ const selectedLines = value.substring(lineStart, actualEnd);
328
+ const lines = selectedLines.split('\n');
329
+
330
+ let newLines: string[];
331
+ if (indent) {
332
+ newLines = lines.map(line => '\t' + line);
333
+ } else {
334
+ newLines = lines.map(line => line.startsWith('\t') ? line.substring(1) : line);
335
+ }
336
+
337
+ const newText = newLines.join('\n');
338
+ const newValue = value.substring(0, lineStart) + newText + value.substring(actualEnd);
339
+
340
+ this.updateValue(newValue);
341
+
342
+ setTimeout(() => {
343
+ const lengthDiff = newText.length - selectedLines.length;
344
+ textarea.selectionStart = lineStart;
345
+ textarea.selectionEnd = actualEnd + lengthDiff;
346
+ textarea.focus();
347
+ }, 0);
348
+ }
349
+
350
+ handleEnterKey(event: KeyboardEvent): void {
351
+ event.preventDefault();
352
+
353
+ const textarea = event.target as HTMLTextAreaElement;
354
+ const start = textarea.selectionStart;
355
+ const value = this.value;
356
+
357
+ const lineStart = value.lastIndexOf('\n', start - 1) + 1;
358
+ const currentLine = value.substring(lineStart, start);
359
+ const indent = currentLine.match(/^[\t ]*/)?.[0] || '';
360
+
361
+ const prevChar = value[start - 1];
362
+ const nextChar = value[start];
363
+
364
+ let newLineContent = '\n' + indent;
365
+ let cursorOffset = newLineContent.length;
366
+
367
+ if (prevChar in this.bracketPairs) {
368
+ newLineContent = '\n' + indent + '\t';
369
+ cursorOffset = newLineContent.length;
370
+
371
+ if (nextChar === this.bracketPairs[prevChar]) {
372
+ newLineContent += '\n' + indent;
373
+ }
374
+ }
375
+
376
+ const newValue = value.substring(0, start) + newLineContent + value.substring(start);
377
+ this.updateValue(newValue);
378
+
379
+ setTimeout(() => {
380
+ textarea.selectionStart = textarea.selectionEnd = start + cursorOffset;
381
+ textarea.focus();
382
+ }, 0);
383
+ }
384
+
385
+ handleBracketInput(event: KeyboardEvent): void {
386
+ const textarea = event.target as HTMLTextAreaElement;
387
+ const char = event.key;
388
+
389
+ if (char in this.bracketPairs) {
390
+ event.preventDefault();
391
+
392
+ const start = textarea.selectionStart;
393
+ const end = textarea.selectionEnd;
394
+ const value = this.value;
395
+
396
+ const selectedText = value.substring(start, end);
397
+ const closingBracket = this.bracketPairs[char];
398
+
399
+ let newValue: string;
400
+ let cursorPos: number;
401
+
402
+ if (selectedText) {
403
+ newValue = value.substring(0, start) + char + selectedText + closingBracket + value.substring(end);
404
+ cursorPos = start + 1 + selectedText.length;
405
+ } else {
406
+ newValue = value.substring(0, start) + char + closingBracket + value.substring(end);
407
+ cursorPos = start + 1;
408
+ }
409
+
410
+ this.updateValue(newValue);
411
+
412
+ setTimeout(() => {
413
+ textarea.selectionStart = textarea.selectionEnd = cursorPos;
414
+ textarea.focus();
415
+ }, 0);
416
+ } else if (Object.values(this.bracketPairs).includes(char)) {
417
+ const start = textarea.selectionStart;
418
+ const value = this.value;
419
+ const nextChar = value[start];
420
+
421
+ if (nextChar === char) {
422
+ event.preventDefault();
423
+ textarea.selectionStart = textarea.selectionEnd = start + 1;
424
+ }
425
+ }
426
+ }
427
+
428
+ handleCursorMove(event: any): void {
429
+ this.cursorPosition = event.target.selectionStart;
430
+ }
431
+
432
+ insertVariable(variable: string): void {
433
+ const textarea = this.textareaRef.nativeElement;
434
+ const start = this.cursorPosition;
435
+ const variableText = `{{${variable}}}`;
436
+
437
+ const newValue = this.value.substring(0, start) + variableText + this.value.substring(start);
438
+ this.updateValue(newValue);
439
+
440
+ setTimeout(() => {
441
+ const newPos = start + variableText.length;
442
+ textarea.selectionStart = textarea.selectionEnd = newPos;
443
+ textarea.focus();
444
+ }, 0);
445
+ }
446
+
447
+ private updateValue(newValue: string): void {
448
+ this.value = newValue;
449
+ this.onChange(newValue);
450
+ }
451
  }