@sd-angular/core 19.0.0-beta.13 → 19.0.0-beta.15

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 (46) hide show
  1. package/assets/scss/ckeditor5.scss +59 -2
  2. package/components/document-builder/src/document-builder.component.d.ts +7 -2
  3. package/components/document-builder/src/document-builder.model.d.ts +5 -1
  4. package/components/document-builder/src/plugins/highlight-range/highlight-range.plugin.d.ts +4 -0
  5. package/components/document-builder/src/plugins/image-custom/image-custom.plugin.d.ts +31 -0
  6. package/components/document-builder/src/plugins/index.d.ts +1 -0
  7. package/components/document-builder/src/plugins/table-fit/table-fit.plugin.d.ts +25 -0
  8. package/components/index.d.ts +1 -0
  9. package/components/mini-editor/index.d.ts +2 -0
  10. package/components/mini-editor/src/mini-editor.component.d.ts +90 -0
  11. package/components/mini-editor/src/mini-editor.model.d.ts +42 -0
  12. package/components/view/index.d.ts +1 -0
  13. package/components/view/src/view.component.d.ts +14 -0
  14. package/directives/index.d.ts +1 -0
  15. package/directives/src/sd-href.directive.d.ts +9 -0
  16. package/fesm2022/sd-angular-core-components-document-builder.mjs +642 -69
  17. package/fesm2022/sd-angular-core-components-document-builder.mjs.map +1 -1
  18. package/fesm2022/sd-angular-core-components-mini-editor.mjs +326 -0
  19. package/fesm2022/sd-angular-core-components-mini-editor.mjs.map +1 -0
  20. package/fesm2022/sd-angular-core-components-view.mjs +88 -0
  21. package/fesm2022/sd-angular-core-components-view.mjs.map +1 -0
  22. package/fesm2022/sd-angular-core-components-workflow.mjs +16 -26
  23. package/fesm2022/sd-angular-core-components-workflow.mjs.map +1 -1
  24. package/fesm2022/sd-angular-core-components.mjs +1 -0
  25. package/fesm2022/sd-angular-core-components.mjs.map +1 -1
  26. package/fesm2022/sd-angular-core-directives.mjs +51 -2
  27. package/fesm2022/sd-angular-core-directives.mjs.map +1 -1
  28. package/fesm2022/sd-angular-core-forms-input-number.mjs +7 -0
  29. package/fesm2022/sd-angular-core-forms-input-number.mjs.map +1 -1
  30. package/fesm2022/sd-angular-core-forms-radio.mjs +3 -2
  31. package/fesm2022/sd-angular-core-forms-radio.mjs.map +1 -1
  32. package/fesm2022/sd-angular-core-forms-select.mjs +7 -4
  33. package/fesm2022/sd-angular-core-forms-select.mjs.map +1 -1
  34. package/fesm2022/sd-angular-core-forms-textarea.mjs +2 -2
  35. package/fesm2022/sd-angular-core-forms-textarea.mjs.map +1 -1
  36. package/fesm2022/sd-angular-core-modules-layout.mjs +52 -17
  37. package/fesm2022/sd-angular-core-modules-layout.mjs.map +1 -1
  38. package/fesm2022/sd-angular-core-modules-oidc.mjs +0 -2
  39. package/fesm2022/sd-angular-core-modules-oidc.mjs.map +1 -1
  40. package/modules/layout/components/sidebar-v1/components/sidebar/sidebar.component.d.ts +1 -0
  41. package/modules/layout/components/sidebar-v1/components/user/user.component.d.ts +5 -2
  42. package/modules/layout/configurations/layout.configuration.d.ts +3 -0
  43. package/modules/layout/services/storage/storage.service.d.ts +1 -0
  44. package/package.json +64 -56
  45. package/sd-angular-core-19.0.0-beta.15.tgz +0 -0
  46. package/sd-angular-core-19.0.0-beta.13.tgz +0 -0
@@ -3,7 +3,7 @@ import { EventEmitter, Output, Input, Component } from '@angular/core';
3
3
  import { CommonModule } from '@angular/common';
4
4
  import * as i1 from '@ckeditor/ckeditor5-angular';
5
5
  import { CKEditorModule } from '@ckeditor/ckeditor5-angular';
6
- import { Plugin, ButtonView, ClassicEditor, Essentials, Paragraph, Bold, Italic, Underline, FontSize, FontColor, FontBackgroundColor, Alignment, Widget, toWidget, GeneralHtmlSupport, FontFamily, Heading, List, Table, TableToolbar, TableProperties, TableCellProperties, TableColumnResize, PasteFromOffice, PageBreak, Undo, Subscript, Superscript, Image, ImageUpload, ImageToolbar, ImageCaption, ImageResize, ImageStyle } from 'ckeditor5';
6
+ import { Plugin, ButtonView, ClassicEditor, Essentials, Paragraph, Bold, Italic, Underline, FontSize, FontColor, FontBackgroundColor, Alignment, Widget, toWidget, GeneralHtmlSupport, FontFamily, Heading, List, Table, TableToolbar, TableProperties, TableCellProperties, TableColumnResize, PasteFromOffice, PageBreak, Undo, Subscript, Superscript, Image, ImageUpload, ImageToolbar, ImageCaption, ImageResize, ImageStyle, ImageBlock, Indent, IndentBlock } from 'ckeditor5';
7
7
  import { Subscription, Subject, throttleTime } from 'rxjs';
8
8
  import { SdResolveMaybeAsync, hslToHex, rgbToHex, SdUtilities } from '@sd-angular/core/utilities';
9
9
  import { v4 } from 'uuid';
@@ -200,7 +200,7 @@ class CommentPlugin extends Plugin {
200
200
  // BẮN EVENT RA NGOÀI - KHÔNG TỰ ADD MARKER
201
201
  // Angular component sẽ xử lý logic (mở modal, validation, etc.)
202
202
  // và gọi lại hàm addComment() nếu cần
203
- option.onAddComment(range);
203
+ option.onAddComment({ range, selectedText });
204
204
  });
205
205
  return view;
206
206
  });
@@ -453,37 +453,111 @@ class VariablePlugin extends Plugin {
453
453
  }
454
454
  }, { priority: 'highest' });
455
455
  // 8. Xử lý sự kiện Copy (Clipboard Output)
