@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.
- package/assets/scss/ckeditor5.scss +59 -2
- package/components/document-builder/src/document-builder.component.d.ts +7 -2
- package/components/document-builder/src/document-builder.model.d.ts +5 -1
- package/components/document-builder/src/plugins/highlight-range/highlight-range.plugin.d.ts +4 -0
- package/components/document-builder/src/plugins/image-custom/image-custom.plugin.d.ts +31 -0
- package/components/document-builder/src/plugins/index.d.ts +1 -0
- package/components/document-builder/src/plugins/table-fit/table-fit.plugin.d.ts +25 -0
- package/components/index.d.ts +1 -0
- package/components/mini-editor/index.d.ts +2 -0
- package/components/mini-editor/src/mini-editor.component.d.ts +90 -0
- package/components/mini-editor/src/mini-editor.model.d.ts +42 -0
- package/components/view/index.d.ts +1 -0
- package/components/view/src/view.component.d.ts +14 -0
- package/directives/index.d.ts +1 -0
- package/directives/src/sd-href.directive.d.ts +9 -0
- package/fesm2022/sd-angular-core-components-document-builder.mjs +642 -69
- package/fesm2022/sd-angular-core-components-document-builder.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components-mini-editor.mjs +326 -0
- package/fesm2022/sd-angular-core-components-mini-editor.mjs.map +1 -0
- package/fesm2022/sd-angular-core-components-view.mjs +88 -0
- package/fesm2022/sd-angular-core-components-view.mjs.map +1 -0
- package/fesm2022/sd-angular-core-components-workflow.mjs +16 -26
- package/fesm2022/sd-angular-core-components-workflow.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components.mjs +1 -0
- package/fesm2022/sd-angular-core-components.mjs.map +1 -1
- package/fesm2022/sd-angular-core-directives.mjs +51 -2
- package/fesm2022/sd-angular-core-directives.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-input-number.mjs +7 -0
- package/fesm2022/sd-angular-core-forms-input-number.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-radio.mjs +3 -2
- package/fesm2022/sd-angular-core-forms-radio.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-select.mjs +7 -4
- package/fesm2022/sd-angular-core-forms-select.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-textarea.mjs +2 -2
- package/fesm2022/sd-angular-core-forms-textarea.mjs.map +1 -1
- package/fesm2022/sd-angular-core-modules-layout.mjs +52 -17
- package/fesm2022/sd-angular-core-modules-layout.mjs.map +1 -1
- package/fesm2022/sd-angular-core-modules-oidc.mjs +0 -2
- package/fesm2022/sd-angular-core-modules-oidc.mjs.map +1 -1
- package/modules/layout/components/sidebar-v1/components/sidebar/sidebar.component.d.ts +1 -0
- package/modules/layout/components/sidebar-v1/components/user/user.component.d.ts +5 -2
- package/modules/layout/configurations/layout.configuration.d.ts +3 -0
- package/modules/layout/services/storage/storage.service.d.ts +1 -0
- package/package.json +64 -56
- package/sd-angular-core-19.0.0-beta.15.tgz +0 -0
- 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
|
-
//
|
|
457
|
-
this.listenTo(editor.editing.view.document, 'clipboardOutput', (
|
|
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
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
//
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
|
507
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
531
|
-
this.listenTo(insertTableCommand, 'execute', (evt, args) => {
|
|
638
|
+
this.listenTo(insertTableCommand, 'execute', () => {
|
|
532
639
|
editor.model.change(writer => {
|
|
533
|
-
|
|
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
|
-
|
|
543
|
-
writer.setAttribute('
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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
|
|
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,
|
|
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\"
|
|
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\"
|
|
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 }]
|