ngx-rich-text-pro 1.0.3 → 1.0.4
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/README.md
CHANGED
|
@@ -409,6 +409,7 @@ editor.setBackgroundColor('#fef08a');
|
|
|
409
409
|
| `highlightHashtags` | `boolean` | `true` | Enable or disable real-time hashtag highlighting |
|
|
410
410
|
| `outputFormat` | `'html' \| 'markdown'` | `'html'` | Format emitted by `[(ngModel)]` / `formControl` / `(valueChange)`. When `'markdown'`, outputs clean markdown syntax (`__underline__`, `**bold**`, `*italic*`, `- list`, `1. list`, `#tag`) |
|
|
411
411
|
| `exclusiveInlineFormats` | `boolean` | `true` | When `true`, enforces mutually exclusive inline formatting (only one of bold, italic, or underline can be active at a time; clicking bold turns off italic and underline) |
|
|
412
|
+
| `stripPastedStyles` | `boolean` | `true` | When `true`, strips external formatting styles (bold, italic, underline, fonts, colors) on copy-paste and inserts clean text without foreign styles |
|
|
412
413
|
| `maxCharacters` | `number` | `undefined` | Hard maximum character limit |
|
|
413
414
|
| `characterCountWarn` | `number` | `undefined` | Threshold to show a warning badge as limit approaches |
|
|
414
415
|
| `customClass` | `string` | `''` | Custom CSS class applied to host container |
|
|
@@ -88,6 +88,7 @@ const DEFAULT_EDITOR_CONFIG = {
|
|
|
88
88
|
highlightHashtags: true,
|
|
89
89
|
outputFormat: 'html',
|
|
90
90
|
exclusiveInlineFormats: true,
|
|
91
|
+
stripPastedStyles: true,
|
|
91
92
|
toolbar: DEFAULT_TOOLBAR_CONFIG,
|
|
92
93
|
};
|
|
93
94
|
|
|
@@ -905,6 +906,7 @@ class NgxRichTextEditorComponent {
|
|
|
905
906
|
highlightHashtags = true;
|
|
906
907
|
outputFormat = 'html';
|
|
907
908
|
exclusiveInlineFormats = true;
|
|
909
|
+
stripPastedStyles = true;
|
|
908
910
|
config = {};
|
|
909
911
|
valueChange = new EventEmitter();
|
|
910
912
|
markdownChange = new EventEmitter();
|
|
@@ -966,7 +968,8 @@ class NgxRichTextEditorComponent {
|
|
|
966
968
|
changes['hashtagColor'] ||
|
|
967
969
|
changes['highlightHashtags'] ||
|
|
968
970
|
changes['outputFormat'] ||
|
|
969
|
-
changes['exclusiveInlineFormats']
|
|
971
|
+
changes['exclusiveInlineFormats'] ||
|
|
972
|
+
changes['stripPastedStyles']) {
|
|
970
973
|
this.mergeConfig();
|
|
971
974
|
if (changes['hashtagColor'] && this.hashtagColor) {
|
|
972
975
|
this.setHashtagColor(this.hashtagColor);
|
|
@@ -1137,6 +1140,9 @@ class NgxRichTextEditorComponent {
|
|
|
1137
1140
|
if (this.exclusiveInlineFormats !== undefined) {
|
|
1138
1141
|
this.computedConfig.exclusiveInlineFormats = this.exclusiveInlineFormats;
|
|
1139
1142
|
}
|
|
1143
|
+
if (this.stripPastedStyles !== undefined) {
|
|
1144
|
+
this.computedConfig.stripPastedStyles = this.stripPastedStyles;
|
|
1145
|
+
}
|
|
1140
1146
|
}
|
|
1141
1147
|
isMarkdownMode() {
|
|
1142
1148
|
return this.computedConfig.outputFormat === 'markdown' || this.outputFormat === 'markdown';
|
|
@@ -1253,28 +1259,64 @@ class NgxRichTextEditorComponent {
|
|
|
1253
1259
|
}
|
|
1254
1260
|
event.preventDefault();
|
|
1255
1261
|
const clipboard = event.clipboardData;
|
|
1256
|
-
const
|
|
1257
|
-
const plainText = clipboard.getData('text/plain');
|
|
1262
|
+
const shouldStripStyles = this.stripPastedStyles ?? this.computedConfig.stripPastedStyles ?? true;
|
|
1258
1263
|
let htmlToInsert = '';
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1264
|
+
if (shouldStripStyles) {
|
|
1265
|
+
let text = clipboard.getData('text/plain');
|
|
1266
|
+
if (!text && clipboard.getData('text/html')) {
|
|
1267
|
+
const rawHtml = clipboard.getData('text/html');
|
|
1268
|
+
if (typeof DOMParser !== 'undefined') {
|
|
1269
|
+
const parser = new DOMParser();
|
|
1270
|
+
const doc = parser.parseFromString(rawHtml, 'text/html');
|
|
1271
|
+
text = doc.body.textContent || doc.body.innerText || '';
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
if (text) {
|
|
1275
|
+
text = this.stripUnicodeFormatting(text);
|
|
1276
|
+
const hasNewlines = text.includes('\n') || text.includes('\r');
|
|
1277
|
+
const hasListMarkers = /^\s*(-\s|\d+\.\s)/m.test(text);
|
|
1268
1278
|
if (hasNewlines || hasListMarkers) {
|
|
1269
|
-
htmlToInsert = this.
|
|
1279
|
+
htmlToInsert = this.plainTextToCleanEditorHtml(text);
|
|
1270
1280
|
}
|
|
1271
1281
|
else {
|
|
1272
|
-
htmlToInsert = this.
|
|
1282
|
+
htmlToInsert = this.escapeHtml(text);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
const rawHtml = clipboard.getData('text/html');
|
|
1288
|
+
const plainText = clipboard.getData('text/plain');
|
|
1289
|
+
// 1. Try extracting and cleaning rich HTML from clipboard
|
|
1290
|
+
if (rawHtml && rawHtml.trim()) {
|
|
1291
|
+
htmlToInsert = this.cleanPastedHtml(rawHtml);
|
|
1292
|
+
}
|
|
1293
|
+
// 2. If no valid HTML or plain text copy
|
|
1294
|
+
if (!htmlToInsert || !htmlToInsert.trim()) {
|
|
1295
|
+
if (plainText && plainText.trim()) {
|
|
1296
|
+
const hasNewlines = plainText.includes('\n') || plainText.includes('\r');
|
|
1297
|
+
const hasListMarkers = /^\s*(-\s|\d+\.\s)/m.test(plainText);
|
|
1298
|
+
if (hasNewlines || hasListMarkers) {
|
|
1299
|
+
htmlToInsert = this.modelTextToEditorHtml(plainText);
|
|
1300
|
+
}
|
|
1301
|
+
else {
|
|
1302
|
+
htmlToInsert = this.modelInlineTextToHtml(plainText);
|
|
1303
|
+
}
|
|
1273
1304
|
}
|
|
1274
1305
|
}
|
|
1275
1306
|
}
|
|
1276
1307
|
if (htmlToInsert) {
|
|
1277
1308
|
this.focusEditor();
|
|
1309
|
+
if (shouldStripStyles) {
|
|
1310
|
+
const sel = window.getSelection();
|
|
1311
|
+
if (sel && !sel.isCollapsed && sel.rangeCount > 0) {
|
|
1312
|
+
try {
|
|
1313
|
+
document.execCommand('removeFormat', false);
|
|
1314
|
+
}
|
|
1315
|
+
catch {
|
|
1316
|
+
// ignore
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1278
1320
|
this.commandService.insertHtml(htmlToInsert);
|
|
1279
1321
|
if (this.highlightHashtags) {
|
|
1280
1322
|
this.highlightHashtagsInElement();
|
|
@@ -1463,6 +1505,28 @@ class NgxRichTextEditorComponent {
|
|
|
1463
1505
|
});
|
|
1464
1506
|
const temp = document.createElement('div');
|
|
1465
1507
|
temp.appendChild(fragment);
|
|
1508
|
+
if (isExclusive) {
|
|
1509
|
+
const formats = temp.querySelectorAll('b, strong, i, em, u, ins');
|
|
1510
|
+
formats.forEach((el) => {
|
|
1511
|
+
let parent = el.parentElement;
|
|
1512
|
+
let hasParentFormat = false;
|
|
1513
|
+
while (parent && parent !== temp) {
|
|
1514
|
+
const pTag = parent.tagName.toLowerCase();
|
|
1515
|
+
if (['b', 'strong', 'i', 'em', 'u', 'ins'].includes(pTag)) {
|
|
1516
|
+
hasParentFormat = true;
|
|
1517
|
+
break;
|
|
1518
|
+
}
|
|
1519
|
+
parent = parent.parentElement;
|
|
1520
|
+
}
|
|
1521
|
+
if (hasParentFormat) {
|
|
1522
|
+
const p = el.parentNode;
|
|
1523
|
+
while (el.firstChild) {
|
|
1524
|
+
p?.insertBefore(el.firstChild, el);
|
|
1525
|
+
}
|
|
1526
|
+
p?.removeChild(el);
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1466
1530
|
return temp.innerHTML;
|
|
1467
1531
|
}
|
|
1468
1532
|
onEditorKeydown(event) {
|
|
@@ -2471,6 +2535,122 @@ class NgxRichTextEditorComponent {
|
|
|
2471
2535
|
flushList();
|
|
2472
2536
|
return html.join('') || '<p><br></p>';
|
|
2473
2537
|
}
|
|
2538
|
+
plainTextToCleanEditorHtml(text) {
|
|
2539
|
+
if (!text)
|
|
2540
|
+
return '<p><br></p>';
|
|
2541
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
2542
|
+
const html = [];
|
|
2543
|
+
let listKind = null;
|
|
2544
|
+
let listStart = 1;
|
|
2545
|
+
let listItems = [];
|
|
2546
|
+
const flushList = () => {
|
|
2547
|
+
if (!listKind)
|
|
2548
|
+
return;
|
|
2549
|
+
const tag = listKind === 'numbered' ? 'ol' : 'ul';
|
|
2550
|
+
const start = listKind === 'numbered' && listStart !== 1 ? ` start="${listStart}"` : '';
|
|
2551
|
+
const listClass = listKind === 'numbered' ? 'ngx-editor-ordered-list' : 'ngx-editor-bullet-list';
|
|
2552
|
+
const items = listItems.map((item) => `<li>${item || '<br>'}</li>`).join('');
|
|
2553
|
+
html.push(`<${tag} class="${listClass}"${start}>${items}</${tag}>`);
|
|
2554
|
+
listKind = null;
|
|
2555
|
+
listStart = 1;
|
|
2556
|
+
listItems = [];
|
|
2557
|
+
};
|
|
2558
|
+
for (const line of lines) {
|
|
2559
|
+
const bullet = line.match(/^-\s?(.*)$/);
|
|
2560
|
+
const numbered = line.match(/^(\d+)\.\s?(.*)$/);
|
|
2561
|
+
if (bullet) {
|
|
2562
|
+
if (listKind !== 'bullet') {
|
|
2563
|
+
flushList();
|
|
2564
|
+
listKind = 'bullet';
|
|
2565
|
+
}
|
|
2566
|
+
listItems.push(this.escapeHtml(bullet[1] ?? ''));
|
|
2567
|
+
continue;
|
|
2568
|
+
}
|
|
2569
|
+
if (numbered) {
|
|
2570
|
+
if (listKind !== 'numbered') {
|
|
2571
|
+
flushList();
|
|
2572
|
+
listKind = 'numbered';
|
|
2573
|
+
listStart = Number(numbered[1]);
|
|
2574
|
+
}
|
|
2575
|
+
listItems.push(this.escapeHtml(numbered[2] ?? ''));
|
|
2576
|
+
continue;
|
|
2577
|
+
}
|
|
2578
|
+
flushList();
|
|
2579
|
+
if (!line.trim()) {
|
|
2580
|
+
html.push('<p><br></p>');
|
|
2581
|
+
}
|
|
2582
|
+
else {
|
|
2583
|
+
html.push(`<p>${this.escapeHtml(line)}</p>`);
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
flushList();
|
|
2587
|
+
return html.join('') || '<p><br></p>';
|
|
2588
|
+
}
|
|
2589
|
+
stripUnicodeFormatting(text) {
|
|
2590
|
+
if (!text)
|
|
2591
|
+
return '';
|
|
2592
|
+
// Remove combining underline (\u0332) and combining strikethrough (\u0336)
|
|
2593
|
+
let processed = text.replace(/[\u0332\u0336]/g, '');
|
|
2594
|
+
const chars = [...processed];
|
|
2595
|
+
let result = '';
|
|
2596
|
+
for (let i = 0; i < chars.length; i++) {
|
|
2597
|
+
const ch = chars[i];
|
|
2598
|
+
const cp = ch.codePointAt(0);
|
|
2599
|
+
// Mathematical Bold or Bold Italic Latin / Digits
|
|
2600
|
+
if (cp >= 0x1d400 && cp <= 0x1d419) {
|
|
2601
|
+
result += String.fromCharCode(cp - 0x1d400 + 65);
|
|
2602
|
+
}
|
|
2603
|
+
else if (cp >= 0x1d41a && cp <= 0x1d433) {
|
|
2604
|
+
result += String.fromCharCode(cp - 0x1d41a + 97);
|
|
2605
|
+
}
|
|
2606
|
+
else if (cp >= 0x1d7ce && cp <= 0x1d7d7) {
|
|
2607
|
+
result += String.fromCharCode(cp - 0x1d7ce + 48);
|
|
2608
|
+
}
|
|
2609
|
+
else if (cp >= 0x1d468 && cp <= 0x1d481) {
|
|
2610
|
+
result += String.fromCharCode(cp - 0x1d468 + 65);
|
|
2611
|
+
}
|
|
2612
|
+
else if (cp >= 0x1d482 && cp <= 0x1d49b) {
|
|
2613
|
+
result += String.fromCharCode(cp - 0x1d482 + 97);
|
|
2614
|
+
// Mathematical Italic Latin
|
|
2615
|
+
}
|
|
2616
|
+
else if (cp >= 0x1d434 && cp <= 0x1d44d) {
|
|
2617
|
+
result += String.fromCharCode(cp - 0x1d434 + 65);
|
|
2618
|
+
}
|
|
2619
|
+
else if (cp >= 0x1d44e && cp <= 0x1d467) {
|
|
2620
|
+
result += String.fromCharCode(cp - 0x1d44e + 97);
|
|
2621
|
+
}
|
|
2622
|
+
else if (ch === '\u210e' || cp === 0x1d455) {
|
|
2623
|
+
result += 'h';
|
|
2624
|
+
// Mathematical Sans-serif Bold / Italic
|
|
2625
|
+
}
|
|
2626
|
+
else if (cp >= 0x1d5d4 && cp <= 0x1d5ed) {
|
|
2627
|
+
result += String.fromCharCode(cp - 0x1d5d4 + 65);
|
|
2628
|
+
}
|
|
2629
|
+
else if (cp >= 0x1d5ee && cp <= 0x1d607) {
|
|
2630
|
+
result += String.fromCharCode(cp - 0x1d5ee + 97);
|
|
2631
|
+
}
|
|
2632
|
+
else if (cp >= 0x1d608 && cp <= 0x1d621) {
|
|
2633
|
+
result += String.fromCharCode(cp - 0x1d608 + 65);
|
|
2634
|
+
}
|
|
2635
|
+
else if (cp >= 0x1d622 && cp <= 0x1d63b) {
|
|
2636
|
+
result += String.fromCharCode(cp - 0x1d622 + 97);
|
|
2637
|
+
}
|
|
2638
|
+
else {
|
|
2639
|
+
result += ch;
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
return result;
|
|
2643
|
+
}
|
|
2644
|
+
escapeHtml(text) {
|
|
2645
|
+
let escaped = text
|
|
2646
|
+
.replace(/&/g, '&')
|
|
2647
|
+
.replace(/</g, '<')
|
|
2648
|
+
.replace(/>/g, '>');
|
|
2649
|
+
if (this.computedConfig.highlightHashtags ?? this.highlightHashtags) {
|
|
2650
|
+
escaped = escaped.replace(/(^|[\s>])#([a-zA-Z0-9_\u0080-\uFFFF]+)/g, '$1<span class="ngx-editor-hashtag post-content-hashtag">#$2</span>');
|
|
2651
|
+
}
|
|
2652
|
+
return escaped;
|
|
2653
|
+
}
|
|
2474
2654
|
isCursorInsideTag(tagName) {
|
|
2475
2655
|
const sel = window.getSelection();
|
|
2476
2656
|
if (!sel || !sel.anchorNode)
|
|
@@ -2516,7 +2696,7 @@ class NgxRichTextEditorComponent {
|
|
|
2516
2696
|
}
|
|
2517
2697
|
}
|
|
2518
2698
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NgxRichTextEditorComponent, deps: [{ token: EditorCommandService }, { token: i2.DomSanitizer }, { token: i0.ElementRef }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
2519
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: NgxRichTextEditorComponent, isStandalone: true, selector: "ngx-rich-text-pro, ngx-rich-text-editor", inputs: { value: "value", placeholder: "placeholder", disabled: "disabled", readonly: "readonly", showToolbar: "showToolbar", showStatusBar: "showStatusBar", height: "height", minHeight: "minHeight", maxHeight: "maxHeight", tools: "tools", maxCharacters: "maxCharacters", fontFamily: "fontFamily", fontSize: "fontSize", fontFamilies: "fontFamilies", customClass: "customClass", editorStyle: "editorStyle", contentStyle: "contentStyle", hashtagColor: "hashtagColor", highlightHashtags: "highlightHashtags", outputFormat: "outputFormat", exclusiveInlineFormats: "exclusiveInlineFormats", config: "config" }, outputs: { valueChange: "valueChange", markdownChange: "markdownChange", textChange: "textChange", editorFocus: "editorFocus", editorBlur: "editorBlur" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, providers: [
|
|
2699
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: NgxRichTextEditorComponent, isStandalone: true, selector: "ngx-rich-text-pro, ngx-rich-text-editor", inputs: { value: "value", placeholder: "placeholder", disabled: "disabled", readonly: "readonly", showToolbar: "showToolbar", showStatusBar: "showStatusBar", height: "height", minHeight: "minHeight", maxHeight: "maxHeight", tools: "tools", maxCharacters: "maxCharacters", fontFamily: "fontFamily", fontSize: "fontSize", fontFamilies: "fontFamilies", customClass: "customClass", editorStyle: "editorStyle", contentStyle: "contentStyle", hashtagColor: "hashtagColor", highlightHashtags: "highlightHashtags", outputFormat: "outputFormat", exclusiveInlineFormats: "exclusiveInlineFormats", stripPastedStyles: "stripPastedStyles", config: "config" }, outputs: { valueChange: "valueChange", markdownChange: "markdownChange", textChange: "textChange", editorFocus: "editorFocus", editorBlur: "editorBlur" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, providers: [
|
|
2520
2700
|
{
|
|
2521
2701
|
provide: NG_VALUE_ACCESSOR,
|
|
2522
2702
|
useExisting: forwardRef(() => NgxRichTextEditorComponent),
|
|
@@ -2581,6 +2761,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2581
2761
|
type: Input
|
|
2582
2762
|
}], exclusiveInlineFormats: [{
|
|
2583
2763
|
type: Input
|
|
2764
|
+
}], stripPastedStyles: [{
|
|
2765
|
+
type: Input
|
|
2584
2766
|
}], config: [{
|
|
2585
2767
|
type: Input
|
|
2586
2768
|
}], valueChange: [{
|