@libs-ui/services-diagram-draw 0.2.356-42 → 0.2.356-43

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
@@ -1,9 +1,46 @@
1
1
  # @libs-ui/services-diagram-draw
2
2
 
3
- > **Version:** 0.2.356-3
4
- > **Angular:** ^18.2.0
3
+ > Bộ ba service Angular quản lý toàn bộ vòng đời diagram flow: tính toán tọa độ, render node động, vẽ SVG connector, quản lý kéo-thả và điều hướng luồng giữa các element.
5
4
 
6
- Service quản lý toàn bộ vòng đời của diagram flow: tính toán tọa độ element, render component động vào canvas, vẽ SVG connector, và quản lý drop zone kéo-thả thông minh.
5
+ ---
6
+
7
+ ## Giới thiệu
8
+
9
+ `@libs-ui/services-diagram-draw` cung cấp ba service độc lập phối hợp với nhau để xây dựng diagram flow tương tác:
10
+
11
+ - **`LibsUiDiagramDrawService`** — service trung tâm: tính tọa độ X/Y, render component node vào canvas, vẽ đường SVG, quản lý drop zone kéo-thả, hỗ trợ virtualization cho diagram lớn.
12
+ - **`LibsUiDiagramDrawCanvasService`** — service quản lý cấu trúc dữ liệu diagram: thêm/xóa/thay thế element, xây dựng flat list, kiểm tra rule kết nối.
13
+ - **`LibsUiDiagramDrawDirectionService`** — service quản lý điều hướng (connection line) giữa các element không liền kề: thêm/xóa/chỉnh sửa đường nối tự do.
14
+
15
+ Hỗ trợ hai chế độ layout: **vertical** (dọc từ trên xuống, mặc định) và **horizontal** (ngang từ trái sang phải).
16
+
17
+ ---
18
+
19
+ ## Tính năng
20
+
21
+ - Tính toán tọa độ tự động cho diagram dọc và ngang
22
+ - Render Angular component động vào canvas (main node và nodeOtherConfig)
23
+ - Vẽ SVG connector với cubic bezier tự động
24
+ - Hiển thị mũi tên chỉ hướng (vertical: ▼, horizontal: ►) giữa các node
25
+ - Render label trên nhánh (branch label) với màu sắc và class tùy chỉnh
26
+ - Drop zone kéo-thả với indicator "+" tùy biến hoàn toàn
27
+ - Virtualization — chỉ render node trong viewport, hỗ trợ diagram hàng trăm node
28
+ - Quản lý thêm/xóa/thay thế/di chuyển element trong cấu trúc linked-list
29
+ - Quản lý điều hướng tự do (next_other_id) và cleanup tự động khi xóa element
30
+ - Build dữ liệu navigation line để kết hợp với `@libs-ui/components-draw-line`
31
+ - Tự động tăng counter tên node khi thêm cùng loại (Email 1, Email 2, …)
32
+ - Kiểm tra rule kết nối (elementCodeConnectableBefore) trước khi cho phép drop
33
+
34
+ ---
35
+
36
+ ## Khi nào sử dụng
37
+
38
+ - Xây dựng giao diện thiết kế luồng tự động hóa (automation flow, journey builder)
39
+ - Cần render động nhiều loại node component khác nhau trên cùng canvas
40
+ - Diagram có cấu trúc phân nhánh (branching workflow) phức tạp
41
+ - Cần tính năng kéo-thả từ sidebar vào luồng với validation rule
42
+ - Diagram cần kết nối điều hướng tự do giữa các element không liền kề
43
+ - Diagram lớn (>50 node) cần virtualization để tránh lag render
7
44
 
8
45
  ---
9
46
 
@@ -18,209 +55,581 @@ npm install @libs-ui/services-diagram-draw
18
55
  ## Import
19
56
 
20
57
  ```typescript
21
- import { LibsUiDiagramDrawService, canvasConfigReadonly, storeDataDefault } from '@libs-ui/services-diagram-draw';
58
+ import {
59
+ LibsUiDiagramDrawService,
60
+ LibsUiDiagramDrawCanvasService,
61
+ LibsUiDiagramDrawDirectionService,
62
+ canvasConfigReadonly,
63
+ storeDataDefault,
64
+ } from '@libs-ui/services-diagram-draw';
65
+
66
+ import type {
67
+ IDiagramElement,
68
+ IDiagramElementBranch,
69
+ ICanvasConfig,
70
+ ICustomCanvasConfig,
71
+ IDiagramStoreData,
72
+ IDropZoneIndicatorStyle,
73
+ IRuleConnectable,
74
+ IHandlerFunction,
75
+ TDropZoneCanDropFn,
76
+ INavigationLineStyleConfig,
77
+ } from '@libs-ui/services-diagram-draw';
22
78
  ```
23
79
 
80
+ Ba service đều có `providedIn: 'root'` — inject trực tiếp bằng `inject()`, không cần khai báo trong `providers`.
81
+
24
82
  ---
25
83
 
26
- ## Khởi tạo
84
+ ## dụ sử dụng
85
+
86
+ ### Ví dụ 1 — Khởi tạo diagram cơ bản (vertical)
27
87
 
