@stackch/angular-richtext-editor 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { EventEmitter, Output, Input, Component,
|
|
2
|
+
import { EventEmitter, Output, Input, Component, HostListener, ViewChild, Directive, forwardRef } from '@angular/core';
|
|
3
3
|
import { CommonModule } from '@angular/common';
|
|
4
4
|
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
5
5
|
|
|
@@ -126,6 +126,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImpor
|
|
|
126
126
|
type: Output
|
|
127
127
|
}] } });
|
|
128
128
|
|
|
129
|
+
// ─── Config & Types ───────────────────────────────────────────────────────────
|
|
129
130
|
class StackchRichtextEditorConfig {
|
|
130
131
|
// Sichtbarkeit einzelner Toolbar-Elemente (Default: an)
|
|
131
132
|
showUndoRedo = true;
|
|
@@ -143,7 +144,6 @@ class StackchRichtextEditorConfig {
|
|
|
143
144
|
// i18n overrides (partial), default is English
|
|
144
145
|
i18n;
|
|
145
146
|
}
|
|
146
|
-
1;
|
|
147
147
|
class StackchRichtextEditorI18n {
|
|
148
148
|
// Generic
|
|
149
149
|
placeholder = 'Write…';
|
|
@@ -184,12 +184,10 @@ class StackchRichtextEditorI18n {
|
|
|
184
184
|
h5Label = 'H5';
|
|
185
185
|
h6Label = 'H6';
|
|
186
186
|
}
|
|
187
|
-
// Predefined i18n bundles
|
|
187
|
+
// ─── Predefined i18n bundles ──────────────────────────────────────────────────
|
|
188
188
|
// Consumers can import these and pass via config.i18n to localize the toolbar/labels.
|
|
189
189
|
const STACKCH_RTE_I18N_DE = {
|
|
190
|
-
// Generic
|
|
191
190
|
placeholder: 'Schreiben…',
|
|
192
|
-
// Toolbar titles
|
|
193
191
|
undoTitle: 'Rückgängig (Strg+Z)',
|
|
194
192
|
redoTitle: 'Wiederholen (Strg+Y)',
|
|
195
193
|
fontPanelTitle: 'Schrift & Größe',
|
|
@@ -216,20 +214,13 @@ const STACKCH_RTE_I18N_DE = {
|
|
|
216
214
|
alignJustifyTitle: 'Blocksatz',
|
|
217
215
|
linkTitle: 'Link',
|
|
218
216
|
removeFormatTitle: 'Formatierung entfernen',
|
|
219
|
-
// Heading menu labels
|
|
220
217
|
paragraphLabel: 'Absatz (P)',
|
|
221
218
|
codeLabel: 'Code (pre)',
|
|
222
|
-
h1Label: 'H1',
|
|
223
|
-
|
|
224
|
-
h3Label: 'H3',
|
|
225
|
-
h4Label: 'H4',
|
|
226
|
-
h5Label: 'H5',
|
|
227
|
-
h6Label: 'H6',
|
|
219
|
+
h1Label: 'H1', h2Label: 'H2', h3Label: 'H3',
|
|
220
|
+
h4Label: 'H4', h5Label: 'H5', h6Label: 'H6',
|
|
228
221
|
};
|
|
229
222
|
const STACKCH_RTE_I18N_FR = {
|
|
230
|
-
// Generic
|
|
231
223
|
placeholder: 'Écrire…',
|
|
232
|
-
// Toolbar titles
|
|
233
224
|
undoTitle: 'Annuler (Ctrl+Z)',
|
|
234
225
|
redoTitle: 'Rétablir (Ctrl+Y)',
|
|
235
226
|
fontPanelTitle: 'Police et taille',
|
|
@@ -256,20 +247,13 @@ const STACKCH_RTE_I18N_FR = {
|
|
|
256
247
|
alignJustifyTitle: 'Justifié',
|
|
257
248
|
linkTitle: 'Lien',
|
|
258
249
|
removeFormatTitle: 'Effacer la mise en forme',
|
|
259
|
-
// Heading menu labels
|
|
260
250
|
paragraphLabel: 'Paragraphe (P)',
|
|
261
251
|
codeLabel: 'Code (pre)',
|
|
262
|
-
h1Label: 'H1',
|
|
263
|
-
|
|
264
|
-
h3Label: 'H3',
|
|
265
|
-
h4Label: 'H4',
|
|
266
|
-
h5Label: 'H5',
|
|
267
|
-
h6Label: 'H6',
|
|
252
|
+
h1Label: 'H1', h2Label: 'H2', h3Label: 'H3',
|
|
253
|
+
h4Label: 'H4', h5Label: 'H5', h6Label: 'H6',
|
|
268
254
|
};
|
|
269
255
|
const STACKCH_RTE_I18N_IT = {
|
|
270
|
-
// Generic
|
|
271
256
|
placeholder: 'Scrivi…',
|
|
272
|
-
// Toolbar titles
|
|
273
257
|
undoTitle: 'Annulla (Ctrl+Z)',
|
|
274
258
|
redoTitle: 'Ripristina (Ctrl+Y)',
|
|
275
259
|
fontPanelTitle: 'Carattere e dimensione',
|
|
@@ -296,18 +280,22 @@ const STACKCH_RTE_I18N_IT = {
|
|
|
296
280
|
alignJustifyTitle: 'Giustificato',
|
|
297
281
|
linkTitle: 'Collegamento',
|
|
298
282
|
removeFormatTitle: 'Rimuovi formattazione',
|
|
299
|
-
// Heading menu labels
|
|
300
283
|
paragraphLabel: 'Paragrafo (P)',
|
|
301
284
|
codeLabel: 'Codice (pre)',
|
|
302
|
-
h1Label: 'H1',
|
|
303
|
-
|
|
304
|
-
h3Label: 'H3',
|
|
305
|
-
h4Label: 'H4',
|
|
306
|
-
h5Label: 'H5',
|
|
307
|
-
h6Label: 'H6',
|
|
285
|
+
h1Label: 'H1', h2Label: 'H2', h3Label: 'H3',
|
|
286
|
+
h4Label: 'H4', h5Label: 'H5', h6Label: 'H6',
|
|
308
287
|
};
|
|
309
|
-
|
|
288
|
+
// ─── Abstract Base ────────────────────────────────────────────────────────────
|
|
289
|
+
/**
|
|
290
|
+
* Abstract base class shared by StackchRichtextEditor (core) and
|
|
291
|
+
* StackchRichtextEditorMaterial (Material variant).
|
|
292
|
+
* Contains all editing logic; concrete subclasses add only the @Component
|
|
293
|
+
* decorator with their own template / styles / toolbar.
|
|
294
|
+
*/
|
|
295
|
+
class StackchRichtextEditorBase {
|
|
310
296
|
cdr;
|
|
297
|
+
// Override in concrete class to distinguish log output
|
|
298
|
+
logPrefix = '[RTE]';
|
|
311
299
|
placeholder = '';
|
|
312
300
|
showToolbar = true;
|
|
313
301
|
fonts = [
|
|
@@ -327,6 +315,7 @@ class StackchRichtextEditor {
|
|
|
327
315
|
get disabled() { return this._disabled; }
|
|
328
316
|
_disabled = false;
|
|
329
317
|
valueChange = new EventEmitter();
|
|
318
|
+
metricsChange = new EventEmitter();
|
|
330
319
|
editorRef;
|
|
331
320
|
constructor(cdr) {
|
|
332
321
|
this.cdr = cdr;
|
|
@@ -361,7 +350,7 @@ class StackchRichtextEditor {
|
|
|
361
350
|
isBoldActive = false;
|
|
362
351
|
isItalicActive = false;
|
|
363
352
|
isUnderlineActive = false;
|
|
364
|
-
// Selection helpers
|
|
353
|
+
// ─── Selection helpers ───────────────────────────────────────────────────
|
|
365
354
|
saveSelection() {
|
|
366
355
|
const sel = window.getSelection();
|
|
367
356
|
if (!sel || sel.rangeCount === 0)
|
|
@@ -374,7 +363,6 @@ class StackchRichtextEditor {
|
|
|
374
363
|
}
|
|
375
364
|
// Prevent toolbar buttons from stealing focus from the editor while preserving selection
|
|
376
365
|
onToolbarMouseDown(evt) {
|
|
377
|
-
// Keep focus on the editor to avoid persistent button focus outlines
|
|
378
366
|
evt.preventDefault();
|
|
379
367
|
evt.stopPropagation();
|
|
380
368
|
this.saveSelection();
|
|
@@ -400,6 +388,7 @@ class StackchRichtextEditor {
|
|
|
400
388
|
const node = container.nodeType === Node.ELEMENT_NODE ? container : container.parentElement;
|
|
401
389
|
return !!node && editor.contains(node);
|
|
402
390
|
}
|
|
391
|
+
// ─── Host listeners ──────────────────────────────────────────────────────
|
|
403
392
|
// Update active-state on selection changes inside the editor
|
|
404
393
|
onSelectionChange() {
|
|
405
394
|
const sel = window.getSelection();
|
|
@@ -423,7 +412,7 @@ class StackchRichtextEditor {
|
|
|
423
412
|
}
|
|
424
413
|
// Keyboard: Undo/Redo
|
|
425
414
|
onKeydown(evt) {
|
|
426
|
-
const isMac = navigator.platform.toLowerCase().includes('mac');
|
|
415
|
+
const isMac = (navigator.userAgentData?.platform ?? navigator.platform).toLowerCase().includes('mac');
|
|
427
416
|
const mod = isMac ? evt.metaKey : evt.ctrlKey;
|
|
428
417
|
if (mod && !evt.shiftKey && (evt.key === 'z' || evt.key === 'Z')) {
|
|
429
418
|
evt.preventDefault();
|
|
@@ -434,6 +423,7 @@ class StackchRichtextEditor {
|
|
|
434
423
|
this.redo();
|
|
435
424
|
}
|
|
436
425
|
}
|
|
426
|
+
// ─── Menu toggles ────────────────────────────────────────────────────────
|
|
437
427
|
toggleFontMenu(evt) {
|
|
438
428
|
this.saveSelection();
|
|
439
429
|
this.showFontMenu = !this.showFontMenu;
|
|
@@ -451,7 +441,6 @@ class StackchRichtextEditor {
|
|
|
451
441
|
toggleFontPanel(evt) {
|
|
452
442
|
this.saveSelection();
|
|
453
443
|
this.showFontPanel = !this.showFontPanel;
|
|
454
|
-
// close others if open
|
|
455
444
|
if (this.showFontPanel) {
|
|
456
445
|
this.showFontMenu = false;
|
|
457
446
|
this.showSizeMenu = false;
|
|
@@ -530,6 +519,7 @@ class StackchRichtextEditor {
|
|
|
530
519
|
}
|
|
531
520
|
evt.stopPropagation();
|
|
532
521
|
}
|
|
522
|
+
// ─── Pick handlers (toolbar → editor) ───────────────────────────────────
|
|
533
523
|
onPickAlign(where) {
|
|
534
524
|
this.focusEditor();
|
|
535
525
|
this.restoreSelection();
|
|
@@ -582,7 +572,7 @@ class StackchRichtextEditor {
|
|
|
582
572
|
this.emitValue();
|
|
583
573
|
this.takeSnapshot('spacing');
|
|
584
574
|
}
|
|
585
|
-
// ControlValueAccessor
|
|
575
|
+
// ─── ControlValueAccessor ────────────────────────────────────────────────
|
|
586
576
|
writeValue(value) {
|
|
587
577
|
const el = this.editorRef?.nativeElement;
|
|
588
578
|
if (!el)
|
|
@@ -592,16 +582,17 @@ class StackchRichtextEditor {
|
|
|
592
582
|
if (this.history.length === 0) {
|
|
593
583
|
this.takeSnapshot('init');
|
|
594
584
|
}
|
|
585
|
+
// Emit initial metrics when value is written programmatically
|
|
586
|
+
this.emitMetrics();
|
|
595
587
|
}
|
|
596
588
|
registerOnChange(fn) { this.onChange = fn; }
|
|
597
589
|
registerOnTouched(fn) { this.onTouched = fn; }
|
|
598
590
|
setDisabledState(isDisabled) { this.disabled = isDisabled; }
|
|
591
|
+
// ─── Toolbar actions ─────────────────────────────────────────────────────
|
|
599
592
|
// Toolbar actions using document.execCommand (deprecated but broadly supported)
|
|
600
593
|
cmd(command, value) {
|
|
601
|
-
// Restore last selection from editor before applying an action triggered by toolbar
|
|
602
594
|
this.focusEditor();
|
|
603
595
|
this.restoreSelection();
|
|
604
|
-
// Bevorzugt: eigene Range-basierte Implementierungen
|
|
605
596
|
switch (command) {
|
|
606
597
|
case 'bold':
|
|
607
598
|
this.applyInlineStyleSmart('fontWeight', 'bold');
|
|
@@ -666,7 +657,6 @@ class StackchRichtextEditor {
|
|
|
666
657
|
}
|
|
667
658
|
// Fallback (deprecated): nur für Format entfernen / Links lösen als Übergang
|
|
668
659
|
try {
|
|
669
|
-
// Hinweis im Dev-Mode ausgeben
|
|
670
660
|
if (!('___rteWarned' in this)) {
|
|
671
661
|
console.warn('[richtext-editor] document.execCommand ist deprecated; Fallback wird nur für removeFormat/unlink verwendet.');
|
|
672
662
|
this.___rteWarned = true;
|
|
@@ -702,10 +692,10 @@ class StackchRichtextEditor {
|
|
|
702
692
|
return;
|
|
703
693
|
const r = sel.getRangeAt(0);
|
|
704
694
|
if (r.collapsed) {
|
|
705
|
-
// Firefox/Edge: wenn keine explizite Auswahl, erweitere auf nächstes Wort
|
|
706
695
|
this.expandRangeToWord(r);
|
|
707
696
|
}
|
|
708
|
-
this.
|
|
697
|
+
this.applySelectionStyles({ color });
|
|
698
|
+
this.saveSelection();
|
|
709
699
|
}
|
|
710
700
|
applyHighlight(color) {
|
|
711
701
|
this.focusEditor();
|
|
@@ -717,15 +707,14 @@ class StackchRichtextEditor {
|
|
|
717
707
|
if (r.collapsed) {
|
|
718
708
|
this.expandRangeToWord(r);
|
|
719
709
|
}
|
|
720
|
-
|
|
721
|
-
this.
|
|
710
|
+
this.applySelectionStyles({ backgroundColor: color });
|
|
711
|
+
this.saveSelection();
|
|
722
712
|
}
|
|
723
713
|
expandRangeToWord(range) {
|
|
724
714
|
try {
|
|
725
715
|
const editor = this.editorRef.nativeElement;
|
|
726
716
|
let node = range.startContainer;
|
|
727
717
|
if (node.nodeType !== Node.TEXT_NODE) {
|
|
728
|
-
// Versuche, einen Textknoten in der Nähe zu finden
|
|
729
718
|
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
|
|
730
719
|
let found = null;
|
|
731
720
|
while (walker.nextNode()) {
|
|
@@ -740,7 +729,6 @@ class StackchRichtextEditor {
|
|
|
740
729
|
}
|
|
741
730
|
const text = node.nodeType === Node.TEXT_NODE ? node.data : '';
|
|
742
731
|
let start = 0, end = text.length;
|
|
743
|
-
// einfache Wortgrenzen-Heuristik
|
|
744
732
|
const pos = range.startOffset;
|
|
745
733
|
for (let i = pos; i > 0; i--) {
|
|
746
734
|
if (/\s/.test(text[i - 1])) {
|
|
@@ -784,7 +772,6 @@ class StackchRichtextEditor {
|
|
|
784
772
|
this.updateInlineStates();
|
|
785
773
|
}
|
|
786
774
|
onPaste(evt) {
|
|
787
|
-
// Optional: Clean paste to plain text while keeping basic formatting minimal.
|
|
788
775
|
if (!evt.clipboardData)
|
|
789
776
|
return;
|
|
790
777
|
evt.preventDefault();
|
|
@@ -793,23 +780,33 @@ class StackchRichtextEditor {
|
|
|
793
780
|
this.emitValue();
|
|
794
781
|
this.takeSnapshot('paste');
|
|
795
782
|
}
|
|
783
|
+
// ─── Internal emit helpers ───────────────────────────────────────────────
|
|
796
784
|
emitValue() {
|
|
797
|
-
// Clean up empty style attributes and redundant spans before emitting
|
|
798
785
|
this.cleanupEmptyStylesAndSpans();
|
|
799
786
|
const val = this.editorRef.nativeElement.innerHTML;
|
|
800
787
|
this.onChange(val);
|
|
801
788
|
this.valueChange.emit(val);
|
|
789
|
+
this.emitMetrics();
|
|
802
790
|
this.updateInlineStates();
|
|
803
791
|
}
|
|
792
|
+
emitMetrics() {
|
|
793
|
+
const editor = this.editorRef?.nativeElement;
|
|
794
|
+
if (!editor)
|
|
795
|
+
return;
|
|
796
|
+
const htmlLen = editor.innerHTML.length;
|
|
797
|
+
const textLen = (editor.textContent || '').length;
|
|
798
|
+
this.metricsChange.emit({ htmlLength: htmlLen, textLength: textLen });
|
|
799
|
+
}
|
|
804
800
|
// Remove empty style attributes (style="") and unwrap spans without any attributes
|
|
805
801
|
cleanupEmptyStylesAndSpans(root) {
|
|
802
|
+
console.log(`${this.logPrefix} cleanupEmptyStylesAndSpans: start cleanup`);
|
|
806
803
|
const editor = root || this.editorRef.nativeElement;
|
|
807
804
|
const toUnwrap = [];
|
|
805
|
+
const toRemove = [];
|
|
808
806
|
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_ELEMENT);
|
|
809
807
|
while (walker.nextNode()) {
|
|
810
808
|
const el = walker.currentNode;
|
|
811
809
|
if (el.hasAttribute('style')) {
|
|
812
|
-
// If no style declarations remain, drop the attribute
|
|
813
810
|
const cssText = el.getAttribute('style') || '';
|
|
814
811
|
const byApiEmpty = (el.style ? el.style.length === 0 : false);
|
|
815
812
|
const normalized = cssText.replace(/[\s;]/g, '');
|
|
@@ -817,18 +814,30 @@ class StackchRichtextEditor {
|
|
|
817
814
|
el.removeAttribute('style');
|
|
818
815
|
}
|
|
819
816
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
817
|
+
if (el.tagName === 'SPAN') {
|
|
818
|
+
if (!el.firstChild) {
|
|
819
|
+
console.log(`${this.logPrefix} cleanup: removing empty span`, el);
|
|
820
|
+
toRemove.push(el);
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (el.attributes.length === 0) {
|
|
824
|
+
console.log(`${this.logPrefix} cleanup: unwrapping style-free span`, el);
|
|
825
|
+
toUnwrap.push(el);
|
|
826
|
+
}
|
|
823
827
|
}
|
|
824
828
|
}
|
|
825
829
|
for (const span of toUnwrap) {
|
|
830
|
+
console.log(`${this.logPrefix} cleanup: unwrap span`, { span, childCount: span.childNodes.length });
|
|
826
831
|
while (span.firstChild)
|
|
827
832
|
span.parentNode?.insertBefore(span.firstChild, span);
|
|
828
833
|
span.remove();
|
|
829
834
|
}
|
|
835
|
+
for (const span of toRemove) {
|
|
836
|
+
console.log(`${this.logPrefix} cleanup: remove span without children`, span);
|
|
837
|
+
span.remove();
|
|
838
|
+
}
|
|
830
839
|
}
|
|
831
|
-
// History API
|
|
840
|
+
// ─── History API ─────────────────────────────────────────────────────────
|
|
832
841
|
get canUndo() { return this.historyIndex > 0; }
|
|
833
842
|
get canRedo() { return this.historyIndex >= 0 && this.historyIndex < this.history.length - 1; }
|
|
834
843
|
undo() {
|
|
@@ -890,12 +899,10 @@ class StackchRichtextEditor {
|
|
|
890
899
|
const editor = this.editorRef.nativeElement;
|
|
891
900
|
this.isRestoringHistory = true;
|
|
892
901
|
editor.innerHTML = snap.html;
|
|
893
|
-
// Selektion wiederherstellen
|
|
894
902
|
if (snap.range) {
|
|
895
903
|
this.restoreSerializedRange(snap.range);
|
|
896
904
|
}
|
|
897
905
|
else {
|
|
898
|
-
// Cursor ans Ende
|
|
899
906
|
const sel = window.getSelection();
|
|
900
907
|
if (sel) {
|
|
901
908
|
sel.removeAllRanges();
|
|
@@ -907,7 +914,6 @@ class StackchRichtextEditor {
|
|
|
907
914
|
}
|
|
908
915
|
this.historyIndex = index;
|
|
909
916
|
this.isRestoringHistory = false;
|
|
910
|
-
// Werte emittieren nach Restore
|
|
911
917
|
this.emitValue();
|
|
912
918
|
}
|
|
913
919
|
serializeRange(range) {
|
|
@@ -981,365 +987,182 @@ class StackchRichtextEditor {
|
|
|
981
987
|
el.focus();
|
|
982
988
|
}
|
|
983
989
|
}
|
|
984
|
-
//
|
|
990
|
+
// ─── Inline style application ────────────────────────────────────────────
|
|
985
991
|
applyInlineStyle(cssProp, value) {
|
|
986
|
-
this.
|
|
987
|
-
this.restoreSelection();
|
|
988
|
-
const sel = window.getSelection();
|
|
989
|
-
if (!sel || sel.rangeCount === 0)
|
|
990
|
-
return;
|
|
991
|
-
const range = sel.getRangeAt(0);
|
|
992
|
-
if (range.collapsed)
|
|
993
|
-
return;
|
|
994
|
-
const span = document.createElement('span');
|
|
995
|
-
span.style[cssProp] = value;
|
|
996
|
-
// Robuster als surroundContents: extract + wrap + insert
|
|
997
|
-
const frag = range.extractContents();
|
|
998
|
-
span.appendChild(frag);
|
|
999
|
-
range.insertNode(span);
|
|
1000
|
-
// Cursor hinter das eingefügte Element setzen
|
|
1001
|
-
sel.removeAllRanges();
|
|
1002
|
-
const after = document.createRange();
|
|
1003
|
-
after.setStartAfter(span);
|
|
1004
|
-
after.collapse(true);
|
|
1005
|
-
sel.addRange(after);
|
|
1006
|
-
this.emitValue();
|
|
1007
|
-
this.takeSnapshot('style');
|
|
992
|
+
this.applySelectionStyles({ [cssProp]: value });
|
|
1008
993
|
}
|
|
1009
994
|
applyInlineStyleSmart(cssProp, value) {
|
|
995
|
+
this.applySelectionStyles({ [cssProp]: value });
|
|
996
|
+
}
|
|
997
|
+
applySelectionStyles(styles) {
|
|
1010
998
|
this.focusEditor();
|
|
1011
999
|
this.restoreSelection();
|
|
1012
1000
|
const sel = window.getSelection();
|
|
1013
|
-
if (!sel || sel.rangeCount === 0)
|
|
1014
|
-
|
|
1015
|
-
const range = sel.getRangeAt(0);
|
|
1016
|
-
if (range.collapsed)
|
|
1017
|
-
return;
|
|
1018
|
-
const isBlockTag = (tag) => /^(P|DIV|PRE|H1|H2|H3|H4|H5|H6|LI|TD|TH)$/i.test(tag);
|
|
1019
|
-
const getBlockAncestor = (node) => {
|
|
1020
|
-
const editor = this.editorRef.nativeElement;
|
|
1021
|
-
let el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
|
1022
|
-
while (el && el !== editor && !isBlockTag(el.tagName))
|
|
1023
|
-
el = el.parentElement;
|
|
1024
|
-
return (el && el !== editor) ? el : null;
|
|
1025
|
-
};
|
|
1026
|
-
const startBlock = getBlockAncestor(range.startContainer);
|
|
1027
|
-
const endBlock = getBlockAncestor(range.endContainer);
|
|
1028
|
-
const crossesBlocks = !!startBlock && !!endBlock && startBlock !== endBlock;
|
|
1029
|
-
if (!crossesBlocks) {
|
|
1030
|
-
// Single block selection: keep using inline wrapper for precise range
|
|
1031
|
-
this.applyInlineStyle(cssProp, value);
|
|
1001
|
+
if (!sel || sel.rangeCount === 0) {
|
|
1002
|
+
console.warn(`${this.logPrefix} applySelectionStyles: no selection available`, { styles });
|
|
1032
1003
|
return;
|
|
1033
1004
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
const walker = document.createTreeWalker(common, NodeFilter.SHOW_ELEMENT);
|
|
1039
|
-
const styled = new Set();
|
|
1040
|
-
while (walker.nextNode()) {
|
|
1041
|
-
const el = walker.currentNode;
|
|
1042
|
-
if (!isBlockTag(el.tagName))
|
|
1043
|
-
continue;
|
|
1044
|
-
try {
|
|
1045
|
-
if (range.intersectsNode && range.intersectsNode(el))
|
|
1046
|
-
styled.add(el);
|
|
1047
|
-
}
|
|
1048
|
-
catch { }
|
|
1005
|
+
let range = sel.getRangeAt(0);
|
|
1006
|
+
if (range.collapsed) {
|
|
1007
|
+
console.warn(`${this.logPrefix} applySelectionStyles: range is collapsed`, { styles });
|
|
1008
|
+
return;
|
|
1049
1009
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1010
|
+
const entries = Object.entries(styles).filter(([, v]) => v != null && v !== '');
|
|
1011
|
+
if (entries.length === 0) {
|
|
1012
|
+
console.warn(`${this.logPrefix} applySelectionStyles: no style entries to apply`, { styles });
|
|
1013
|
+
return;
|
|
1052
1014
|
}
|
|
1053
|
-
this.
|
|
1054
|
-
this.
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
const walker = document.createTreeWalker(common, NodeFilter.SHOW_ELEMENT);
|
|
1062
|
-
const blocks = [];
|
|
1063
|
-
while (walker.nextNode()) {
|
|
1064
|
-
const el = walker.currentNode;
|
|
1065
|
-
if (!isBlockTag(el.tagName))
|
|
1066
|
-
continue;
|
|
1067
|
-
try {
|
|
1068
|
-
if (range.intersectsNode && range.intersectsNode(el))
|
|
1069
|
-
blocks.push(el);
|
|
1015
|
+
const propsToClear = Array.from(new Set(entries.map(([prop]) => this.toCssProperty(prop))));
|
|
1016
|
+
console.log(`${this.logPrefix} applySelectionStyles: clearing props before apply`, { propsToClear, entries });
|
|
1017
|
+
for (const cssProp of propsToClear) {
|
|
1018
|
+
const beforeClear = range.cloneRange();
|
|
1019
|
+
const clearedRange = this.removeInlineStyleInRange(range, cssProp, { suppressEmit: true });
|
|
1020
|
+
const refreshedSel = window.getSelection();
|
|
1021
|
+
if (refreshedSel && refreshedSel.rangeCount > 0) {
|
|
1022
|
+
range = refreshedSel.getRangeAt(0);
|
|
1070
1023
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
return blocks;
|
|
1074
|
-
}
|
|
1075
|
-
// Variant of applyInlineStyle that returns the created span and does not emit/snapshot by itself
|
|
1076
|
-
wrapSelectionInline(cssProp, value) {
|
|
1077
|
-
this.focusEditor();
|
|
1078
|
-
this.restoreSelection();
|
|
1079
|
-
const sel = window.getSelection();
|
|
1080
|
-
if (!sel || sel.rangeCount === 0)
|
|
1081
|
-
return null;
|
|
1082
|
-
const range = sel.getRangeAt(0);
|
|
1083
|
-
if (range.collapsed)
|
|
1084
|
-
return null;
|
|
1085
|
-
const span = document.createElement('span');
|
|
1086
|
-
span.style[cssProp] = value;
|
|
1087
|
-
const frag = range.extractContents();
|
|
1088
|
-
span.appendChild(frag);
|
|
1089
|
-
range.insertNode(span);
|
|
1090
|
-
// Restore selection just after for continuity
|
|
1091
|
-
sel.removeAllRanges();
|
|
1092
|
-
const after = document.createRange();
|
|
1093
|
-
after.setStartAfter(span);
|
|
1094
|
-
after.collapse(true);
|
|
1095
|
-
sel.addRange(after);
|
|
1096
|
-
return span;
|
|
1097
|
-
}
|
|
1098
|
-
isBoldCarrier(el) {
|
|
1099
|
-
if (!el)
|
|
1100
|
-
return false;
|
|
1101
|
-
const tag = el.tagName;
|
|
1102
|
-
if (tag === 'B' || tag === 'STRONG')
|
|
1103
|
-
return true;
|
|
1104
|
-
const fw = el.style?.fontWeight || '';
|
|
1105
|
-
if (fw && fw !== 'normal' && fw !== '400')
|
|
1106
|
-
return true;
|
|
1107
|
-
return false;
|
|
1108
|
-
}
|
|
1109
|
-
boldAncestorContainingRange(range) {
|
|
1110
|
-
let el = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
|
|
1111
|
-
? range.commonAncestorContainer
|
|
1112
|
-
: range.commonAncestorContainer.parentElement;
|
|
1113
|
-
const editor = this.editorRef.nativeElement;
|
|
1114
|
-
while (el && el !== editor) {
|
|
1115
|
-
if (this.isBoldCarrier(el)) {
|
|
1116
|
-
// ensure el contains both boundary containers fully
|
|
1117
|
-
const sc = range.startContainer;
|
|
1118
|
-
const ec = range.endContainer;
|
|
1119
|
-
if (el.contains(sc) && el.contains(ec))
|
|
1120
|
-
return el;
|
|
1024
|
+
else if (clearedRange) {
|
|
1025
|
+
range = clearedRange;
|
|
1121
1026
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
const editor = this.editorRef.nativeElement;
|
|
1131
|
-
while (el && el !== editor) {
|
|
1132
|
-
if (isCarrier(el)) {
|
|
1133
|
-
const sc = range.startContainer;
|
|
1134
|
-
const ec = range.endContainer;
|
|
1135
|
-
if (el.contains(sc) && el.contains(ec))
|
|
1136
|
-
return el;
|
|
1027
|
+
if (range.collapsed && !beforeClear.collapsed) {
|
|
1028
|
+
console.warn(`${this.logPrefix} applySelectionStyles: range collapsed after clearing, restoring previous range`);
|
|
1029
|
+
const sel2 = window.getSelection();
|
|
1030
|
+
if (sel2) {
|
|
1031
|
+
sel2.removeAllRanges();
|
|
1032
|
+
sel2.addRange(beforeClear);
|
|
1033
|
+
}
|
|
1034
|
+
range = beforeClear;
|
|
1137
1035
|
}
|
|
1138
|
-
el = el.parentElement;
|
|
1139
1036
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1037
|
+
const segments = this.collectTextSegments(range);
|
|
1038
|
+
if (segments.length === 0) {
|
|
1039
|
+
console.warn(`${this.logPrefix} applySelectionStyles: no text segments found`, {
|
|
1040
|
+
entries,
|
|
1041
|
+
commonAncestor: range.commonAncestorContainer
|
|
1042
|
+
});
|
|
1145
1043
|
return;
|
|
1146
|
-
const rRight = document.createRange();
|
|
1147
|
-
rRight.setStart(range.endContainer, range.endOffset);
|
|
1148
|
-
rightGuard(carrier, rRight);
|
|
1149
|
-
const rightFrag = rRight.extractContents();
|
|
1150
|
-
const rLeft = document.createRange();
|
|
1151
|
-
rLeft.setStart(carrier, 0);
|
|
1152
|
-
leftGuard(range, rLeft);
|
|
1153
|
-
const leftFrag = rLeft.extractContents();
|
|
1154
|
-
if (leftFrag.childNodes.length) {
|
|
1155
|
-
const leftClone = carrier.cloneNode(false);
|
|
1156
|
-
leftClone.appendChild(leftFrag);
|
|
1157
|
-
parent.insertBefore(leftClone, carrier);
|
|
1158
1044
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1045
|
+
console.log(`${this.logPrefix} applySelectionStyles: applying styles to segments`, {
|
|
1046
|
+
entries,
|
|
1047
|
+
segmentCount: segments.length
|
|
1048
|
+
});
|
|
1049
|
+
const wrappedSegments = [];
|
|
1050
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
1051
|
+
const wrapped = this.applyStylesToSegment(segments[i], entries);
|
|
1052
|
+
if (wrapped)
|
|
1053
|
+
wrappedSegments.unshift(wrapped);
|
|
1054
|
+
}
|
|
1055
|
+
if (wrappedSegments.length) {
|
|
1056
|
+
const first = wrappedSegments[0];
|
|
1057
|
+
const last = wrappedSegments[wrappedSegments.length - 1];
|
|
1058
|
+
const newRange = document.createRange();
|
|
1059
|
+
newRange.setStartBefore(first);
|
|
1060
|
+
newRange.setEndAfter(last);
|
|
1061
|
+
const selAfter = window.getSelection();
|
|
1062
|
+
if (selAfter) {
|
|
1063
|
+
selAfter.removeAllRanges();
|
|
1064
|
+
selAfter.addRange(newRange);
|
|
1172
1065
|
}
|
|
1173
|
-
|
|
1066
|
+
this.saveSelectionFromRange(newRange);
|
|
1174
1067
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
r.setEnd(rng.startContainer, rng.startOffset);
|
|
1178
|
-
}
|
|
1179
|
-
catch { }
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
// Unwrap bold formatting only for the selected content inside a containing bold ancestor by splitting it into left/selection/right parts
|
|
1183
|
-
deselectBoldBySplitting(range) {
|
|
1184
|
-
const ancestor = this.boldAncestorContainingRange(range);
|
|
1185
|
-
if (!ancestor)
|
|
1186
|
-
return false;
|
|
1187
|
-
this.splitCarrierAroundSelection(range, ancestor);
|
|
1188
|
-
return true;
|
|
1189
|
-
}
|
|
1190
|
-
deselectItalicBySplitting(range) {
|
|
1191
|
-
const anc = this.ancestorContainingRange(range, el => this.isItalicCarrier(el));
|
|
1192
|
-
if (!anc)
|
|
1193
|
-
return false;
|
|
1194
|
-
this.splitCarrierAroundSelection(range, anc);
|
|
1195
|
-
return true;
|
|
1196
|
-
}
|
|
1197
|
-
deselectUnderlineBySplitting(range) {
|
|
1198
|
-
const anc = this.ancestorContainingRange(range, el => this.isUnderlineCarrier(el));
|
|
1199
|
-
if (!anc)
|
|
1200
|
-
return false;
|
|
1201
|
-
this.splitCarrierAroundSelection(range, anc);
|
|
1202
|
-
return true;
|
|
1068
|
+
this.emitValue();
|
|
1069
|
+
this.takeSnapshot('style');
|
|
1203
1070
|
}
|
|
1204
|
-
|
|
1205
|
-
const
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
const
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
const onlyWhitespace = !el.firstChild || (normalized.length === 0 && el.querySelectorAll('*').length === 0);
|
|
1215
|
-
if (onlyWhitespace)
|
|
1216
|
-
toRemove.push(el);
|
|
1071
|
+
collectTextSegments(range) {
|
|
1072
|
+
const segments = [];
|
|
1073
|
+
const root = range.commonAncestorContainer;
|
|
1074
|
+
if (root.nodeType === Node.TEXT_NODE) {
|
|
1075
|
+
const text = root;
|
|
1076
|
+
const start = text === range.startContainer ? range.startOffset : 0;
|
|
1077
|
+
const end = text === range.endContainer ? range.endOffset : text.data.length;
|
|
1078
|
+
if (start < end)
|
|
1079
|
+
segments.push({ node: text, start, end });
|
|
1080
|
+
return segments;
|
|
1217
1081
|
}
|
|
1218
|
-
|
|
1219
|
-
el.remove();
|
|
1220
|
-
}
|
|
1221
|
-
cleanupEmptyUnderlineSpans(root) {
|
|
1222
|
-
const toRemove = [];
|
|
1223
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
1082
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1224
1083
|
while (walker.nextNode()) {
|
|
1225
|
-
const
|
|
1226
|
-
|
|
1227
|
-
if (!isU)
|
|
1084
|
+
const text = walker.currentNode;
|
|
1085
|
+
if (!text.data)
|
|
1228
1086
|
continue;
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1233
|
-
|
|
1087
|
+
if (!this.intersectsRange(range, text))
|
|
1088
|
+
continue;
|
|
1089
|
+
const start = text === range.startContainer ? range.startOffset : 0;
|
|
1090
|
+
const end = text === range.endContainer ? range.endOffset : text.data.length;
|
|
1091
|
+
if (start >= end)
|
|
1092
|
+
continue;
|
|
1093
|
+
segments.push({ node: text, start, end });
|
|
1234
1094
|
}
|
|
1235
|
-
|
|
1236
|
-
el.remove();
|
|
1095
|
+
return segments;
|
|
1237
1096
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT);
|
|
1242
|
-
while (walker.nextNode()) {
|
|
1243
|
-
toProcess.push(walker.currentNode);
|
|
1097
|
+
intersectsRange(range, node) {
|
|
1098
|
+
try {
|
|
1099
|
+
return range.intersectsNode ? range.intersectsNode(node) : true;
|
|
1244
1100
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
while (el.firstChild)
|
|
1248
|
-
el.parentNode?.insertBefore(el.firstChild, el);
|
|
1249
|
-
el.remove();
|
|
1250
|
-
continue;
|
|
1251
|
-
}
|
|
1252
|
-
if (el.tagName === 'SPAN') {
|
|
1253
|
-
if (el.style.fontWeight)
|
|
1254
|
-
el.style.removeProperty('font-weight');
|
|
1255
|
-
// Clean empty style spans
|
|
1256
|
-
if (!el.getAttribute('style')) {
|
|
1257
|
-
while (el.firstChild)
|
|
1258
|
-
el.parentNode?.insertBefore(el.firstChild, el);
|
|
1259
|
-
el.remove();
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1101
|
+
catch {
|
|
1102
|
+
return false;
|
|
1262
1103
|
}
|
|
1263
1104
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
// Split arbitrary ancestor element so that `node` becomes a sibling outside of it, preserving order even if `node` is nested deeply
|
|
1274
|
-
splitOutOfAncestor(ancestor, node) {
|
|
1275
|
-
// First, climb from node up to direct child of ancestor, splitting wrappers after `node` at each level
|
|
1276
|
-
let child = node;
|
|
1277
|
-
let parent = child.parentElement;
|
|
1278
|
-
while (parent && parent !== ancestor) {
|
|
1279
|
-
const right = parent.cloneNode(false);
|
|
1280
|
-
// move siblings after `child` into right clone
|
|
1281
|
-
while (child.nextSibling)
|
|
1282
|
-
right.appendChild(child.nextSibling);
|
|
1283
|
-
// insert right after parent
|
|
1284
|
-
parent.after(right);
|
|
1285
|
-
// ascend
|
|
1286
|
-
child = parent;
|
|
1287
|
-
parent = child.parentElement;
|
|
1105
|
+
applyStylesToSegment(segment, entries) {
|
|
1106
|
+
const segRange = document.createRange();
|
|
1107
|
+
segRange.setStart(segment.node, segment.start);
|
|
1108
|
+
segRange.setEnd(segment.node, segment.end);
|
|
1109
|
+
const wrapper = document.createElement('span');
|
|
1110
|
+
for (const [prop, value] of entries) {
|
|
1111
|
+
wrapper.style.setProperty(this.toCssProperty(prop), value);
|
|
1288
1112
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
const before = ancestor.cloneNode(false);
|
|
1293
|
-
while (ancestor.firstChild && ancestor.firstChild !== child) {
|
|
1294
|
-
before.appendChild(ancestor.firstChild);
|
|
1295
|
-
}
|
|
1296
|
-
const after = ancestor.cloneNode(false);
|
|
1297
|
-
while (child.nextSibling)
|
|
1298
|
-
after.appendChild(child.nextSibling);
|
|
1299
|
-
const gp = ancestor.parentNode;
|
|
1300
|
-
if (!gp)
|
|
1301
|
-
return;
|
|
1302
|
-
if (before.childNodes.length)
|
|
1303
|
-
gp.insertBefore(before, ancestor);
|
|
1304
|
-
// move node out, place where ancestor was
|
|
1305
|
-
gp.insertBefore(node, ancestor);
|
|
1306
|
-
if (after.childNodes.length)
|
|
1307
|
-
gp.insertBefore(after, ancestor);
|
|
1308
|
-
ancestor.remove();
|
|
1309
|
-
}
|
|
1310
|
-
cleanupEmptyBoldSpans(root) {
|
|
1311
|
-
const toRemove = [];
|
|
1312
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
1313
|
-
while (walker.nextNode()) {
|
|
1314
|
-
const el = walker.currentNode;
|
|
1315
|
-
const isBoldEl = (el.tagName === 'B' || el.tagName === 'STRONG' || (el.tagName === 'SPAN' && el.style.fontWeight && el.style.fontWeight !== 'normal' && el.style.fontWeight !== '400'));
|
|
1316
|
-
if (!isBoldEl)
|
|
1317
|
-
continue;
|
|
1318
|
-
// remove if no children or only whitespace (including NBSP \u00A0 and zero-width space \u200B)
|
|
1319
|
-
const text = el.textContent ?? '';
|
|
1320
|
-
const normalized = text.replace(/[\u00A0\u200B\s]/g, '');
|
|
1321
|
-
const onlyWhitespace = !el.firstChild || (normalized.length === 0 && el.querySelectorAll('*').length === 0);
|
|
1322
|
-
if (onlyWhitespace) {
|
|
1323
|
-
toRemove.push(el);
|
|
1324
|
-
}
|
|
1113
|
+
try {
|
|
1114
|
+
segRange.surroundContents(wrapper);
|
|
1115
|
+
return this.mergeAdjacentStyledSpans(wrapper);
|
|
1325
1116
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1117
|
+
catch (err) {
|
|
1118
|
+
console.error(`${this.logPrefix} applyStylesToSegment: failed to surround contents`, {
|
|
1119
|
+
error: err,
|
|
1120
|
+
segment,
|
|
1121
|
+
entries
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
return null;
|
|
1328
1125
|
}
|
|
1329
|
-
|
|
1126
|
+
toCssProperty(prop) {
|
|
1127
|
+
return prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
1128
|
+
}
|
|
1129
|
+
mergeAdjacentStyledSpans(span) {
|
|
1330
1130
|
if (span.tagName !== 'SPAN')
|
|
1331
|
-
return;
|
|
1332
|
-
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1131
|
+
return span;
|
|
1132
|
+
let current = span;
|
|
1133
|
+
const normalize = (el) => el && el.tagName === 'SPAN' ? el : null;
|
|
1134
|
+
const styleText = (el) => (el ? el.getAttribute('style') || '' : '');
|
|
1135
|
+
// Merge with parent if identical
|
|
1136
|
+
let parent = normalize(current.parentElement);
|
|
1137
|
+
if (parent && styleText(parent) === styleText(current) && parent.attributes.length === current.attributes.length) {
|
|
1138
|
+
while (current.firstChild)
|
|
1139
|
+
parent.insertBefore(current.firstChild, current);
|
|
1140
|
+
current.remove();
|
|
1141
|
+
current = parent;
|
|
1142
|
+
}
|
|
1143
|
+
// Merge with previous sibling ignoring empty text nodes
|
|
1144
|
+
let prev = current.previousSibling;
|
|
1145
|
+
while (prev && prev.nodeType === Node.TEXT_NODE && !prev.data.trim())
|
|
1146
|
+
prev = prev.previousSibling;
|
|
1147
|
+
if (prev instanceof HTMLElement && prev.tagName === 'SPAN' && styleText(prev) === styleText(current) && prev.attributes.length === current.attributes.length) {
|
|
1148
|
+
while (current.firstChild)
|
|
1149
|
+
prev.appendChild(current.firstChild);
|
|
1150
|
+
current.remove();
|
|
1151
|
+
current = prev;
|
|
1342
1152
|
}
|
|
1153
|
+
// Merge with next sibling ignoring empty text nodes
|
|
1154
|
+
let next = current.nextSibling;
|
|
1155
|
+
while (next && next.nodeType === Node.TEXT_NODE && !next.data.trim())
|
|
1156
|
+
next = next.nextSibling;
|
|
1157
|
+
if (next instanceof HTMLElement && next.tagName === 'SPAN' && styleText(next) === styleText(current) && next.attributes.length === current.attributes.length) {
|
|
1158
|
+
while (next.firstChild)
|
|
1159
|
+
current.appendChild(next.firstChild);
|
|
1160
|
+
next.remove();
|
|
1161
|
+
}
|
|
1162
|
+
return current;
|
|
1163
|
+
}
|
|
1164
|
+
saveSelectionFromRange(range) {
|
|
1165
|
+
this.savedRange = range.cloneRange();
|
|
1343
1166
|
}
|
|
1344
1167
|
wrapSelectionWith(tag, attrs) {
|
|
1345
1168
|
const sel = window.getSelection();
|
|
@@ -1358,7 +1181,6 @@ class StackchRichtextEditor {
|
|
|
1358
1181
|
const frag = range.extractContents();
|
|
1359
1182
|
el.appendChild(frag);
|
|
1360
1183
|
range.insertNode(el);
|
|
1361
|
-
// Auswahl hinter das Element setzen
|
|
1362
1184
|
sel.removeAllRanges();
|
|
1363
1185
|
const after = document.createRange();
|
|
1364
1186
|
after.setStartAfter(el);
|
|
@@ -1373,14 +1195,13 @@ class StackchRichtextEditor {
|
|
|
1373
1195
|
range.deleteContents();
|
|
1374
1196
|
const node = document.createTextNode(text);
|
|
1375
1197
|
range.insertNode(node);
|
|
1376
|
-
// Cursor ans Ende des eingefügten Textes
|
|
1377
1198
|
sel.removeAllRanges();
|
|
1378
1199
|
const after = document.createRange();
|
|
1379
1200
|
after.setStartAfter(node);
|
|
1380
1201
|
after.collapse(true);
|
|
1381
1202
|
sel.addRange(after);
|
|
1382
1203
|
}
|
|
1383
|
-
//
|
|
1204
|
+
// ─── Inline toggle logic (Bold / Italic / Underline) ─────────────────────
|
|
1384
1205
|
updateInlineStates() {
|
|
1385
1206
|
const sel = window.getSelection();
|
|
1386
1207
|
if (!sel || sel.rangeCount === 0) {
|
|
@@ -1402,63 +1223,19 @@ class StackchRichtextEditor {
|
|
|
1402
1223
|
this.isBoldActive = this.computeBoldAnyForRange(range);
|
|
1403
1224
|
this.isItalicActive = this.computeItalicAnyForRange(range);
|
|
1404
1225
|
this.isUnderlineActive = this.computeUnderlineAnyForRange(range);
|
|
1405
|
-
// ensure UI reflects changes immediately
|
|
1406
1226
|
try {
|
|
1407
1227
|
this.cdr.detectChanges();
|
|
1408
1228
|
}
|
|
1409
1229
|
catch { }
|
|
1410
1230
|
}
|
|
1411
1231
|
computeBoldAnyForRange(range) {
|
|
1412
|
-
|
|
1413
|
-
const root = this.editorRef.nativeElement;
|
|
1414
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1415
|
-
while (walker.nextNode()) {
|
|
1416
|
-
const n = walker.currentNode;
|
|
1417
|
-
if (!n.data.trim())
|
|
1418
|
-
continue;
|
|
1419
|
-
try {
|
|
1420
|
-
if (range.intersectsNode(n)) {
|
|
1421
|
-
if (this.isNodeBold(n.parentElement))
|
|
1422
|
-
return true;
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
catch { }
|
|
1426
|
-
}
|
|
1427
|
-
return false;
|
|
1232
|
+
return this.selectionHasStyle(range, 'font-weight');
|
|
1428
1233
|
}
|
|
1429
1234
|
computeItalicAnyForRange(range) {
|
|
1430
|
-
|
|
1431
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1432
|
-
while (walker.nextNode()) {
|
|
1433
|
-
const n = walker.currentNode;
|
|
1434
|
-
if (!n.data.trim())
|
|
1435
|
-
continue;
|
|
1436
|
-
try {
|
|
1437
|
-
if (range.intersectsNode(n)) {
|
|
1438
|
-
if (this.isNodeItalic(n.parentElement))
|
|
1439
|
-
return true;
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
catch { }
|
|
1443
|
-
}
|
|
1444
|
-
return false;
|
|
1235
|
+
return this.selectionHasStyle(range, 'font-style');
|
|
1445
1236
|
}
|
|
1446
1237
|
computeUnderlineAnyForRange(range) {
|
|
1447
|
-
|
|
1448
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1449
|
-
while (walker.nextNode()) {
|
|
1450
|
-
const n = walker.currentNode;
|
|
1451
|
-
if (!n.data.trim())
|
|
1452
|
-
continue;
|
|
1453
|
-
try {
|
|
1454
|
-
if (range.intersectsNode(n)) {
|
|
1455
|
-
if (this.isNodeUnderline(n.parentElement))
|
|
1456
|
-
return true;
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
catch { }
|
|
1460
|
-
}
|
|
1461
|
-
return false;
|
|
1238
|
+
return this.selectionHasStyle(range, 'text-decoration');
|
|
1462
1239
|
}
|
|
1463
1240
|
isNodeBold(node) {
|
|
1464
1241
|
let el = node;
|
|
@@ -1499,120 +1276,244 @@ class StackchRichtextEditor {
|
|
|
1499
1276
|
}
|
|
1500
1277
|
return false;
|
|
1501
1278
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
const tag = el.tagName;
|
|
1506
|
-
if (tag === 'I' || tag === 'EM')
|
|
1507
|
-
return true;
|
|
1508
|
-
const fs = el.style?.fontStyle || '';
|
|
1509
|
-
return !!fs && fs !== 'normal';
|
|
1510
|
-
}
|
|
1511
|
-
isUnderlineCarrier(el) {
|
|
1512
|
-
if (!el)
|
|
1513
|
-
return false;
|
|
1514
|
-
const tag = el.tagName;
|
|
1515
|
-
if (tag === 'U')
|
|
1516
|
-
return true;
|
|
1517
|
-
const td = el.style?.textDecoration || '';
|
|
1518
|
-
return typeof td === 'string' && td.includes('underline');
|
|
1519
|
-
}
|
|
1520
|
-
computeBoldActiveForRange(range) {
|
|
1279
|
+
removeInlineStyleInRange(range, cssPropKebab, options) {
|
|
1280
|
+
const shouldRestoreSelection = options?.restoreSelection !== false;
|
|
1281
|
+
this.isolateRangeFromStyleCarriers(range, cssPropKebab);
|
|
1521
1282
|
const editor = this.editorRef.nativeElement;
|
|
1522
|
-
const
|
|
1523
|
-
const
|
|
1524
|
-
const
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
try {
|
|
1530
|
-
if (range.intersectsNode(n))
|
|
1531
|
-
nodes.push(n);
|
|
1532
|
-
}
|
|
1533
|
-
catch { /* Safari may throw if node not in same tree */ }
|
|
1534
|
-
}
|
|
1535
|
-
if (nodes.length === 0)
|
|
1536
|
-
return false;
|
|
1537
|
-
return nodes.every(t => this.isNodeBold(t.parentElement));
|
|
1538
|
-
}
|
|
1539
|
-
computeItalicActiveForRange(range) {
|
|
1540
|
-
const editor = this.editorRef.nativeElement;
|
|
1541
|
-
const root = this.isRangeInEditor(range) ? this.editorRef.nativeElement : range.commonAncestorContainer;
|
|
1542
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1543
|
-
const nodes = [];
|
|
1544
|
-
while (walker.nextNode()) {
|
|
1545
|
-
const n = walker.currentNode;
|
|
1546
|
-
if (!n.data.trim())
|
|
1547
|
-
continue;
|
|
1283
|
+
const selAfter = window.getSelection();
|
|
1284
|
+
const effectiveRange = selAfter && selAfter.rangeCount > 0 ? selAfter.getRangeAt(0) : range;
|
|
1285
|
+
const common = effectiveRange.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
|
|
1286
|
+
? effectiveRange.commonAncestorContainer
|
|
1287
|
+
: (effectiveRange.commonAncestorContainer.parentElement || editor);
|
|
1288
|
+
const affected = [];
|
|
1289
|
+
const intersects = (node) => {
|
|
1548
1290
|
try {
|
|
1549
|
-
|
|
1550
|
-
nodes.push(n);
|
|
1291
|
+
return effectiveRange.intersectsNode ? effectiveRange.intersectsNode(node) : true;
|
|
1551
1292
|
}
|
|
1552
|
-
catch {
|
|
1553
|
-
|
|
1554
|
-
if (nodes.length === 0)
|
|
1555
|
-
return false;
|
|
1556
|
-
return nodes.every(t => this.isNodeItalic(t.parentElement));
|
|
1557
|
-
}
|
|
1558
|
-
computeUnderlineActiveForRange(range) {
|
|
1559
|
-
const editor = this.editorRef.nativeElement;
|
|
1560
|
-
const root = this.isRangeInEditor(range) ? this.editorRef.nativeElement : range.commonAncestorContainer;
|
|
1561
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1562
|
-
const nodes = [];
|
|
1563
|
-
while (walker.nextNode()) {
|
|
1564
|
-
const n = walker.currentNode;
|
|
1565
|
-
if (!n.data.trim())
|
|
1566
|
-
continue;
|
|
1567
|
-
try {
|
|
1568
|
-
if (range.intersectsNode(n))
|
|
1569
|
-
nodes.push(n);
|
|
1293
|
+
catch {
|
|
1294
|
+
return false;
|
|
1570
1295
|
}
|
|
1571
|
-
|
|
1296
|
+
};
|
|
1297
|
+
const matchesEl = (el) => this.isStyleCarrier(el, cssPropKebab);
|
|
1298
|
+
console.log(`${this.logPrefix} removeInlineStyleInRange:start`, {
|
|
1299
|
+
cssPropKebab,
|
|
1300
|
+
rangeCollapsed: effectiveRange.collapsed,
|
|
1301
|
+
commonTag: common.tagName,
|
|
1302
|
+
commonHasStyle: common.style ? common.style.getPropertyValue?.(cssPropKebab) : undefined
|
|
1303
|
+
});
|
|
1304
|
+
if (common instanceof HTMLElement && matchesEl(common) && intersects(common)) {
|
|
1305
|
+
affected.push(common);
|
|
1572
1306
|
}
|
|
1573
|
-
if (nodes.length === 0)
|
|
1574
|
-
return false;
|
|
1575
|
-
return nodes.every(t => this.isNodeUnderline(t.parentElement));
|
|
1576
|
-
}
|
|
1577
|
-
removeInlineStyleInRange(range, cssPropKebab) {
|
|
1578
|
-
const editor = this.editorRef.nativeElement;
|
|
1579
|
-
const common = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE ? range.commonAncestorContainer : range.commonAncestorContainer.parentElement || editor;
|
|
1580
|
-
const affected = [];
|
|
1581
1307
|
const walker = document.createTreeWalker(common, NodeFilter.SHOW_ELEMENT);
|
|
1582
1308
|
while (walker.nextNode()) {
|
|
1583
1309
|
const el = walker.currentNode;
|
|
1584
|
-
|
|
1585
|
-
if (el.tagName === 'SPAN' && el.style && el.style.getPropertyValue(cssPropKebab))
|
|
1586
|
-
matches = true;
|
|
1587
|
-
if (cssPropKebab === 'font-weight' && (el.tagName === 'B' || el.tagName === 'STRONG'))
|
|
1588
|
-
matches = true;
|
|
1589
|
-
if (cssPropKebab === 'font-style' && (el.tagName === 'I' || el.tagName === 'EM'))
|
|
1590
|
-
matches = true;
|
|
1591
|
-
if (cssPropKebab === 'text-decoration' && el.tagName === 'U')
|
|
1592
|
-
matches = true;
|
|
1593
|
-
if (!matches)
|
|
1310
|
+
if (!matchesEl(el))
|
|
1594
1311
|
continue;
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1312
|
+
if (intersects(el)) {
|
|
1313
|
+
affected.push(el);
|
|
1314
|
+
console.log(`${this.logPrefix} removeInlineStyleInRange:match`, el.tagName, el.getAttribute('style'));
|
|
1598
1315
|
}
|
|
1599
|
-
catch { }
|
|
1600
1316
|
}
|
|
1317
|
+
let removedTags = 0;
|
|
1318
|
+
let removedProps = 0;
|
|
1601
1319
|
for (const el of affected) {
|
|
1602
1320
|
if (el.tagName === 'B' || el.tagName === 'STRONG' || el.tagName === 'I' || el.tagName === 'EM' || el.tagName === 'U') {
|
|
1321
|
+
console.log(`${this.logPrefix} unwrap tag`, el.tagName);
|
|
1603
1322
|
while (el.firstChild)
|
|
1604
1323
|
el.parentNode?.insertBefore(el.firstChild, el);
|
|
1605
1324
|
el.remove();
|
|
1325
|
+
removedTags++;
|
|
1606
1326
|
continue;
|
|
1607
1327
|
}
|
|
1328
|
+
console.log(`${this.logPrefix} remove style`, cssPropKebab, 'from', el.tagName, el.getAttribute('style'));
|
|
1608
1329
|
el.style.removeProperty(cssPropKebab);
|
|
1330
|
+
if (!el.style.length) {
|
|
1331
|
+
console.log(`${this.logPrefix} removeInlineStyleInRange: style attribute now empty, removing attribute`, el);
|
|
1332
|
+
el.removeAttribute('style');
|
|
1333
|
+
}
|
|
1334
|
+
removedProps++;
|
|
1609
1335
|
if (!el.getAttribute('style')) {
|
|
1336
|
+
console.log(`${this.logPrefix} unwrap empty styled span`, el.tagName);
|
|
1610
1337
|
while (el.firstChild)
|
|
1611
1338
|
el.parentNode?.insertBefore(el.firstChild, el);
|
|
1612
1339
|
el.remove();
|
|
1613
1340
|
}
|
|
1614
1341
|
}
|
|
1615
|
-
this.
|
|
1342
|
+
this.pruneDanglingStyleCarriers(common, cssPropKebab);
|
|
1343
|
+
console.log(`${this.logPrefix} removeInlineStyleInRange:done`, { affected: affected.length, removedTags, removedProps });
|
|
1344
|
+
let resultRange = null;
|
|
1345
|
+
if (!options?.suppressEmit) {
|
|
1346
|
+
this.emitValue();
|
|
1347
|
+
resultRange = null;
|
|
1348
|
+
}
|
|
1349
|
+
else {
|
|
1350
|
+
const sel2 = window.getSelection();
|
|
1351
|
+
if (sel2 && sel2.rangeCount > 0) {
|
|
1352
|
+
resultRange = sel2.getRangeAt(0).cloneRange();
|
|
1353
|
+
}
|
|
1354
|
+
else if (shouldRestoreSelection) {
|
|
1355
|
+
resultRange = effectiveRange.cloneRange();
|
|
1356
|
+
}
|
|
1357
|
+
if (shouldRestoreSelection && resultRange) {
|
|
1358
|
+
const selRestore = window.getSelection();
|
|
1359
|
+
if (selRestore) {
|
|
1360
|
+
selRestore.removeAllRanges();
|
|
1361
|
+
selRestore.addRange(resultRange);
|
|
1362
|
+
this.saveSelectionFromRange(resultRange);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return resultRange;
|
|
1367
|
+
}
|
|
1368
|
+
pruneDanglingStyleCarriers(root, cssPropKebab) {
|
|
1369
|
+
const toRemove = [];
|
|
1370
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
1371
|
+
if (root instanceof HTMLElement && this.isStyleCarrier(root, cssPropKebab) && !this.elementHasVisibleContent(root)) {
|
|
1372
|
+
toRemove.push(root);
|
|
1373
|
+
}
|
|
1374
|
+
while (walker.nextNode()) {
|
|
1375
|
+
const el = walker.currentNode;
|
|
1376
|
+
if (!this.isStyleCarrier(el, cssPropKebab))
|
|
1377
|
+
continue;
|
|
1378
|
+
if (this.elementHasVisibleContent(el))
|
|
1379
|
+
continue;
|
|
1380
|
+
toRemove.push(el);
|
|
1381
|
+
}
|
|
1382
|
+
for (const el of toRemove) {
|
|
1383
|
+
console.log(`${this.logPrefix} pruneDanglingStyleCarriers: removing empty carrier`, { tag: el.tagName, style: el.getAttribute('style') });
|
|
1384
|
+
el.remove();
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
elementHasVisibleContent(node) {
|
|
1388
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1389
|
+
const text = node.data;
|
|
1390
|
+
for (let i = 0; i < text.length; i++) {
|
|
1391
|
+
const ch = text[i];
|
|
1392
|
+
if (ch === '\u00a0')
|
|
1393
|
+
return true;
|
|
1394
|
+
if (!/\s/.test(ch) && ch !== '\u200b' && ch !== '\u200c' && ch !== '\u200d' && ch !== '\ufeff') {
|
|
1395
|
+
return true;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
1400
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1401
|
+
const el = node;
|
|
1402
|
+
if (el.tagName === 'BR')
|
|
1403
|
+
return true;
|
|
1404
|
+
for (const child of Array.from(node.childNodes)) {
|
|
1405
|
+
if (this.elementHasVisibleContent(child))
|
|
1406
|
+
return true;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return false;
|
|
1410
|
+
}
|
|
1411
|
+
isolateRangeFromStyleCarriers(range, cssPropKebab) {
|
|
1412
|
+
let carrier = this.findCarrierContainingRange(range, cssPropKebab);
|
|
1413
|
+
while (carrier) {
|
|
1414
|
+
this.splitCarrierAroundSelection(range, carrier);
|
|
1415
|
+
carrier = this.findCarrierContainingRange(range, cssPropKebab);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
selectionHasStyle(range, cssPropKebab) {
|
|
1419
|
+
const editor = this.editorRef.nativeElement;
|
|
1420
|
+
const common = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
|
|
1421
|
+
? range.commonAncestorContainer
|
|
1422
|
+
: (range.commonAncestorContainer.parentElement || editor);
|
|
1423
|
+
const intersects = (node) => this.intersectsRange(range, node);
|
|
1424
|
+
if (common instanceof HTMLElement && this.isStyleCarrier(common, cssPropKebab) && intersects(common)) {
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1427
|
+
const walker = document.createTreeWalker(common, NodeFilter.SHOW_ELEMENT);
|
|
1428
|
+
while (walker.nextNode()) {
|
|
1429
|
+
const el = walker.currentNode;
|
|
1430
|
+
if (!this.isStyleCarrier(el, cssPropKebab))
|
|
1431
|
+
continue;
|
|
1432
|
+
if (intersects(el))
|
|
1433
|
+
return true;
|
|
1434
|
+
}
|
|
1435
|
+
return false;
|
|
1436
|
+
}
|
|
1437
|
+
findCarrierContainingRange(range, cssPropKebab) {
|
|
1438
|
+
let el = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
|
|
1439
|
+
? range.commonAncestorContainer
|
|
1440
|
+
: range.commonAncestorContainer.parentElement;
|
|
1441
|
+
const editor = this.editorRef.nativeElement;
|
|
1442
|
+
while (el && el !== editor) {
|
|
1443
|
+
if (this.isStyleCarrier(el, cssPropKebab)) {
|
|
1444
|
+
const sc = range.startContainer;
|
|
1445
|
+
const ec = range.endContainer;
|
|
1446
|
+
if (el.contains(sc) && el.contains(ec))
|
|
1447
|
+
return el;
|
|
1448
|
+
}
|
|
1449
|
+
el = el.parentElement;
|
|
1450
|
+
}
|
|
1451
|
+
return null;
|
|
1452
|
+
}
|
|
1453
|
+
isStyleCarrier(el, cssPropKebab) {
|
|
1454
|
+
const tag = el.tagName;
|
|
1455
|
+
if (cssPropKebab === 'font-weight') {
|
|
1456
|
+
if (tag === 'B' || tag === 'STRONG')
|
|
1457
|
+
return true;
|
|
1458
|
+
const fw = el.style?.fontWeight || '';
|
|
1459
|
+
return !!fw && fw !== 'normal' && fw !== '400';
|
|
1460
|
+
}
|
|
1461
|
+
if (cssPropKebab === 'font-style') {
|
|
1462
|
+
if (tag === 'I' || tag === 'EM')
|
|
1463
|
+
return true;
|
|
1464
|
+
const fs = el.style?.fontStyle || '';
|
|
1465
|
+
return !!fs && fs !== 'normal';
|
|
1466
|
+
}
|
|
1467
|
+
if (cssPropKebab === 'text-decoration') {
|
|
1468
|
+
if (tag === 'U')
|
|
1469
|
+
return true;
|
|
1470
|
+
const td = el.style?.textDecoration || el.style?.textDecorationLine || '';
|
|
1471
|
+
return typeof td === 'string' && td.includes('underline');
|
|
1472
|
+
}
|
|
1473
|
+
const inline = el.style?.getPropertyValue(cssPropKebab) || '';
|
|
1474
|
+
if (!inline)
|
|
1475
|
+
return false;
|
|
1476
|
+
if (cssPropKebab === 'color') {
|
|
1477
|
+
return inline !== 'inherit' && inline !== 'initial';
|
|
1478
|
+
}
|
|
1479
|
+
if (cssPropKebab === 'background-color') {
|
|
1480
|
+
return inline !== 'transparent' && inline !== 'initial';
|
|
1481
|
+
}
|
|
1482
|
+
return true;
|
|
1483
|
+
}
|
|
1484
|
+
splitCarrierAroundSelection(range, carrier) {
|
|
1485
|
+
const parent = carrier.parentNode;
|
|
1486
|
+
if (!parent)
|
|
1487
|
+
return;
|
|
1488
|
+
const rightRange = document.createRange();
|
|
1489
|
+
rightRange.setStart(range.endContainer, range.endOffset);
|
|
1490
|
+
try {
|
|
1491
|
+
rightRange.setEnd(carrier, carrier.childNodes.length);
|
|
1492
|
+
}
|
|
1493
|
+
catch { }
|
|
1494
|
+
const rightFrag = rightRange.extractContents();
|
|
1495
|
+
const leftRange = document.createRange();
|
|
1496
|
+
leftRange.setStart(carrier, 0);
|
|
1497
|
+
try {
|
|
1498
|
+
leftRange.setEnd(range.startContainer, range.startOffset);
|
|
1499
|
+
}
|
|
1500
|
+
catch { }
|
|
1501
|
+
const leftFrag = leftRange.extractContents();
|
|
1502
|
+
if (leftFrag.childNodes.length) {
|
|
1503
|
+
const leftClone = carrier.cloneNode(false);
|
|
1504
|
+
leftClone.appendChild(leftFrag);
|
|
1505
|
+
parent.insertBefore(leftClone, carrier);
|
|
1506
|
+
}
|
|
1507
|
+
let insertAfter = carrier;
|
|
1508
|
+
if (rightFrag.childNodes.length) {
|
|
1509
|
+
const rightClone = carrier.cloneNode(false);
|
|
1510
|
+
rightClone.appendChild(rightFrag);
|
|
1511
|
+
parent.insertBefore(rightClone, carrier.nextSibling);
|
|
1512
|
+
insertAfter = rightClone;
|
|
1513
|
+
}
|
|
1514
|
+
while (carrier.firstChild)
|
|
1515
|
+
parent.insertBefore(carrier.firstChild, insertAfter);
|
|
1516
|
+
carrier.remove();
|
|
1616
1517
|
}
|
|
1617
1518
|
toggleBold() {
|
|
1618
1519
|
this.focusEditor();
|
|
@@ -1624,45 +1525,12 @@ class StackchRichtextEditor {
|
|
|
1624
1525
|
if (range.collapsed)
|
|
1625
1526
|
this.expandRangeToWord(range);
|
|
1626
1527
|
const anyActive = this.computeBoldAnyForRange(range);
|
|
1627
|
-
|
|
1628
|
-
const multiBlock = blocks.length > 1;
|
|
1528
|
+
console.log(`${this.logPrefix} toggleBold`, { anyActive, rangeCollapsed: range.collapsed });
|
|
1629
1529
|
if (anyActive) {
|
|
1630
|
-
|
|
1631
|
-
for (const b of blocks)
|
|
1632
|
-
b.style.removeProperty('font-weight');
|
|
1633
|
-
this.emitValue();
|
|
1634
|
-
this.updateInlineStates();
|
|
1635
|
-
this.takeSnapshot('toggle-bold-blocks');
|
|
1636
|
-
return;
|
|
1637
|
-
}
|
|
1638
|
-
// Preferred: split bold ancestor around selection so we keep surrounding text intact
|
|
1639
|
-
const changed = this.deselectBoldBySplitting(range);
|
|
1640
|
-
if (changed) {
|
|
1641
|
-
this.cleanupEmptyBoldSpans(this.editorRef.nativeElement);
|
|
1642
|
-
this.emitValue();
|
|
1643
|
-
}
|
|
1644
|
-
else {
|
|
1645
|
-
// Fallback: previous neutralization approach for complex cases
|
|
1646
|
-
const normal = this.wrapSelectionInline('fontWeight', '400');
|
|
1647
|
-
if (normal) {
|
|
1648
|
-
this.stripBoldWithin(normal);
|
|
1649
|
-
this.liftOutOfBoldAncestors(normal);
|
|
1650
|
-
this.cleanupEmptyBoldSpans(this.editorRef.nativeElement);
|
|
1651
|
-
this.maybeUnwrapNormalSpan(normal);
|
|
1652
|
-
this.emitValue();
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1530
|
+
this.removeInlineStyleInRange(range, 'font-weight');
|
|
1655
1531
|
}
|
|
1656
1532
|
else {
|
|
1657
|
-
|
|
1658
|
-
for (const b of blocks)
|
|
1659
|
-
b.style.fontWeight = 'bold';
|
|
1660
|
-
this.emitValue();
|
|
1661
|
-
this.updateInlineStates();
|
|
1662
|
-
this.takeSnapshot('toggle-bold-blocks');
|
|
1663
|
-
return;
|
|
1664
|
-
}
|
|
1665
|
-
this.applyInlineStyleSmart('fontWeight', 'bold');
|
|
1533
|
+
this.applySelectionStyles({ fontWeight: 'bold' });
|
|
1666
1534
|
}
|
|
1667
1535
|
this.updateInlineStates();
|
|
1668
1536
|
this.takeSnapshot('toggle-bold');
|
|
@@ -1678,18 +1546,10 @@ class StackchRichtextEditor {
|
|
|
1678
1546
|
this.expandRangeToWord(range);
|
|
1679
1547
|
const anyActive = this.computeItalicAnyForRange(range);
|
|
1680
1548
|
if (anyActive) {
|
|
1681
|
-
|
|
1682
|
-
if (changed) {
|
|
1683
|
-
this.cleanupEmptyItalicSpans(this.editorRef.nativeElement);
|
|
1684
|
-
this.emitValue();
|
|
1685
|
-
}
|
|
1686
|
-
else {
|
|
1687
|
-
this.removeInlineStyleInRange(range, 'font-style');
|
|
1688
|
-
this.applyInlineStyle('fontStyle', 'normal');
|
|
1689
|
-
}
|
|
1549
|
+
this.removeInlineStyleInRange(range, 'font-style');
|
|
1690
1550
|
}
|
|
1691
1551
|
else {
|
|
1692
|
-
this.
|
|
1552
|
+
this.applySelectionStyles({ fontStyle: 'italic' });
|
|
1693
1553
|
}
|
|
1694
1554
|
this.updateInlineStates();
|
|
1695
1555
|
this.takeSnapshot('toggle-italic');
|
|
@@ -1705,22 +1565,15 @@ class StackchRichtextEditor {
|
|
|
1705
1565
|
this.expandRangeToWord(range);
|
|
1706
1566
|
const anyActive = this.computeUnderlineAnyForRange(range);
|
|
1707
1567
|
if (anyActive) {
|
|
1708
|
-
|
|
1709
|
-
if (changed) {
|
|
1710
|
-
this.cleanupEmptyUnderlineSpans(this.editorRef.nativeElement);
|
|
1711
|
-
this.emitValue();
|
|
1712
|
-
}
|
|
1713
|
-
else {
|
|
1714
|
-
this.removeInlineStyleInRange(range, 'text-decoration');
|
|
1715
|
-
this.applyInlineStyle('textDecoration', 'none');
|
|
1716
|
-
}
|
|
1568
|
+
this.removeInlineStyleInRange(range, 'text-decoration');
|
|
1717
1569
|
}
|
|
1718
1570
|
else {
|
|
1719
|
-
this.
|
|
1571
|
+
this.applySelectionStyles({ textDecoration: 'underline' });
|
|
1720
1572
|
}
|
|
1721
1573
|
this.updateInlineStates();
|
|
1722
1574
|
this.takeSnapshot('toggle-underline');
|
|
1723
1575
|
}
|
|
1576
|
+
// ─── Block operations ─────────────────────────────────────────────────────
|
|
1724
1577
|
getCurrentRange() {
|
|
1725
1578
|
const sel = window.getSelection();
|
|
1726
1579
|
if (!sel || sel.rangeCount === 0)
|
|
@@ -1734,7 +1587,6 @@ class StackchRichtextEditor {
|
|
|
1734
1587
|
while (el && el.parentElement && el.parentElement !== editor) {
|
|
1735
1588
|
el = el.parentElement;
|
|
1736
1589
|
}
|
|
1737
|
-
// Wenn direktes Kind des Editors
|
|
1738
1590
|
if (el && el.parentElement === editor)
|
|
1739
1591
|
return el;
|
|
1740
1592
|
return editor;
|
|
@@ -1753,34 +1605,29 @@ class StackchRichtextEditor {
|
|
|
1753
1605
|
if (!range)
|
|
1754
1606
|
return;
|
|
1755
1607
|
const editor = this.editorRef.nativeElement;
|
|
1756
|
-
// Wenn bereits in einer Liste, Liste aufheben
|
|
1757
1608
|
const listAncestor = this.findClosest(range.commonAncestorContainer, 'ul,ol');
|
|
1758
1609
|
if (listAncestor) {
|
|
1759
|
-
// unwrap: li-Inhalte an die Stelle der Liste setzen
|
|
1760
1610
|
const frag = document.createDocumentFragment();
|
|
1761
|
-
const items = Array.from(listAncestor.children);
|
|
1611
|
+
const items = Array.from(listAncestor.children);
|
|
1762
1612
|
for (const li of items) {
|
|
1763
1613
|
while (li.firstChild)
|
|
1764
1614
|
frag.appendChild(li.firstChild);
|
|
1765
|
-
// Optional: Zeilenumbruch zwischen Items
|
|
1766
1615
|
frag.appendChild(document.createElement('br'));
|
|
1767
1616
|
}
|
|
1768
1617
|
listAncestor.replaceWith(frag);
|
|
1769
1618
|
return;
|
|
1770
1619
|
}
|
|
1771
|
-
// Neue Liste um die aktuelle Auswahl legen
|
|
1772
1620
|
const list = document.createElement(kind);
|
|
1773
1621
|
const li = document.createElement('li');
|
|
1774
1622
|
const contents = range.extractContents();
|
|
1775
1623
|
if (!contents.hasChildNodes()) {
|
|
1776
|
-
li.textContent = '\u200b';
|
|
1624
|
+
li.textContent = '\u200b';
|
|
1777
1625
|
}
|
|
1778
1626
|
else {
|
|
1779
1627
|
li.appendChild(contents);
|
|
1780
1628
|
}
|
|
1781
1629
|
list.appendChild(li);
|
|
1782
1630
|
range.insertNode(list);
|
|
1783
|
-
// Cursor ins li setzen
|
|
1784
1631
|
const sel = window.getSelection();
|
|
1785
1632
|
if (sel) {
|
|
1786
1633
|
sel.removeAllRanges();
|
|
@@ -1806,7 +1653,6 @@ class StackchRichtextEditor {
|
|
|
1806
1653
|
applyAlign(startBlock);
|
|
1807
1654
|
}
|
|
1808
1655
|
else {
|
|
1809
|
-
// grob: alle direkten Kinder zwischen start und end ausrichten
|
|
1810
1656
|
const children = Array.from(editor.children);
|
|
1811
1657
|
const i1 = children.indexOf(startBlock);
|
|
1812
1658
|
const i2 = children.indexOf(endBlock);
|
|
@@ -1860,8 +1706,8 @@ class StackchRichtextEditor {
|
|
|
1860
1706
|
}
|
|
1861
1707
|
}
|
|
1862
1708
|
}
|
|
1863
|
-
// Versucht, Margin/Padding auf die konkrete Text-Selektion anzuwenden
|
|
1864
|
-
// Liefert true, wenn inline angewendet werden konnte; sonst false
|
|
1709
|
+
// Versucht, Margin/Padding auf die konkrete Text-Selektion anzuwenden (inline Wrapper).
|
|
1710
|
+
// Liefert true, wenn inline angewendet werden konnte; sonst false.
|
|
1865
1711
|
applySpacingToSelection(kind, target, value) {
|
|
1866
1712
|
const sel = window.getSelection();
|
|
1867
1713
|
if (!sel || sel.rangeCount === 0)
|
|
@@ -1871,7 +1717,6 @@ class StackchRichtextEditor {
|
|
|
1871
1717
|
return false;
|
|
1872
1718
|
const startBlock = this.getEditorChildAncestor(range.startContainer);
|
|
1873
1719
|
const endBlock = this.getEditorChildAncestor(range.endContainer);
|
|
1874
|
-
// Nur inline, wenn innerhalb desselben Blocks
|
|
1875
1720
|
if (!startBlock || !endBlock || startBlock !== endBlock)
|
|
1876
1721
|
return false;
|
|
1877
1722
|
const wrapper = document.createElement('span');
|
|
@@ -1887,15 +1732,12 @@ class StackchRichtextEditor {
|
|
|
1887
1732
|
wrapper.style[`${kind}Left`] = px;
|
|
1888
1733
|
wrapper.style[`${kind}Right`] = px;
|
|
1889
1734
|
}
|
|
1890
|
-
// Für vertikale Margins auf Inline-Elementen sicherstellen, dass sie greifen
|
|
1891
1735
|
if (kind === 'margin' && (target === 'all' || target === 'vertical')) {
|
|
1892
1736
|
wrapper.style.display = 'inline-block';
|
|
1893
1737
|
}
|
|
1894
|
-
// Auswahl extrahieren und in Wrapper einsetzen
|
|
1895
1738
|
const frag = range.extractContents();
|
|
1896
1739
|
wrapper.appendChild(frag);
|
|
1897
1740
|
range.insertNode(wrapper);
|
|
1898
|
-
// Gleichartige Wrapper zusammenführen und Cursor korrekt setzen
|
|
1899
1741
|
const normalized = this.normalizeSpacingSpans(wrapper, kind, target, value) || wrapper;
|
|
1900
1742
|
sel.removeAllRanges();
|
|
1901
1743
|
const after = document.createRange();
|
|
@@ -1903,7 +1745,6 @@ class StackchRichtextEditor {
|
|
|
1903
1745
|
after.setStartAfter(normalized);
|
|
1904
1746
|
}
|
|
1905
1747
|
else {
|
|
1906
|
-
// Fallback: an das Ende des Startblocks
|
|
1907
1748
|
const sb = this.getEditorChildAncestor(range.startContainer);
|
|
1908
1749
|
if (sb)
|
|
1909
1750
|
after.selectNodeContents(sb);
|
|
@@ -1912,7 +1753,7 @@ class StackchRichtextEditor {
|
|
|
1912
1753
|
sel.addRange(after);
|
|
1913
1754
|
return true;
|
|
1914
1755
|
}
|
|
1915
|
-
normalizeSpacingSpans(span, kind, target,
|
|
1756
|
+
normalizeSpacingSpans(span, kind, target, _value) {
|
|
1916
1757
|
const props = [];
|
|
1917
1758
|
if (target === 'all')
|
|
1918
1759
|
props.push(kind);
|
|
@@ -1921,7 +1762,6 @@ class StackchRichtextEditor {
|
|
|
1921
1762
|
if (target === 'horizontal')
|
|
1922
1763
|
props.push(`${kind}Left`, `${kind}Right`);
|
|
1923
1764
|
const displayNeeded = kind === 'margin' && (target === 'all' || target === 'vertical');
|
|
1924
|
-
const v = `${value}px`;
|
|
1925
1765
|
const hasSameSpacing = (a, b) => {
|
|
1926
1766
|
for (const p of props) {
|
|
1927
1767
|
if (a.style[p] !== b.style[p])
|
|
@@ -1933,7 +1773,6 @@ class StackchRichtextEditor {
|
|
|
1933
1773
|
}
|
|
1934
1774
|
return true;
|
|
1935
1775
|
};
|
|
1936
|
-
// Downward: verschachtelte identische Spans in span zusammenführen
|
|
1937
1776
|
if (span.children.length === 1) {
|
|
1938
1777
|
const only = span.children[0];
|
|
1939
1778
|
if (only && only.tagName === 'SPAN' && hasSameSpacing(span, only)) {
|
|
@@ -1943,7 +1782,6 @@ class StackchRichtextEditor {
|
|
|
1943
1782
|
}
|
|
1944
1783
|
}
|
|
1945
1784
|
let current = span;
|
|
1946
|
-
// Upward: mit Elternelement verschmelzen, wenn identischer Span
|
|
1947
1785
|
const parent = current.parentElement;
|
|
1948
1786
|
if (parent && parent.tagName === 'SPAN' && hasSameSpacing(parent, current)) {
|
|
1949
1787
|
while (current.firstChild)
|
|
@@ -1953,7 +1791,6 @@ class StackchRichtextEditor {
|
|
|
1953
1791
|
}
|
|
1954
1792
|
if (!current)
|
|
1955
1793
|
return null;
|
|
1956
|
-
// Left merge: vorherige Geschwister-Spans mit gleicher Formatierung in current ziehen
|
|
1957
1794
|
let prev = current.previousElementSibling;
|
|
1958
1795
|
if (prev && prev.tagName === 'SPAN' && hasSameSpacing(prev, current)) {
|
|
1959
1796
|
while (current.firstChild)
|
|
@@ -1961,7 +1798,6 @@ class StackchRichtextEditor {
|
|
|
1961
1798
|
current.remove();
|
|
1962
1799
|
current = prev;
|
|
1963
1800
|
}
|
|
1964
|
-
// Right merge: folgende Geschwister-Spans in current ziehen
|
|
1965
1801
|
let next = current.nextElementSibling;
|
|
1966
1802
|
while (next && next.tagName === 'SPAN' && hasSameSpacing(current, next)) {
|
|
1967
1803
|
while (next.firstChild)
|
|
@@ -1977,6 +1813,7 @@ class StackchRichtextEditor {
|
|
|
1977
1813
|
if (!range)
|
|
1978
1814
|
return;
|
|
1979
1815
|
const editor = this.editorRef.nativeElement;
|
|
1816
|
+
const PRE_STYLE = 'white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;';
|
|
1980
1817
|
const replaceTag = (el, newTag) => {
|
|
1981
1818
|
if (!el || el === editor)
|
|
1982
1819
|
return null;
|
|
@@ -1988,24 +1825,23 @@ class StackchRichtextEditor {
|
|
|
1988
1825
|
};
|
|
1989
1826
|
const startBlock = this.getEditorChildAncestor(range.startContainer);
|
|
1990
1827
|
const endBlock = this.getEditorChildAncestor(range.endContainer);
|
|
1991
|
-
if (startBlock && startBlock !== editor &&
|
|
1828
|
+
if (startBlock && startBlock !== editor && startBlock === endBlock) {
|
|
1992
1829
|
const currentTag = (startBlock.tagName || '').toLowerCase();
|
|
1993
1830
|
if (currentTag === tag)
|
|
1994
1831
|
return;
|
|
1995
1832
|
const convertible = /^(p|div|pre|h1|h2|h3|h4|h5|h6)$/i.test(currentTag);
|
|
1996
1833
|
if (convertible) {
|
|
1997
1834
|
const neo = replaceTag(startBlock, tag);
|
|
1998
|
-
if (neo && tag === 'pre')
|
|
1999
|
-
neo.setAttribute('style',
|
|
2000
|
-
}
|
|
1835
|
+
if (neo && tag === 'pre')
|
|
1836
|
+
neo.setAttribute('style', PRE_STYLE);
|
|
2001
1837
|
if (neo) {
|
|
2002
1838
|
const sel = window.getSelection();
|
|
2003
1839
|
if (sel) {
|
|
2004
1840
|
sel.removeAllRanges();
|
|
2005
|
-
const
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
sel.addRange(
|
|
1841
|
+
const r = document.createRange();
|
|
1842
|
+
r.selectNodeContents(neo);
|
|
1843
|
+
r.collapse(false);
|
|
1844
|
+
sel.addRange(r);
|
|
2009
1845
|
}
|
|
2010
1846
|
}
|
|
2011
1847
|
return;
|
|
@@ -2020,12 +1856,10 @@ class StackchRichtextEditor {
|
|
|
2020
1856
|
let last = null;
|
|
2021
1857
|
for (let i = from; i <= to; i++) {
|
|
2022
1858
|
const el = children[i];
|
|
2023
|
-
|
|
2024
|
-
if (/^(p|div|pre|h1|h2|h3|h4|h5|h6)$/i.test(currentTag)) {
|
|
1859
|
+
if (/^(p|div|pre|h1|h2|h3|h4|h5|h6)$/i.test(el.tagName)) {
|
|
2025
1860
|
const neo = replaceTag(el, tag);
|
|
2026
|
-
if (neo && tag === 'pre')
|
|
2027
|
-
neo.setAttribute('style',
|
|
2028
|
-
}
|
|
1861
|
+
if (neo && tag === 'pre')
|
|
1862
|
+
neo.setAttribute('style', PRE_STYLE);
|
|
2029
1863
|
if (neo)
|
|
2030
1864
|
last = neo;
|
|
2031
1865
|
}
|
|
@@ -2034,10 +1868,10 @@ class StackchRichtextEditor {
|
|
|
2034
1868
|
const sel = window.getSelection();
|
|
2035
1869
|
if (sel) {
|
|
2036
1870
|
sel.removeAllRanges();
|
|
2037
|
-
const
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
sel.addRange(
|
|
1871
|
+
const r = document.createRange();
|
|
1872
|
+
r.selectNodeContents(last);
|
|
1873
|
+
r.collapse(false);
|
|
1874
|
+
sel.addRange(r);
|
|
2041
1875
|
}
|
|
2042
1876
|
}
|
|
2043
1877
|
return;
|
|
@@ -2045,40 +1879,26 @@ class StackchRichtextEditor {
|
|
|
2045
1879
|
}
|
|
2046
1880
|
if (!range.collapsed) {
|
|
2047
1881
|
const el = document.createElement(tag);
|
|
2048
|
-
if (tag === 'pre')
|
|
2049
|
-
el.setAttribute('style',
|
|
2050
|
-
}
|
|
1882
|
+
if (tag === 'pre')
|
|
1883
|
+
el.setAttribute('style', PRE_STYLE);
|
|
2051
1884
|
const frag = range.extractContents();
|
|
2052
1885
|
el.appendChild(frag);
|
|
2053
1886
|
range.insertNode(el);
|
|
2054
1887
|
const sel = window.getSelection();
|
|
2055
1888
|
if (sel) {
|
|
2056
1889
|
sel.removeAllRanges();
|
|
2057
|
-
const
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
sel.addRange(
|
|
1890
|
+
const r = document.createRange();
|
|
1891
|
+
r.selectNodeContents(el);
|
|
1892
|
+
r.collapse(false);
|
|
1893
|
+
sel.addRange(r);
|
|
2061
1894
|
}
|
|
2062
1895
|
}
|
|
2063
1896
|
}
|
|
2064
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type:
|
|
2065
|
-
static
|
|
2066
|
-
{
|
|
2067
|
-
provide: NG_VALUE_ACCESSOR,
|
|
2068
|
-
useExisting: forwardRef(() => StackchRichtextEditor),
|
|
2069
|
-
multi: true,
|
|
2070
|
-
},
|
|
2071
|
-
], viewQueries: [{ propertyName: "editorRef", first: true, predicate: ["editor"], descendants: true, static: true }], ngImport: i0, template: "<div class=\"stackch_rte\" [class.stackch_rte--disabled]=\"disabled\">\r\n @if (showToolbar) {\r\n <stackch-richtext-editor-toolbar\r\n [cfg]=\"cfg\"\r\n [i18n]=\"i18n\"\r\n [disabled]=\"disabled\"\r\n [fonts]=\"fonts\"\r\n [fontSizes]=\"fontSizes\"\r\n [isBoldActive]=\"isBoldActive\"\r\n [isItalicActive]=\"isItalicActive\"\r\n [isUnderlineActive]=\"isUnderlineActive\"\r\n [canUndo]=\"canUndo\"\r\n [canRedo]=\"canRedo\"\r\n [uiState]=\"{ showFontPanel: showFontPanel, showHeadingMenu: showHeadingMenu, showSpacingMenu: showSpacingMenu, showAlignMenu: showAlignMenu, showColorMenu: showColorMenu, showListMenu: showListMenu }\"\r\n\r\n (undo)=\"undo()\"\r\n (redo)=\"redo()\"\r\n (toggleFontPanel)=\"toggleFontPanel($event)\"\r\n (toggleHeadingMenu)=\"toggleHeadingMenu($event)\"\r\n (toggleSpacingMenu)=\"toggleSpacingMenu($event)\"\r\n (toggleAlignMenu)=\"toggleAlignMenu($event)\"\r\n (toggleColorMenu)=\"toggleColorMenu($event)\"\r\n (toggleListMenu)=\"toggleListMenu($event)\"\r\n (pickFont)=\"onPickFont($any($event))\"\r\n (pickFontSize)=\"onPickFontSize($any($event))\"\r\n (pickAlign)=\"onPickAlign($any($event))\"\r\n (pickList)=\"onPickList($any($event))\"\r\n (pickHeading)=\"onPickHeading($any($event))\"\r\n (pickSpacing)=\"onPickSpacing($any($event).kind, $any($event).target, $any($event).value)\"\r\n (applyColor)=\"applyColor($any($event))\"\r\n (applyHighlight)=\"applyHighlight($any($event))\"\r\n (toggleBold)=\"toggleBold()\"\r\n (toggleItalic)=\"toggleItalic()\"\r\n (toggleUnderline)=\"toggleUnderline()\"\r\n (insertLink)=\"insertLink()\"\r\n (removeFormat)=\"removeFormat()\"\r\n (saveSelectionRequest)=\"saveSelection()\"\r\n />\r\n }\r\n\r\n <div #editor\r\n class=\"stackch_rte__editor\"\r\n [attr.contenteditable]=\"!disabled\"\r\n [attr.data-placeholder]=\"placeholder || i18n.placeholder\"\r\n [style.minHeight.px]=\"minHeight\"\r\n [style.maxHeight.px]=\"maxHeight\"\r\n [style.height.px]=\"height\"\r\n (mouseup)=\"saveSelection()\"\r\n (keyup)=\"onKeyup($event)\"\r\n (input)=\"onInput()\"\r\n (blur)=\"onTouched()\"\r\n (paste)=\"onPaste($event)\"></div>\r\n</div>\r\n", styles: [".stackch_rte{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Arial,sans-serif}.stackch_rte--disabled{opacity:.7;pointer-events:none}.stackch_rte__toolbar{display:flex;flex-wrap:wrap;gap:.25rem;align-items:center;border:1px solid #d0d7de;border-bottom:none;background:#f6f8fa;padding:.25rem;border-radius:6px 6px 0 0}.stackch_rte__btn{appearance:none;border:1px solid #d0d7de;background:#fff;padding:.25rem .5rem;border-radius:4px;cursor:pointer;transition:background .12s ease,border-color .12s ease,color .12s ease;outline:none}.stackch_rte__btn:hover{background:#f3f4f6}.stackch_rte__btn:focus{outline:none;box-shadow:none}.stackch_rte__btn:focus-visible{outline:2px solid rgba(9,105,218,.2)}.stackch_rte__btn.is-active{background:#e7f3ff;border-color:#0969da;color:#084298}.stackch_rte__select{border:1px solid #d0d7de;border-radius:4px;padding:.2rem .4rem;background:#fff}.stackch_rte__color{display:inline-flex;align-items:center;gap:.25rem;border:1px solid #d0d7de;padding:0 .4rem;border-radius:4px;background:#fff}.stackch_rte__color>input{inline-size:1.75rem;block-size:1.5rem;padding:0;border:none;background:none}.stackch_rte__sep{width:1px;height:1.25rem;background:#d0d7de;margin:0 .125rem}.stackch_rte__editor{border:1px solid #d0d7de;border-radius:0 0 6px 6px;padding:.5rem;background:#fff;overflow:auto}.stackch_rte__editor:empty:before{content:attr(data-placeholder);color:#97a1ad;pointer-events:none}.stackch_rte__editor:focus{outline:none;box-shadow:inset 0 0 0 1px #0969da;border-color:#0969da}.stackch_rte__dropdown{position:relative;display:inline-block}.stackch_rte__menu{position:absolute;top:100%;left:0;z-index:2;background:#fff;border:1px solid #d0d7de;border-radius:6px;box-shadow:0 4px 12px #00000014;padding:.25rem;min-width:220px;max-height:220px;overflow:auto}.stackch_rte__menu-item{display:block;width:100%;text-align:left;background:#fff;border:none;padding:.375rem .5rem;border-radius:4px;cursor:pointer}.stackch_rte__menu-item:hover{background:#f3f4f6}.stackch_rte__menu--row{display:flex;flex-direction:row;gap:.25rem;align-items:center;min-width:auto;max-height:none;flex-wrap:nowrap}.stackch_rte__menu--row .stackch_rte__menu-item{display:inline-flex;width:auto;text-align:center;justify-content:center;align-items:center;padding:.3rem .45rem;white-space:nowrap;flex:0 0 auto;word-break:keep-all;overflow-wrap:normal}.stackch_rte__menu--row .stackch_rte__menu-item[style*=\"width:24px\"]{padding:0;width:24px;height:24px}.stackch_rte__menu--grid{display:grid;grid-template-columns:1fr 1fr;gap:.5rem 1rem;min-width:420px}.stackch_rte__menu-section{display:flex;flex-direction:column;gap:.25rem}.stackch_rte__menu-title{font-size:12px;color:#57606a;padding:.25rem;text-transform:uppercase;letter-spacing:.04em}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: StackchRichtextEditorToolbar, selector: "stackch-richtext-editor-toolbar", inputs: ["cfg", "i18n", "disabled", "fonts", "fontSizes", "isBoldActive", "isItalicActive", "isUnderlineActive", "canUndo", "canRedo", "uiState"], outputs: ["undo", "redo", "toggleFontPanel", "toggleHeadingMenu", "toggleSpacingMenu", "toggleAlignMenu", "toggleColorMenu", "toggleListMenu", "pickFont", "pickFontSize", "pickAlign", "pickList", "pickHeading", "pickSpacing", "applyColor", "applyHighlight", "toggleBold", "toggleItalic", "toggleUnderline", "insertLink", "removeFormat", "saveSelectionRequest"] }] });
|
|
1897
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: StackchRichtextEditorBase, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Directive });
|
|
1898
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.4", type: StackchRichtextEditorBase, isStandalone: true, inputs: { placeholder: "placeholder", showToolbar: "showToolbar", fonts: "fonts", fontSizes: "fontSizes", height: "height", minHeight: "minHeight", maxHeight: "maxHeight", disabled: "disabled", config: "config" }, outputs: { valueChange: "valueChange", metricsChange: "metricsChange" }, host: { listeners: { "document:selectionchange": "onSelectionChange()", "document:click": "closeMenus()", "keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "editorRef", first: true, predicate: ["editor"], descendants: true, static: true }], ngImport: i0 });
|
|
2072
1899
|
}
|
|
2073
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type:
|
|
2074
|
-
type:
|
|
2075
|
-
args: [{ selector: 'stackch-richtext-editor', standalone: true, imports: [CommonModule, StackchRichtextEditorToolbar], providers: [
|
|
2076
|
-
{
|
|
2077
|
-
provide: NG_VALUE_ACCESSOR,
|
|
2078
|
-
useExisting: forwardRef(() => StackchRichtextEditor),
|
|
2079
|
-
multi: true,
|
|
2080
|
-
},
|
|
2081
|
-
], template: "<div class=\"stackch_rte\" [class.stackch_rte--disabled]=\"disabled\">\r\n @if (showToolbar) {\r\n <stackch-richtext-editor-toolbar\r\n [cfg]=\"cfg\"\r\n [i18n]=\"i18n\"\r\n [disabled]=\"disabled\"\r\n [fonts]=\"fonts\"\r\n [fontSizes]=\"fontSizes\"\r\n [isBoldActive]=\"isBoldActive\"\r\n [isItalicActive]=\"isItalicActive\"\r\n [isUnderlineActive]=\"isUnderlineActive\"\r\n [canUndo]=\"canUndo\"\r\n [canRedo]=\"canRedo\"\r\n [uiState]=\"{ showFontPanel: showFontPanel, showHeadingMenu: showHeadingMenu, showSpacingMenu: showSpacingMenu, showAlignMenu: showAlignMenu, showColorMenu: showColorMenu, showListMenu: showListMenu }\"\r\n\r\n (undo)=\"undo()\"\r\n (redo)=\"redo()\"\r\n (toggleFontPanel)=\"toggleFontPanel($event)\"\r\n (toggleHeadingMenu)=\"toggleHeadingMenu($event)\"\r\n (toggleSpacingMenu)=\"toggleSpacingMenu($event)\"\r\n (toggleAlignMenu)=\"toggleAlignMenu($event)\"\r\n (toggleColorMenu)=\"toggleColorMenu($event)\"\r\n (toggleListMenu)=\"toggleListMenu($event)\"\r\n (pickFont)=\"onPickFont($any($event))\"\r\n (pickFontSize)=\"onPickFontSize($any($event))\"\r\n (pickAlign)=\"onPickAlign($any($event))\"\r\n (pickList)=\"onPickList($any($event))\"\r\n (pickHeading)=\"onPickHeading($any($event))\"\r\n (pickSpacing)=\"onPickSpacing($any($event).kind, $any($event).target, $any($event).value)\"\r\n (applyColor)=\"applyColor($any($event))\"\r\n (applyHighlight)=\"applyHighlight($any($event))\"\r\n (toggleBold)=\"toggleBold()\"\r\n (toggleItalic)=\"toggleItalic()\"\r\n (toggleUnderline)=\"toggleUnderline()\"\r\n (insertLink)=\"insertLink()\"\r\n (removeFormat)=\"removeFormat()\"\r\n (saveSelectionRequest)=\"saveSelection()\"\r\n />\r\n }\r\n\r\n <div #editor\r\n class=\"stackch_rte__editor\"\r\n [attr.contenteditable]=\"!disabled\"\r\n [attr.data-placeholder]=\"placeholder || i18n.placeholder\"\r\n [style.minHeight.px]=\"minHeight\"\r\n [style.maxHeight.px]=\"maxHeight\"\r\n [style.height.px]=\"height\"\r\n (mouseup)=\"saveSelection()\"\r\n (keyup)=\"onKeyup($event)\"\r\n (input)=\"onInput()\"\r\n (blur)=\"onTouched()\"\r\n (paste)=\"onPaste($event)\"></div>\r\n</div>\r\n", styles: [".stackch_rte{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Arial,sans-serif}.stackch_rte--disabled{opacity:.7;pointer-events:none}.stackch_rte__toolbar{display:flex;flex-wrap:wrap;gap:.25rem;align-items:center;border:1px solid #d0d7de;border-bottom:none;background:#f6f8fa;padding:.25rem;border-radius:6px 6px 0 0}.stackch_rte__btn{appearance:none;border:1px solid #d0d7de;background:#fff;padding:.25rem .5rem;border-radius:4px;cursor:pointer;transition:background .12s ease,border-color .12s ease,color .12s ease;outline:none}.stackch_rte__btn:hover{background:#f3f4f6}.stackch_rte__btn:focus{outline:none;box-shadow:none}.stackch_rte__btn:focus-visible{outline:2px solid rgba(9,105,218,.2)}.stackch_rte__btn.is-active{background:#e7f3ff;border-color:#0969da;color:#084298}.stackch_rte__select{border:1px solid #d0d7de;border-radius:4px;padding:.2rem .4rem;background:#fff}.stackch_rte__color{display:inline-flex;align-items:center;gap:.25rem;border:1px solid #d0d7de;padding:0 .4rem;border-radius:4px;background:#fff}.stackch_rte__color>input{inline-size:1.75rem;block-size:1.5rem;padding:0;border:none;background:none}.stackch_rte__sep{width:1px;height:1.25rem;background:#d0d7de;margin:0 .125rem}.stackch_rte__editor{border:1px solid #d0d7de;border-radius:0 0 6px 6px;padding:.5rem;background:#fff;overflow:auto}.stackch_rte__editor:empty:before{content:attr(data-placeholder);color:#97a1ad;pointer-events:none}.stackch_rte__editor:focus{outline:none;box-shadow:inset 0 0 0 1px #0969da;border-color:#0969da}.stackch_rte__dropdown{position:relative;display:inline-block}.stackch_rte__menu{position:absolute;top:100%;left:0;z-index:2;background:#fff;border:1px solid #d0d7de;border-radius:6px;box-shadow:0 4px 12px #00000014;padding:.25rem;min-width:220px;max-height:220px;overflow:auto}.stackch_rte__menu-item{display:block;width:100%;text-align:left;background:#fff;border:none;padding:.375rem .5rem;border-radius:4px;cursor:pointer}.stackch_rte__menu-item:hover{background:#f3f4f6}.stackch_rte__menu--row{display:flex;flex-direction:row;gap:.25rem;align-items:center;min-width:auto;max-height:none;flex-wrap:nowrap}.stackch_rte__menu--row .stackch_rte__menu-item{display:inline-flex;width:auto;text-align:center;justify-content:center;align-items:center;padding:.3rem .45rem;white-space:nowrap;flex:0 0 auto;word-break:keep-all;overflow-wrap:normal}.stackch_rte__menu--row .stackch_rte__menu-item[style*=\"width:24px\"]{padding:0;width:24px;height:24px}.stackch_rte__menu--grid{display:grid;grid-template-columns:1fr 1fr;gap:.5rem 1rem;min-width:420px}.stackch_rte__menu-section{display:flex;flex-direction:column;gap:.25rem}.stackch_rte__menu-title{font-size:12px;color:#57606a;padding:.25rem;text-transform:uppercase;letter-spacing:.04em}\n"] }]
|
|
1900
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: StackchRichtextEditorBase, decorators: [{
|
|
1901
|
+
type: Directive
|
|
2082
1902
|
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { placeholder: [{
|
|
2083
1903
|
type: Input
|
|
2084
1904
|
}], showToolbar: [{
|
|
@@ -2097,6 +1917,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImpor
|
|
|
2097
1917
|
type: Input
|
|
2098
1918
|
}], valueChange: [{
|
|
2099
1919
|
type: Output
|
|
1920
|
+
}], metricsChange: [{
|
|
1921
|
+
type: Output
|
|
2100
1922
|
}], editorRef: [{
|
|
2101
1923
|
type: ViewChild,
|
|
2102
1924
|
args: ['editor', { static: true }]
|
|
@@ -2113,6 +1935,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImpor
|
|
|
2113
1935
|
args: ['keydown', ['$event']]
|
|
2114
1936
|
}] } });
|
|
2115
1937
|
|
|
1938
|
+
class StackchRichtextEditor extends StackchRichtextEditorBase {
|
|
1939
|
+
logPrefix = '[RTE]';
|
|
1940
|
+
constructor(cdr) {
|
|
1941
|
+
super(cdr);
|
|
1942
|
+
}
|
|
1943
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: StackchRichtextEditor, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
1944
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.4", type: StackchRichtextEditor, isStandalone: true, selector: "stackch-richtext-editor", providers: [
|
|
1945
|
+
{
|
|
1946
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1947
|
+
useExisting: forwardRef(() => StackchRichtextEditor),
|
|
1948
|
+
multi: true,
|
|
1949
|
+
},
|
|
1950
|
+
], usesInheritance: true, ngImport: i0, template: "<div class=\"stackch_rte\" [class.stackch_rte--disabled]=\"disabled\">\r\n @if (showToolbar) {\r\n <stackch-richtext-editor-toolbar\r\n [cfg]=\"cfg\"\r\n [i18n]=\"i18n\"\r\n [disabled]=\"disabled\"\r\n [fonts]=\"fonts\"\r\n [fontSizes]=\"fontSizes\"\r\n [isBoldActive]=\"isBoldActive\"\r\n [isItalicActive]=\"isItalicActive\"\r\n [isUnderlineActive]=\"isUnderlineActive\"\r\n [canUndo]=\"canUndo\"\r\n [canRedo]=\"canRedo\"\r\n [uiState]=\"{ showFontPanel: showFontPanel, showHeadingMenu: showHeadingMenu, showSpacingMenu: showSpacingMenu, showAlignMenu: showAlignMenu, showColorMenu: showColorMenu, showListMenu: showListMenu }\"\r\n\r\n (undo)=\"undo()\"\r\n (redo)=\"redo()\"\r\n (toggleFontPanel)=\"toggleFontPanel($event)\"\r\n (toggleHeadingMenu)=\"toggleHeadingMenu($event)\"\r\n (toggleSpacingMenu)=\"toggleSpacingMenu($event)\"\r\n (toggleAlignMenu)=\"toggleAlignMenu($event)\"\r\n (toggleColorMenu)=\"toggleColorMenu($event)\"\r\n (toggleListMenu)=\"toggleListMenu($event)\"\r\n (pickFont)=\"onPickFont($any($event))\"\r\n (pickFontSize)=\"onPickFontSize($any($event))\"\r\n (pickAlign)=\"onPickAlign($any($event))\"\r\n (pickList)=\"onPickList($any($event))\"\r\n (pickHeading)=\"onPickHeading($any($event))\"\r\n (pickSpacing)=\"onPickSpacing($any($event).kind, $any($event).target, $any($event).value)\"\r\n (applyColor)=\"applyColor($any($event))\"\r\n (applyHighlight)=\"applyHighlight($any($event))\"\r\n (toggleBold)=\"toggleBold()\"\r\n (toggleItalic)=\"toggleItalic()\"\r\n (toggleUnderline)=\"toggleUnderline()\"\r\n (insertLink)=\"insertLink()\"\r\n (removeFormat)=\"removeFormat()\"\r\n (saveSelectionRequest)=\"saveSelection()\"\r\n />\r\n }\r\n\r\n <div #editor\r\n class=\"stackch_rte__editor\"\r\n [attr.contenteditable]=\"!disabled\"\r\n [attr.data-placeholder]=\"placeholder || i18n.placeholder\"\r\n [style.minHeight.px]=\"minHeight\"\r\n [style.maxHeight.px]=\"maxHeight\"\r\n [style.height.px]=\"height\"\r\n (mouseup)=\"saveSelection()\"\r\n (keyup)=\"onKeyup($event)\"\r\n (input)=\"onInput()\"\r\n (blur)=\"onTouched()\"\r\n (paste)=\"onPaste($event)\"></div>\r\n</div>\r\n", styles: [".stackch_rte{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Arial,sans-serif}.stackch_rte--disabled{opacity:.7;pointer-events:none}.stackch_rte__toolbar{display:flex;flex-wrap:wrap;gap:.25rem;align-items:center;border:1px solid #d0d7de;border-bottom:none;background:#f6f8fa;padding:.25rem;border-radius:6px 6px 0 0}.stackch_rte__btn{appearance:none;border:1px solid #d0d7de;background:#fff;padding:.25rem .5rem;border-radius:4px;cursor:pointer;transition:background .12s ease,border-color .12s ease,color .12s ease;outline:none}.stackch_rte__btn:hover{background:#f3f4f6}.stackch_rte__btn:focus{outline:none;box-shadow:none}.stackch_rte__btn:focus-visible{outline:2px solid rgba(9,105,218,.2)}.stackch_rte__btn.is-active{background:#e7f3ff;border-color:#0969da;color:#084298}.stackch_rte__select{border:1px solid #d0d7de;border-radius:4px;padding:.2rem .4rem;background:#fff}.stackch_rte__color{display:inline-flex;align-items:center;gap:.25rem;border:1px solid #d0d7de;padding:0 .4rem;border-radius:4px;background:#fff}.stackch_rte__color>input{inline-size:1.75rem;block-size:1.5rem;padding:0;border:none;background:none}.stackch_rte__sep{width:1px;height:1.25rem;background:#d0d7de;margin:0 .125rem}.stackch_rte__editor{border:1px solid #d0d7de;border-radius:0 0 6px 6px;padding:.5rem;background:#fff;overflow:auto}.stackch_rte__editor:empty:before{content:attr(data-placeholder);color:#97a1ad;pointer-events:none}.stackch_rte__editor:focus{outline:none;box-shadow:inset 0 0 0 1px #0969da;border-color:#0969da}.stackch_rte__dropdown{position:relative;display:inline-block}.stackch_rte__menu{position:absolute;top:100%;left:0;z-index:2;background:#fff;border:1px solid #d0d7de;border-radius:6px;box-shadow:0 4px 12px #00000014;padding:.25rem;min-width:220px;max-height:220px;overflow:auto}.stackch_rte__menu-item{display:block;width:100%;text-align:left;background:#fff;border:none;padding:.375rem .5rem;border-radius:4px;cursor:pointer}.stackch_rte__menu-item:hover{background:#f3f4f6}.stackch_rte__menu--row{display:flex;flex-direction:row;gap:.25rem;align-items:center;min-width:auto;max-height:none;flex-wrap:nowrap}.stackch_rte__menu--row .stackch_rte__menu-item{display:inline-flex;width:auto;text-align:center;justify-content:center;align-items:center;padding:.3rem .45rem;white-space:nowrap;flex:0 0 auto;word-break:keep-all;overflow-wrap:normal}.stackch_rte__menu--row .stackch_rte__menu-item[style*=\"width:24px\"]{padding:0;width:24px;height:24px}.stackch_rte__menu--grid{display:grid;grid-template-columns:1fr 1fr;gap:.5rem 1rem;min-width:420px}.stackch_rte__menu-section{display:flex;flex-direction:column;gap:.25rem}.stackch_rte__menu-title{font-size:12px;color:#57606a;padding:.25rem;text-transform:uppercase;letter-spacing:.04em}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: StackchRichtextEditorToolbar, selector: "stackch-richtext-editor-toolbar", inputs: ["cfg", "i18n", "disabled", "fonts", "fontSizes", "isBoldActive", "isItalicActive", "isUnderlineActive", "canUndo", "canRedo", "uiState"], outputs: ["undo", "redo", "toggleFontPanel", "toggleHeadingMenu", "toggleSpacingMenu", "toggleAlignMenu", "toggleColorMenu", "toggleListMenu", "pickFont", "pickFontSize", "pickAlign", "pickList", "pickHeading", "pickSpacing", "applyColor", "applyHighlight", "toggleBold", "toggleItalic", "toggleUnderline", "insertLink", "removeFormat", "saveSelectionRequest"] }] });
|
|
1951
|
+
}
|
|
1952
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: StackchRichtextEditor, decorators: [{
|
|
1953
|
+
type: Component,
|
|
1954
|
+
args: [{ selector: 'stackch-richtext-editor', standalone: true, imports: [CommonModule, StackchRichtextEditorToolbar], providers: [
|
|
1955
|
+
{
|
|
1956
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1957
|
+
useExisting: forwardRef(() => StackchRichtextEditor),
|
|
1958
|
+
multi: true,
|
|
1959
|
+
},
|
|
1960
|
+
], template: "<div class=\"stackch_rte\" [class.stackch_rte--disabled]=\"disabled\">\r\n @if (showToolbar) {\r\n <stackch-richtext-editor-toolbar\r\n [cfg]=\"cfg\"\r\n [i18n]=\"i18n\"\r\n [disabled]=\"disabled\"\r\n [fonts]=\"fonts\"\r\n [fontSizes]=\"fontSizes\"\r\n [isBoldActive]=\"isBoldActive\"\r\n [isItalicActive]=\"isItalicActive\"\r\n [isUnderlineActive]=\"isUnderlineActive\"\r\n [canUndo]=\"canUndo\"\r\n [canRedo]=\"canRedo\"\r\n [uiState]=\"{ showFontPanel: showFontPanel, showHeadingMenu: showHeadingMenu, showSpacingMenu: showSpacingMenu, showAlignMenu: showAlignMenu, showColorMenu: showColorMenu, showListMenu: showListMenu }\"\r\n\r\n (undo)=\"undo()\"\r\n (redo)=\"redo()\"\r\n (toggleFontPanel)=\"toggleFontPanel($event)\"\r\n (toggleHeadingMenu)=\"toggleHeadingMenu($event)\"\r\n (toggleSpacingMenu)=\"toggleSpacingMenu($event)\"\r\n (toggleAlignMenu)=\"toggleAlignMenu($event)\"\r\n (toggleColorMenu)=\"toggleColorMenu($event)\"\r\n (toggleListMenu)=\"toggleListMenu($event)\"\r\n (pickFont)=\"onPickFont($any($event))\"\r\n (pickFontSize)=\"onPickFontSize($any($event))\"\r\n (pickAlign)=\"onPickAlign($any($event))\"\r\n (pickList)=\"onPickList($any($event))\"\r\n (pickHeading)=\"onPickHeading($any($event))\"\r\n (pickSpacing)=\"onPickSpacing($any($event).kind, $any($event).target, $any($event).value)\"\r\n (applyColor)=\"applyColor($any($event))\"\r\n (applyHighlight)=\"applyHighlight($any($event))\"\r\n (toggleBold)=\"toggleBold()\"\r\n (toggleItalic)=\"toggleItalic()\"\r\n (toggleUnderline)=\"toggleUnderline()\"\r\n (insertLink)=\"insertLink()\"\r\n (removeFormat)=\"removeFormat()\"\r\n (saveSelectionRequest)=\"saveSelection()\"\r\n />\r\n }\r\n\r\n <div #editor\r\n class=\"stackch_rte__editor\"\r\n [attr.contenteditable]=\"!disabled\"\r\n [attr.data-placeholder]=\"placeholder || i18n.placeholder\"\r\n [style.minHeight.px]=\"minHeight\"\r\n [style.maxHeight.px]=\"maxHeight\"\r\n [style.height.px]=\"height\"\r\n (mouseup)=\"saveSelection()\"\r\n (keyup)=\"onKeyup($event)\"\r\n (input)=\"onInput()\"\r\n (blur)=\"onTouched()\"\r\n (paste)=\"onPaste($event)\"></div>\r\n</div>\r\n", styles: [".stackch_rte{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Arial,sans-serif}.stackch_rte--disabled{opacity:.7;pointer-events:none}.stackch_rte__toolbar{display:flex;flex-wrap:wrap;gap:.25rem;align-items:center;border:1px solid #d0d7de;border-bottom:none;background:#f6f8fa;padding:.25rem;border-radius:6px 6px 0 0}.stackch_rte__btn{appearance:none;border:1px solid #d0d7de;background:#fff;padding:.25rem .5rem;border-radius:4px;cursor:pointer;transition:background .12s ease,border-color .12s ease,color .12s ease;outline:none}.stackch_rte__btn:hover{background:#f3f4f6}.stackch_rte__btn:focus{outline:none;box-shadow:none}.stackch_rte__btn:focus-visible{outline:2px solid rgba(9,105,218,.2)}.stackch_rte__btn.is-active{background:#e7f3ff;border-color:#0969da;color:#084298}.stackch_rte__select{border:1px solid #d0d7de;border-radius:4px;padding:.2rem .4rem;background:#fff}.stackch_rte__color{display:inline-flex;align-items:center;gap:.25rem;border:1px solid #d0d7de;padding:0 .4rem;border-radius:4px;background:#fff}.stackch_rte__color>input{inline-size:1.75rem;block-size:1.5rem;padding:0;border:none;background:none}.stackch_rte__sep{width:1px;height:1.25rem;background:#d0d7de;margin:0 .125rem}.stackch_rte__editor{border:1px solid #d0d7de;border-radius:0 0 6px 6px;padding:.5rem;background:#fff;overflow:auto}.stackch_rte__editor:empty:before{content:attr(data-placeholder);color:#97a1ad;pointer-events:none}.stackch_rte__editor:focus{outline:none;box-shadow:inset 0 0 0 1px #0969da;border-color:#0969da}.stackch_rte__dropdown{position:relative;display:inline-block}.stackch_rte__menu{position:absolute;top:100%;left:0;z-index:2;background:#fff;border:1px solid #d0d7de;border-radius:6px;box-shadow:0 4px 12px #00000014;padding:.25rem;min-width:220px;max-height:220px;overflow:auto}.stackch_rte__menu-item{display:block;width:100%;text-align:left;background:#fff;border:none;padding:.375rem .5rem;border-radius:4px;cursor:pointer}.stackch_rte__menu-item:hover{background:#f3f4f6}.stackch_rte__menu--row{display:flex;flex-direction:row;gap:.25rem;align-items:center;min-width:auto;max-height:none;flex-wrap:nowrap}.stackch_rte__menu--row .stackch_rte__menu-item{display:inline-flex;width:auto;text-align:center;justify-content:center;align-items:center;padding:.3rem .45rem;white-space:nowrap;flex:0 0 auto;word-break:keep-all;overflow-wrap:normal}.stackch_rte__menu--row .stackch_rte__menu-item[style*=\"width:24px\"]{padding:0;width:24px;height:24px}.stackch_rte__menu--grid{display:grid;grid-template-columns:1fr 1fr;gap:.5rem 1rem;min-width:420px}.stackch_rte__menu-section{display:flex;flex-direction:column;gap:.25rem}.stackch_rte__menu-title{font-size:12px;color:#57606a;padding:.25rem;text-transform:uppercase;letter-spacing:.04em}\n"] }]
|
|
1961
|
+
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }] });
|
|
1962
|
+
|
|
2116
1963
|
/*
|
|
2117
1964
|
* Public API Surface of richtext-editor
|
|
2118
1965
|
*/
|
|
@@ -2121,5 +1968,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImpor
|
|
|
2121
1968
|
* Generated bundle index. Do not edit.
|
|
2122
1969
|
*/
|
|
2123
1970
|
|
|
2124
|
-
export { STACKCH_RTE_I18N_DE, STACKCH_RTE_I18N_FR, STACKCH_RTE_I18N_IT, StackchRichtextEditor, StackchRichtextEditorConfig, StackchRichtextEditorI18n, StackchRichtextEditorToolbar };
|
|
1971
|
+
export { STACKCH_RTE_I18N_DE, STACKCH_RTE_I18N_FR, STACKCH_RTE_I18N_IT, StackchRichtextEditor, StackchRichtextEditorBase, StackchRichtextEditorConfig, StackchRichtextEditorI18n, StackchRichtextEditorToolbar };
|
|
2125
1972
|
//# sourceMappingURL=stackch-angular-richtext-editor.mjs.map
|