@sd-angular/core 19.0.0-beta.55 → 19.0.0-beta.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/assets/scss/core/form.scss +4 -1
  2. package/components/button/src/button.component.d.ts +5 -1
  3. package/components/document-builder/src/document-builder.component.d.ts +3 -14
  4. package/components/document-builder/src/document-builder.model.d.ts +4 -1
  5. package/components/document-builder/src/plugins/ck-comment/ck-comment.plugin.model.d.ts +1 -0
  6. package/components/document-builder/src/plugins/variable/variable.plugin.d.ts +39 -0
  7. package/components/section/src/section.component.d.ts +1 -1
  8. package/components/side-drawer/src/side-drawer.component.d.ts +11 -23
  9. package/components/table/src/components/desktop-cell/desktop-cell.component.d.ts +12 -2
  10. package/components/table/src/components/desktop-cell-view/desktop-cell-view.component.d.ts +12 -2
  11. package/components/table/src/components/selector-action/selector-action.component.d.ts +5 -3
  12. package/components/table/src/models/table-column.model.d.ts +1 -0
  13. package/components/table/src/models/table-option-config.model.d.ts +5 -0
  14. package/fesm2022/sd-angular-core-components-badge.mjs +11 -11
  15. package/fesm2022/sd-angular-core-components-badge.mjs.map +1 -1
  16. package/fesm2022/sd-angular-core-components-button.mjs +8 -2
  17. package/fesm2022/sd-angular-core-components-button.mjs.map +1 -1
  18. package/fesm2022/sd-angular-core-components-document-builder.mjs +458 -186
  19. package/fesm2022/sd-angular-core-components-document-builder.mjs.map +1 -1
  20. package/fesm2022/sd-angular-core-components-import-excel.mjs +1 -1
  21. package/fesm2022/sd-angular-core-components-import-excel.mjs.map +1 -1
  22. package/fesm2022/sd-angular-core-components-preview.mjs +1 -1
  23. package/fesm2022/sd-angular-core-components-preview.mjs.map +1 -1
  24. package/fesm2022/sd-angular-core-components-section.mjs +3 -3
  25. package/fesm2022/sd-angular-core-components-section.mjs.map +1 -1
  26. package/fesm2022/sd-angular-core-components-side-drawer.mjs +70 -89
  27. package/fesm2022/sd-angular-core-components-side-drawer.mjs.map +1 -1
  28. package/fesm2022/sd-angular-core-components-table.mjs +88 -26
  29. package/fesm2022/sd-angular-core-components-table.mjs.map +1 -1
  30. package/fesm2022/sd-angular-core-components-upload-file.mjs +1 -1
  31. package/fesm2022/sd-angular-core-components-upload-file.mjs.map +1 -1
  32. package/fesm2022/sd-angular-core-components-workflow.mjs +23 -23
  33. package/fesm2022/sd-angular-core-components-workflow.mjs.map +1 -1
  34. package/fesm2022/sd-angular-core-forms-chip-calendar.mjs +5 -2
  35. package/fesm2022/sd-angular-core-forms-chip-calendar.mjs.map +1 -1
  36. package/fesm2022/sd-angular-core-forms-chip.mjs +5 -2
  37. package/fesm2022/sd-angular-core-forms-chip.mjs.map +1 -1
  38. package/fesm2022/sd-angular-core-forms-date-range.mjs +4 -3
  39. package/fesm2022/sd-angular-core-forms-date-range.mjs.map +1 -1
  40. package/fesm2022/sd-angular-core-forms-date.mjs +4 -3
  41. package/fesm2022/sd-angular-core-forms-date.mjs.map +1 -1
  42. package/fesm2022/sd-angular-core-forms-datetime.mjs +4 -3
  43. package/fesm2022/sd-angular-core-forms-datetime.mjs.map +1 -1
  44. package/fesm2022/sd-angular-core-forms-input-number.mjs +3 -2
  45. package/fesm2022/sd-angular-core-forms-input-number.mjs.map +1 -1
  46. package/fesm2022/sd-angular-core-forms-input.mjs +6 -4
  47. package/fesm2022/sd-angular-core-forms-input.mjs.map +1 -1
  48. package/fesm2022/sd-angular-core-forms-select.mjs +3 -2
  49. package/fesm2022/sd-angular-core-forms-select.mjs.map +1 -1
  50. package/fesm2022/sd-angular-core-forms-textarea.mjs +3 -2
  51. package/fesm2022/sd-angular-core-forms-textarea.mjs.map +1 -1
  52. package/fesm2022/sd-angular-core-modules-layout.mjs +5 -5
  53. package/fesm2022/sd-angular-core-modules-layout.mjs.map +1 -1
  54. package/fesm2022/sd-angular-core-services-confirm.mjs +1 -1
  55. package/fesm2022/sd-angular-core-services-confirm.mjs.map +1 -1
  56. package/fesm2022/sd-angular-core-services-notify.mjs.map +1 -1
  57. package/forms/chip/src/chip.component.d.ts +3 -2
  58. package/forms/chip-calendar/src/chip-calendar.component.d.ts +3 -2
  59. package/forms/date/src/date.component.d.ts +3 -2
  60. package/forms/date-range/src/date-range.component.d.ts +3 -2
  61. package/forms/datetime/src/datetime.component.d.ts +3 -2
  62. package/forms/input/src/input.component.d.ts +3 -2
  63. package/forms/input-number/src/input-number.component.d.ts +3 -2
  64. package/forms/select/src/select.component.d.ts +3 -2
  65. package/forms/textarea/src/textarea.component.d.ts +3 -2
  66. package/modules/layout/pipes/high-light-search.pipe.d.ts +1 -1
  67. package/package.json +55 -55
  68. package/sd-angular-core-19.0.0-beta.57.tgz +0 -0
  69. package/services/notify/index.d.ts +1 -0
  70. package/services/notify/src/notify.model.d.ts +1 -1
  71. package/services/notify/src/notify.service.d.ts +5 -5
  72. package/sd-angular-core-19.0.0-beta.55.tgz +0 -0
@@ -364,7 +364,10 @@ function createHeadingHandler(editor, modelName, headingDefaults) {
364
364
  }
365
365
 
366
366
  class VariablePlugin extends Plugin {
367
- // Khai báo dependencies nếu cần (VD: Widget)
367
+ // Tên plugin đăng với CKEditor — bắt buộc để tìm kiếm bằng string và ổn định trong build minified
368
+ static get pluginName() {
369
+ return 'VariablePlugin';
370
+ }
368
371
  static get requires() {
369
372
  return [Widget];
370
373
  }
@@ -379,9 +382,11 @@ class VariablePlugin extends Plugin {
379
382
  allowWhere: '$text',
380
383
  isInline: true,
381
384
  isObject: true,
382
- allowAttributes: ['id', 'uuid', 'value', 'display'],
385
+ allowAttributes: ['id', 'uuid', 'value', 'display', 'bindingValue'],
383
386
  });
384
387
  // 2. Model -> HTML
388
+ // model là string 'variable' → chỉ chạy khi element được TẠO MỚI (không reconvert khi attribute đổi)
389
+ // Luôn render trạng thái UNBOUND tại đây — binding state được xử lý riêng bởi converter bên dưới
385
390
  conversion.for('downcast').elementToElement({
386
391
  model: 'variable',
387
392
  view: (modelItem, { writer: viewWriter }) => {
@@ -395,18 +400,40 @@ class VariablePlugin extends Plugin {
395
400
  'data-uuid': uuid,
396
401
  'data-value': value,
397
402
  'data-display': display,
403
+ 'data-binding': 'false', // Luôn bắt đầu unbound — binding chỉ set qua bindValue()
398
404
  });
399
405
  const innerText = viewWriter.createText(`{{${display}}}`);
400
406
  viewWriter.insert(viewWriter.createPositionAt(widgetElement, 0), innerText);
401
407
  return toWidget(widgetElement, viewWriter);
402
408
  },
403
409
  });
410
+ // 2b. One-way attribute converter cho bindingValue (chỉ editing view, không ảnh hưởng getData())
411
+ // Chỉ chạy khi bindValue() / clearValue() gọi model.change → setAttribute/removeAttribute
412
+ // Binding được persist vào HTML (getData() và setData() có binding state)
413
+ // conversion.for('downcast') ảnh hưởng cả editing view và data view (getData)
414
+ conversion.for('downcast').add(dispatcher => {
415
+ dispatcher.on('attribute:bindingValue:variable', (evt, data, conversionApi) => {
416
+ if (!conversionApi.consumable.consume(data.item, evt.name))
417
+ return;
418
+ const viewWriter = conversionApi.writer;
419
+ const viewElement = conversionApi.mapper.toViewElement(data.item);
420
+ if (!viewElement)
421
+ return;
422
+ const bindingValue = data.attributeNewValue;
423
+ const isBound = !!bindingValue;
424
+ const display = data.item.getAttribute('display');
425
+ // Cập nhật data-binding attribute trên container span
426
+ viewWriter.setAttribute('data-binding', isBound ? 'true' : 'false', viewElement);
427
+ // Xóa text node cũ và chèn text mới (bound value hoặc {{display}})
428
+ viewWriter.remove(viewWriter.createRangeIn(viewElement));
429
+ viewWriter.insert(viewWriter.createPositionAt(viewElement, 0), viewWriter.createText(isBound ? bindingValue : `{{${display}}}`));
430
+ });
431
+ });
404
432
  // 3. HTML -> Model