28
88
  ```typescript
29
- @Component({ ... })
89
+ import { Component, AfterViewInit, ElementRef, ViewContainerRef, inject, viewChild } from '@angular/core';
90
+ import {
91
+ LibsUiDiagramDrawService,
92
+ LibsUiDiagramDrawCanvasService,
93
+ storeDataDefault,
94
+ IDiagramElement,
95
+ IRuleConnectable,
96
+ } from '@libs-ui/services-diagram-draw';
97
+ import { MyNodeComponent } from './my-node.component';
98
+
99
+ @Component({
100
+ selector: 'app-diagram',
101
+ standalone: true,
102
+ changeDetection: ChangeDetectionStrategy.OnPush,
103
+ template: `
104
+ <div style="position: relative; overflow: auto; width: 100%; height: 100vh;">
105
+ <svg #svgContainer style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;">
106
+ <path style="fill: none; stroke: #9ca2ad; stroke-width: 1;"></path>
107
+ </svg>
108
+ <div #nodesContainer style="position: relative; width: 100%; height: 100%;"></div>
109
+ <ng-container #vcr></ng-container>
110
+ </div>
111
+ `,
112
+ })
30
113
  export class DiagramComponent implements AfterViewInit {
31
- @ViewChild('svgContainer') svgContainer!: ElementRef<SVGElement>;
32
- @ViewChild('nodesContainer') nodesContainer!: ElementRef<HTMLDivElement>;
33
- @ViewChild('vcr', { read: ViewContainerRef }) vcr!: ViewContainerRef;
114
+ private readonly svgContainer = viewChild<ElementRef<HTMLDivElement>>('svgContainer');
115
+ private readonly nodesContainer = viewChild<ElementRef<HTMLDivElement>>('nodesContainer');
116
+ private readonly vcr = viewChild('vcr', { read: ViewContainerRef });
34
117
 
35
118
  private readonly diagramService = inject(LibsUiDiagramDrawService);
119
+ private readonly canvasService = inject(LibsUiDiagramDrawCanvasService);
120
+
121
+ private readonly elements: IDiagramElement[] = [
122
+ { id: '1', code: 'EMAIL', name: 'Gửi Email 1', specific_width: 165, specific_height: 48, next_id: '2', pre_id: '' },
123
+ { id: '2', code: 'SMS', name: 'Gửi SMS 1', specific_width: 165, specific_height: 48, next_id: '3', pre_id: '1' },
124
+ { id: '3', code: 'EXIT', element_type: 'EXIT', name: 'Kết thúc', specific_width: 165, specific_height: 48, pre_id: '2' },
125
+ ];
126
+
127
+ private readonly rules: IRuleConnectable[] = [
128
+ { elementCode: 'SMS', elementCodeConnectableBefore: ['EMAIL', 'SMS'] },
129
+ { elementCode: 'EMAIL', elementCodeConnectableBefore: ['EMAIL', 'SMS'] },
130
+ ];
36
131
 
37
132
  ngAfterViewInit() {
38
- // 1. Đăng ký config
39
- this.diagramService.setConfig({
133
+ this.diagramService.setConfig = {
40
134
  storeDataDefine: { ...storeDataDefault },
41
- svgContainer: this.svgContainer,
42
- nodesContainer: this.nodesContainer,
43
- viewContainerRef: this.vcr,
135
+ svgContainer: this.svgContainer(),
136
+ nodesContainer: this.nodesContainer(),
137
+ viewContainerRef: this.vcr(),
44
138
  nodeComponentType: MyNodeComponent,
45
- nodeOtherConfigComponentType: MyWaitNodeComponent, // optional
46
- });
139
+ };
47
140
 
48
- // 2. Tính toán tọa độ
141
+ this.canvasService.loadElements(this.elements, { x: 200, y: 40 }, this.rules);
49
142
  this.diagramService.configXYElements(this.elements);
143
+ this.diagramService.renderNodes(this.elements);
144
+ }
145
+ }
146
+ ```
147
+
148
+ ---
149
+
150
+ ### Ví dụ 2 — Diagram ngang (horizontal) với drop zone
151
+
152
+ ```typescript
153
+ import { Component, AfterViewInit, ElementRef, ViewContainerRef, inject, viewChild } from '@angular/core';
154
+ import {
155
+ LibsUiDiagramDrawService,
156
+ LibsUiDiagramDrawCanvasService,
157
+ storeDataDefault,
158
+ IDiagramElement,
159
+ IDiagramElementBranch,
160
+ IRuleConnectable,
161
+ TDropZoneCanDropFn,
162
+ } from '@libs-ui/services-diagram-draw';
163
+ import { MyNodeComponent } from './my-node.component';
164
+
165
+ @Component({
166
+ selector: 'app-diagram-horizontal',
167
+ standalone: true,
168
+ changeDetection: ChangeDetectionStrategy.OnPush,
169
+ template: `
170
+ <div style="position: relative; overflow: auto; width: 100%; height: 100vh;">
171
+ <svg #svgContainer style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;">
172
+ <path style="fill: none; stroke: #9ca2ad; stroke-width: 1;"></path>
173
+ </svg>
174
+ <div #nodesContainer style="position: relative; width: 100%; height: 100%;"></div>
175
+ <ng-container #vcr></ng-container>
176
+ </div>
177
+ `,
178
+ })
179
+ export class DiagramHorizontalComponent implements AfterViewInit {
180
+ private readonly svgContainer = viewChild<ElementRef<HTMLDivElement>>('svgContainer');
181
+ private readonly nodesContainer = viewChild<ElementRef<HTMLDivElement>>('nodesContainer');
182
+ private readonly vcr = viewChild('vcr', { read: ViewContainerRef });
183
+
184
+ private readonly diagramService = inject(LibsUiDiagramDrawService);
185
+ private readonly canvasService = inject(LibsUiDiagramDrawCanvasService);
50
186
 
51
- // 3. Render nodes + SVG connector
187
+ private readonly elements: IDiagramElement[] = [
188
+ { id: '1', code: 'TRIGGER', name: 'Trigger 1', specific_width: 165, specific_height: 48, next_id: '2', pre_id: '' },
189
+ { id: '2', code: 'ACTION', name: 'Action 1', specific_width: 165, specific_height: 48, next_id: '3', pre_id: '1' },
190
+ { id: '3', code: 'EXIT', element_type: 'EXIT', name: 'Kết thúc', specific_width: 165, specific_height: 48, pre_id: '2' },
191
+ ];
192
+
193
+ private readonly rules: IRuleConnectable[] = [
194
+ { elementCode: 'ACTION', elementCodeConnectableBefore: ['TRIGGER', 'ACTION'] },
195
+ ];
196
+
197
+ ngAfterViewInit() {
198
+ // Chuyển sang chế độ ngang
199
+ this.diagramService.orientation.set('horizontal');
200
+
201
+ this.diagramService.setConfig = {
202
+ storeDataDefine: { ...storeDataDefault },
203
+ svgContainer: this.svgContainer(),
204
+ nodesContainer: this.nodesContainer(),
205
+ viewContainerRef: this.vcr(),
206
+ nodeComponentType: MyNodeComponent,
207
+ };
208
+
209
+ this.canvasService.loadElements(this.elements, { x: 40, y: 200 }, this.rules);
210
+ this.diagramService.configXYElements(this.elements);
52
211
  this.diagramService.renderNodes(this.elements);
212
+ this.renderDropZones();
213
+ }
214
+
215
+ private renderDropZones() {
216
+ const canDropFn: TDropZoneCanDropFn = (_before, current, _after, _rules) => {
217
+ // Trả về true = ẨN drop zone, false = HIỂN THỊ
218
+ return current?.code === 'EXIT';
219
+ };
220
+
221
+ this.diagramService.renderDropZones(
222
+ this.canvasService.FlatElements,
223
+ (element: IDiagramElement, branch: IDiagramElementBranch | undefined, dragData: string) => {
224
+ const data: IDiagramElement = JSON.parse(dragData);
225
+ this.canvasService.addElement(element, branch, data);
226
+ this.diagramService.configXYElements(this.elements);
227
+ this.diagramService.renderNodes(this.elements);
228
+ this.renderDropZones();
229
+ },
230
+ canDropFn,
231
+ this.rules,
232
+ );
53
233
  }
54
234
  }
55
235
  ```
