@sd-angular/core 19.0.0-beta.14 → 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.
@@ -1,4 +1,29 @@
1
1
  import { Plugin } from 'ckeditor5';
2
2
  export declare class TableFitPlugin extends Plugin {
3
3
  init(): void;
4
+ /**
5
+ * Apply default table width
6
+ */
7
+ private _applyTableDefaults;
8
+ /**
9
+ * Apply default borders to all cells in a table
10
+ */
11
+ private _applyCellBorders;
12
+ /**
13
+ * Setup listener to preserve cell/table styles when model changes
14
+ */
15
+ private _setupStylePreservationOnModelChange;
16
+ /**
17
+ * Find tables that need border fixes from model changes
18
+ */
19
+ private _findTablesNeedingFix;
20
+ /**
21
+ * Find parent table element
22
+ */
23
+ private _findParentTable;
24
+ /**
25
+ * Cleanup listeners when plugin is destroyed
26
+ * Note: this.listenTo() listeners are automatically cleaned up by super.destroy()
27
+ */
28
+ destroy(): void;
4
29
  }
@@ -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
  }
@@ -501,75 +575,20 @@ class VariablePlugin extends Plugin {
501
575
  class TableFitPlugin extends Plugin {
502
576
  init() {
503
577
  const editor = this.editor;
504
- const applyTableDefaults = (writer, tableElement) => {
505
- if (!tableElement)
506
- return;
507
- // Always force table width to 100% on paste/insert
508
- writer.setAttribute('tableWidth', '100%', tableElement);
509
- };
510
- const applyCellBorders = (writer, tableElement) => {
511
- if (!tableElement)
512
- return;
513
- for (const row of tableElement.getChildren()) {
514
- for (const cell of row.getChildren()) {
515
- const hasBorderColor = cell.getAttribute('tableCellBorderColor');
516
- const hasBorderStyle = cell.getAttribute('tableCellBorderStyle');
517
- const hasBorderWidth = cell.getAttribute('tableCellBorderWidth');
518
- const hasPadding = cell.getAttribute('tableCellPadding');
519
- if (!hasBorderColor) {
520
- writer.setAttribute('tableCellBorderColor', '#000000', cell);
521
- }
522
- if (!hasBorderStyle) {
523
- writer.setAttribute('tableCellBorderStyle', 'solid', cell);
524
- }
525
- if (!hasBorderWidth) {
526
- writer.setAttribute('tableCellBorderWidth', '1pt', cell);
527
- }
528
- if (!hasPadding) {
529
- writer.setAttribute('tableCellPadding', '0.4em', cell);
530
- }
531
- }
532
- }
533
- };
534
- const listenAndApplyOnExecute = (commandName) => {
535
- const cmd = editor.commands.get(commandName);
536
- if (!cmd)
537
- return;
538
- this.listenTo(cmd, 'execute', () => {
539
- editor.model.change(writer => {
540
- const selection = editor.model.document.selection;
541
- const position = selection.getFirstPosition();
542
- if (!position)
543
- return;
544
- const tableElement = position.findAncestor('table');
545
- applyTableDefaults(writer, tableElement);
546
- applyCellBorders(writer, tableElement);
547
- });
548
- });
549
- };
550
578
  // Can thiệp vào quá trình convert từ View (HTML Paste) sang Model
551
579
  editor.conversion.for('upcast').add(dispatcher => {
552
- dispatcher.on('element:table', (evt, data, conversionApi) => {
553
- // 1. Gọi consume để báo với CKEditor là chúng ta sẽ xử lý element này
554
- if (!conversionApi.consumable.consume(data.viewItem, { name: true })) {
580
+ dispatcher.on('element:table', (evt, data) => {
581
+ if (!data.modelRange)
555
582
  return;
556
- }
557
- // 2. Thực hiện chuyển đổi mặc định để tạo ra model element
558
- const { modelCursor, modelRange } = conversionApi.convertChildren(data.viewItem, data.modelCursor);
559
- // 3. Bây giờ modelRange chắc chắn tồn tại, ta tìm element table trong đó
560
- for (const item of modelRange.getItems()) {
583
+ for (const item of data.modelRange.getItems()) {
561
584
  if (item.is('element', 'table')) {
562
585
  editor.model.change(writer => {
563
- applyTableDefaults(writer, item);
564
- applyCellBorders(writer, item);
586
+ this._applyTableDefaults(writer, item);
587
+ this._applyCellBorders(writer, item);
565
588
  });
566
589
  }
567
590
  }
568
- // 4. Cập nhật modelCursor để dispatcher biết đã xử lý xong tới đâu
569
- data.modelRange = modelRange;
570
- data.modelCursor = modelCursor;
571
591
  }, { priority: 'low' });
572
- // Chạy sau cùng để ghi đè các logic mặc định
573
592
  });
574
593
  const findInnerTable = (viewElement) => {
575
594
  if (!viewElement)
@@ -586,7 +605,6 @@ class TableFitPlugin extends Plugin {
586
605
  return null;
587
606
  };
588
607
  editor.conversion.for('downcast').add(dispatcher => {
589
- // Handle when tableWidth attribute changes
590
608
  dispatcher.on('attribute:tableWidth:table', (evt, data, conversionApi) => {
591
609
  const viewWriter = conversionApi.writer;
592
610
  const viewElement = conversionApi.mapper.toViewElement(data.item);
@@ -598,10 +616,8 @@ class TableFitPlugin extends Plugin {
598
616
  viewWriter.setStyle('border-collapse', 'collapse', innerTable);
599
617
  viewWriter.setStyle('margin', '0', innerTable);
600
618
  viewWriter.setStyle('width', '100%', innerTable);
601
- // Set width on the wrapper container (ck-widget) as well
602
619
  viewWriter.setStyle('width', '100%', viewElement);
603
620
  });
604
- // Handle when table is first inserted
605
621
  dispatcher.on('insert:table', (evt, data, conversionApi) => {
606
622
  const viewWriter = conversionApi.writer;
607
623
  const viewElement = conversionApi.mapper.toViewElement(data.item);
@@ -613,40 +629,157 @@ class TableFitPlugin extends Plugin {
613
629
  viewWriter.setStyle('border-collapse', 'collapse', innerTable);
614
630
  viewWriter.setStyle('margin', '0', innerTable);
615
631
  viewWriter.setStyle('width', '100%', innerTable);
616
- // Set width on the wrapper container (ck-widget) as well
617
632
  viewWriter.setStyle('width', '100%', viewElement);
618
633
  });
619
634
  });
620
- // Lắng nghe lệnh insertTable để can thiệp ngay sau khi bảng được tạo
635
+ // Lắng nghe lệnh insertTable
621
636
  const insertTableCommand = editor.commands.get('insertTable');
622
637
  if (insertTableCommand) {
623
- // Dùng 'on' event để hook vào sau khi lệnh thực thi
624
- this.listenTo(insertTableCommand, 'execute', (evt, args) => {
638
+ this.listenTo(insertTableCommand, 'execute', () => {
625
639
  editor.model.change(writer => {
626
- // Lấy vị trí con trỏ hiện tại (nơi bảng vừa được chèn)
627
- const selection = editor.model.document.selection;
628
- const position = selection.getFirstPosition();
640
+ const position = editor.model.document.selection.getFirstPosition();
629
641
  if (!position)
630
642
  return;
631
- // Tìm element table vừa chèn (nó nằm ở vị trí cha của selection)
632
- // Khi vừa insert, con trỏ thường nằm trong ô đầu tiên của bảng
633
643
  const tableElement = position.findAncestor('table');
634
644
  if (tableElement) {
635
- // Ép width 100% cho bảng mới vẽ
636
- applyTableDefaults(writer, tableElement);
637
- // Apply border style 1pt solid black
645
+ this._applyTableDefaults(writer, tableElement);
638
646
  writer.setAttribute('tableBorderColor', '#000000', tableElement);
639
647
  writer.setAttribute('tableBorderStyle', 'solid', tableElement);
640
648
  writer.setAttribute('tableBorderWidth', '1pt', tableElement);
641
- applyCellBorders(writer, tableElement);
649
+ this._applyCellBorders(writer, tableElement);
642
650
  }
643
651
  });
644
652
  });
645
653
  }
646
- listenAndApplyOnExecute('insertTableRowAbove');
647
- listenAndApplyOnExecute('insertTableRowBelow');
648
- listenAndApplyOnExecute('insertTableColumnLeft');
649
- listenAndApplyOnExecute('insertTableColumnRight');
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();
650
783
  }
651
784
  }
652
785