456
- // Khi copy, thay thế variable bằng text
457
- this.listenTo(editor.editing.view.document, 'clipboardOutput', (_, data) => {
456
+ // Chỉ set thêm text/plain fallback, không thay đổi HTML content
457
+ this.listenTo(editor.editing.view.document, 'clipboardOutput', (evt, data) => {
458
458
  const isCopyOrCut = data.method === 'copy' || data.method === 'cut';
459
- // Nếu không phải hành động copy hoặc cut thì thoát hàm
460
- if (!isCopyOrCut) {
459
+ if (!isCopyOrCut)
461
460
  return;
462
- }
461
+ // Set thêm plain text fallback cho external apps
462
+ const dataTransfer = data.dataTransfer;
463
463
  const content = data.content;
464
- editor.editing.view.change(writer => {
465
- // Tạo range bao quanh toàn bộ nội dung clipboard
466
- const range = writer.createRangeIn(content);
467
- const itemsToReplace = [];
468
- // Duyệt qua tất cả các phần tử trong clipboard để tìm variable
469
- for (const item of range.getItems()) {
470
- // Kiểm tra đúng là thẻ span và có class variable-widget
471
- if (item.is('element', 'span') && item.hasClass('variable-widget')) {
472
- itemsToReplace.push(item);
464
+ // Lấy tất cả text từ content (bao gồm cả variable dạng {{text}})
465
+ let plainText = '';
466
+ const viewRange = editor.editing.view.createRangeIn(content);
467
+ for (const item of viewRange.getItems()) {
468
+ if (item.is('$text') || item.is('element', 'span')) {
469
+ const itemAny = item;
470
+ if (item.is('$text') && itemAny.data) {
471
+ plainText += itemAny.data;
472
+ }
473
+ else if (item.is('element', 'span') && item.hasClass('variable-widget')) {
474
+ const display = item.getAttribute('data-display');
475
+ if (display)
476
+ plainText += `{{${display}}}`;
477
+ }
478
+ }
479
+ }
480
+ if (plainText) {
481
+ dataTransfer.setData('text/plain', plainText);
482
+ }
483
+ // HTML content giữ nguyên - CKEditor sẽ tự xử lý
484
+ }, { priority: 'low' });
485
+ // 9. Xử lý sự kiện Paste (Clipboard Input)
486
+ // Nếu paste từ external source (chỉ có text, không có HTML variable)
487
+ // thì chuyển {{text}} thành variable widget
488
+ this.listenTo(editor.editing.view.document, 'clipboardInput', (evt, data) => {
489
+ const dataTransfer = data.dataTransfer;
490
+ // Nếu có HTML chứa variable-widget thì để CKEditor xử lý (upcast converter)
491
+ const html = dataTransfer.getData('text/html');
492
+ if (html && html.includes('variable-widget')) {
493
+ return; // Để CKEditor upcast converter xử lý
494
+ }
495
+ // Chỉ xử lý nếu chỉ có plain text với pattern {{text}}
496
+ let text = dataTransfer.getData('text/plain');
497
+ if (!text)
498
+ return;
499
+ // Kiểm tra có chứa pattern {{text}} không (không cần id, value)
500
+ const variablePattern = /\{\{([^}]+)\}\}/g;
501
+ if (!variablePattern.test(text)) {
502
+ return;
503
+ }
504
+ // Reset lastIndex sau khi test
505
+ variablePattern.lastIndex = 0;
506
+ evt.stop();
507
+ editor.model.change(writer => {
508
+ const selection = editor.model.document.selection;
509
+ const position = selection.getFirstPosition();
510
+ if (!position)
511
+ return;
512
+ // Tách text thành các phần: normal text và variables
513
+ let lastIndex = 0;
514
+ let match;
515
+ const fragments = [];
516
+ while ((match = variablePattern.exec(text)) !== null) {
517
+ // Thêm text trước variable
518
+ if (match.index > lastIndex) {
519
+ fragments.push({
520
+ type: 'text',
521
+ content: text.slice(lastIndex, match.index),
522
+ });
473
523
  }
524
+ // Thêm variable
525
+ const display = match[1];
526
+ fragments.push({
527
+ type: 'variable',
528
+ content: match[0],
529
+ display,
530
+ });
531
+ lastIndex = match.index + match[0].length;
474
532
  }
475
- // Thay thế variable bằng text
476
- for (const item of itemsToReplace) {
477
- const displayText = item.getAttribute('data-display');
478
- if (displayText) {
479
- // Tạo một node text thuần túy
480
- const textNode = writer.createText(`{{${displayText}}}`);
481
- // Chèn text node vào ngay trước widget cũ
482
- writer.insert(writer.createPositionBefore(item), textNode);
483
- // Xóa widget cũ đi
484
- writer.remove(item);
533
+ // Thêm text còn lại sau variable cuối cùng
534
+ if (lastIndex < text.length) {
535
+ fragments.push({
536
+ type: 'text',
537
+ content: text.slice(lastIndex),
538
+ });
539
+ }
540
+ // Chèn từng fragment vào document
541
+ let currentPosition = position;
542
+ for (const fragment of fragments) {
543
+ if (fragment.type === 'text' && fragment.content) {
544
+ const textNode = writer.createText(fragment.content);
545
+ writer.insert(textNode, currentPosition);
546
+ currentPosition = writer.createPositionAfter(textNode);
547
+ }
548
+ else if (fragment.type === 'variable' && fragment.display) {
549
+ const variableElem = writer.createElement('variable', {
550
+ id: v4(),
551
+ uuid: v4(),
552
+ value: fragment.display,
553
+ display: fragment.display,
554
+ });
555
+ writer.insert(variableElem, currentPosition);
556
+ currentPosition = writer.createPositionAfter(variableElem);
485
557
  }
486
558
  }
559
+ // Đặt con trỏ sau nội dung vừa paste
560
+ writer.setSelection(currentPosition);
487
561
  });
488
562
  });
489
563
  }
@@ -503,48 +577,209 @@ class TableFitPlugin extends Plugin {
503
577
  const editor = this.editor;
504
578
  // Can thiệp vào quá trình convert từ View (HTML Paste) sang Model
505
579
  editor.conversion.for('upcast').add(dispatcher => {
506
- dispatcher.on('element:table', (evt, data, conversionApi) => {
507
- // 1. Gọi consume để báo với CKEditor là chúng ta sẽ xử lý element này
508
- if (!conversionApi.consumable.consume(data.viewItem, { name: true })) {
580
+ dispatcher.on('element:table', (evt, data) => {
581
+ if (!data.modelRange)
509
582
  return;
510
- }
511
- // 2. Thực hiện chuyển đổi mặc định để tạo ra model element
512
- const { modelCursor, modelRange } = conversionApi.convertChildren(data.viewItem, data.modelCursor);
513
- // 3. Bây giờ modelRange chắc chắn tồn tại, ta tìm element table trong đó
514
- for (const item of modelRange.getItems()) {
583
+ for (const item of data.modelRange.getItems()) {
515
584
  if (item.is('element', 'table')) {
516
585
  editor.model.change(writer => {
517
- writer.setAttribute('tableWidth', '100%', item);
586
+ this._applyTableDefaults(writer, item);
587
+ this._applyCellBorders(writer, item);
518
588
  });
519
589
  }
520
590
  }
521
- // 4. Cập nhật modelCursor để dispatcher biết đã xử lý xong tới đâu
522
- data.modelRange = modelRange;
523
- data.modelCursor = modelCursor;
524
591
  }, { priority: 'low' });
525
- // Chạy sau cùng để ghi đè các logic mặc định
526
592
  });
527
- // Lắng nghe lệnh insertTable để can thiệp ngay sau khi bảng được tạo
593
+ const findInnerTable = (viewElement) => {
594
+ if (!viewElement)
595
+ return null;
596
+ if (viewElement.name === 'table')
597
+ return viewElement;
598
+ for (const child of viewElement.getChildren()) {
599
+ if (child.name === 'table')
600
+ return child;
601
+ const found = findInnerTable(child);
602
+ if (found)
603
+ return found;
604
+ }
605
+ return null;
606
+ };
607
+ editor.conversion.for('downcast').add(dispatcher => {
608
+ dispatcher.on('attribute:tableWidth:table', (evt, data, conversionApi) => {
609
+ const viewWriter = conversionApi.writer;
610
+ const viewElement = conversionApi.mapper.toViewElement(data.item);
611
+ if (!viewElement)
612
+ return;
613
+ const innerTable = findInnerTable(viewElement);
614
+ if (!innerTable)
615
+ return;
616
+ viewWriter.setStyle('border-collapse', 'collapse', innerTable);
617
+ viewWriter.setStyle('margin', '0', innerTable);
618
+ viewWriter.setStyle('width', '100%', innerTable);
619
+ viewWriter.setStyle('width', '100%', viewElement);
620
+ });
621
+ dispatcher.on('insert:table', (evt, data, conversionApi) => {
622
+ const viewWriter = conversionApi.writer;
623
+ const viewElement = conversionApi.mapper.toViewElement(data.item);
624
+ if (!viewElement)
625
+ return;
626
+ const innerTable = findInnerTable(viewElement);
627
+ if (!innerTable)
628
+ return;
629
+ viewWriter.setStyle('border-collapse', 'collapse', innerTable);
630
+ viewWriter.setStyle('margin', '0', innerTable);
631
+ viewWriter.setStyle('width', '100%', innerTable);
632
+ viewWriter.setStyle('width', '100%', viewElement);
633
+ });
634
+ });
635
+ // Lắng nghe lệnh insertTable
528
636
  const insertTableCommand = editor.commands.get('insertTable');
529
637
  if (insertTableCommand) {
530
- // Dùng 'on' event để hook vào sau khi lệnh thực thi
531
- this.listenTo(insertTableCommand, 'execute', (evt, args) => {
638
+ this.listenTo(insertTableCommand, 'execute', () => {
532
639
  editor.model.change(writer => {
533
- // Lấy vị trí con trỏ hiện tại (nơi bảng vừa được chèn)
534
- const selection = editor.model.document.selection;
535
- const position = selection.getFirstPosition();
640
+ const position = editor.model.document.selection.getFirstPosition();
536
641
  if (!position)
537
642
  return;
538
- // Tìm element table vừa chèn (nó nằm ở vị trí cha của selection)
539
- // Khi vừa insert, con trỏ thường nằm trong ô đầu tiên của bảng
540
643
  const tableElement = position.findAncestor('table');
541
644
  if (tableElement) {
542
- // Ép width 100% cho bảng mới vẽ
543
- writer.setAttribute('tableWidth', '100%', tableElement);
645
+ this._applyTableDefaults(writer, tableElement);
646
+ writer.setAttribute('tableBorderColor', '#000000', tableElement);
647
+ writer.setAttribute('tableBorderStyle', 'solid', tableElement);
648
+ writer.setAttribute('tableBorderWidth', '1pt', tableElement);
649
+ this._applyCellBorders(writer, tableElement);
544
650
  }
545
651
  });
546
652
  });
547
653
  }
654
+ // Listen for row/column commands
655
+ const tableCommands = [
656
+ 'insertTableRowAbove',
657
+ 'insertTableRowBelow',
658
+ 'insertTableColumnLeft',
659
+ 'insertTableColumnRight',
660
+ 'resizeTableRow',
661
+ 'resizeTableColumn',
662
+ 'setTableColumnWidth',
663
+ 'tableColumnWidth'
664
+ ];
665
+ tableCommands.forEach(cmdName => {
666
+ const cmd = editor.commands.get(cmdName);
667
+ if (cmd) {
668
+ this.listenTo(cmd, 'execute', () => {
669
+ editor.model.change(writer => {
670
+ const position = editor.model.document.selection.getFirstPosition();
671
+ if (!position)
672
+ return;
673
+ const tableElement = position.findAncestor('table');
674
+ if (tableElement) {
675
+ this._applyTableDefaults(writer, tableElement);
676
+ this._applyCellBorders(writer, tableElement);
677
+ }
678
+ });
679
+ });
680
+ }
681
+ });
682
+ // Setup style preservation on model change
683
+ this._setupStylePreservationOnModelChange();
684
+ }
685
+ /**
686
+ * Apply default table width
687
+ */
688
+ _applyTableDefaults(writer, tableElement) {
689
+ if (!tableElement)
690
+ return;
691
+ writer.setAttribute('tableWidth', '100%', tableElement);
692
+ }
693
+ /**
694
+ * Apply default borders to all cells in a table
695
+ */
696
+ _applyCellBorders(writer, tableElement) {
697
+ if (!tableElement)
698
+ return;
699
+ for (const row of tableElement.getChildren()) {
700
+ for (const cell of row.getChildren()) {
701
+ if (!cell.getAttribute('tableCellBorderColor')) {
702
+ writer.setAttribute('tableCellBorderColor', '#000000', cell);
703
+ }
704
+ if (!cell.getAttribute('tableCellBorderStyle')) {
705
+ writer.setAttribute('tableCellBorderStyle', 'solid', cell);
706
+ }
707
+ if (!cell.getAttribute('tableCellBorderWidth')) {
708
+ writer.setAttribute('tableCellBorderWidth', '1pt', cell);
709
+ }
710
+ if (!cell.getAttribute('tableCellPadding')) {
711
+ writer.setAttribute('tableCellPadding', '0.4em', cell);
712
+ }
713
+ }
714
+ }
715
+ }
716
+ /**
717
+ * Setup listener to preserve cell/table styles when model changes
718
+ */
719
+ _setupStylePreservationOnModelChange() {
720
+ const editor = this.editor;
721
+ // Use listenTo for proper cleanup via destroy()
722
+ this.listenTo(editor.model.document, 'change', (evt, batch) => {
723
+ if (batch?.isLocal === false)
724
+ return;
725
+ const changes = editor.model.document.differ.getChanges();
726
+ const tablesToFix = this._findTablesNeedingFix(changes);
727
+ if (tablesToFix.size > 0) {
728
+ editor.model.enqueueChange(() => {
729
+ editor.model.change(writer => {
730
+ for (const table of tablesToFix) {
731
+ this._applyTableDefaults(writer, table);
732
+ this._applyCellBorders(writer, table);
733
+ }
734
+ });
735
+ });
736
+ }
737
+ });
738
+ }
739
+ /**
740
+ * Find tables that need border fixes from model changes
741
+ */
742
+ _findTablesNeedingFix(changes) {
743
+ const tablesToFix = new Set();
744
+ for (const change of changes) {
745
+ if (change.type === 'attribute') {
746
+ const attrKey = change.attributeKey;
747
+ if (attrKey && (attrKey.includes('table') || attrKey.includes('column') || attrKey.includes('width'))) {
748
+ const element = change.item;
749
+ if (element) {
750
+ const parentTable = this._findParentTable(element);
751
+ if (parentTable)
752
+ tablesToFix.add(parentTable);
753
+ }
754
+ }
755
+ }
756
+ if (change.type === 'insert' && change.position) {
757
+ const tableElement = change.position.findAncestor?.('table') ||
758
+ change.position.parent?.findAncestor?.('table');
759
+ if (tableElement)
760
+ tablesToFix.add(tableElement);
761
+ }
762
+ }
763
+ return tablesToFix;
764
+ }
765
+ /**
766
+ * Find parent table element
767
+ */
768
+ _findParentTable(element) {
769
+ if (!element)
770
+ return null;
771
+ let parent = element;
772
+ while (parent && !parent.is?.('element', 'table')) {
773
+ parent = parent.parent;
774
+ }
775
+ return parent;
776
+ }
777
+ /**
778
+ * Cleanup listeners when plugin is destroyed
779
+ * Note: this.listenTo() listeners are automatically cleaned up by super.destroy()
780
+ */
781
+ destroy() {
782
+ super.destroy();
548
783
  }
549
784
  }
550
785
 
@@ -592,6 +827,243 @@ class Base64UploadAdapter {
592
827
  }
593
828
  }
594
829
 
830
+ class ImageCustomPlugin extends Plugin {
831
+ static get pluginName() {
832
+ return 'ImageCustomPlugin';
833
+ }
834
+ init() {
835
+ const editor = this.editor;
836
+ // Thiết lập style mặc định là alignCenter khi chèn ảnh
837
+ editor.commands.get('imageUpload')?.on('execute', (evt, args) => {
838
+ // Đặt style mặc định sau khi ảnh được chèn
839
+ setTimeout(() => {
840
+ const selection = editor.model.document.selection;
841
+ const imageElement = selection.getSelectedElement();
842
+ if (imageElement && (imageElement.name === 'imageBlock' || imageElement.name === 'imageInline')) {
843
+ const currentStyle = imageElement.getAttribute('imageStyle');
844
+ // Chỉ đặt mặc định nếu chưa có style nào
845
+ if (!currentStyle) {
846
+ editor.model.change(writer => {
847
+ writer.setAttribute('imageStyle', 'alignCenter', imageElement);
848
+ });
849
+ }
850
+ }
851
+ }, 0);
852
+ });
853
+ // Downcast: Model -> View (HTML output)
854
+ // CKEditor 5 có 2 loại ảnh: imageBlock và imageInline
855
+ editor.conversion.for('downcast').add(dispatcher => {
856
+ // Xử lý ảnh block (được wrap trong figure)
857
+ dispatcher.on('insert:imageBlock', (evt, data, conversionApi) => {
858
+ this.handleImageInsert(evt, data, conversionApi);
859
+ }, { priority: 'low' });
860
+ // Xử lý ảnh inline
861
+ dispatcher.on('insert:imageInline', (evt, data, conversionApi) => {
862
+ this.handleImageInsert(evt, data, conversionApi);
863
+ }, { priority: 'low' });
864
+ // Xử lý thay đổi attribute cho cả 2 loại ảnh
865
+ ['imageBlock', 'imageInline'].forEach(imageType => {
866
+ // Xử lý thay đổi src
867
+ dispatcher.on(`attribute:src:${imageType}`, (evt, data, conversionApi) => {
868
+ this.handleImageAttributeChange(evt, data, conversionApi);
869
+ }, { priority: 'low' });
870
+ // Xử lý thay đổi width
871
+ dispatcher.on(`attribute:width:${imageType}`, (evt, data, conversionApi) => {
872
+ this.handleImageAttributeChange(evt, data, conversionApi);
873
+ }, { priority: 'low' });
874
+ // Xử lý thay đổi height
875
+ dispatcher.on(`attribute:height:${imageType}`, (evt, data, conversionApi) => {
876
+ this.handleImageAttributeChange(evt, data, conversionApi);
877
+ }, { priority: 'low' });
878
+ // Xử lý thay đổi imageStyle (căn chỉnh) - thêm float inline style
879
+ dispatcher.on(`attribute:imageStyle:${imageType}`, (evt, data, conversionApi) => {
880
+ this.handleImageStyleChange(evt, data, conversionApi);
881
+ }, { priority: 'low' });
882
+ });
883
+ });
884
+ // Xử lý upcast (HTML paste) - xóa aspect-ratio từ HTML đầu vào
885
+ editor.conversion.for('upcast').add(dispatcher => {
886
+ dispatcher.on('element:img', (evt, data, conversionApi) => {
887
+ const viewItem = data.viewItem;
888
+ if (!viewItem)
889
+ return;
890
+ // Kiểm tra viewItem có các method cần thiết
891
+ if (typeof viewItem.getStyle !== 'function')
892
+ return;
893
+ // Xóa aspect-ratio từ inline styles nếu có
894
+ const hasAspectRatio = viewItem.getStyle('aspect-ratio');
895
+ if (hasAspectRatio && typeof viewItem.removeStyle === 'function') {
896
+ viewItem.removeStyle('aspect-ratio');
897
+ }
898
+ // Đặt custom styles nếu _styles map tồn tại
899
+ if (viewItem._styles && typeof viewItem._styles.set === 'function') {
900
+ viewItem._styles.set('margin', '0');
901
+ viewItem._styles.set('border', '0');
902
+ viewItem._styles.set('max-width', '100%');
903
+ viewItem._styles.set('height', 'auto');
904
+ }
905
+ }, { priority: 'high' });
906
+ });
907
+ }
908
+ /**
909
+ * Xử lý sự kiện chèn ảnh
910
+ */
911
+ handleImageInsert(evt, data, conversionApi) {
912
+ const viewWriter = conversionApi.writer;
913
+ const viewElement = conversionApi.mapper.toViewElement(data.item);
914
+ if (!viewElement)
915
+ return;
916
+ // viewElement có thể là figure (cho block) hoặc img itself (cho inline)
917
+ // Tìm element img thực tế
918
+ const imgElement = this.findImgElement(viewElement);
919
+ if (!imgElement)
920
+ return;
921
+ this.applyCustomStyles(viewWriter, imgElement);
922
+ }
923
+ /**
924
+ * Xử lý sự kiện thay đổi attribute ảnh
925
+ */
926
+ handleImageAttributeChange(evt, data, conversionApi) {
927
+ const viewWriter = conversionApi.writer;
928
+ const viewElement = conversionApi.mapper.toViewElement(data.item);
929
+ if (!viewElement)
930
+ return;
931
+ const imgElement = this.findImgElement(viewElement);
932
+ if (!imgElement)
933
+ return;
934
+ this.applyCustomStyles(viewWriter, imgElement);
935
+ }
936
+ /**
937
+ * Xử lý sự kiện thay đổi style ảnh - thêm float inline style cho căn chỉnh
938
+ */
939
+ handleImageStyleChange(evt, data, conversionApi) {
940
+ const viewWriter = conversionApi.writer;
941
+ const viewElement = conversionApi.mapper.toViewElement(data.item);
942
+ if (!viewElement)
943
+ return;
944
+ // Tìm container ck-widget (element figure)
945
+ const widgetElement = this.findWidgetElement(viewElement);
946
+ if (!widgetElement)
947
+ return;
948
+ // Lấy giá trị imageStyle (căn chỉnh)
949
+ const imageStyle = data.item.getAttribute('imageStyle');
950
+ // Xóa các style hiện có trước
951
+ viewWriter.removeStyle('float', widgetElement);
952
+ viewWriter.removeStyle('margin', widgetElement);
953
+ viewWriter.removeStyle('text-align', widgetElement);
954
+ // Áp dụng style dựa trên căn chỉnh ảnh
955
+ // Các options đã cấu hình: ['inline', 'alignLeft', 'alignRight', 'alignCenter']
956
+ switch (imageStyle) {
957
+ case 'inline':
958
+ // Ảnh inline - không style đặc biệt, chỉ flow inline
959
+ break;
960
+ case 'alignLeft':
961
+ // Float trái
962
+ viewWriter.setStyle('float', 'left', widgetElement);
963
+ viewWriter.setStyle('margin', '0 16px 16px 0', widgetElement);
964
+ break;
965
+ case 'alignRight':
966
+ // Float phải
967
+ viewWriter.setStyle('float', 'right', widgetElement);
968
+ viewWriter.setStyle('margin', '0 0 16px 16px', widgetElement);
969
+ break;
970
+ case 'alignCenter':
971
+ case 'block':
972
+ // Căn giữa
973
+ viewWriter.setStyle('text-align', 'center', widgetElement);
974
+ viewWriter.setStyle('margin', '16px auto', widgetElement);
975
+ break;
976
+ default:
977
+ // Mặc định - căn giữa
978
+ viewWriter.setStyle('text-align', 'center', widgetElement);
979
+ viewWriter.setStyle('margin', '16px auto', widgetElement);
980
+ break;
981
+ }
982
+ // Áp dụng custom styles cơ bản cho element img
983
+ const imgElement = this.findImgElement(viewElement);
984
+ if (imgElement) {
985
+ this.applyCustomStyles(viewWriter, imgElement);
986
+ }
987
+ }
988
+ /**
989
+ * Áp dụng custom styles cho element ảnh
990
+ */
991
+ applyCustomStyles(viewWriter, imgElement) {
992
+ // Xóa aspect-ratio nếu tồn tại
993
+ if (imgElement.getStyle('aspect-ratio')) {
994
+ viewWriter.removeStyle('aspect-ratio', imgElement);
995
+ }
996
+ // Áp dụng custom styles
997
+ viewWriter.setStyle('margin', '0', imgElement);
998
+ viewWriter.setStyle('border', '0', imgElement);
999
+ viewWriter.setStyle('max-width', '100%', imgElement);
1000
+ viewWriter.setStyle('height', 'auto', imgElement);
1001
+ }
1002
+ /**
1003
+ * Tìm container ck-widget (element figure)
1004
+ * CKEditor wrap ảnh block trong <figure class="ck-widget"><img></figure>
1005
+ */
1006
+ findWidgetElement(viewElement) {
1007
+ if (!viewElement)
1008
+ return null;
1009
+ // Nếu đây là element figure itself
1010
+ if (viewElement.name === 'figure') {
1011
+ return viewElement;
1012
+ }
1013
+ // Cho ảnh inline, trả về element itself (span wrapper)
1014
+ if (viewElement.name === 'span') {
1015
+ return viewElement;
1016
+ }
1017
+ // Tìm ngược lên tree để tìm figure/ck-widget
1018
+ let current = viewElement;
1019
+ while (current) {
1020
+ if (current.name === 'figure' || current.name === 'span') {
1021
+ return current;
1022
+ }
1023
+ // Di chuyển lên parent
1024
+ if (current.parent) {
1025
+ current = current.parent;
1026
+ }
1027
+ else {
1028
+ break;
1029
+ }
1030
+ }
1031
+ // Nếu không tìm thấy widget, trả về element gốc
1032
+ return viewElement;
1033
+ }
1034
+ /**
1035
+ * Tìm element img thực tế bên trong widget structure
1036
+ * CKEditor wrap ảnh block trong <figure class="ck-widget"><img></figure>
1037
+ */
1038
+ findImgElement(viewElement) {
1039
+ if (!viewElement)
1040
+ return null;
1041
+ // Nếu đây là element img itself
1042
+ if (viewElement.name === 'img') {
1043
+ return viewElement;
1044
+ }
1045
+ // Cho structure widget của CKEditor, tìm đệ quy
1046
+ // Ảnh block: figure > span > img
1047
+ // Ảnh thường được wrap trong một container
1048
+ const queue = [viewElement];
1049
+ while (queue.length > 0) {
1050
+ const current = queue.shift();
1051
+ if (!current)
1052
+ continue;
1053
+ if (current.name === 'img') {
1054
+ return current;
1055
+ }
1056
+ // Thêm children vào queue
1057
+ if (current.getChildren) {
1058
+ for (const child of current.getChildren()) {
1059
+ queue.push(child);
1060
+ }
1061
+ }
1062
+ }
1063
+ return null;
1064
+ }
1065
+ }
1066
+
595
1067
  // Icon khổ dọc (Mặc định cũ)
596
1068
  const ICON_PORTRAIT = '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M14 2H6c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6V4h8v12z"/></svg>';
597
1069
  // Icon khổ ngang (Mới)
@@ -855,6 +1327,18 @@ function normalize(content) {
855
1327
  return normalized;
856
1328
  }
857
1329
 
1330
+ class HighlightRangePlugin extends Plugin {
1331
+ init() {
1332
+ const editor = this.editor;
1333
+ editor.conversion.for('editingDowncast').markerToHighlight({
1334
+ model: 'highlightRange',
1335
+ view: {
1336
+ classes: 'highlight-range',
1337
+ },
1338
+ });
1339
+ }
1340
+ }
1341
+
858
1342
  class SdDocumentBuilder {
859
1343
  option;
860
1344
  disabled = false;
@@ -909,13 +1393,18 @@ class SdDocumentBuilder {
909
1393
  ImageCaption,
910
1394
  ImageResize,
911
1395
  ImageStyle,
1396
+ ImageBlock,
912
1397
  // Custom Plugin
913
1398
  HeadingPlugin,
914
1399
  CommentPlugin,
915
1400
  VariablePlugin,
916
1401
  TableFitPlugin,
917
- ImageUploadPlugin,
1402
+ Indent,
1403
+ IndentBlock,
918
1404
  PageOrientationPlugin,
1405
+ ImageUploadPlugin,
1406
+ ImageCustomPlugin,
1407
+ HighlightRangePlugin,
919
1408
  ],
920
1409
  toolbar: {
921
1410
  items: [
@@ -950,7 +1439,18 @@ class SdDocumentBuilder {
950
1439
  shouldNotGroupWhenFull: true,
951
1440
  },
952
1441
  image: {
953
- toolbar: ['toggleImageCaption', '|', 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side'],
1442
+ styles: {
1443
+ options: ['inline', 'alignLeft', 'alignRight', 'alignCenter'],
1444
+ },
1445
+ toolbar: [
1446
+ 'imageStyle:inline',
1447
+ 'imageStyle:alignCenter',
1448
+ {
1449
+ name: 'imageStyle:alignDropdown',
1450
+ items: ['imageStyle:alignLeft', 'imageStyle:alignRight'],
1451
+ defaultItem: 'imageStyle:alignLeft',
1452
+ },
1453
+ ],
954
1454
  },
955
1455
  fontSize: {
956
1456
  options: this.#fontSizeOptions,
@@ -976,13 +1476,29 @@ class SdDocumentBuilder {
976
1476
  contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells', '|', 'tableProperties', 'tableCellProperties'],
977
1477
  tableProperties: {
978
1478
  borderColors: this.#sharedColors,
1479
+ backgroundColors: this.#sharedColors,
979
1480
  colorPicker: this.#colorPickerConfig,
1481
+ defaultProperties: {
1482
+ borderStyle: 'solid',
1483
+ borderWidth: '1px',
1484
+ borderColor: '#ccc',
1485
+ },
980
1486
  },
981
1487
  tableCellProperties: {
982
1488
  borderColors: this.#sharedColors,
1489
+ backgroundColors: this.#sharedColors,
983
1490
  colorPicker: this.#colorPickerConfig,
1491
+ defaultProperties: {
1492
+ borderStyle: 'solid',
1493
+ borderWidth: '1px',
1494
+ borderColor: '#ccc',
1495
+ },
984
1496
  },
985
1497
  },
1498
+ indentBlock: {
1499
+ offset: 48, // Đơn vị px cho mỗi mức indent (tương đương 0.5 inch)
1500
+ unit: 'px',
1501
+ },
986
1502
  // Quan trọng: Cho phép paste style từ Word nhưng bỏ qua margin/padding
987
1503
  htmlSupport: {
988
1504
  allow: [
@@ -1034,6 +1550,27 @@ class SdDocumentBuilder {
1034
1550
  const content = editor.getData();
1035
1551
  this.#contentChangeSubject.next(content);
1036
1552
  });
1553
+ try {
1554
+ // Manual keybinding cho Tab nếu cần
1555
+ editor.keystrokes.set('Tab', (evt, cancel) => {
1556
+ const command = editor.commands.get('indentBlock');
1557
+ if (command && command.isEnabled) {
1558
+ editor.execute('indentBlock');
1559
+ cancel();
1560
+ }
1561
+ });
1562
+ // Manual keybinding cho Shift+Tab
1563
+ editor.keystrokes.set('Shift+Tab', (evt, cancel) => {
1564
+ const command = editor.commands.get('outdentBlock');
1565
+ if (command && command.isEnabled) {
1566
+ editor.execute('outdentBlock');
1567
+ cancel();
1568
+ }
1569
+ });
1570
+ }
1571
+ catch (error) {
1572
+ console.warn('Error setting up indent keybindings:', error);
1573
+ }
1037
1574
  this.#updateState();
1038
1575
  }
1039
1576
  setContent = (html) => {
@@ -1248,21 +1785,28 @@ class SdDocumentBuilder {
1248
1785
  all: () => {
1249
1786
  if (!this.#editor)
1250
1787
  return [];
1251
- const markers = this.#editor.model.markers;
1252
- const comments = [];
1253
- // Duyệt qua tất cả markers trong Model
1254
- for (const marker of markers) {
1255
- // Chỉ lấy marker do plugin comment tạo ra (prefix 'comment:')
1256
- if (marker.name.startsWith('comment:')) {
1257
- // Lấy text nằm trong vùng marker đó
1258
- const currentText = this.#getTextFromRange(marker.getRange());
1259
- comments.push({
1260
- markerId: marker.name,
1261
- selectedText: currentText,
1262
- });
1788
+ const editableElement = this.#editor.ui.view.editable.element;
1789
+ if (!editableElement)
1790
+ return [];
1791
+ const commentMarkers = editableElement.querySelectorAll('.ck-comment-marker[data-comment-id^="comment:"]') || [];
1792
+ const commentsMap = new Map();
1793
+ commentMarkers.forEach(el => {
1794
+ const markerId = el.getAttribute('data-comment-id');
1795
+ if (markerId) {
1796
+ const existing = commentsMap.get(markerId);
1797
+ const text = el.textContent || '';
1798
+ if (existing) {
1799
+ existing.selectedText += text;
1800
+ }
1801
+ else {
1802
+ commentsMap.set(markerId, {
1803
+ markerId,
1804
+ selectedText: text,
1805
+ });
1806
+ }
1263
1807
  }
1264
- }
1265
- return comments;
1808
+ });
1809
+ return Array.from(commentsMap.values());
1266
1810
  },
1267
1811
  /**
1268
1812
  * Thêm comment vào vùng text đang được chọn
@@ -1270,7 +1814,11 @@ class SdDocumentBuilder {
1270
1814
  * @param data - Dữ liệu extra data
1271
1815
  * @returns SdDocumentBuilderComment hoặc null nếu không có text được chọn
1272
1816
  */
1273
- add: (range, comment, data) => {
1817
+ add: (range, comment, args) => {
1818
+ // markerIdExternal khi truyền từ bên ngoài vào sẽ có thể là Date.now() hoặc uuidv4().
1819
+ // Miễn là đảm bảo markerIdExternal là unique.
1820
+ // Phục vụ cho case gọi API comment thành công thì mới sinh ra markerId.
1821
+ const { markerIdExternal, data } = args ?? {};
1274
1822
  if (!this.#editor)
1275
1823
  return null;
1276
1824
  const model = this.#editor.model;
@@ -1283,7 +1831,7 @@ class SdDocumentBuilder {
1283
1831
  return null;
1284
1832
  }
1285
1833
  // 4. Tạo ID unique cho marker
1286
- const markerId = `comment:${Date.now()}`;
1834
+ const markerId = markerIdExternal ? `comment:${markerIdExternal}` : `comment:${Date.now()}`;
1287
1835
  // 5. Tạo marker trong model
1288
1836
  model.change(writer => {
1289
1837
  writer.addMarker(markerId, {
@@ -1561,12 +2109,37 @@ class SdDocumentBuilder {
1561
2109
  const blob = new Blob(['\ufeff', fullHtml], { type: 'application/msword' });
1562
2110
  SdUtilities.downloadBlob(blob, fileName);
1563
2111
  }
2112
+ hightSelectRange = (range) => {
2113
+ if (!range)
2114
+ return;
2115
+ const editor = this.#editor;
2116
+ editor.model.change(writer => {
2117
+ // Xóa marker cũ (nếu có)
2118
+ if (editor.model.markers.has('highlightRange')) {
2119
+ writer.removeMarker('highlightRange');
2120
+ }
2121
+ // Tạo marker mới
2122
+ writer.addMarker('highlightRange', {
2123
+ usingOperation: false, // Không lưu vào lịch sử Undo/Redo
2124
+ affectsData: false, // Không ảnh hưởng đến data lấy ra (getData)
2125
+ range: range,
2126
+ });
2127
+ });
2128
+ };
2129
+ removeHighlightSeclectRange = () => {
2130
+ const editor = this.#editor;
2131
+ editor.model.change(writer => {
2132
+ if (editor.model.markers.has('highlightRange')) {
2133
+ writer.removeMarker('highlightRange');
2134
+ }
2135
+ });
2136
+ };
1564
2137
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: SdDocumentBuilder, deps: [], target: i0.ɵɵFactoryTarget.Component });
1565
- 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\";::ng-deep .ck-editor{--ck-font-size-base: 11px !important;--ck-icon-size: 16px !important;--ck-content-font-family: \"Times New Roman\", serif !important;--ck-content-font-size: 13pt;--ck-content-line-height: 1.5;--ck-spacing-small: 2px !important;--ck-spacing-standard: 4px !important;--ck-spacing-large: 8px !important}::ng-deep .ck-editor .ck-editor__top{position:sticky;top:0;z-index:100;width:100%;min-width:600px;margin-bottom:10px}::ng-deep .ck-editor .ck-editor__top .ck-sticky-panel__content{border:none!important}::ng-deep .ck-editor .ck-editor__top .ck-toolbar{background:#fff!important;box-shadow:0 4px 6px -1px #0000001a!important;padding:8px!important}::ng-deep .ck-editor .ck-editor__top .ck-toolbar .ck-toolbar__items{display:flex;justify-content:center;flex-wrap:wrap;align-items:center}::ng-deep .ck-editor .ck-toolbar{min-height:32px!important;padding:2px!important}::ng-deep .ck-editor .ck-button{padding:2px 4px!important;min-height:24px!important}::ng-deep .ck-editor .ck-dropdown__button{min-height:24px!important}::ng-deep .ck.ck-toolbar{background:#f8f9fa!important;border-bottom:1px solid #e0e0e0!important}\n", "@charset \"UTF-8\";.builder-container{background-color:#f3f4f6;height:100%;overflow-y:auto;width:100%;display:flex;flex-direction:column;align-items:center;padding-bottom:20px}:host{display:inline-block}:host ::ng-deep .ck-editor{display:flex;flex-direction:column;align-items:center;width:100%}:host ::ng-deep .ck-editor .ck-editor__top,:host ::ng-deep .ck-editor .ck-editor__main{border:none!important;box-shadow:none!important}: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}:host ::ng-deep .ck-content.ck-focused{outline:none!important;border-color:#d1d5db!important}: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 img{max-width:100%!important;height:auto!important;object-fit:contain}:host ::ng-deep .ck-content p{margin-left:0!important;margin-right:0!important;margin-bottom:var(--ck-spacing-large);text-indent:0}:host ::ng-deep .ck-content ul,:host ::ng-deep .ck-content ol{padding-left:20px!important;margin-left:0!important}\n", ":host ::ng-deep .ck-heading-highlight{background-color:#fef08a}\n", ":host ::ng-deep .ck-comment-marker{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}:host ::ng-deep .ck-comment-marker:hover{background-color:#ffeb3bcc}:host ::ng-deep .ck-comment-marker.active-highlight{background-color:#ffeb3b;outline:2px dashed #f57f17}\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 .ck-editor__editable .ck-widget.table{float:none!important;display:block!important;max-width:100%!important;width:100%!important;margin:0!important;clear:both}:host ::ng-deep .ck-editor__editable table{table-layout:auto!important;width:100%!important;border-collapse:collapse;margin:0!important}:host ::ng-deep .ck-editor__editable table td,:host ::ng-deep .ck-editor__editable table th{word-wrap:break-word;white-space:normal!important;padding:.4em!important}:host ::ng-deep .ck-editor__editable table td img,:host ::ng-deep .ck-editor__editable table th img{max-width:100%;height:auto}\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"] }] });
2138
+ 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.5}: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}: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:var(--ck-spacing-large);text-indent:0}:host ::ng-deep .ck-content ul,:host ::ng-deep .ck-content ol{padding-left:20px!important;margin-left:0!important}\n", ":host ::ng-deep .ck-heading-highlight{background-color:#fef08a}\n", ":host ::ng-deep .ck-comment-marker{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}:host ::ng-deep .ck-comment-marker:hover{background-color:#ffeb3bcc}:host ::ng-deep .ck-comment-marker.active-highlight{background-color:#ffeb3b;outline:2px dashed #f57f17}\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"], 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"] }] });
1566
2139
  }
1567
2140
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: SdDocumentBuilder, decorators: [{
1568
2141
  type: Component,
1569
- 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\";::ng-deep .ck-editor{--ck-font-size-base: 11px !important;--ck-icon-size: 16px !important;--ck-content-font-family: \"Times New Roman\", serif !important;--ck-content-font-size: 13pt;--ck-content-line-height: 1.5;--ck-spacing-small: 2px !important;--ck-spacing-standard: 4px !important;--ck-spacing-large: 8px !important}::ng-deep .ck-editor .ck-editor__top{position:sticky;top:0;z-index:100;width:100%;min-width:600px;margin-bottom:10px}::ng-deep .ck-editor .ck-editor__top .ck-sticky-panel__content{border:none!important}::ng-deep .ck-editor .ck-editor__top .ck-toolbar{background:#fff!important;box-shadow:0 4px 6px -1px #0000001a!important;padding:8px!important}::ng-deep .ck-editor .ck-editor__top .ck-toolbar .ck-toolbar__items{display:flex;justify-content:center;flex-wrap:wrap;align-items:center}::ng-deep .ck-editor .ck-toolbar{min-height:32px!important;padding:2px!important}::ng-deep .ck-editor .ck-button{padding:2px 4px!important;min-height:24px!important}::ng-deep .ck-editor .ck-dropdown__button{min-height:24px!important}::ng-deep .ck.ck-toolbar{background:#f8f9fa!important;border-bottom:1px solid #e0e0e0!important}\n", "@charset \"UTF-8\";.builder-container{background-color:#f3f4f6;height:100%;overflow-y:auto;width:100%;display:flex;flex-direction:column;align-items:center;padding-bottom:20px}:host{display:inline-block}:host ::ng-deep .ck-editor{display:flex;flex-direction:column;align-items:center;width:100%}:host ::ng-deep .ck-editor .ck-editor__top,:host ::ng-deep .ck-editor .ck-editor__main{border:none!important;box-shadow:none!important}: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}:host ::ng-deep .ck-content.ck-focused{outline:none!important;border-color:#d1d5db!important}: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 img{max-width:100%!important;height:auto!important;object-fit:contain}:host ::ng-deep .ck-content p{margin-left:0!important;margin-right:0!important;margin-bottom:var(--ck-spacing-large);text-indent:0}:host ::ng-deep .ck-content ul,:host ::ng-deep .ck-content ol{padding-left:20px!important;margin-left:0!important}\n", ":host ::ng-deep .ck-heading-highlight{background-color:#fef08a}\n", ":host ::ng-deep .ck-comment-marker{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}:host ::ng-deep .ck-comment-marker:hover{background-color:#ffeb3bcc}:host ::ng-deep .ck-comment-marker.active-highlight{background-color:#ffeb3b;outline:2px dashed #f57f17}\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 .ck-editor__editable .ck-widget.table{float:none!important;display:block!important;max-width:100%!important;width:100%!important;margin:0!important;clear:both}:host ::ng-deep .ck-editor__editable table{table-layout:auto!important;width:100%!important;border-collapse:collapse;margin:0!important}:host ::ng-deep .ck-editor__editable table td,:host ::ng-deep .ck-editor__editable table th{word-wrap:break-word;white-space:normal!important;padding:.4em!important}:host ::ng-deep .ck-editor__editable table td img,:host ::ng-deep .ck-editor__editable table th img{max-width:100%;height:auto}\n"] }]
2142
+ 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.5}: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}: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:var(--ck-spacing-large);text-indent:0}:host ::ng-deep .ck-content ul,:host ::ng-deep .ck-content ol{padding-left:20px!important;margin-left:0!important}\n", ":host ::ng-deep .ck-heading-highlight{background-color:#fef08a}\n", ":host ::ng-deep .ck-comment-marker{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}:host ::ng-deep .ck-comment-marker:hover{background-color:#ffeb3bcc}:host ::ng-deep .ck-comment-marker.active-highlight{background-color:#ffeb3b;outline:2px dashed #f57f17}\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"] }]
1570
2143
  }], propDecorators: { option: [{
1571
2144
  type: Input,
1572
2145
  args: [{ required: true }]