56
236
 
57
237
  ---
58
238
 
59
- ## API
239
+ ### Ví dụ 3 — Diagram phân nhánh (WORKFLOW element)
60
240
 
61
- ### Methods
241
+ ```typescript
242
+ const elementsWithBranch: IDiagramElement[] = [
243
+ {
244
+ id: '1',
245
+ code: 'START',
246
+ name: 'Bắt đầu',
247
+ specific_width: 165,
248
+ specific_height: 48,
249
+ next_id: '2',
250
+ pre_id: '',
251
+ },
252
+ {
253
+ id: '2',
254
+ code: 'CONDITION',
255
+ element_type: 'WORKFLOW',
256
+ name: 'Điều kiện 1',
257
+ specific_width: 165,
258
+ specific_height: 48,
259
+ next_id: '5',
260
+ pre_id: '1',
261
+ branches: [
262
+ {
263
+ code: 'YES',
264
+ label: 'Đúng',
265
+ labelBgColor: '#dcfce7',
266
+ labelColor: '#15803d',
267
+ elements: [
268
+ { id: '3', code: 'EMAIL', name: 'Email 1', specific_width: 165, specific_height: 48, next_id: '', pre_id: '2' },
269
+ ],
270
+ },
271
+ {
272
+ code: 'NO',
273
+ label: 'Sai',
274
+ labelBgColor: '#fee2e2',
275
+ labelColor: '#dc2626',
276
+ elements: [
277
+ { id: '4', code: 'SMS', name: 'SMS 1', specific_width: 165, specific_height: 48, next_id: '', pre_id: '2' },
278
+ ],
279
+ },
280
+ ],
281
+ },
282
+ {
283
+ id: '5',
284
+ code: 'EXIT',
285
+ element_type: 'EXIT',
286
+ name: 'Kết thúc',
287
+ specific_width: 165,
288
+ specific_height: 48,
289
+ pre_id: '2',
290
+ },
291
+ ];
292
+ ```
62
293
 
63
- | Method | Parameters | Returns | Mô tả |
64
- | --------------------------------------------------------- | -------------------------------------- | ------- | --------------------------------------------------------------------------------------------------- |
65
- | `set setConfig` | `IConfig` | `void` | Đăng ký config (ViewContainerRef, container refs, component types). Phải gọi trước mọi method khác. |
66
- | `set setConfigCanvas` | `Partial<ICanvasConfig>` | `void` | Ghi đè các tham số canvas config (margin, kích thước, màu SVG, …). |
67
- | `configXYElements(elements, updateSVG?, positionX?)` | `IDiagramElement[], boolean?, number?` | `void` | Tính toán tọa độ X/Y cho tất cả elements và cập nhật SVG path. |
68
- | `renderNodes(elements)` | `IDiagramElement[]` | `void` | Clear nodes cũ, render component động, branch labels và arrow indicators. |
69
- | `clearNodes()` | — | `void` | Destroy tất cả ComponentRef, xóa node-dynamic, branch-label, arrow-indicator khỏi DOM. |
70
- | `renderDropZones(flatElements, onDrop, canDropFn, rules)` | Xem bên dưới | `void` | Render drop zone "+" tại các vị trí cho phép kéo-thả. |
71
- | `clearDropZones()` | — | `void` | Xóa tất cả `.diagram-drop-zone` khỏi nodesContainer. |
294
+ ---
72
295
 
73
- #### `renderDropZones`chi tiết
296
+ ### dụ 4 Virtualization cho diagram lớn
297
+
298
+ ```typescript
299
+ ngAfterViewInit() {
300
+ this.diagramService.setConfig = {
301
+ storeDataDefine: { ...storeDataDefault },
302
+ svgContainer: this.svgContainer(),
303
+ nodesContainer: this.nodesContainer(),
304
+ viewContainerRef: this.vcr(),
305
+ nodeComponentType: MyNodeComponent,
306
+ useVirtualizationTemplate: true, // Bật virtualization
307
+ };
308
+
309
+ this.diagramService.configXYElements(this.elements);
310
+ this.diagramService.renderNodes(this.elements);
311
+
312
+ // Cập nhật viewport khi scroll
313
+ const container = this.nodesContainer()?.nativeElement.parentElement;
314
+ container?.addEventListener('scroll', () => {
315
+ this.diagramService.updateViewport({
316
+ x: container.scrollLeft,
317
+ y: container.scrollTop,
318
+ width: container.clientWidth,
319
+ height: container.clientHeight,
320
+ });
321
+ });
322
+ }
323
+ ```
324
+
325
+ ---
326
+
327
+ ### Ví dụ 5 — Navigation line (kết nối tự do)
328
+
329
+ ```typescript
330
+ import {
331
+ LibsUiDiagramDrawDirectionService,
332
+ LibsUiDiagramDrawCanvasService,
333
+ LibsUiDiagramDrawService,
334
+ INavigationLineStyleConfig,
335
+ } from '@libs-ui/services-diagram-draw';
336
+
337
+ // Trong component:
338
+ private readonly directionService = inject(LibsUiDiagramDrawDirectionService);
339
+ private readonly canvasService = inject(LibsUiDiagramDrawCanvasService);
340
+ private readonly diagramService = inject(LibsUiDiagramDrawService);
341
+
342
+ // Thêm điều hướng từ element tới target
343
+ addDirection(sourceElement: IDiagramElement, targetId: string) {
344
+ const flatElements = this.canvasService.FlatElements;
345
+ this.directionService.addElementDirection(targetId, sourceElement, flatElements);
346
+ }
347
+
348
+ // Build dữ liệu navigation line để render qua draw-line directive
349
+ buildNavigationLines() {
350
+ const styleConfig: INavigationLineStyleConfig = {
351
+ lineStyle: { color: '#6366f1', width: 1.5 },
352
+ arrowStyle: { color: '#6366f1', size: 8 },
353
+ endpointGap: 8,
354
+ };
355
+ const navData = this.diagramService.buildNavigationData(
356
+ this.canvasService.FlatElements,
357
+ styleConfig,
358
+ );
359
+ // Truyền navData vào drawLineFunctionControl.setData(navData)
360
+ }
361
+ ```
362
+
363
+ ---
364
+
365
+ ## Methods — `LibsUiDiagramDrawService`
366
+
367
+ | Method | Signature | Mô tả |
368
+ |---|---|---|
369
+ | `set setConfig` | `(config: ICustomCanvasConfig): void` | Đăng ký config (container refs, component types). Phải gọi trước mọi method khác. |
370
+ | `set setConfigCanvas` | `(config: Partial<ICanvasConfig>): void` | Ghi đè tham số canvas config (margin, kích thước, màu SVG). |
371
+ | `configXYElements` | `(elements: IDiagramElement[], updateSVG?: boolean, positionX?: number): void` | Tính toán tọa độ X/Y cho tất cả elements và cập nhật SVG path. |
372
+ | `renderNodes` | `(elements: IDiagramElement[]): void` | Xóa node cũ, render component động, branch labels và arrow indicators. |
373
+ | `clearNodes` | `(): void` | Destroy tất cả ComponentRef, xóa `.diagram-node-dynamic`, `.branch-label`, `.node-arrow-indicator` khỏi DOM. |
374
+ | `renderDropZones` | `(flatElements, onDrop, canDropFn, rules): void` | Render drop zone "+" tại các vị trí cho phép kéo-thả. |
375
+ | `clearDropZones` | `(): void` | Xóa tất cả `.diagram-drop-zone` khỏi nodesContainer. |
376
+ | `updateViewport` | `(vp: { x, y, width, height }): void` | Cập nhật viewport để virtualization tính toán node hiển thị. Chỉ có hiệu lực khi `useVirtualizationTemplate: true`. |
377
+ | `buildNavigationData` | `(flatElements: IDiagramElement[], styleConfig?: INavigationLineStyleConfig): IDrawLineDataInput[]` | Build dữ liệu navigation line từ `next_other_id` để render qua draw-line directive. |
378
+
379
+ ### Chi tiết `renderDropZones`
74
380
 
