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 rawHtml = clipboard.getData('text/html');
1257
- const plainText = clipboard.getData('text/plain');
1262
+ const shouldStripStyles = this.stripPastedStyles ?? this.computedConfig.stripPastedStyles ?? true;
1258
1263
  let htmlToInsert = '';
1259
- // 1. Try extracting and cleaning rich HTML from clipboard
1260
- if (rawHtml && rawHtml.trim()) {
1261
- htmlToInsert = this.cleanPastedHtml(rawHtml);
1262
- }
1263
- // 2. If no valid HTML or plain text copy (e.g. from markdown/unicode/chat/plain editors)
1264
- if (!htmlToInsert || !htmlToInsert.trim()) {
1265
- if (plainText && plainText.trim()) {
1266
- const hasNewlines = plainText.includes('\n') || plainText.includes('\r');
1267
- const hasListMarkers = /^\s*(-\s|\d+\.\s)/m.test(plainText);
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.modelTextToEditorHtml(plainText);
1279
+ htmlToInsert = this.plainTextToCleanEditorHtml(text);
1270
1280
  }
1271
1281
  else {
1272
- htmlToInsert = this.modelInlineTextToHtml(plainText);
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, '&amp;')
2647
+ .replace(/</g, '&lt;')
2648
+ .replace(/>/g, '&gt;');
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: [{