@sd-angular/core 19.0.0-beta.14 → 19.0.0-beta.16
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/components/document-builder/src/plugins/table-fit/table-fit.plugin.d.ts +25 -0
- package/components/table/src/directives/index.d.ts +2 -0
- package/components/table/src/directives/sd-table-column-filter-def.directive.d.ts +9 -0
- package/components/table/src/directives/sticky-shadow.directive.d.ts +7 -0
- package/components/table/src/models/table-command.model.d.ts +4 -0
- package/components/table/src/models/table-option.model.d.ts +9 -8
- package/fesm2022/sd-angular-core-components-document-builder.mjs +237 -104
- package/fesm2022/sd-angular-core-components-document-builder.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components-table.mjs +82 -6
- package/fesm2022/sd-angular-core-components-table.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-select.mjs +3 -2
- package/fesm2022/sd-angular-core-forms-select.mjs.map +1 -1
- package/package.json +69 -69
- package/sd-angular-core-19.0.0-beta.16.tgz +0 -0
- package/sd-angular-core-19.0.0-beta.14.tgz +0 -0
|
@@ -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
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TemplateRef } from '@angular/core';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
export declare class SdTableColumnFilterDefDirective {
|
|
4
|
+
templateRef: TemplateRef<any>;
|
|
5
|
+
sdTableColumnFilterDef?: string;
|
|
6
|
+
constructor(templateRef: TemplateRef<any>);
|
|
7
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<SdTableColumnFilterDefDirective, never>;
|
|
8
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<SdTableColumnFilterDefDirective, "[sdTableColumnFilterDef]", never, { "sdTableColumnFilterDef": { "alias": "sdTableColumnFilterDef"; "required": false; }; }, {}, never, never, true, never>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
export declare class StickyShadowDirective {
|
|
3
|
+
#private;
|
|
4
|
+
constructor();
|
|
5
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<StickyShadowDirective, never>;
|
|
6
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<StickyShadowDirective, "[stickyShadow]", never, {}, {}, never, never, true, never>;
|
|
7
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { SdColor } from '@sd-angular/core/utilities/models';
|
|
2
|
+
export interface SdTableCommandOption<T = any> {
|
|
3
|
+
align?: 'left' | 'right';
|
|
4
|
+
commands?: SdTableCommand<T>[];
|
|
5
|
+
}
|
|
2
6
|
export type SdTableCommand<T = any> = SdTableCommandNormal<T> | SdTableCommandChildren<T>;
|
|
3
7
|
export interface SdTableCommandNormal<T = any> {
|
|
4
8
|
color?: SdColor;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { SdTableCommand } from './table-command.model';
|
|
1
|
+
import { SdPagingReq } from '@sd-angular/core/utilities';
|
|
3
2
|
import { SdTableFilterRequest, SdTableOptionFilter } from '../services/table-filter/table-filter.model';
|
|
3
|
+
import { SdTableColumn } from './table-column.model';
|
|
4
|
+
import { SdTableCommand, SdTableCommandOption } from './table-command.model';
|
|
5
|
+
import { TableOptionConfig } from './table-option-config.model';
|
|
6
|
+
import { SdTableOptionExpand } from './table-option-expand.model';
|
|
4
7
|
import { SdTableOptionExport } from './table-option-export.model';
|
|
8
|
+
import { SdTableOptionGroup } from './table-option-group.model';
|
|
9
|
+
import { SdTableOptionPaginate } from './table-option-paginate.model';
|
|
5
10
|
import { SdTableOptionReload } from './table-option-reload.model';
|
|
6
|
-
import { SdTableOptionExpand } from './table-option-expand.model';
|
|
7
11
|
import { SdTableOptionSelector } from './table-option-selector.model';
|
|
8
|
-
import { SdTableOptionStyle } from './table-option-style.model';
|
|
9
|
-
import { SdTableOptionGroup } from './table-option-group.model';
|
|
10
12
|
import { SdTableOptionSort } from './table-option-sort.model';
|
|
11
|
-
import {
|
|
12
|
-
import { TableOptionConfig } from './table-option-config.model';
|
|
13
|
-
import { SdPagingReq } from '@sd-angular/core/utilities';
|
|
13
|
+
import { SdTableOptionStyle } from './table-option-style.model';
|
|
14
14
|
export type SdTableOption<T = any> = SdTableLocalOption<T> | SdTableServerOption<T>;
|
|
15
15
|
interface SdTableBaseOption<T = any> {
|
|
16
16
|
key?: string;
|
|
@@ -24,6 +24,7 @@ interface SdTableBaseOption<T = any> {
|
|
|
24
24
|
group?: SdTableOptionGroup<T>;
|
|
25
25
|
filter?: SdTableOptionFilter;
|
|
26
26
|
commands?: SdTableCommand<T>[];
|
|
27
|
+
command?: SdTableCommandOption<T>;
|
|
27
28
|
columns: SdTableColumn<T>[];
|
|
28
29
|
style?: SdTableOptionStyle<T>;
|
|
29
30
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
|
553
|
-
|
|
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
|
-
|
|
564
|
-
|
|
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
|
|
635
|
+
// Lắng nghe lệnh insertTable
|
|
621
636
|
const insertTableCommand = editor.commands.get('insertTable');
|
|
622
637
|
if (insertTableCommand) {
|
|
623
|
-
|
|
624
|
-
this.listenTo(insertTableCommand, 'execute', (evt, args) => {
|
|
638
|
+
this.listenTo(insertTableCommand, 'execute', () => {
|
|
625
639
|
editor.model.change(writer => {
|
|
626
|
-
|
|
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
|
-
|
|
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
|
-
|
|
649
|
+
this._applyCellBorders(writer, tableElement);
|
|
642
650
|
}
|
|
643
651
|
});
|
|
644
652
|
});
|
|
645
653
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
|