75
381
  ```typescript
76
382
  renderDropZones(
77
383
  flatElements: IDiagramElement[],
78
384
  onDrop: (element: IDiagramElement, branch: IDiagramElementBranch | undefined, dragData: string) => void,
79
- canDropFn: TDropZoneCanDropFn, // true = ẨN drop zone, false = HIỂN THỊ
80
- ruleConnectableElements: IRuleConnectable[]
385
+ canDropFn: TDropZoneCanDropFn,
386
+ ruleConnectableElements: IRuleConnectable[],
81
387
  ): void
82
388
  ```
83
389
 
84
- ### Exported Constants
390
+ **Lưu ý:** `canDropFn` trả về `true` → **ẨN** drop zone; `false` → **HIỂN THỊ** drop zone (ngược với convention thông thường).
391
+
392
+ ---
85
393
 
86
- | Export | Type | Mô tả |
87
- | ---------------------- | ---------------------------------- | ---------------------------------------------------------------------------------- |
88
- | `canvasConfigReadonly` | `Signal<ICanvasConfig>` (readonly) | Signal đọc config canvas global, dùng trong component mà không cần inject service. |
89
- | `storeDataDefault` | `IDiagramStoreData` | Giá trị mặc định cho `storeDataDefine`. |
394
+ ## Methods `LibsUiDiagramDrawCanvasService`
395
+
396
+ | Method | Signature | tả |
397
+ |---|---|---|
398
+ | `loadElements` | `(elements: IDiagramElement[], positionFirstNode: { x, y }, rules: IRuleConnectable[]): void` | Khởi tạo danh sách elements, đặt vị trí node đầu tiên, lưu rule kết nối. |
399
+ | `addElement` | `(itemDrop, branchDrop, dataDrop, branchChoice?): void` | Thêm element vào sau `itemDrop`, tự động tạo EXIT node cho WORKFLOW. |
400
+ | `removeOrChangeElement` | `(elementRemove, elementChange, branchChoose?): Promise<void>` | Xóa hoặc thay thế element, tự động thêm EXIT khi cần. |
401
+ | `deleteChangeElementAndInsertBranchKeep` | `(elementRemove, elementChange, branchKeepElement, branchChoice?): Promise<void>` | Xóa element và giữ nguyên nhánh chỉ định. |
402
+ | `addElementExitForAllElementPreviousWhenElementDelete` | `(elementsDelete: IDiagramElement[]): void` | Khi xóa hàng loạt element, tự động thêm EXIT cho các element đang trỏ vào vùng bị xóa. |
403
+ | `checkRuleDragDrop` | `(before, current, after, rules): boolean` | Kiểm tra rule kết nối trước khi cho phép drop. |
404
+ | `checkExistNameElement` | `(name: string, flatElements: IDiagramElement[]): boolean` | Kiểm tra tên element đã tồn tại chưa (case-insensitive). |
405
+ | `get FlatElements` | `IDiagramElement[]` | Trả về flat list của tất cả element (bao gồm element trong branches). |
406
+ | `set Elements` | `(elements: IDiagramElement[]): void` | Gán lại root elements array. |
407
+ | `get Elements` | `IDiagramElement[]` | Lấy root elements array. |
408
+ | `set HandlerFunction` | `(fn: IHandlerFunction): void` | Đăng ký callback tạo default config branches cho WORKFLOW element. |
409
+
410
+ ---
411
+
412
+ ## Methods — `LibsUiDiagramDrawDirectionService`
413
+
414
+ | Method | Signature | Mô tả |
415
+ |---|---|---|
416
+ | `buildElementConnectTo` | `(element, flatElements, branch?, checkRule?): IDiagramElement[]` | Build danh sách element có thể kết nối tới. |
417
+ | `addElementDirection` | `(targetId: string, element: IDiagramElement, flatElements: IDiagramElement[]): void` | Thêm điều hướng từ element tới target (cập nhật `next_other_id` và `pre_other_id`). |
418
+ | `addBranchDirection` | `(targetId, element, branch, flatElements): void` | Thêm điều hướng từ branch của WORKFLOW element tới target. |
419
+ | `editDirection` | `(flatElements, targetId, element, branch?): void` | Thay đổi điều hướng hiện tại sang target mới. |
420
+ | `deleteElementsBehindElementConnectTo` | `(element, flatElements): void` | Xóa tất cả element phía sau khi element chuyển sang điều hướng. |
421
+ | `deleteElementsBehindBranchConnectTo` | `(branch: IDiagramElementBranch): void` | Xóa tất cả element trong branch khi branch chuyển sang điều hướng. |
422
+ | `set ElementDirection` | `(element: IDiagramElement | undefined): void` | Lưu element đang chọn điều hướng. |
423
+ | `get ElementDirection` | `IDiagramElement | undefined` | Lấy element đang chọn điều hướng. |
424
+ | `set BranchDirection` | `(branch: { branch, index } | undefined): void` | Lưu branch đang chọn điều hướng. |
425
+ | `get OnViewDirection` | `Subject<{ id: string }>` | Observable phát khi cần highlight element đang được xem qua điều hướng. |
426
+
427
+ ---
428
+
429
+ ## Exported Constants
430
+
431
+ | Export | Type | Mô tả |
432
+ |---|---|---|
433
+ | `canvasConfigReadonly` | `Signal<ICanvasConfig>` (readonly) | Signal đọc canvas config global — dùng trong component mà không cần inject service. |
434
+ | `storeDataDefault` | `IDiagramStoreData` | Giá trị mặc định cho `storeDataDefine` trong `ICustomCanvasConfig`. |
90
435
 