405
433
  conversion.for('upcast').elementToElement({
406
- // NOTE: Khi bổ sung thêm attribute vào element variable, dev nên bổ sung thêm "[atribute]: true" vào view
407
- // Để:
408
- // - Nếu lọc chính xác sẽ không sinh ra thẻ span thừa bọc ngoài
409
- // - Nếu chưa bổ sung thì sẽ sinh ra thẻ <span> bọc ngoài kèm [atribute] chưa lọc
434
+ // NOTE: Chỉ khai báo các attribute CẦN THIẾT để nhận biết variable widget.
435
+ // - data-binding KHÔNG được đưa vào required attributes (HTML cũ không có sẽ không được nhận biết).
436
+ // - Nếu data-binding="true" trong HTML đọc text content làm bindingValue để restore trạng thái bound.
410
437
  view: {
411
438
  name: 'span',
412
439
  classes: 'variable-widget ck-widget',
@@ -419,11 +446,23 @@ class VariablePlugin extends Plugin {
419
446
  },
420
447
  },
421
448
  model: (viewElement, { writer: modelWriter }) => {
449
+ const isBound = viewElement.getAttribute('data-binding') === 'true';
450
+ let bindingValue;
451
+ if (isBound) {
452
+ // Đọc text content làm bindingValue (bound value được lưu trực tiếp vào innerHTML)
453
+ for (const child of viewElement.getChildren()) {
454
+ if (child.is('$text')) {
455
+ bindingValue = child.data;
456
+ break;
457
+ }
458
+ }
459
+ }
422
460
  return modelWriter.createElement('variable', {
423
461
  id: viewElement.getAttribute('data-id'),
424
462
  uuid: viewElement.getAttribute('data-uuid'),
425
463
  value: viewElement.getAttribute('data-value'),
426
464
  display: viewElement.getAttribute('data-display'),
465
+ ...(bindingValue ? { bindingValue } : {}),
427
466
  });
428
467
  },
429
468
  });
@@ -443,7 +482,8 @@ class VariablePlugin extends Plugin {
443
482
  const getOption = config.get('getOption');
444
483
  const option = getOption?.();
445
484
  if (option?.onDropVariable) {
446
- const result = await SdResolveMaybeAsync(option.onDropVariable(variable, 0));
485
+ // Bug 4 Fix (Q2-A): Xóa tham số dropIndex — không còn tryền giá trị hardcode 0
486
+ const result = await SdResolveMaybeAsync(option.onDropVariable(variable));
447
487
  // * Hỗ trợ dữ liệu có sẵn sẽ chỉ cần nhận vào boolean có cho phép thả hay không?
448
488
  if (typeof result === 'boolean') {
449
489
  if (!result) {
@@ -460,11 +500,13 @@ class VariablePlugin extends Plugin {
460
500
  }
461
501
  }
462
502
  }
503
+ let insertedUuid = '';
463
504
  editor.model.change(writer => {
464
505
  // 4.1. Chèn biến
506
+ insertedUuid = v4();
465
507
  const variableElem = writer.createElement('variable', {
466
508
  id: variable.id,
467
- uuid: v4(),
509
+ uuid: insertedUuid,
468
510
  value: variable.value,
469
511
  display: variable.display,
470
512
  });
@@ -472,6 +514,9 @@ class VariablePlugin extends Plugin {
472
514
  // 4.2. Đặt con trỏ ra sau biến
473
515
  writer.setSelection(variableElem, 'after');
474
516
  });
517
+ // onAfterDropVariable: fires SAU model.change() → variable đã có trong model
518
+ // Consumer có thể gọi variable.all() tại đây và thấy biến mới nhất
519
+ option?.onAfterDropVariable?.({ ...variable, uuid: insertedUuid });
475
520
  }
476
521
  catch (e) {
477
522
  // Đặt con trỏ ngay tại vị trí lỗi
@@ -493,20 +538,66 @@ class VariablePlugin extends Plugin {
493
538
  });
494
539
  }
495
540
  });
496
- // 5. Lắng nghe sự kiện bàn phím
541
+ // -------------------------------------------------------------------------
542
+ // 5 & 6. Navigation (Arrow keys + Mouse) + Cursor spacing sau variable
543
+ // -------------------------------------------------------------------------
544
+ // Bug 1 Fix: isNavigating cần được reset sau mỗi lần change:range xử lý xong
545
+ // để tránh logic chèn \u00A0 chạy lặp khi có selection change programmatic.
497
546
  let isNavigating = false;