91
436
  ---
92
437
 
93
438
  ## Canvas Config (`ICanvasConfig`)
94
439
 
95
- Tuỳ chỉnh qua `setConfigCanvas`:
96
-
97
- | Property | Default | Mô tả |
98
- | ------------------------------------------------ | ------------ | ------------------------------------------------------- |
99
- | `ELEMENT_WIDTH_DEFAULT_BRANCH_WHEN_NOT_ELEMENT` | `60` | Chiều rộng branch rỗng |
100
- | `ELEMENT_MARGIN_DEFAULT` | `60` | Margin dọc giữa các element |
101
- | `ELEMENT_MARGIN_DEFAULT_BRANCH` | `16` | Margin element trong branch |
102
- | `ELEMENT_MARGIN_BETWEEN_BRANCH_DEFAULT` | `60` | Margin ngang giữa các branch |
103
- | `DEFAULT_BRANCH_WHEN_NO_ELEMENT` | `100` | Chiều cao branch khi rỗng |
104
- | `ELEMENT_HEIGHT_CURVE` | `10` | Độ cong SVG connector |
105
- | `ELEMENT_SVG_STROKE_COLOR` | `'9ca2ad'` | Màu đường kẻ SVG (không có `#`) |
106
- | `ELEMENT_SVG_STROKE_WIDTH` | `'1'` | Độ dày đường kẻ SVG |
107
- | `ELEMENT_MARGIN_DEFAULT_BRANCH_TO_ELEMENT_FIRST` | `78` | Khoảng cách từ parent đến element đầu tiên trong branch |
108
- | `DISTANCE_TO_WAIT` | `30` | Khoảng cách element chính nodeOtherConfig |
109
- | `WAIT_TO_ELEMENT` | `50` | Khoảng cách nodeOtherConfig element kế tiếp |
110
- | `ELEMENT_WAIT_DEFAULT` | `28` | Chiều cao mặc định nodeOtherConfig |
111
- | `DISTANCE_WAIT_TO_NEXT_LINE` | `4` | Khoảng cách nodeOtherConfig đường nối kế tiếp |
112
- | `TYPE_ELEMENT_EXIT` | `'EXIT'` | Loại element kết thúc (không có drop zone bên dưới) |
113
- | `TYPE_ELEMENT_WORKFLOW` | `'WORKFLOW'` | Loại element có branches |
114
- | `WIDTH_ELEMENT_DEFAULT` | `165` | Chiều rộng mặc định node |
115
- | `HEIGHT_ELEMENT_DEFAULT` | `48` | Chiều cao mặc định node |
116
- | `ADD_ICON_DIAMETER` | `24` | Đường kính icon "+" drop zone |
440
+ Tùy chỉnh qua `set setConfigCanvas`:
441
+
442
+ | Property | Default | Mô tả |
443
+ |---|---|---|
444
+ | `ELEMENT_WIDTH_DEFAULT_BRANCH_WHEN_NOT_ELEMENT` | `60` | Chiều rộng branch rỗng (không có element) |
445
+ | `ELEMENT_MARGIN_DEFAULT` | `60` | Khoảng cách dọc giữa các element (vertical) hoặc ngang (horizontal) |
446
+ | `ELEMENT_MARGIN_DEFAULT_BRANCH` | `16` | Khoảng cách từ element nhánh đến đường rẽ nhánh |
447
+ | `ELEMENT_MARGIN_BETWEEN_BRANCH_DEFAULT` | `60` | Khoảng cách ngang giữa các branch (tối thiểu 32 để tương thích navigation line) |
448
+ | `DEFAULT_BRANCH_WHEN_NO_ELEMENT` | `100` | Chiều cao/rộng branch khi không có element |
449
+ | `ELEMENT_HEIGHT_CURVE` | `10` | Độ cong của SVG connector |
450
+ | `ELEMENT_SVG_STROKE_COLOR` | `'9ca2ad'` | Màu đường kẻ SVG (không có ký tự `#` đứng trước) |
451
+ | `ELEMENT_SVG_STROKE_WIDTH` | `'1'` | Độ dày đường kẻ SVG |
452
+ | `ELEMENT_MARGIN_DEFAULT_BRANCH_TO_ELEMENT_FIRST` | `78` | Khoảng cách từ element cha đến element đầu tiên trong branch |
453
+ | `DISTANCE_TO_WAIT` | `30` | Khoảng cách từ element chính đến `nodeOtherConfig` |
454
+ | `WAIT_TO_ELEMENT` | `50` | Khoảng cách từ `nodeOtherConfig` đến element kế tiếp |
455
+ | `ELEMENT_WAIT_DEFAULT` | `28` | Chiều cao mặc định của `nodeOtherConfig` |
456
+ | `DISTANCE_WAIT_TO_NEXT_LINE` | `4` | Khoảng cách từ `nodeOtherConfig` đến đường nối tiếp theo |
457
+ | `TYPE_ELEMENT_EXIT` | `'EXIT'` | Giá trị `code`/`element_type` cho element kết thúc (không có drop zone bên dưới) |
458
+ | `TYPE_ELEMENT_WORKFLOW` | `'WORKFLOW'` | Giá trị `element_type` cho element có branches |
459
+ | `WIDTH_ELEMENT_DEFAULT` | `165` | Chiều rộng mặc định node (dùng khi `specific_width` không được set) |
460
+ | `HEIGHT_ELEMENT_DEFAULT` | `48` | Chiều cao mặc định node (dùng khi `specific_height` không được set) |
461
+ | `ADD_ICON_DIAMETER` | `24` | Đường kính của icon "+" trong drop zone |
462
+
463
+ Ví dụ tùy chỉnh:
464
+
465
+ ```typescript
466
+ this.diagramService.setConfigCanvas = {
467
+ ELEMENT_MARGIN_DEFAULT: 80,
468
+ ELEMENT_SVG_STROKE_COLOR: '6366f1',
469
+ WIDTH_ELEMENT_DEFAULT: 200,
470
+ HEIGHT_ELEMENT_DEFAULT: 56,
471
+ };
472
+ ```
117
473
 
118
474
  ---
119
475
 
120
- ## Interfaces
476
+ ## Types & Interfaces
477
+
478
+ ```typescript
479
+ import type {
480
+ IDiagramElement,
481
+ IDiagramElementBranch,
482
+ ICanvasConfig,
483
+ ICustomCanvasConfig,
484
+ IDiagramStoreData,
485
+ IDropZoneIndicatorStyle,
486
+ IRuleConnectable,
487
+ IHandlerFunction,
488
+ TDropZoneCanDropFn,
489
+ INavigationLineStyleConfig,
490
+ } from '@libs-ui/services-diagram-draw';
491
+ ```
492
+
493
+ ### `IDiagramElement`
121
494
 
122
495
  ```typescript
123
496
  interface IDiagramElement {
124
- code?: string; // Loại element (EXIT, WORKFLOW, …)
125
- name?: string; // Tên hiển thị
126
- id?: string; // Unique identifier
127
- element_type?: string; // Type phân loại
128
- branches?: IDiagramElementBranch[];
129
- translateX?: number; // Tọa độ X (computed)
130
- translateY?: number; // Tọa độ Y (computed)
131
- specific_x?: number;
132
- specific_y?: number;
133
- specific_width?: number;
134
- specific_height?: number;
135
- attributeSvgD?: string; // SVG path string connector
136
- nodeOtherConfig?: IDiagramElement; // Node phụ (wait/delay)
137
- pre_id?: string;
138
- next_id?: string;
139
- disable?: boolean;
497
+ id?: string; // Unique identifier
498
+ code?: string; // Loại element (VD: 'EMAIL', 'SMS', 'EXIT', 'CONDITION')
499
+ name?: string; // Tên hiển thị
500
+ element_type?: string; // 'WORKFLOW' cho element có nhánh, 'EXIT' cho kết thúc
501
+ branches?: IDiagramElementBranch[]; // Danh sách nhánh (chỉ có khi element_type = 'WORKFLOW')
502
+ next_id?: string; // ID element kế tiếp trong luồng chính
503
+ pre_id?: string; // ID element trước trong luồng chính
504
+ next_other_id?: string[]; // ID các element được điều hướng đến (kết nối tự do)
505
+ pre_other_id?: string[]; // ID các element điều hướng đến element này
506
+ nodeOtherConfig?: IDiagramElement; // Node phụ (VD: wait node, delay config)
507
+ specific_x?: number; // Vị trí X do người dùng/service set trước khi tính toán
508
+ specific_y?: number; // Vị trí Y do người dùng/service set trước khi tính toán
509
+ specific_width?: number; // Chiều rộng node (ghi đè WIDTH_ELEMENT_DEFAULT)
510
+ specific_height?: number; // Chiều cao node (ghi đè HEIGHT_ELEMENT_DEFAULT)
511
+ translateX?: number; // Tọa độ X cuối cùng sau khi tính toán (computed)
512
+ translateY?: number; // Tọa độ Y cuối cùng sau khi tính toán (computed)
513
+ attributeSvgD?: string; // SVG path string cho connector
514
+ subCodeOfElement?: string; // Sub-type code dùng khi element có điều hướng
515
+ position_end?: number; // Vị trí cuối của đường nối (computed)
516
+ disable?: boolean; // Ẩn/vô hiệu hóa element
517
+ specific_counter_current_code?: number; // Counter đếm số lượng element cùng code
140
518
  }
519
+ ```
520
+
521
+ ### `IDiagramElementBranch`
141
522
 
523
+ ```typescript
142
524
  interface IDiagramElementBranch {
143
- code?: string;
144
- label?: string; // Label hiển thị trên nhánh
145
- labelBgColor?: string; // Màu nền label
146
- elements: IDiagramElement[];
147
- onTheSide?: 'above' | 'under' | 'center';
148
- specific_start_branch_x?: number;
149
- specific_start_branch_y?: number;
150
- nodeOtherConfig?: IDiagramElement;
151
- isDirection?: boolean;
525
+ code?: string; // Định danh nhánh (VD: 'YES', 'NO')
526
+ label?: string; // Nhãn hiển thị trên nhánh (VD: 'Đúng', 'Sai')
527
+ labelBgColor?: string; // Màu nền nhãn (VD: '#dcfce7')
528
+ labelColor?: string; // Màu chữ nhãn (VD: '#15803d')
529
+ labelClassName?: string; // CSS class cho nhãn (mặc định: 'libs-ui-font-h7r')
530
+ elements: IDiagramElement[]; // Danh sách element trong nhánh
531
+ onTheSide?: 'above' | 'under' | 'center'; // Vị trí nhãn tương đối với nhánh
532
+ specific_start_branch_x?: number; // Tọa độ X điểm đầu đường nhánh (computed)
533
+ specific_start_branch_y?: number; // Tọa độ Y điểm đầu đường nhánh (computed)
534
+ next_other_id?: string[]; // ID các element được điều hướng từ nhánh này
535
+ isDirection?: boolean; // Nhánh đang ở chế độ điều hướng
536
+ nodeOtherConfig?: IDiagramElement; // Node phụ tại điểm rẽ nhánh
152
537
  }
538
+ ```
153
539
 