498
- this.editor.editing.view.document.on('keydown', (evt, data) => {
547
+ // Opt 1: Gộp arrow-key detection + Backspace/Delete handler vào 1 listener keydown
548
+ // Bug 3 Fix: Dùng this.listenTo() thay .on() để CKEditor tự cleanup khi plugin destroy
549
+ this.listenTo(editingView.document, 'keydown', (evt, data) => {
550
+ const keyCode = data.keyCode;
551
+ // --- Arrow key navigation detection (priority: high) ---
499
552
  // Mã phím mũi tên: 37 (Left), 38 (Up), 39 (Right), 40 (Down)
500
- const isArrowKey = data.keyCode >= 37 && data.keyCode <= 40;
553
+ const isArrowKey = keyCode >= 37 && keyCode <= 40;
501
554
  if (isArrowKey) {
502
555
  isNavigating = true;
556
+ return; // không xử lý thêm cho arrow key ở đây
503
557
  }
504
- else {
558
+ // Reset flag nếu nhấn phím khác (không phải arrow, không phải Backspace/Delete)
559
+ const btnBackspace = keyCode === 8;
560
+ const btnDelete = keyCode === 46;
561
+ if (!btnBackspace && !btnDelete) {
505
562
  isNavigating = false;
563
+ return;
506
564
  }
507
- }, { priority: 'high' });
508
- // 6. Lắng nghe sự kiện Click chuột
509
- this.editor.editing.view.document.on('mousedown', () => {
565
+ // --- Opt 1: Backspace / Delete handler (đã gộp vào cùng listener) ---
566
+ // priority: highest chạy trước mọi handler khác để bắt xóa variable 2 bước
567
+ const selection = editor.model.document.selection;
568
+ const model = editor.model;
569
+ // CASE 1: Nếu con trỏ đang nhấp nháy (Collapsed)
570
+ if (selection.isCollapsed) {
571
+ const position = selection.getFirstPosition();
572
+ // Với Backspace ta kiểm tra nodeBefore, với Delete ta kiểm tra nodeAfter
573
+ const targetNode = btnBackspace ? position?.nodeBefore : position?.nodeAfter;
574
+ if (targetNode && targetNode.is('element', 'variable')) {
575
+ data.preventDefault();
576
+ evt.stop();
577
+ model.change(writer => {
578
+ // Chọn bao quanh Variable đó (lần bấm tiếp theo sẽ xóa)
579
+ writer.setSelection(targetNode, 'on');
580
+ });
581
+ return;
582
+ }
583
+ }
584
+ // CASE 2: Nếu đang có một vùng chọn (đã được highlight từ lần bấm trước)
585
+ else {
586
+ const selectedElement = selection.getSelectedElement();
587
+ // Nếu phần tử đang được chọn chính là variable → xóa hẳn
588
+ if (selectedElement && selectedElement.is('element', 'variable')) {
589
+ data.preventDefault();
590
+ evt.stop();
591
+ model.change(writer => {
592
+ writer.remove(selectedElement);
593
+ });
594
+ }
595
+ }
596
+ },
597
+ // priority: highest — Backspace/Delete phải chạy trước CKEditor default để bắt 2-step deletion
598
+ { priority: 'highest' });
599
+ // Bug 3 Fix: mousedown cũng dùng this.listenTo() để tránh memory leak
600
+ this.listenTo(editingView.document, 'mousedown', () => {
510
601
  isNavigating = true;
511
602
  });
512
603
  this.listenTo(editor.model.document.selection, 'change:range', () => {
@@ -514,6 +605,9 @@ class VariablePlugin extends Plugin {
514
605
  if (!isNavigating) {
515
606
  return;
516
607
  }
608
+ // Bug 1 Fix: Reset ngay sau khi vào handler để tránh chạy lặp
609
+ // khi có thêm selection change programmatic (VD: writer.insertText bên dưới tự trigger lại)
610
+ isNavigating = false;
517
611
  const model = editor.model;
518
612
  const selection = model.document.selection;
519
613
  if (!selection.isCollapsed)
@@ -526,105 +620,64 @@ class VariablePlugin extends Plugin {
526
620
  if (nodeBefore && nodeBefore.is('element', 'variable')) {
527
621
  // Lấy node ngay sau variable để kiểm tra
528
622
  const nextNode = nodeBefore.nextSibling;
529
- // Logic: Nếu phía sau KHÔNG CÓ GÌ hoặc KHÔNG PHẢI LÀ TEXT
623
+ // Logic: Nếu phía sau KHÔNG CÓ GÌ hoặc KHÔNG PHẢI LÀ TEXT → chèn \u00A0 để có thể gõ tiếp
530
624
  if (!nextNode || !nextNode.is('$text')) {
531
625
  model.change(writer => {
532
- // Chèn thêm con trỏ variable
533
626
  writer.insertText('\u00A0', nodeBefore, 'after');
534
627
  // Lấy vị trí ngay sau variable (lúc này đang là đầu của text node mới)
535
628
  const posAfterVariable = writer.createPositionAfter(nodeBefore);
536
629
  // Dịch chuyển vị trí đó sang phải 1 đơn vị (bỏ qua ký tự vừa thêm)
537
630
  const targetPos = posAfterVariable.getShiftedBy(1);
538
- // 3. Đặt con trỏ vào vị trí đã tính toán
539
631
  writer.setSelection(targetPos);
540
632
  });
541
633
  }
542
634
  }
543
635
  });
544
- // 7. Handle xóa variable
545
- this.editor.editing.view.document.on('keydown', (evt, data) => {
546
- // Mã phím 8 là Backspace, 46 là Delete
547
- const btnBackspace = data.keyCode === 8;
548
- const btnDelete = data.keyCode === 46;
549
- if (btnBackspace || btnDelete) {
550
- const selection = editor.model.document.selection;
551
- const model = editor.model;
552
- // CASE 1: Nếu con trỏ đang nhấp nháy (Collapsed)
553
- if (selection.isCollapsed) {
554
- const position = selection.getFirstPosition();
555
- // Với Backspace ta kiểm tra nodeBefore, với Delete ta kiểm tra nodeAfter
556
- const targetNode = data.keyCode === 8 ? position?.nodeBefore : position?.nodeAfter;
557
- if (targetNode && targetNode.is('element', 'variable')) {
558
- data.preventDefault();
559
- evt.stop();
560
- model.change(writer => {
561
- // Chọn bao quanh Variable đó
562
- writer.setSelection(targetNode, 'on');
563
- });
564
- return;
565
- }
566
- }
567
- // CASE 2: Nếu đang có một vùng chọn (đã được highlight từ lần bấm trước)
568
- else {
569
- const selectedElement = selection.getSelectedElement();
570
- // Nếu phần tử đang được chọn chính là variable
571
- if (selectedElement && selectedElement.is('element', 'variable')) {
572
- // Cho phép hành động mặc định diễn ra (CKEditor sẽ tự xóa phần tử đang được chọn)
573
- // Hoặc chủ động xóa để chắc chắn:
574
- data.preventDefault();
575
- evt.stop();
576
- model.change(writer => {
577
- writer.remove(selectedElement);
578
- });
579
- }
580
- }
581
- }
582
- }, { priority: 'highest' });
583
- // 8. Xử lý sự kiện Copy (Clipboard Output)
584
- // Chỉ set thêm text/plain fallback, không thay đổi HTML content
636
+ // -------------------------------------------------------------------------
637
+ // 8. Xử lý sự kiện Copy / Cut (Clipboard Output)
638
+ // -------------------------------------------------------------------------
639
+ // priority: low chạy sau CKEditor để bổ sung text/plain fallback, không can thiệp HTML
585
640
  this.listenTo(editor.editing.view.document, 'clipboardOutput', (evt, data) => {
586
641
  const isCopyOrCut = data.method === 'copy' || data.method === 'cut';
587
642
  if (!isCopyOrCut)
588
643
  return;
589
- // Set thêm plain text fallback cho external apps
590
644
  const dataTransfer = data.dataTransfer;
591
645
  const content = data.content;
592
- // Lấy tất cả text từ content (bao gồm cả variable dạng {{text}})
646
+ // Opt 3: Chỉ visit element nodes, bỏ qua các node không liên quan
593
647
  let plainText = '';
594
648
  const viewRange = editor.editing.view.createRangeIn(content);
595
649
  for (const item of viewRange.getItems()) {
596
- if (item.is('$text') || item.is('element', 'span')) {
597
- const itemAny = item;
598
- if (item.is('$text') && itemAny.data) {
599
- plainText += itemAny.data;
600
- }
601
- else if (item.is('element', 'span') && item.hasClass('variable-widget')) {
602
- const display = item.getAttribute('data-display');
603
- if (display)
604
- plainText += `{{${display}}}`;
605
- }
650
+ if (item.is('$text')) {
651
+ plainText += item.data;
652
+ }
653
+ else if (item.is('element', 'span') && item.hasClass('variable-widget')) {
654
+ const display = item.getAttribute('data-display');
655
+ if (display)
656
+ plainText += `{{${display}}}`;
606
657
  }
607
658
  }
608
659
  if (plainText) {
609
660
  dataTransfer.setData('text/plain', plainText);
610
661
  }
611
- // HTML content giữ nguyên - CKEditor sẽ tự xử lý
662
+ // HTML content giữ nguyên CKEditor sẽ tự xử lý upcast khi paste lại
612
663
  }, { priority: 'low' });
664
+ // -------------------------------------------------------------------------
613
665
  // 9. Xử lý sự kiện Paste (Clipboard Input)
666
+ // -------------------------------------------------------------------------
614
667
  // Nếu paste từ external source (chỉ có text, không có HTML variable)
615
668
  // thì chuyển {{text}} thành variable widget
616
- this.listenTo(editor.editing.view.document, 'clipboardInput', (evt, data) => {
669
+ this.listenTo(editor.editing.view.document, 'clipboardInput', async (evt, data) => {
617
670
  const dataTransfer = data.dataTransfer;
618
671
  // Nếu có HTML chứa variable-widget thì để CKEditor xử lý (upcast converter)
619
672
  const html = dataTransfer.getData('text/html');
620
673
  if (html && html.includes('variable-widget')) {
621
- return; // Để CKEditor upcast converter xử lý
674
+ return;
622
675
  }
623
676
  // Chỉ xử lý nếu chỉ có plain text với pattern {{text}}
624
- let text = dataTransfer.getData('text/plain');
677
+ const text = dataTransfer.getData('text/plain');
625
678
  if (!text)
626
679
  return;
627
- // Kiểm tra có chứa pattern {{text}} không (không cần id, value)
680
+ // Kiểm tra có chứa pattern {{text}} không
628
681
  const variablePattern = /\{\{([^}]+)\}\}/g;
629
682
  if (!variablePattern.test(text)) {
630
683
  return;
@@ -632,53 +685,85 @@ class VariablePlugin extends Plugin {
632
685
  // Reset lastIndex sau khi test
633
686
  variablePattern.lastIndex = 0;
634
687
  evt.stop();
635
- editor.model.change(writer => {
636
- const selection = editor.model.document.selection;
637
- const position = selection.getFirstPosition();
638
- if (!position)
639
- return;
640
- // Tách text thành các phần: normal text và variables
641
- let lastIndex = 0;
642
- let match;
643
- const fragments = [];
644
- while ((match = variablePattern.exec(text)) !== null) {
645
- // Thêm text trước variable
646
- if (match.index > lastIndex) {
647
- fragments.push({
648
- type: 'text',
649
- content: text.slice(lastIndex, match.index),
650
- });
688
+ // Bug 2 Fix (Q1-A): Lấy option để gọi onPasteVariable callback nếu có
689
+ const config = editor.config;
690
+ const getOption = config.get('getOption');
691
+ const option = getOption?.();
692
+ // Tách text thành các phần: normal text và variables
693
+ let lastIndex = 0;
694
+ let match;
695
+ const fragments = [];
696
+ while ((match = variablePattern.exec(text)) !== null) {
697
+ // Thêm text trước variable
698
+ if (match.index > lastIndex) {
699
+ fragments.push({ type: 'text', content: text.slice(lastIndex, match.index) });
700
+ }
701
+ fragments.push({ type: 'variable', content: match[0], display: match[1] });
702
+ lastIndex = match.index + match[0].length;
703
+ }
704
+ // Thêm text còn lại sau variable cuối cùng
705
+ if (lastIndex < text.length) {
706
+ fragments.push({ type: 'text', content: text.slice(lastIndex) });
707
+ }
708
+ // Bug 2 Fix: Resolve tất cả variable fragments trước khi thực hiện model.change
709
+ // Tránh async operation bên trong model.change (CKEditor không hỗ trợ async writer)
710
+ // Xây dựng lookup map từ variables đang có trong document (theo display name)
711
+ // Dùng để fallback khi không có onPasteVariable — giữ lại đầy đủ id/value của biến gốc
712
+ // khi copy-paste nội bộ bị mất HTML (chỉ còn plain text {{display}})
713
+ const existingVariableMap = this.#buildDisplayMap();
714
+ const resolvedFragments = [];
715
+ for (const fragment of fragments) {
716
+ if (fragment.type === 'variable' && fragment.display) {
717
+ let resolved = null;
718
+ if (option?.onPasteVariable) {
719
+ try {
720
+ resolved = await SdResolveMaybeAsync(option.onPasteVariable(fragment.display));
721
+ }
722
+ catch (e) {
723
+ console.warn(`[VariablePlugin] onPasteVariable("${fragment.display}") thất bại:`, e);
724
+ }
725
+ }
726
+ // Fallback 1: Tra cứu variable đang có trong document theo display name
727
+ // → Giữ lại đầy đủ id/value khi copy-paste nội bộ bị mất HTML
728
+ if (!resolved) {
729
+ resolved = existingVariableMap.get(fragment.display) ?? null;
651
730
  }
652
- // Thêm variable
653
- const display = match[1];
654
- fragments.push({
731
+ // Fallback 2: Không tìm thấy → tạo variable sentinel với id = ''
732
+ // Consumer thể filter qua variable.all() và nhận biết bằng id === ''
733
+ resolvedFragments.push({
655
734
  type: 'variable',
656
- content: match[0],
657
- display,
735
+ content: fragment.content,
736
+ variable: resolved ?? {
737
+ id: '',
738
+ uuid: v4(),
739
+ value: fragment.display,
740
+ display: fragment.display,
741
+ },
658
742
  });
659
- lastIndex = match.index + match[0].length;
660
743
  }
661
- // Thêm text còn lại sau variable cuối cùng
662
- if (lastIndex < text.length) {
663
- fragments.push({
664
- type: 'text',
665
- content: text.slice(lastIndex),
666
- });
744
+ else {
745
+ resolvedFragments.push({ type: 'text', content: fragment.content });
667
746
  }
668
- // Chèn từng fragment vào document
747
+ }
748
+ // Chèn từng fragment vào document
749
+ editor.model.change(writer => {
750
+ const selection = editor.model.document.selection;
751
+ const position = selection.getFirstPosition();
752
+ if (!position)
753
+ return;
669
754
  let currentPosition = position;
670
- for (const fragment of fragments) {
755
+ for (const fragment of resolvedFragments) {
671
756
  if (fragment.type === 'text' && fragment.content) {
672
757
  const textNode = writer.createText(fragment.content);
673
758
  writer.insert(textNode, currentPosition);
674
759
  currentPosition = writer.createPositionAfter(textNode);
675
760
  }
676
- else if (fragment.type === 'variable' && fragment.display) {
761
+ else if (fragment.type === 'variable' && fragment.variable) {
677
762
  const variableElem = writer.createElement('variable', {
678
- id: v4(),
679
- uuid: v4(),
680
- value: fragment.display,
681
- display: fragment.display,
763
+ id: fragment.variable.id,
764
+ uuid: fragment.variable.uuid ?? v4(),
765
+ value: fragment.variable.value,
766
+ display: fragment.variable.display,
682
767
  });
683
768
  writer.insert(variableElem, currentPosition);
684
769
  currentPosition = writer.createPositionAfter(variableElem);
@@ -698,6 +783,204 @@ class VariablePlugin extends Plugin {
698
783
  typeof obj.value === 'string' &&
699
784
  typeof obj.display === 'string');
700
785
  };
786
+ /**
787
+ * Quét tất cả variable elements trong document hiện tại, trả về Map<display, SdDocumentBuilderVariable>.
788
+ * Dùng làm fallback khi paste {{display}} mà không có onPasteVariable callback:
789
+ * nếu document đã có variable cùng display → tái sử dụng id/value của biến gốc.
790
+ * Nếu có nhiều variable cùng display → lấy cái đầu tiên tìm được.
791
+ */
792
+ #buildDisplayMap = () => {
793
+ const map = new Map();
794
+ const root = this.editor.model.document.getRoot();
795
+ if (!root)
796
+ return map;
797
+ const range = this.editor.model.createRangeIn(root);
798
+ for (const item of range.getItems()) {
799
+ if (item.is('element', 'variable')) {
800
+ const display = item.getAttribute('display');
801
+ // Chỉ lưu lần đầu tiên gặp display này
802
+ if (display && !map.has(display)) {
803
+ map.set(display, {
804
+ id: item.getAttribute('id'),
805
+ uuid: item.getAttribute('uuid'),
806
+ value: item.getAttribute('value'),
807
+ display,
808
+ });
809
+ }
810
+ }
811
+ }
812
+ return map;
813
+ };
814
+ // =========================================================================
815
+ // PUBLIC API — Variable management
816
+ // =========================================================================
817
+ /**
818
+ * Lấy tất cả variables trong document.
819
+ * @returns Danh sách tất cả variables (bao gồm bindingValue nếu đã binding)
820
+ */
821
+ all() {
822
+ const model = this.editor.model;
823
+ const root = model.document.getRoot();
824
+ if (!root)
825
+ return [];
826
+ const variables = [];
827
+ try {
828
+ const range = model.createRangeIn(root);
829
+ for (const item of range.getItems()) {
830
+ if (item.is('element', 'variable')) {
831
+ variables.push({
832
+ id: item.getAttribute('id'),
833
+ uuid: item.getAttribute('uuid'),
834
+ value: item.getAttribute('value'),
835
+ display: item.getAttribute('display'),
836
+ bindingValue: item.getAttribute('bindingValue'),
837
+ });
838
+ }
839
+ }
840
+ }
841
+ catch (e) {
842
+ console.error(e);
843
+ return [];
844
+ }
845
+ return variables;
846
+ }
847
+ /**
848
+ * Scroll tới vị trí của variable theo uuid.
849
+ * @param uuid - uuid của variable (FE tự sinh sau mỗi lần drop)
850
+ */
851
+ scroll(uuid) {
852
+ const model = this.editor.model;
853
+ const root = model.document.getRoot();
854
+ if (!root)
855
+ return;
856
+ let targetElement = null;
857
+ const range = model.createRangeIn(root);
858
+ for (const item of range.getItems()) {
859
+ if (item.is('element', 'variable') && item.getAttribute('uuid') === uuid) {
860
+ targetElement = item;
861
+ break;
862
+ }
863
+ }
864
+ if (targetElement) {
865
+ const viewElement = this.editor.editing.mapper.toViewElement(targetElement);
866
+ if (viewElement) {
867
+ const domElement = this.editor.editing.view.domConverter.viewToDom(viewElement);
868
+ if (domElement) {
869
+ domElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
870
+ model.change(writer => {
871
+ writer.setSelection(targetElement, 'on');
872
+ });
873
+ }
874
+ }
875
+ }
876
+ else {
877
+ console.warn(`Variable với uuid "${uuid}" không tìm thấy trong tài liệu.`);
878
+ }
879
+ }
880
+ /**
881
+ * Gán giá trị cho TẤT CẢ variable có cùng id trong document.
882
+ * Nếu value rỗng → tự động gọi clearValue(id).
883
+ * @param id - id của variable definition
884
+ * @param value - giá trị binding cần hiển thị
885
+ * @returns số instance đã được cập nhật
886
+ */
887
+ bindValue(id, value) {
888
+ if (!value)
889
+ return this.clearValue(id);
890
+ const model = this.editor.model;
891
+ const root = model.document.getRoot();
892
+ if (!root)
893
+ return 0;
894
+ let count = 0;
895
+ model.change(writer => {
896
+ const range = model.createRangeIn(root);
897
+ for (const item of range.getItems()) {
898
+ if (item.is('element', 'variable') && item.getAttribute('id') === id) {
899
+ writer.setAttribute('bindingValue', value, item);
900
+ count++;
901
+ }
902
+ }
903
+ });
904
+ return count;
905
+ }
906
+ /**
907
+ * Xóa binding value của TẤT CẢ variable có cùng id.
908
+ * @param id - id của variable definition
909
+ * @returns số instance đã được cập nhật
910
+ */
911
+ clearValue(id) {
912
+ const model = this.editor.model;
913
+ const root = model.document.getRoot();
914
+ if (!root)
915
+ return 0;
916
+ let count = 0;
917
+ model.change(writer => {
918
+ const range = model.createRangeIn(root);
919
+ for (const item of range.getItems()) {
920
+ if (item.is('element', 'variable') && item.getAttribute('id') === id && item.hasAttribute('bindingValue')) {
921
+ writer.removeAttribute('bindingValue', item);
922
+ count++;
923
+ }
924
+ }
925
+ });
926
+ return count;
927
+ }
928
+ /**
929
+ * Batch bind nhiều variables theo map { id → value }.
930
+ * Toàn bộ thực hiện trong 1 model.change() → 1 undo step duy nhất.
931
+ * @param map - { [id]: boundValue }
932
+ */
933
+ bindValues(map) {
934
+ if (!Object.keys(map).length)
935
+ return;
936
+ const model = this.editor.model;
937
+ const root = model.document.getRoot();
938
+ if (!root)
939
+ return;
940
+ model.change(writer => {
941
+ const range = model.createRangeIn(root);
942
+ for (const item of range.getItems()) {
943
+ if (!item.is('element', 'variable'))
944
+ continue;
945
+ const id = item.getAttribute('id');
946
+ if (!(id in map))
947
+ continue;
948
+ const value = map[id];
949
+ if (value) {
950
+ writer.setAttribute('bindingValue', value, item);
951
+ }
952
+ else if (item.hasAttribute('bindingValue')) {
953
+ writer.removeAttribute('bindingValue', item);
954
+ }
955
+ }
956
+ });
957
+ }
958
+ /**
959
+ * Batch clear binding của nhiều variables.
960
+ * @param ids - danh sách id cần clear; nếu không truyền/rỗng → clear TẤT CẢ
961
+ */
962
+ clearValues(ids) {
963
+ const model = this.editor.model;
964
+ const root = model.document.getRoot();
965
+ if (!root)
966
+ return;
967
+ const idSet = ids?.length ? new Set(ids) : null;
968
+ model.change(writer => {
969
+ const range = model.createRangeIn(root);
970
+ for (const item of range.getItems()) {
971
+ if (!item.is('element', 'variable') || !item.hasAttribute('bindingValue'))
972
+ continue;
973
+ const id = item.getAttribute('id');
974
+ if (!idSet || idSet.has(id)) {
975
+ writer.removeAttribute('bindingValue', item);
976
+ }
977
+ }
978
+ });
979
+ }
980
+ /** Xóa toàn bộ binding values trong document. Shorthand của clearValues(). */
981
+ clearAllValues() {
982
+ this.clearValues();
983
+ }
701
984
  }
702
985
 
703
986
  class TableCustom extends Plugin {
@@ -3201,9 +3484,14 @@ class CkCommentPlugin extends Plugin {
3201
3484
  tooltip: true,
3202
3485
  isEnabled: false,
3203
3486
  });
3204
- // Enable khi có selection không phải chỉ khoảng trắng
3487
+ // Enable khi có selection, không phải chỉ khoảng trắng, và allowCreating = true
3205
3488
  const selection = editor.model.document.selection;
3206
3489
  this.listenTo(selection, 'change', () => {
3490
+ // Disabled ngay khi allowCreating = false
3491
+ if (!(this.#config.allowCreating ?? true)) {
3492
+ view.isEnabled = false;
3493
+ return;
3494
+ }
3207
3495
  const isCollapsed = selection.isCollapsed;
3208
3496
  const range = selection.getFirstRange();
3209
3497
  // Kiểm tra xem selection có content không phải khoảng trắng không
@@ -3419,6 +3707,11 @@ class CkCommentPlugin extends Plugin {
3419
3707
  this.#log('Skipping clear pending - isCreatingPending flag is set');
3420
3708
  }
3421
3709
  if (!selection.isCollapsed) {
3710
+ // Chỉ hiện balloon khi allowCreating = true (mặc định)
3711
+ if (!(this.#config.allowCreating ?? true)) {
3712
+ this.#hideBalloon();
3713
+ return;
3714
+ }
3422
3715
  const range = selection.getFirstRange();
3423
3716
  if (range) {
3424
3717
  // Chỉ hiện balloon khi selection có content không phải khoảng trắng và không vượt quá max length
@@ -3740,6 +4033,15 @@ class CkCommentPlugin extends Plugin {
3740
4033
  if (!range || range.isCollapsed) {
3741
4034
  return null;
3742
4035
  }
4036
+ // Validate: start/end không được nằm bên trong isObject element (path.length > 2)
4037
+ // Trường hợp xảy ra khi user drag-select qua bound variable widget
4038
+ if (range.start.path.length > 2 || range.end.path.length > 2) {
4039
+ this.#warn('Selection contains invalid path (inside isObject element) - aborting comment creation', {
4040
+ startPath: Array.from(range.start.path),
4041
+ endPath: Array.from(range.end.path),
4042
+ });
4043
+ return null;
4044
+ }
3743
4045
  const text = this.#getTextFromRange(range);
3744
4046
  const trimmedText = text.trim();
3745
4047
  if (!trimmedText) {
@@ -3765,6 +4067,11 @@ class CkCommentPlugin extends Plugin {
3765
4067
  if (leadingWhitespace > 0 || trailingWhitespace > 0) {
3766
4068
  adjustedRange = this.#adjustRangeForTrim(range, leadingWhitespace, trailingWhitespace);
3767
4069
  }
4070
+ // Validate lại sau khi trim
4071
+ if (adjustedRange.start.path.length > 2 || adjustedRange.end.path.length > 2) {
4072
+ this.#warn('Adjusted range has invalid path depth - aborting');
4073
+ return null;
4074
+ }
3768
4075
  return {
3769
4076
  range: adjustedRange,
3770
4077
  startPath: Array.from(adjustedRange.start.path),
@@ -3773,10 +4080,32 @@ class CkCommentPlugin extends Plugin {
3773
4080
  };
3774
4081
  }
3775
4082
  /**
3776
- * Điều chỉnh range để loại bỏ khoảng trắng đầu/cuối
4083
+ * Điều chỉnh range để loại bỏ khoảng trắng đầu/cuối.
4084
+ * Đảm bảo các vị trí sau khi dịch chuyển không rơi vào bên trong isObject element
4085
+ * (ví dụ: variable widget đã bound) để tránh lỗi document-selection-wrong-position.
3777
4086
  */
3778
4087
  #adjustRangeForTrim(range, leadingTrim, trailingTrim) {
3779
4088
  const model = this.editor.model;
4089
+ /** Kiểm tra pos có hợp lệ (không nằm bên trong isObject element) */
4090
+ const isValidPosition = (pos) => {
4091
+ try {
4092
+ // path.length > 2 nghĩa là position nằm sâu hơn paragraph → bên trong element
4093
+ if (pos.path && pos.path.length > 2)
4094
+ return false;
4095
+ // Kiểm tra node tại vị trí đó
4096
+ const nodeAfter = pos.nodeAfter;
4097
+ const nodeBefore = pos.nodeBefore;
4098
+ // Không cho phép position bắt đầu/kết thúc bên trong isObject
4099
+ if (nodeAfter && model.schema.isObject(nodeAfter))
4100
+ return true; // trước object = ok
4101
+ if (nodeBefore && model.schema.isObject(nodeBefore))
4102
+ return true; // sau object = ok
4103
+ return model.schema.checkChild(pos, '$text') || true;
4104
+ }
4105
+ catch {
4106
+ return false;
4107
+ }
4108
+ };
3780
4109
  return model.change(writer => {
3781
4110
  let startPos = range.start;
3782
4111
  let endPos = range.end;
@@ -3784,7 +4113,7 @@ class CkCommentPlugin extends Plugin {
3784
4113
  if (leadingTrim > 0) {
3785
4114
  for (let i = 0; i < leadingTrim && startPos; i++) {
3786
4115
  const nextPos = startPos.getShiftedBy(1);
3787
- if (nextPos) {
4116
+ if (nextPos && isValidPosition(nextPos)) {
3788
4117
  startPos = nextPos;
3789
4118
  }
3790
4119
  else {
@@ -3796,7 +4125,7 @@ class CkCommentPlugin extends Plugin {
3796
4125
  if (trailingTrim > 0) {
3797
4126
  for (let i = 0; i < trailingTrim && endPos; i++) {
3798
4127
  const prevPos = endPos.getShiftedBy(-1);
3799
- if (prevPos && prevPos.isAfter(startPos)) {
4128
+ if (prevPos && prevPos.isAfter(startPos) && isValidPosition(prevPos)) {
3800
4129
  endPos = prevPos;
3801
4130
  }
3802
4131
  else {
@@ -4456,75 +4785,18 @@ class SdDocumentBuilder {
4456
4785
  // ========================================================================
4457
4786
  // 3. QUẢN LÝ VARIABLE
4458
4787
  // ========================================================================
4459
- variable = {
4460
- /**
4461
- * Lấy tất cả variabes trong document
4462
- * @returns Danh sách tất cả variables
4463
- */
4464
- all: () => {
4465
- if (!this.#editor)
4466
- return [];
4467
- const model = this.#editor.model;
4468
- const root = model.document.getRoot();
4469
- if (!root)
4470
- return [];
4471
- const variables = [];
4472
- try {
4473
- const range = model.createRangeIn(root);
4474
- for (const item of range.getItems()) {
4475
- // Sử dụng item.is('element', 'variable') là chính xác
4476
- if (item.is('element', 'variable')) {
4477
- variables.push({
4478
- id: item.getAttribute('id'),
4479
- uuid: item.getAttribute('uuid'),
4480
- value: item.getAttribute('value'),
4481
- display: item.getAttribute('display'),
4482
- });
4483
- }
4484
- }
4485
- }
4486
- catch (e) {
4487
- console.error(e);
4488
- return [];
4489
- }
4490
- return variables;
4491
- },
4492
- /**
4493
- * Scroll tới vị trí của variable
4494
- * @param uuid - uuid của variable FE sẽ tự sinh sau mỗi lần drop vào editor
4495
- */
4496
- scroll: (uuid) => {
4497
- if (!this.#editor)
4498
- return;
4499
- const model = this.#editor.model;
4500
- const root = model.document.getRoot();
4501
- if (!root)
4502
- return;
4503
- let targetElement = null;
4504
- const range = model.createRangeIn(root);
4505
- for (const item of range.getItems()) {
4506
- if (item.is('element', 'variable') && item.getAttribute('uuid') === uuid) {
4507
- targetElement = item;
4508
- break;
4509
- }
4510
- }
4511
- if (targetElement) {
4512
- const viewElement = this.#editor.editing.mapper.toViewElement(targetElement);
4513
- if (viewElement) {
4514
- const domElement = this.#editor.editing.view.domConverter.viewToDom(viewElement);
4515
- if (domElement) {
4516
- domElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
4517
- model.change(writer => {
4518
- writer.setSelection(targetElement, 'on');
4519
- });
4520
- }
4521
- }
4522
- }
4523
- else {
4524
- console.warn(`Variable với id ${uuid} không tìm thấy trong tài liệu.`);
4525
- }
4526
- },
4527
- };
4788
+ getVariablePluginAPI() {
4789
+ if (!this.#editor)
4790
+ return null;
4791
+ try {
4792
+ // Dùng class reference (không dùng string) → an toàn trong build minified, TypeScript-typed
4793
+ return this.#editor.plugins.get(VariablePlugin);
4794
+ }
4795
+ catch (error) {
4796
+ console.warn('VariablePlugin not available:', error);
4797
+ return null;
4798
+ }
4799
+ }
4528
4800
  // ========================================================================
4529
4801
  // 4. HÀM EXPORT DOCX (FULL HEADER/FOOTER + PAGE NUMBER)
4530
4802
  // ========================================================================
@@ -4656,11 +4928,11 @@ class SdDocumentBuilder {
4656
4928
  });
4657
4929
  };
4658
4930
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: SdDocumentBuilder, deps: [], target: i0.ɵɵFactoryTarget.Component });
4659
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: SdDocumentBuilder, isStandalone: true, selector: "sd-document-builder", inputs: { option: "option", _disabled: ["disabled", "_disabled"] }, outputs: { contentChange: "contentChange" }, ngImport: i0, template: "<div class=\"builder-container\">\n <ckeditor\n style=\"width: 100%\"\n [editor]=\"Editor\" \n [config]=\"config\" \n (ready)=\"onReady($event)\"\n [disabled]=\"disabled\">\n </ckeditor>\n</div>", styles: ["@charset \"UTF-8\";.builder-container{background-color:#f3f4f6;height:100%;overflow-y:auto;width:100%;display:flex;flex-direction:column;align-items:center}:host{display:inline-block}:host ::ng-deep .ck-editor{display:flex;flex-direction:column;align-items:center;width:100%;--ck-content-font-family: \"Times New Roman\", serif !important;--ck-content-font-size: 13pt;--ck-content-line-height: 1.15}:host ::ng-deep .ck-editor__top,:host ::ng-deep .ck-editor__main{border:none!important;box-shadow:none!important}:host ::ng-deep .ck-editor__top{position:sticky;top:0;z-index:100;width:100%;min-width:600px;margin-bottom:10px}:host ::ng-deep .ck-editor__top .ck-sticky-panel__content{border:none!important}:host ::ng-deep .ck-editor__top .ck-toolbar{background:#fff!important;box-shadow:0 4px 6px -1px #0000001a!important;padding:8px!important}:host ::ng-deep .ck-editor__top .ck-toolbar .ck-toolbar__items{display:flex;justify-content:center;flex-wrap:wrap;align-items:center}:host ::ng-deep .ck-content{background-color:#fff;width:210mm;min-height:1123px;padding:20mm!important;box-sizing:border-box!important;box-shadow:0 10px 15px -3px #0000001a}:host ::ng-deep .ck-content h1,:host ::ng-deep .ck-content h2,:host ::ng-deep .ck-content h3,:host ::ng-deep .ck-content h4,:host ::ng-deep .ck-content h5,:host ::ng-deep .ck-content h6{font-weight:400;font-size:inherit;line-height:inherit;margin-bottom:4px}:host ::ng-deep .ck-content.landscape{width:297mm}:host ::ng-deep .ck-content>*{max-width:100%!important;box-sizing:border-box!important}:host ::ng-deep .ck-content p{margin-bottom:4px;text-indent:0}:host ::ng-deep .ck-content ul,:host ::ng-deep .ck-content ol{padding-left:20px!important;margin-left:0!important;margin-bottom:4px}:host ::ng-deep .ck-content li{margin-bottom:0}:host ::ng-deep .ck-content table{margin-bottom:4px}\n", ":host ::ng-deep .ck-heading-highlight{background-color:#fef08a}\n", "@charset \"UTF-8\";::ng-deep .ck-clipboard-drop-target-line{display:none!important}:host ::ng-deep .variable-widget{background-color:#e3f2fd;color:#1976d2;border:1px solid #90caf9!important;border-radius:4px;padding:2px 6px;font-weight:600;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;font-size:10px;-webkit-user-select:none;user-select:none;display:inline-block;margin:0 4px;vertical-align:middle;font-size:0;cursor:default}:host ::ng-deep .variable-widget:before{content:attr(data-display);font-size:10px}:host ::ng-deep .variable-widget:hover{background-color:#bbdefb;box-shadow:0 1px 2px #0000001a}:host ::ng-deep .variable-widget.ck-widget_selected{outline:2px solid #2196f3;background-color:#bbdefb}:host ::ng-deep .ck.ck-content .ck-widget,:host ::ng-deep .ck.ck-content .ck-widget:hover,:host ::ng-deep .ck.ck-content .ck-widget:focus,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected:hover{outline:none!important;box-shadow:none!important}\n", ":host ::ng-deep .highlight-range{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}\n", ":host ::ng-deep .ck-comment-marker{background-color:var(--comment-bg, rgba(59, 130, 246, .3));border-bottom:2px solid rgb(59,130,246);cursor:pointer;transition:all .2s ease}:host ::ng-deep .ck-comment-marker:hover{opacity:.8}:host ::ng-deep .ck-comment-marker.ck-comment-pending{background-color:var(--comment-pending-bg, rgba(245, 158, 11, .4));border-bottom-color:#f59e0b}:host ::ng-deep .ck-comment-marker.ck-comment-normal{background-color:var(--comment-bg, rgba(59, 130, 246, .2));border-bottom-color:#3b82f6}:host ::ng-deep .ck-comment-marker.ck-comment-selected{background-color:var(--comment-selected-bg, 59, 130, 246, .5);border-bottom-color:#3b82f6}:host ::ng-deep .ck-comment-marker.ck-comment-modified{background-color:var(--comment-modified-bg, rgba(249, 115, 22, .3));border-bottom-color:#f97316}:host ::ng-deep .ck-comment-marker.ck-comment-broken{background-color:var(--comment-broken-bg, rgba(239, 68, 68, .3));border-bottom-color:#ef4444;text-decoration:line-through;text-decoration-color:#ef4444}:host ::ng-deep .ck-balloon-panel .ck-button.ck-on:hover,:host ::ng-deep .ck-balloon-panel .ck-button.ck-off:hover{background:var(--ck-color-button-default-hover-background)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: CKEditorModule }, { kind: "component", type: i1.CKEditorComponent, selector: "ckeditor", inputs: ["editor", "config", "data", "tagName", "watchdog", "editorWatchdogConfig", "disableWatchdog", "disableTwoWayDataBinding", "disabled"], outputs: ["ready", "change", "blur", "focus", "error"] }] });
4931
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: SdDocumentBuilder, isStandalone: true, selector: "sd-document-builder", inputs: { option: "option", _disabled: ["disabled", "_disabled"] }, outputs: { contentChange: "contentChange" }, ngImport: i0, template: "<div class=\"builder-container\">\n <ckeditor\n style=\"width: 100%\"\n [editor]=\"Editor\" \n [config]=\"config\" \n (ready)=\"onReady($event)\"\n [disabled]=\"disabled\">\n </ckeditor>\n</div>", styles: ["@charset \"UTF-8\";.builder-container{background-color:#f3f4f6;height:100%;overflow-y:auto;width:100%;display:flex;flex-direction:column;align-items:center}:host{display:inline-block}:host ::ng-deep .ck-editor{display:flex;flex-direction:column;align-items:center;width:100%;--ck-content-font-family: \"Times New Roman\", serif !important;--ck-content-font-size: 13pt;--ck-content-line-height: 1.15}:host ::ng-deep .ck-editor__top,:host ::ng-deep .ck-editor__main{border:none!important;box-shadow:none!important}:host ::ng-deep .ck-editor__top{position:sticky;top:0;z-index:100;width:100%;min-width:600px;margin-bottom:10px}:host ::ng-deep .ck-editor__top .ck-sticky-panel__content{border:none!important}:host ::ng-deep .ck-editor__top .ck-toolbar{background:#fff!important;box-shadow:0 4px 6px -1px #0000001a!important;padding:8px!important}:host ::ng-deep .ck-editor__top .ck-toolbar .ck-toolbar__items{display:flex;justify-content:center;flex-wrap:wrap;align-items:center}:host ::ng-deep .ck-content{background-color:#fff;width:210mm;min-height:1123px;padding:20mm!important;box-sizing:border-box!important;box-shadow:0 10px 15px -3px #0000001a}:host ::ng-deep .ck-content h1,:host ::ng-deep .ck-content h2,:host ::ng-deep .ck-content h3,:host ::ng-deep .ck-content h4,:host ::ng-deep .ck-content h5,:host ::ng-deep .ck-content h6{font-weight:400;font-size:inherit;line-height:inherit;margin-bottom:4px}:host ::ng-deep .ck-content.landscape{width:297mm}:host ::ng-deep .ck-content>*{max-width:100%!important;box-sizing:border-box!important}:host ::ng-deep .ck-content p{margin-bottom:4px;text-indent:0}:host ::ng-deep .ck-content ul,:host ::ng-deep .ck-content ol{padding-left:20px!important;margin-left:0!important;margin-bottom:4px}:host ::ng-deep .ck-content li{margin-bottom:0}:host ::ng-deep .ck-content table{margin-bottom:4px}\n", ":host ::ng-deep .ck-heading-highlight{background-color:#fef08a}\n", "@charset \"UTF-8\";::ng-deep .ck-clipboard-drop-target-line{display:none!important}:host ::ng-deep .variable-widget{display:inline-block;-webkit-user-select:none;user-select:none}:host ::ng-deep .variable-widget:not([data-binding=true]){background-color:#e3f2fd;color:#1976d2;border:1px solid #90caf9!important;border-radius:4px;padding:2px 6px;font-size:0;margin:0 2px;vertical-align:middle;font-weight:600;cursor:default}:host ::ng-deep .variable-widget:not([data-binding=true]):before{content:attr(data-display);font-size:10px;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif}:host ::ng-deep .variable-widget:not([data-binding=true]):hover{background-color:#bbdefb;box-shadow:0 1px 2px #0000001a}:host ::ng-deep .variable-widget:not([data-binding=true]).ck-widget_selected{outline:2px solid #2196f3;background-color:#bbdefb}:host ::ng-deep .variable-widget[data-binding=true]{background-color:#4caf501f;color:#2e7d32;border-bottom:2px solid #4caf50!important;border-radius:0;padding:0 1px;font-size:inherit;cursor:default}:host ::ng-deep .variable-widget[data-binding=true]:before{content:none}:host ::ng-deep .variable-widget[data-binding=true]:hover{background-color:#4caf5038}:host ::ng-deep .variable-widget[data-binding=true].ck-widget_selected{outline:2px solid #4caf50;background-color:#4caf5047}:host ::ng-deep .ck.ck-content .ck-widget,:host ::ng-deep .ck.ck-content .ck-widget:hover,:host ::ng-deep .ck.ck-content .ck-widget:focus,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected:hover{outline:none!important;box-shadow:none!important}\n", ":host ::ng-deep .highlight-range{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}\n", ":host ::ng-deep .ck-comment-marker{background-color:var(--comment-bg, rgba(59, 130, 246, .3));border-bottom:2px solid rgb(59,130,246);cursor:pointer;transition:all .2s ease}:host ::ng-deep .ck-comment-marker:hover{opacity:.8}:host ::ng-deep .ck-comment-marker.ck-comment-pending{background-color:var(--comment-pending-bg, rgba(245, 158, 11, .4));border-bottom-color:#f59e0b}:host ::ng-deep .ck-comment-marker.ck-comment-normal{background-color:var(--comment-bg, rgba(59, 130, 246, .2));border-bottom-color:#3b82f6}:host ::ng-deep .ck-comment-marker.ck-comment-selected{background-color:var(--comment-selected-bg, 59, 130, 246, .5);border-bottom-color:#3b82f6}:host ::ng-deep .ck-comment-marker.ck-comment-modified{background-color:var(--comment-modified-bg, rgba(249, 115, 22, .3));border-bottom-color:#f97316}:host ::ng-deep .ck-comment-marker.ck-comment-broken{background-color:var(--comment-broken-bg, rgba(239, 68, 68, .3));border-bottom-color:#ef4444;text-decoration:line-through;text-decoration-color:#ef4444}:host ::ng-deep .ck-balloon-panel .ck-button.ck-on:hover,:host ::ng-deep .ck-balloon-panel .ck-button.ck-off:hover{background:var(--ck-color-button-default-hover-background)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: CKEditorModule }, { kind: "component", type: i1.CKEditorComponent, selector: "ckeditor", inputs: ["editor", "config", "data", "tagName", "watchdog", "editorWatchdogConfig", "disableWatchdog", "disableTwoWayDataBinding", "disabled"], outputs: ["ready", "change", "blur", "focus", "error"] }] });
4660
4932
  }
4661
4933
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: SdDocumentBuilder, decorators: [{
4662
4934
  type: Component,
4663
- args: [{ selector: 'sd-document-builder', standalone: true, imports: [CommonModule, CKEditorModule], template: "<div class=\"builder-container\">\n <ckeditor\n style=\"width: 100%\"\n [editor]=\"Editor\" \n [config]=\"config\" \n (ready)=\"onReady($event)\"\n [disabled]=\"disabled\">\n </ckeditor>\n</div>", styles: ["@charset \"UTF-8\";.builder-container{background-color:#f3f4f6;height:100%;overflow-y:auto;width:100%;display:flex;flex-direction:column;align-items:center}:host{display:inline-block}:host ::ng-deep .ck-editor{display:flex;flex-direction:column;align-items:center;width:100%;--ck-content-font-family: \"Times New Roman\", serif !important;--ck-content-font-size: 13pt;--ck-content-line-height: 1.15}:host ::ng-deep .ck-editor__top,:host ::ng-deep .ck-editor__main{border:none!important;box-shadow:none!important}:host ::ng-deep .ck-editor__top{position:sticky;top:0;z-index:100;width:100%;min-width:600px;margin-bottom:10px}:host ::ng-deep .ck-editor__top .ck-sticky-panel__content{border:none!important}:host ::ng-deep .ck-editor__top .ck-toolbar{background:#fff!important;box-shadow:0 4px 6px -1px #0000001a!important;padding:8px!important}:host ::ng-deep .ck-editor__top .ck-toolbar .ck-toolbar__items{display:flex;justify-content:center;flex-wrap:wrap;align-items:center}:host ::ng-deep .ck-content{background-color:#fff;width:210mm;min-height:1123px;padding:20mm!important;box-sizing:border-box!important;box-shadow:0 10px 15px -3px #0000001a}:host ::ng-deep .ck-content h1,:host ::ng-deep .ck-content h2,:host ::ng-deep .ck-content h3,:host ::ng-deep .ck-content h4,:host ::ng-deep .ck-content h5,:host ::ng-deep .ck-content h6{font-weight:400;font-size:inherit;line-height:inherit;margin-bottom:4px}:host ::ng-deep .ck-content.landscape{width:297mm}:host ::ng-deep .ck-content>*{max-width:100%!important;box-sizing:border-box!important}:host ::ng-deep .ck-content p{margin-bottom:4px;text-indent:0}:host ::ng-deep .ck-content ul,:host ::ng-deep .ck-content ol{padding-left:20px!important;margin-left:0!important;margin-bottom:4px}:host ::ng-deep .ck-content li{margin-bottom:0}:host ::ng-deep .ck-content table{margin-bottom:4px}\n", ":host ::ng-deep .ck-heading-highlight{background-color:#fef08a}\n", "@charset \"UTF-8\";::ng-deep .ck-clipboard-drop-target-line{display:none!important}:host ::ng-deep .variable-widget{background-color:#e3f2fd;color:#1976d2;border:1px solid #90caf9!important;border-radius:4px;padding:2px 6px;font-weight:600;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;font-size:10px;-webkit-user-select:none;user-select:none;display:inline-block;margin:0 4px;vertical-align:middle;font-size:0;cursor:default}:host ::ng-deep .variable-widget:before{content:attr(data-display);font-size:10px}:host ::ng-deep .variable-widget:hover{background-color:#bbdefb;box-shadow:0 1px 2px #0000001a}:host ::ng-deep .variable-widget.ck-widget_selected{outline:2px solid #2196f3;background-color:#bbdefb}:host ::ng-deep .ck.ck-content .ck-widget,:host ::ng-deep .ck.ck-content .ck-widget:hover,:host ::ng-deep .ck.ck-content .ck-widget:focus,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected:hover{outline:none!important;box-shadow:none!important}\n", ":host ::ng-deep .highlight-range{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}\n", ":host ::ng-deep .ck-comment-marker{background-color:var(--comment-bg, rgba(59, 130, 246, .3));border-bottom:2px solid rgb(59,130,246);cursor:pointer;transition:all .2s ease}:host ::ng-deep .ck-comment-marker:hover{opacity:.8}:host ::ng-deep .ck-comment-marker.ck-comment-pending{background-color:var(--comment-pending-bg, rgba(245, 158, 11, .4));border-bottom-color:#f59e0b}:host ::ng-deep .ck-comment-marker.ck-comment-normal{background-color:var(--comment-bg, rgba(59, 130, 246, .2));border-bottom-color:#3b82f6}:host ::ng-deep .ck-comment-marker.ck-comment-selected{background-color:var(--comment-selected-bg, 59, 130, 246, .5);border-bottom-color:#3b82f6}:host ::ng-deep .ck-comment-marker.ck-comment-modified{background-color:var(--comment-modified-bg, rgba(249, 115, 22, .3));border-bottom-color:#f97316}:host ::ng-deep .ck-comment-marker.ck-comment-broken{background-color:var(--comment-broken-bg, rgba(239, 68, 68, .3));border-bottom-color:#ef4444;text-decoration:line-through;text-decoration-color:#ef4444}:host ::ng-deep .ck-balloon-panel .ck-button.ck-on:hover,:host ::ng-deep .ck-balloon-panel .ck-button.ck-off:hover{background:var(--ck-color-button-default-hover-background)}\n"] }]
4935
+ args: [{ selector: 'sd-document-builder', standalone: true, imports: [CommonModule, CKEditorModule], template: "<div class=\"builder-container\">\n <ckeditor\n style=\"width: 100%\"\n [editor]=\"Editor\" \n [config]=\"config\" \n (ready)=\"onReady($event)\"\n [disabled]=\"disabled\">\n </ckeditor>\n</div>", styles: ["@charset \"UTF-8\";.builder-container{background-color:#f3f4f6;height:100%;overflow-y:auto;width:100%;display:flex;flex-direction:column;align-items:center}:host{display:inline-block}:host ::ng-deep .ck-editor{display:flex;flex-direction:column;align-items:center;width:100%;--ck-content-font-family: \"Times New Roman\", serif !important;--ck-content-font-size: 13pt;--ck-content-line-height: 1.15}:host ::ng-deep .ck-editor__top,:host ::ng-deep .ck-editor__main{border:none!important;box-shadow:none!important}:host ::ng-deep .ck-editor__top{position:sticky;top:0;z-index:100;width:100%;min-width:600px;margin-bottom:10px}:host ::ng-deep .ck-editor__top .ck-sticky-panel__content{border:none!important}:host ::ng-deep .ck-editor__top .ck-toolbar{background:#fff!important;box-shadow:0 4px 6px -1px #0000001a!important;padding:8px!important}:host ::ng-deep .ck-editor__top .ck-toolbar .ck-toolbar__items{display:flex;justify-content:center;flex-wrap:wrap;align-items:center}:host ::ng-deep .ck-content{background-color:#fff;width:210mm;min-height:1123px;padding:20mm!important;box-sizing:border-box!important;box-shadow:0 10px 15px -3px #0000001a}:host ::ng-deep .ck-content h1,:host ::ng-deep .ck-content h2,:host ::ng-deep .ck-content h3,:host ::ng-deep .ck-content h4,:host ::ng-deep .ck-content h5,:host ::ng-deep .ck-content h6{font-weight:400;font-size:inherit;line-height:inherit;margin-bottom:4px}:host ::ng-deep .ck-content.landscape{width:297mm}:host ::ng-deep .ck-content>*{max-width:100%!important;box-sizing:border-box!important}:host ::ng-deep .ck-content p{margin-bottom:4px;text-indent:0}:host ::ng-deep .ck-content ul,:host ::ng-deep .ck-content ol{padding-left:20px!important;margin-left:0!important;margin-bottom:4px}:host ::ng-deep .ck-content li{margin-bottom:0}:host ::ng-deep .ck-content table{margin-bottom:4px}\n", ":host ::ng-deep .ck-heading-highlight{background-color:#fef08a}\n", "@charset \"UTF-8\";::ng-deep .ck-clipboard-drop-target-line{display:none!important}:host ::ng-deep .variable-widget{display:inline-block;-webkit-user-select:none;user-select:none}:host ::ng-deep .variable-widget:not([data-binding=true]){background-color:#e3f2fd;color:#1976d2;border:1px solid #90caf9!important;border-radius:4px;padding:2px 6px;font-size:0;margin:0 2px;vertical-align:middle;font-weight:600;cursor:default}:host ::ng-deep .variable-widget:not([data-binding=true]):before{content:attr(data-display);font-size:10px;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif}:host ::ng-deep .variable-widget:not([data-binding=true]):hover{background-color:#bbdefb;box-shadow:0 1px 2px #0000001a}:host ::ng-deep .variable-widget:not([data-binding=true]).ck-widget_selected{outline:2px solid #2196f3;background-color:#bbdefb}:host ::ng-deep .variable-widget[data-binding=true]{background-color:#4caf501f;color:#2e7d32;border-bottom:2px solid #4caf50!important;border-radius:0;padding:0 1px;font-size:inherit;cursor:default}:host ::ng-deep .variable-widget[data-binding=true]:before{content:none}:host ::ng-deep .variable-widget[data-binding=true]:hover{background-color:#4caf5038}:host ::ng-deep .variable-widget[data-binding=true].ck-widget_selected{outline:2px solid #4caf50;background-color:#4caf5047}:host ::ng-deep .ck.ck-content .ck-widget,:host ::ng-deep .ck.ck-content .ck-widget:hover,:host ::ng-deep .ck.ck-content .ck-widget:focus,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected:hover{outline:none!important;box-shadow:none!important}\n", ":host ::ng-deep .highlight-range{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}\n", ":host ::ng-deep .ck-comment-marker{background-color:var(--comment-bg, rgba(59, 130, 246, .3));border-bottom:2px solid rgb(59,130,246);cursor:pointer;transition:all .2s ease}:host ::ng-deep .ck-comment-marker:hover{opacity:.8}:host ::ng-deep .ck-comment-marker.ck-comment-pending{background-color:var(--comment-pending-bg, rgba(245, 158, 11, .4));border-bottom-color:#f59e0b}:host ::ng-deep .ck-comment-marker.ck-comment-normal{background-color:var(--comment-bg, rgba(59, 130, 246, .2));border-bottom-color:#3b82f6}:host ::ng-deep .ck-comment-marker.ck-comment-selected{background-color:var(--comment-selected-bg, 59, 130, 246, .5);border-bottom-color:#3b82f6}:host ::ng-deep .ck-comment-marker.ck-comment-modified{background-color:var(--comment-modified-bg, rgba(249, 115, 22, .3));border-bottom-color:#f97316}:host ::ng-deep .ck-comment-marker.ck-comment-broken{background-color:var(--comment-broken-bg, rgba(239, 68, 68, .3));border-bottom-color:#ef4444;text-decoration:line-through;text-decoration-color:#ef4444}:host ::ng-deep .ck-balloon-panel .ck-button.ck-on:hover,:host ::ng-deep .ck-balloon-panel .ck-button.ck-off:hover{background:var(--ck-color-button-default-hover-background)}\n"] }]
4664
4936
  }], propDecorators: { option: [{
4665
4937
  type: Input,
4666
4938
  args: [{ required: true }]