154
- interface IConfig {
155
- storeDataDefine: IDiagramStoreData;
156
- svgContainer?: ElementRef<HTMLDivElement>;
157
- nodesContainer?: ElementRef<HTMLDivElement>;
158
- viewContainerRef?: ViewContainerRef;
159
- nodeComponentType?: Type<any>;
160
- nodeOtherConfigComponentType?: Type<any>;
161
- dropZoneStyle?: IDropZoneIndicatorStyle; // Tuỳ chỉnh indicator "+"
540
+ ### `ICustomCanvasConfig`
541
+
542
+ ```typescript
543
+ interface ICustomCanvasConfig {
544
+ storeDataDefine: IDiagramStoreData; // Lưu trữ tọa độ max — dùng storeDataDefault
545
+ svgContainer?: ElementRef<HTMLDivElement>; // Ref đến SVG container để vẽ đường nối
546
+ nodesContainer?: ElementRef<HTMLDivElement>; // Ref đến div chứa node component
547
+ viewContainerRef?: ViewContainerRef; // ViewContainerRef để tạo component động
548
+ nodeComponentType?: Type<any>; // Component type cho main node
549
+ nodeOtherConfigComponentType?: Type<any>; // Component type cho nodeOtherConfig
550
+ useVirtualizationTemplate?: boolean; // Bật virtualization (mặc định: false)
551
+ dropZoneStyle?: IDropZoneIndicatorStyle; // Tùy biến giao diện indicator "+"
162
552
  }
553
+ ```
554
+
555
+ ### `IRuleConnectable`
163
556
 
557
+ ```typescript
164
558
  interface IRuleConnectable {
165
- elementCode: string;
166
- elementCodeConnectableBefore: string[];
559
+ elementCode: string; // Code của element cần kiểm tra
560
+ elementCodeConnectableBefore: string[]; // Danh sách code được phép đứng trước
167
561
  }
168
-
169
- type TDropZoneCanDropFn = (before: IDiagramElement | undefined, current: IDiagramElement | undefined, after: IDiagramElement | undefined, rules: IRuleConnectable[]) => boolean;
170
562
  ```
171
563
 
172
- ---
564
+ ### `IHandlerFunction`
173
565
 
174
- ## Tuỳ chỉnh indicator "+" (`IDropZoneIndicatorStyle`)
566
+ ```typescript
567
+ interface IHandlerFunction {
568
+ getConfigDefaultBranches(code: string, dataDefault: IDiagramElement): IDiagramElement;
569
+ }
570
+ ```
175
571
 
176
- Truyền vào `dropZoneStyle` của `IConfig` để kiểm soát giao diện nút `"+"` trong drop zone.
177
- Tất cả các field đều optional — giá trị nào không truyền sẽ dùng mặc định nội bộ của service.
572
+ ### `TDropZoneCanDropFn`
178
573
 
179
- ### Thứ tự ưu tiên render
574
+ ```typescript
575
+ type TDropZoneCanDropFn = (
576
+ beforeElementDrag: IDiagramElement | undefined, // Element trước vị trí drop
577
+ currentElementDrag: IDiagramElement | undefined, // Element tại vị trí drop
578
+ afterElementDrag: IDiagramElement | undefined, // Element sau vị trí drop
579
+ ruleConnectableElements: IRuleConnectable[],
580
+ ) => boolean; // true = ẨN drop zone, false = HIỂN THỊ
581
+ ```
180
582
 
181
- | Ưu tiên | Điều kiện | Kết quả |
182
- | ------- | ----------------------------- | ------------------------------------------------------------------- |
183
- | **1** | `renderIndicator` được truyền | Toàn bộ HTML indicator do hàm này sinh ra |
184
- | **2** | Không có `renderIndicator` | SVG `"+"` tự sinh từ `backgroundColor`, `iconColor`, `borderRadius` |
583
+ ### `INavigationLineStyleConfig`
185
584
 
186
- | Ưu tiên | Điều kiện | Kết quả hover |
187
- | ------- | ---------------------------------- | ----------------------------------------------- |
188
- | **1** | `renderHoverIndicator` được truyền | Nội dung bên trong rectangle do hàm này sinh ra |
189
- | **2** | Không `renderHoverIndicator` | Rectangle mở rộng trống (chỉ có nền + viền) |
585
+ ```typescript
586
+ interface INavigationLineStyleConfig {
587
+ lineStyle?: ILineStyle; // Style đường kẻ (color, width, dash)
588
+ arrowStyle?: IArrowStyle; // Style mũi tên (color, size)
589
+ endpointGap?: number; // Khoảng cách từ cạnh node đến endpoint (mặc định: 8)
590
+ modeResolver?: (start: { x, y }, end: { x, y }) => TYPE_MODE; // Override chọn kiểu đường
591
+ }
592
+ ```
190
593
 
191
- ### Bảng thuộc tính
594
+ ---
192
595
 
193
- #### Indicator (trạng thái bình thường)
596
+ ## Tùy biến indicator "+" (`IDropZoneIndicatorStyle`)
194
597
 
195
- | Thuộc tính | Kiểu | Mặc định | tả |
196
- | ----------------- | -------------------------- | ----------- | -------------------------------------------------------------------------------------------- |
197
- | `backgroundColor` | `string` | `'#3b82f6'` | Màu nền của indicator |
198
- | `iconColor` | `string` | `'#ffffff'` | Màu dấu `"+"` bên trong indicator |
199
- | `borderRadius` | `string` | `'50%'` | Bo góc indicator — `'50%'` cho tròn, `'6px'` cho vuông bo góc |
200
- | `opacity` | `number` | `0.95` | Độ mờ của indicator ở trạng thái bình thường |
201
- | `renderIndicator` | `(size: number) => string` | — | **[Động]** Hàm trả về HTML tùy ý thay thế toàn bộ indicator. Ghi đè mọi field tĩnh bên trên. |
598
+ Truyền vào `dropZoneStyle` của `ICustomCanvasConfig` để kiểm soát giao diện nút "+" trong drop zone.
202
599
 
203
- #### Hover / drag-enter (rectangle mở rộng)
600
+ ### Trạng thái bình thường
204
601
 
205
- | Thuộc tính | Kiểu | Mặc định | Mô tả |
206
- | ---------------------- | -------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------- |
207
- | `hoverBackgroundColor` | `string` | `'#eef4ff'` | Màu nền rectangle khi kéo vào vùng drop |
208
- | `hoverBorderColor` | `string` | `'rgba(59, 130, 246, 0.7)'` | Màu đường viền rectangle |
209
- | `hoverBorderStyle` | `string` | `'dashed'` | Kiểu viền — `'dashed'` \| `'solid'` \| `'dotted'` |
210
- | `hoverBorderRadius` | `number` | `10` | Border-radius (px) của rectangle |
211
- | `renderHoverIndicator` | `() => string` | — | **[Động]** Hàm trả về HTML inject vào bên trong rectangle. Nếu không truyền, rectangle chỉ hiển thị nền trống. |
602
+ | Thuộc tính | Type | Mặc định | Mô tả |
603
+ |---|---|---|---|
604
+ | `backgroundColor` | `string` | `'#3b82f6'` | Màu nền của nút "+" |
605
+ | `iconColor` | `string` | `'#ffffff'` | Màu dấu "+" bên trong |
606
+ | `borderRadius` | `string` | `'50%'` | Bo góc — `'50%'` tròn, `'6px'` vuông |
607
+ | `opacity` | `number` | `0.95` | Độ mờ trạng thái bình thường |
608
+ | `renderIndicator` | `(size: number) => string` | — | Hàm trả về HTML tùy ý, ghi đè toàn bộ indicator |
212
609
 
213
- ### dụ
610
+ ### Trạng thái hover/drag-enter
214
611
 
215
- **Chỉ đổi màu bo góc:**
612
+ | Thuộc tính | Type | Mặc định | Mô tả |
613
+ |---|---|---|---|
614
+ | `hoverBackgroundColor` | `string` | `'#eef4ff'` | Màu nền vùng highlight khi kéo vào |
615
+ | `hoverBorderColor` | `string` | `'rgba(59, 130, 246, 0.7)'` | Màu viền vùng highlight |
616
+ | `hoverBorderStyle` | `string` | `'dashed'` | Kiểu viền: `'dashed'`, `'solid'`, `'dotted'` |
617
+ | `hoverBorderRadius` | `number` | `10` | Border-radius (px) của vùng highlight |
618
+ | `renderHoverIndicator` | `() => string` | — | Hàm trả về HTML inject vào bên trong vùng highlight |
619
+
620
+ Ví dụ tùy biến màu sắc:
216
621
 
217
622
  ```typescript
218
623
  this.diagramService.setConfig = {
219
- // ... các config khác
624
+ storeDataDefine: { ...storeDataDefault },
625
+ svgContainer: this.svgContainer(),
626
+ nodesContainer: this.nodesContainer(),
627
+ viewContainerRef: this.vcr(),
628
+ nodeComponentType: MyNodeComponent,
220
629
  dropZoneStyle: {
221
630
  backgroundColor: '#10b981',
222
631
  iconColor: '#ffffff',
223
- borderRadius: '4px', // hình vuông bo góc thay vì tròn
632
+ borderRadius: '4px',
224
633
  opacity: 1,
225
634
  hoverBackgroundColor: '#f0fdf4',
226
635
  hoverBorderColor: 'rgba(16, 185, 129, 0.8)',
@@ -230,11 +639,15 @@ this.diagramService.setConfig = {
230
639
  };
231
640
  ```
232
641
 
233
- **Render indicator hoàn toàn tùy chỉnh:**
642
+ dụ render indicator hoàn toàn tùy chỉnh:
234
643
 
235
644
  ```typescript
236
645
  this.diagramService.setConfig = {
237
- // ... các config khác
646
+ storeDataDefine: { ...storeDataDefault },
647
+ svgContainer: this.svgContainer(),
648
+ nodesContainer: this.nodesContainer(),
649
+ viewContainerRef: this.vcr(),
650
+ nodeComponentType: MyNodeComponent,
238
651
  dropZoneStyle: {
239
652
  renderIndicator: (size: number) => `
240
653
  <div style="
@@ -242,8 +655,8 @@ this.diagramService.setConfig = {
242
655
  background: #f59e0b; border-radius: 6px;
243
656
  display: flex; align-items: center; justify-content: center;
244
657
  ">
245
- <svg viewBox="0 0 24 24" fill="white" width="14" height="14">
246
- <path d="M12 5v14M5 12h14" stroke="white" stroke-width="2"/>
658
+ <svg viewBox="0 0 24 24" fill="none" width="14" height="14">
659
+ <path d="M12 5v14M5 12h14" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
247
660
  </svg>
248
661
  </div>
249
662
  `,
@@ -252,9 +665,9 @@ this.diagramService.setConfig = {
252
665
  + Thêm vào đây
253
666
  </span>
254
667
  `,
255
- hoverBackgroundColor: '#fafafa',
256
- hoverBorderColor: 'rgba(139, 92, 246, 0.5)',
257
- hoverBorderStyle: 'solid',
668
+ hoverBackgroundColor: '#fffbeb',
669
+ hoverBorderColor: 'rgba(245, 158, 11, 0.6)',
670
+ hoverBorderStyle: 'dashed',
258
671
  hoverBorderRadius: 12,
259
672
  },
260
673
  };
@@ -264,16 +677,28 @@ this.diagramService.setConfig = {
264
677
 
265
678
  ## Lưu ý quan trọng
266
679
 
267
- > ⚠️ Phải gọi `setConfig` trước khi dùng bất kỳ method nào khác.
680
+ ⚠️ **Thứ tự khởi tạo bắt buộc**: Phải gọi `set setConfig` trước mọi method khác của `LibsUiDiagramDrawService`.
681
+
682
+ ⚠️ **canvasConfig là signal global**: Thay đổi qua `set setConfigCanvas` sẽ ảnh hưởng đến mọi instance trong cùng app. Cần cẩn thận khi có nhiều diagram trên cùng trang.
268
683
 
269
- > ⚠️ `canvasConfig` signal **global** thay đổi sẽ ảnh hưởng đến mọi instance trong cùng app.
684
+ ⚠️ **`renderNodes()` tự gọi `clearNodes()` trước**: Không cần gọi thủ công trước khi render lại.
270
685
 
271
- > ⚠️ `renderNodes()` sẽ tự gọi `clearNodes()` trước khi render lại không cần gọi thủ công trước.
686
+ ⚠️ **`canDropFn` logic ngược**: Trả về `true` ẨN drop zone; `false` HIỂN THỊ drop zone. Đây thiết kế chủ ý để hàm đóng vai trò "filter loại trừ".
272
687
 
273
- > ⚠️ `canDropFn` trả về `true` **ẨN** drop zone; `false` **HIỂN THỊ** drop zone.
688
+ ⚠️ **`ELEMENT_SVG_STROKE_COLOR` không tự `#`**: Giá trị phải hex thuần (VD: `'9ca2ad'`, không phải `'#9ca2ad'`).
689
+
690
+ ⚠️ **Virtualization yêu cầu scroll listener**: Khi bật `useVirtualizationTemplate: true`, cần tự lắng nghe sự kiện scroll của container và gọi `updateViewport()` để service cập nhật node hiển thị.
691
+
692
+ ⚠️ **`ELEMENT_MARGIN_BETWEEN_BRANCH_DEFAULT` tối thiểu 32**: Giá trị dưới 32px chưa tương thích với navigation line rendering.
693
+
694
+ ⚠️ **`loadElements` phải gọi trước `configXYElements`**: `LibsUiDiagramDrawCanvasService.loadElements()` khởi tạo vị trí node đầu tiên, cần được gọi trước khi service tính toán tọa độ.
274
695
 
275
696
  ---
276
697
 
277
698
  ## Demo
278
699
 
279
- Xem demo tại: `services/diagram-draw` trong core-ui app.
700
+ ```bash
701
+ npx nx serve core-ui
702
+ ```
703
+
704
+ Truy cập: `http://localhost:4500/services/diagram-draw`