@stackch/angular-material-richtext-editor 1.0.0 → 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.
|
@@ -344,6 +344,8 @@ class StackchRichtextEditorMaterial {
|
|
|
344
344
|
get disabled() { return this._disabled; }
|
|
345
345
|
_disabled = false;
|
|
346
346
|
valueChange = new EventEmitter();
|
|
347
|
+
// Structured metrics event (future-proof)
|
|
348
|
+
metricsChange = new EventEmitter();
|
|
347
349
|
editorRef;
|
|
348
350
|
constructor(cdr) {
|
|
349
351
|
this.cdr = cdr;
|
|
@@ -607,6 +609,8 @@ class StackchRichtextEditorMaterial {
|
|
|
607
609
|
if (this.history.length === 0) {
|
|
608
610
|
this.takeSnapshot('init');
|
|
609
611
|
}
|
|
612
|
+
// Emit initial metrics when value is written programmatically
|
|
613
|
+
this.emitMetrics();
|
|
610
614
|
}
|
|
611
615
|
registerOnChange(fn) { this.onChange = fn; }
|
|
612
616
|
registerOnTouched(fn) { this.onTouched = fn; }
|
|
@@ -720,7 +724,8 @@ class StackchRichtextEditorMaterial {
|
|
|
720
724
|
// Firefox/Edge: wenn keine explizite Auswahl, erweitere auf nächstes Wort
|
|
721
725
|
this.expandRangeToWord(r);
|
|
722
726
|
}
|
|
723
|
-
this.
|
|
727
|
+
this.applySelectionStyles({ color });
|
|
728
|
+
this.saveSelection();
|
|
724
729
|
}
|
|
725
730
|
applyHighlight(color) {
|
|
726
731
|
this.focusEditor();
|
|
@@ -732,8 +737,8 @@ class StackchRichtextEditorMaterial {
|
|
|
732
737
|
if (r.collapsed) {
|
|
733
738
|
this.expandRangeToWord(r);
|
|
734
739
|
}
|
|
735
|
-
|
|
736
|
-
this.
|
|
740
|
+
this.applySelectionStyles({ backgroundColor: color });
|
|
741
|
+
this.saveSelection();
|
|
737
742
|
}
|
|
738
743
|
expandRangeToWord(range) {
|
|
739
744
|
try {
|
|
@@ -814,12 +819,23 @@ class StackchRichtextEditorMaterial {
|
|
|
814
819
|
const val = this.editorRef.nativeElement.innerHTML;
|
|
815
820
|
this.onChange(val);
|
|
816
821
|
this.valueChange.emit(val);
|
|
822
|
+
this.emitMetrics();
|
|
817
823
|
this.updateInlineStates();
|
|
818
824
|
}
|
|
825
|
+
emitMetrics() {
|
|
826
|
+
const editor = this.editorRef?.nativeElement;
|
|
827
|
+
if (!editor)
|
|
828
|
+
return;
|
|
829
|
+
const htmlLen = editor.innerHTML.length;
|
|
830
|
+
const textLen = (editor.textContent || '').length;
|
|
831
|
+
this.metricsChange.emit({ htmlLength: htmlLen, textLength: textLen });
|
|
832
|
+
}
|
|
819
833
|
// Remove empty style attributes (style="") and unwrap spans without any attributes
|
|
820
834
|
cleanupEmptyStylesAndSpans(root) {
|
|
835
|
+
console.log('[RTE-material] cleanupEmptyStylesAndSpans: start cleanup');
|
|
821
836
|
const editor = root || this.editorRef.nativeElement;
|
|
822
837
|
const toUnwrap = [];
|
|
838
|
+
const toRemove = [];
|
|
823
839
|
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_ELEMENT);
|
|
824
840
|
while (walker.nextNode()) {
|
|
825
841
|
const el = walker.currentNode;
|
|
@@ -832,16 +848,28 @@ class StackchRichtextEditorMaterial {
|
|
|
832
848
|
el.removeAttribute('style');
|
|
833
849
|
}
|
|
834
850
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
851
|
+
if (el.tagName === 'SPAN') {
|
|
852
|
+
if (!el.firstChild) {
|
|
853
|
+
console.log('[RTE-material] cleanup: removing empty span', el);
|
|
854
|
+
toRemove.push(el);
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
if (el.attributes.length === 0) {
|
|
858
|
+
console.log('[RTE-material] cleanup: unwrapping style-free span', el);
|
|
859
|
+
toUnwrap.push(el);
|
|
860
|
+
}
|
|
838
861
|
}
|
|
839
862
|
}
|
|
840
863
|
for (const span of toUnwrap) {
|
|
864
|
+
console.log('[RTE-material] cleanup: unwrap span', { span, childCount: span.childNodes.length });
|
|
841
865
|
while (span.firstChild)
|
|
842
866
|
span.parentNode?.insertBefore(span.firstChild, span);
|
|
843
867
|
span.remove();
|
|
844
868
|
}
|
|
869
|
+
for (const span of toRemove) {
|
|
870
|
+
console.log('[RTE-material] cleanup: remove span without children', span);
|
|
871
|
+
span.remove();
|
|
872
|
+
}
|
|
845
873
|
}
|
|
846
874
|
// History API
|
|
847
875
|
get canUndo() { return this.historyIndex > 0; }
|
|
@@ -998,117 +1026,176 @@ class StackchRichtextEditorMaterial {
|
|
|
998
1026
|
}
|
|
999
1027
|
// Minimal inline style applier for inline styles
|
|
1000
1028
|
applyInlineStyle(cssProp, value) {
|
|
1001
|
-
|
|
1002
|
-
this.
|
|
1003
|
-
const sel = window.getSelection();
|
|
1004
|
-
if (!sel || sel.rangeCount === 0)
|
|
1005
|
-
return;
|
|
1006
|
-
const range = sel.getRangeAt(0);
|
|
1007
|
-
if (range.collapsed)
|
|
1008
|
-
return;
|
|
1009
|
-
const span = document.createElement('span');
|
|
1010
|
-
span.style[cssProp] = value;
|
|
1011
|
-
// Robuster als surroundContents: extract + wrap + insert
|
|
1012
|
-
const frag = range.extractContents();
|
|
1013
|
-
span.appendChild(frag);
|
|
1014
|
-
range.insertNode(span);
|
|
1015
|
-
// Cursor hinter das eingefügte Element setzen
|
|
1016
|
-
sel.removeAllRanges();
|
|
1017
|
-
const after = document.createRange();
|
|
1018
|
-
after.setStartAfter(span);
|
|
1019
|
-
after.collapse(true);
|
|
1020
|
-
sel.addRange(after);
|
|
1021
|
-
this.emitValue();
|
|
1022
|
-
this.takeSnapshot('style');
|
|
1029
|
+
// Delegate to a generic, style-agnostic applier
|
|
1030
|
+
this.applySelectionStyles({ [cssProp]: value });
|
|
1023
1031
|
}
|
|
1024
1032
|
applyInlineStyleSmart(cssProp, value) {
|
|
1033
|
+
// Use generic, which handles single vs. multi-block selection
|
|
1034
|
+
this.applySelectionStyles({ [cssProp]: value });
|
|
1035
|
+
}
|
|
1036
|
+
applySelectionStyles(styles) {
|
|
1025
1037
|
this.focusEditor();
|
|
1026
1038
|
this.restoreSelection();
|
|
1027
1039
|
const sel = window.getSelection();
|
|
1028
|
-
if (!sel || sel.rangeCount === 0)
|
|
1040
|
+
if (!sel || sel.rangeCount === 0) {
|
|
1041
|
+
console.warn('[RTE-material] applySelectionStyles: no selection available', { styles });
|
|
1029
1042
|
return;
|
|
1030
|
-
|
|
1031
|
-
|
|
1043
|
+
}
|
|
1044
|
+
let range = sel.getRangeAt(0);
|
|
1045
|
+
if (range.collapsed) {
|
|
1046
|
+
console.warn('[RTE-material] applySelectionStyles: range is collapsed', { styles });
|
|
1032
1047
|
return;
|
|
1033
|
-
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
while (el && el !== editor && !isBlockTag(el.tagName))
|
|
1038
|
-
el = el.parentElement;
|
|
1039
|
-
return (el && el !== editor) ? el : null;
|
|
1040
|
-
};
|
|
1041
|
-
const startBlock = getBlockAncestor(range.startContainer);
|
|
1042
|
-
const endBlock = getBlockAncestor(range.endContainer);
|
|
1043
|
-
const crossesBlocks = !!startBlock && !!endBlock && startBlock !== endBlock;
|
|
1044
|
-
if (!crossesBlocks) {
|
|
1045
|
-
// Single block selection: keep using inline wrapper for precise range
|
|
1046
|
-
this.applyInlineStyle(cssProp, value);
|
|
1048
|
+
}
|
|
1049
|
+
const entries = Object.entries(styles).filter(([, v]) => v != null && v !== '');
|
|
1050
|
+
if (entries.length === 0) {
|
|
1051
|
+
console.warn('[RTE-material] applySelectionStyles: no style entries to apply', { styles });
|
|
1047
1052
|
return;
|
|
1048
1053
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1054
|
+
const propsToClear = Array.from(new Set(entries.map(([prop]) => this.toCssProperty(prop))));
|
|
1055
|
+
console.log('[RTE-material] applySelectionStyles: clearing props before apply', { propsToClear, entries });
|
|
1056
|
+
for (const cssProp of propsToClear) {
|
|
1057
|
+
const beforeClear = range.cloneRange();
|
|
1058
|
+
const clearedRange = this.removeInlineStyleInRange(range, cssProp, { suppressEmit: true });
|
|
1059
|
+
const refreshedSel = window.getSelection();
|
|
1060
|
+
if (refreshedSel && refreshedSel.rangeCount > 0) {
|
|
1061
|
+
range = refreshedSel.getRangeAt(0);
|
|
1062
|
+
}
|
|
1063
|
+
else if (clearedRange) {
|
|
1064
|
+
range = clearedRange;
|
|
1065
|
+
}
|
|
1066
|
+
if (range.collapsed && !beforeClear.collapsed) {
|
|
1067
|
+
console.warn('[RTE-material] applySelectionStyles: range collapsed after clearing, restoring previous range');
|
|
1068
|
+
const sel = window.getSelection();
|
|
1069
|
+
if (sel) {
|
|
1070
|
+
sel.removeAllRanges();
|
|
1071
|
+
sel.addRange(beforeClear);
|
|
1072
|
+
}
|
|
1073
|
+
range = beforeClear;
|
|
1062
1074
|
}
|
|
1063
|
-
catch { }
|
|
1064
1075
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1076
|
+
const segments = this.collectTextSegments(range);
|
|
1077
|
+
if (segments.length === 0) {
|
|
1078
|
+
console.warn('[RTE-material] applySelectionStyles: no text segments found', {
|
|
1079
|
+
entries,
|
|
1080
|
+
commonAncestor: range.commonAncestorContainer
|
|
1081
|
+
});
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
console.log('[RTE-material] applySelectionStyles: applying styles to segments', {
|
|
1085
|
+
entries,
|
|
1086
|
+
segmentCount: segments.length
|
|
1087
|
+
});
|
|
1088
|
+
const wrappedSegments = [];
|
|
1089
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
1090
|
+
const wrapped = this.applyStylesToSegment(segments[i], entries);
|
|
1091
|
+
if (wrapped)
|
|
1092
|
+
wrappedSegments.unshift(wrapped);
|
|
1093
|
+
}
|
|
1094
|
+
if (wrappedSegments.length) {
|
|
1095
|
+
const first = wrappedSegments[0];
|
|
1096
|
+
const last = wrappedSegments[wrappedSegments.length - 1];
|
|
1097
|
+
const newRange = document.createRange();
|
|
1098
|
+
newRange.setStartBefore(first);
|
|
1099
|
+
newRange.setEndAfter(last);
|
|
1100
|
+
const selAfter = window.getSelection();
|
|
1101
|
+
if (selAfter) {
|
|
1102
|
+
selAfter.removeAllRanges();
|
|
1103
|
+
selAfter.addRange(newRange);
|
|
1104
|
+
}
|
|
1105
|
+
this.saveSelectionFromRange(newRange);
|
|
1067
1106
|
}
|
|
1068
1107
|
this.emitValue();
|
|
1069
|
-
this.takeSnapshot('style
|
|
1108
|
+
this.takeSnapshot('style');
|
|
1070
1109
|
}
|
|
1071
|
-
|
|
1072
|
-
const
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1110
|
+
collectTextSegments(range) {
|
|
1111
|
+
const segments = [];
|
|
1112
|
+
const root = range.commonAncestorContainer;
|
|
1113
|
+
if (root.nodeType === Node.TEXT_NODE) {
|
|
1114
|
+
const text = root;
|
|
1115
|
+
const start = text === range.startContainer ? range.startOffset : 0;
|
|
1116
|
+
const end = text === range.endContainer ? range.endOffset : text.data.length;
|
|
1117
|
+
if (start < end)
|
|
1118
|
+
segments.push({ node: text, start, end });
|
|
1119
|
+
return segments;
|
|
1120
|
+
}
|
|
1121
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1078
1122
|
while (walker.nextNode()) {
|
|
1079
|
-
const
|
|
1080
|
-
if (!
|
|
1123
|
+
const text = walker.currentNode;
|
|
1124
|
+
if (!text.data)
|
|
1081
1125
|
continue;
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1126
|
+
if (!this.intersectsRange(range, text))
|
|
1127
|
+
continue;
|
|
1128
|
+
const start = text === range.startContainer ? range.startOffset : 0;
|
|
1129
|
+
const end = text === range.endContainer ? range.endOffset : text.data.length;
|
|
1130
|
+
if (start >= end)
|
|
1131
|
+
continue;
|
|
1132
|
+
segments.push({ node: text, start, end });
|
|
1087
1133
|
}
|
|
1088
|
-
return
|
|
1134
|
+
return segments;
|
|
1089
1135
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1136
|
+
intersectsRange(range, node) {
|
|
1137
|
+
try {
|
|
1138
|
+
return range.intersectsNode ? range.intersectsNode(node) : true;
|
|
1139
|
+
}
|
|
1140
|
+
catch {
|
|
1141
|
+
return false;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
applyStylesToSegment(segment, entries) {
|
|
1145
|
+
const segRange = document.createRange();
|
|
1146
|
+
segRange.setStart(segment.node, segment.start);
|
|
1147
|
+
segRange.setEnd(segment.node, segment.end);
|
|
1148
|
+
const wrapper = document.createElement('span');
|
|
1149
|
+
for (const [prop, value] of entries) {
|
|
1150
|
+
wrapper.style.setProperty(this.toCssProperty(prop), value);
|
|
1151
|
+
}
|
|
1152
|
+
try {
|
|
1153
|
+
segRange.surroundContents(wrapper);
|
|
1154
|
+
return this.mergeAdjacentStyledSpans(wrapper);
|
|
1155
|
+
}
|
|
1156
|
+
catch (err) {
|
|
1157
|
+
console.error('[RTE-material] applyStylesToSegment: failed to surround contents', {
|
|
1158
|
+
error: err,
|
|
1159
|
+
segment,
|
|
1160
|
+
entries
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
toCssProperty(prop) {
|
|
1166
|
+
return prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
1167
|
+
}
|
|
1168
|
+
mergeAdjacentStyledSpans(span) {
|
|
1169
|
+
if (span.tagName !== 'SPAN')
|
|
1170
|
+
return span;
|
|
1171
|
+
let current = span;
|
|
1172
|
+
const normalize = (el) => el && el.tagName === 'SPAN' ? el : null;
|
|
1173
|
+
const styleText = (el) => (el ? el.getAttribute('style') || '' : '');
|
|
1174
|
+
let parent = normalize(current.parentElement);
|
|
1175
|
+
if (parent && styleText(parent) === styleText(current) && parent.attributes.length === current.attributes.length) {
|
|
1176
|
+
while (current.firstChild)
|
|
1177
|
+
parent.insertBefore(current.firstChild, current);
|
|
1178
|
+
current.remove();
|
|
1179
|
+
current = parent;
|
|
1180
|
+
}
|
|
1181
|
+
let prev = current.previousSibling;
|
|
1182
|
+
while (prev && prev.nodeType === Node.TEXT_NODE && !prev.data.trim())
|
|
1183
|
+
prev = prev.previousSibling;
|
|
1184
|
+
if (prev instanceof HTMLElement && prev.tagName === 'SPAN' && styleText(prev) === styleText(current) && prev.attributes.length === current.attributes.length) {
|
|
1185
|
+
while (current.firstChild)
|
|
1186
|
+
prev.appendChild(current.firstChild);
|
|
1187
|
+
current.remove();
|
|
1188
|
+
current = prev;
|
|
1189
|
+
}
|
|
1190
|
+
let next = current.nextSibling;
|
|
1191
|
+
while (next && next.nodeType === Node.TEXT_NODE && !next.data.trim())
|
|
1192
|
+
next = next.nextSibling;
|
|
1193
|
+
if (next instanceof HTMLElement && next.tagName === 'SPAN' && styleText(next) === styleText(current) && next.attributes.length === current.attributes.length) {
|
|
1194
|
+
while (next.firstChild)
|
|
1195
|
+
current.appendChild(next.firstChild);
|
|
1196
|
+
next.remove();
|
|
1197
|
+
}
|
|
1198
|
+
return current;
|
|
1112
1199
|
}
|
|
1113
1200
|
isBoldCarrier(el) {
|
|
1114
1201
|
if (!el)
|
|
@@ -1154,45 +1241,21 @@ class StackchRichtextEditorMaterial {
|
|
|
1154
1241
|
}
|
|
1155
1242
|
return null;
|
|
1156
1243
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
parent.insertBefore(leftClone, carrier);
|
|
1173
|
-
}
|
|
1174
|
-
let insertAfterRef = carrier;
|
|
1175
|
-
if (rightFrag.childNodes.length) {
|
|
1176
|
-
const rightClone = carrier.cloneNode(false);
|
|
1177
|
-
rightClone.appendChild(rightFrag);
|
|
1178
|
-
parent.insertBefore(rightClone, carrier.nextSibling);
|
|
1179
|
-
insertAfterRef = rightClone;
|
|
1180
|
-
}
|
|
1181
|
-
while (carrier.firstChild)
|
|
1182
|
-
parent.insertBefore(carrier.firstChild, insertAfterRef);
|
|
1183
|
-
carrier.remove();
|
|
1184
|
-
function rightGuard(node, r) {
|
|
1185
|
-
try {
|
|
1186
|
-
r.setEnd(node, node.childNodes.length);
|
|
1187
|
-
}
|
|
1188
|
-
catch { }
|
|
1189
|
-
}
|
|
1190
|
-
function leftGuard(rng, r) {
|
|
1191
|
-
try {
|
|
1192
|
-
r.setEnd(rng.startContainer, rng.startOffset);
|
|
1193
|
-
}
|
|
1194
|
-
catch { }
|
|
1195
|
-
}
|
|
1244
|
+
isItalicCarrier(el) {
|
|
1245
|
+
if (!el)
|
|
1246
|
+
return false;
|
|
1247
|
+
if (el.tagName === 'I' || el.tagName === 'EM')
|
|
1248
|
+
return true;
|
|
1249
|
+
const fs = el.style?.fontStyle || '';
|
|
1250
|
+
return !!fs && fs !== 'normal';
|
|
1251
|
+
}
|
|
1252
|
+
isUnderlineCarrier(el) {
|
|
1253
|
+
if (!el)
|
|
1254
|
+
return false;
|
|
1255
|
+
if (el.tagName === 'U')
|
|
1256
|
+
return true;
|
|
1257
|
+
const td = el.style?.textDecoration || el.style?.textDecorationLine || '';
|
|
1258
|
+
return typeof td === 'string' && td.includes('underline');
|
|
1196
1259
|
}
|
|
1197
1260
|
// Unwrap bold formatting only for the selected content inside a containing bold ancestor by splitting it into left/selection/right parts
|
|
1198
1261
|
deselectBoldBySplitting(range) {
|
|
@@ -1295,8 +1358,15 @@ class StackchRichtextEditorMaterial {
|
|
|
1295
1358
|
// move siblings after `child` into right clone
|
|
1296
1359
|
while (child.nextSibling)
|
|
1297
1360
|
right.appendChild(child.nextSibling);
|
|
1298
|
-
// insert right after parent
|
|
1299
|
-
parent.
|
|
1361
|
+
// insert right after parent (avoid Document-level issues)
|
|
1362
|
+
const pParent = parent.parentNode;
|
|
1363
|
+
if (pParent && pParent.nodeType !== Node.DOCUMENT_NODE) {
|
|
1364
|
+
pParent.insertBefore(right, parent.nextSibling);
|
|
1365
|
+
}
|
|
1366
|
+
else {
|
|
1367
|
+
while (right.firstChild)
|
|
1368
|
+
parent.appendChild(right.firstChild);
|
|
1369
|
+
}
|
|
1300
1370
|
// ascend
|
|
1301
1371
|
child = parent;
|
|
1302
1372
|
parent = child.parentElement;
|
|
@@ -1314,6 +1384,19 @@ class StackchRichtextEditorMaterial {
|
|
|
1314
1384
|
const gp = ancestor.parentNode;
|
|
1315
1385
|
if (!gp)
|
|
1316
1386
|
return;
|
|
1387
|
+
if (gp.nodeType === Node.DOCUMENT_NODE) {
|
|
1388
|
+
if (before.childNodes.length) {
|
|
1389
|
+
while (before.firstChild)
|
|
1390
|
+
ancestor.parentNode?.insertBefore(before.firstChild, ancestor);
|
|
1391
|
+
}
|
|
1392
|
+
ancestor.parentNode?.insertBefore(node, ancestor);
|
|
1393
|
+
if (after.childNodes.length) {
|
|
1394
|
+
while (after.firstChild)
|
|
1395
|
+
ancestor.parentNode?.insertBefore(after.firstChild, ancestor);
|
|
1396
|
+
}
|
|
1397
|
+
ancestor.remove();
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1317
1400
|
if (before.childNodes.length)
|
|
1318
1401
|
gp.insertBefore(before, ancestor);
|
|
1319
1402
|
// move node out, place where ancestor was
|
|
@@ -1322,39 +1405,8 @@ class StackchRichtextEditorMaterial {
|
|
|
1322
1405
|
gp.insertBefore(after, ancestor);
|
|
1323
1406
|
ancestor.remove();
|
|
1324
1407
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
1328
|
-
while (walker.nextNode()) {
|
|
1329
|
-
const el = walker.currentNode;
|
|
1330
|
-
const isBoldEl = (el.tagName === 'B' || el.tagName === 'STRONG' || (el.tagName === 'SPAN' && el.style.fontWeight && el.style.fontWeight !== 'normal' && el.style.fontWeight !== '400'));
|
|
1331
|
-
if (!isBoldEl)
|
|
1332
|
-
continue;
|
|
1333
|
-
// remove if no children or only whitespace (including NBSP \u00A0 and zero-width space \u200B)
|
|
1334
|
-
const text = el.textContent ?? '';
|
|
1335
|
-
const normalized = text.replace(/[\u00A0\u200B\s]/g, '');
|
|
1336
|
-
const onlyWhitespace = !el.firstChild || (normalized.length === 0 && el.querySelectorAll('*').length === 0);
|
|
1337
|
-
if (onlyWhitespace) {
|
|
1338
|
-
toRemove.push(el);
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
for (const el of toRemove)
|
|
1342
|
-
el.remove();
|
|
1343
|
-
}
|
|
1344
|
-
maybeUnwrapNormalSpan(span) {
|
|
1345
|
-
if (span.tagName !== 'SPAN')
|
|
1346
|
-
return;
|
|
1347
|
-
// If no bold applies from ancestors, drop the 400 style and unwrap if style empty
|
|
1348
|
-
const hasBoldAbove = this.isNodeBold(span.parentElement);
|
|
1349
|
-
if (!hasBoldAbove) {
|
|
1350
|
-
if (span.style.fontWeight === '400')
|
|
1351
|
-
span.style.removeProperty('font-weight');
|
|
1352
|
-
if (!span.getAttribute('style')) {
|
|
1353
|
-
while (span.firstChild)
|
|
1354
|
-
span.parentNode?.insertBefore(span.firstChild, span);
|
|
1355
|
-
span.remove();
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1408
|
+
saveSelectionFromRange(range) {
|
|
1409
|
+
this.savedRange = range.cloneRange();
|
|
1358
1410
|
}
|
|
1359
1411
|
wrapSelectionWith(tag, attrs) {
|
|
1360
1412
|
const sel = window.getSelection();
|
|
@@ -1424,55 +1476,13 @@ class StackchRichtextEditorMaterial {
|
|
|
1424
1476
|
catch { }
|
|
1425
1477
|
}
|
|
1426
1478
|
computeBoldAnyForRange(range) {
|
|
1427
|
-
|
|
1428
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1429
|
-
while (walker.nextNode()) {
|
|
1430
|
-
const n = walker.currentNode;
|
|
1431
|
-
if (!n.data.trim())
|
|
1432
|
-
continue;
|
|
1433
|
-
try {
|
|
1434
|
-
if (range.intersectsNode(n)) {
|
|
1435
|
-
if (this.isNodeBold(n.parentElement))
|
|
1436
|
-
return true;
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
catch { }
|
|
1440
|
-
}
|
|
1441
|
-
return false;
|
|
1479
|
+
return this.selectionHasStyle(range, 'font-weight');
|
|
1442
1480
|
}
|
|
1443
1481
|
computeItalicAnyForRange(range) {
|
|
1444
|
-
|
|
1445
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1446
|
-
while (walker.nextNode()) {
|
|
1447
|
-
const n = walker.currentNode;
|
|
1448
|
-
if (!n.data.trim())
|
|
1449
|
-
continue;
|
|
1450
|
-
try {
|
|
1451
|
-
if (range.intersectsNode(n)) {
|
|
1452
|
-
if (this.isNodeItalic(n.parentElement))
|
|
1453
|
-
return true;
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
catch { }
|
|
1457
|
-
}
|
|
1458
|
-
return false;
|
|
1482
|
+
return this.selectionHasStyle(range, 'font-style');
|
|
1459
1483
|
}
|
|
1460
1484
|
computeUnderlineAnyForRange(range) {
|
|
1461
|
-
|
|
1462
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1463
|
-
while (walker.nextNode()) {
|
|
1464
|
-
const n = walker.currentNode;
|
|
1465
|
-
if (!n.data.trim())
|
|
1466
|
-
continue;
|
|
1467
|
-
try {
|
|
1468
|
-
if (range.intersectsNode(n)) {
|
|
1469
|
-
if (this.isNodeUnderline(n.parentElement))
|
|
1470
|
-
return true;
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
catch { }
|
|
1474
|
-
}
|
|
1475
|
-
return false;
|
|
1485
|
+
return this.selectionHasStyle(range, 'text-decoration');
|
|
1476
1486
|
}
|
|
1477
1487
|
isNodeBold(node) {
|
|
1478
1488
|
let el = node;
|
|
@@ -1513,117 +1523,248 @@ class StackchRichtextEditorMaterial {
|
|
|
1513
1523
|
}
|
|
1514
1524
|
return false;
|
|
1515
1525
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
const
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
return false;
|
|
1528
|
-
const tag = el.tagName;
|
|
1529
|
-
if (tag === 'U')
|
|
1530
|
-
return true;
|
|
1531
|
-
const td = el.style?.textDecoration || '';
|
|
1532
|
-
return typeof td === 'string' && td.includes('underline');
|
|
1533
|
-
}
|
|
1534
|
-
computeBoldActiveForRange(range) {
|
|
1535
|
-
const root = this.isRangeInEditor(range) ? this.editorRef.nativeElement : range.commonAncestorContainer;
|
|
1536
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1537
|
-
const nodes = [];
|
|
1538
|
-
while (walker.nextNode()) {
|
|
1539
|
-
const n = walker.currentNode;
|
|
1540
|
-
if (!n.data.trim())
|
|
1541
|
-
continue;
|
|
1542
|
-
try {
|
|
1543
|
-
if (range.intersectsNode(n))
|
|
1544
|
-
nodes.push(n);
|
|
1545
|
-
}
|
|
1546
|
-
catch { /* Safari may throw if node not in same tree */ }
|
|
1547
|
-
}
|
|
1548
|
-
if (nodes.length === 0)
|
|
1549
|
-
return false;
|
|
1550
|
-
return nodes.every(t => this.isNodeBold(t.parentElement));
|
|
1551
|
-
}
|
|
1552
|
-
computeItalicActiveForRange(range) {
|
|
1553
|
-
const root = this.isRangeInEditor(range) ? this.editorRef.nativeElement : range.commonAncestorContainer;
|
|
1554
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1555
|
-
const nodes = [];
|
|
1556
|
-
while (walker.nextNode()) {
|
|
1557
|
-
const n = walker.currentNode;
|
|
1558
|
-
if (!n.data.trim())
|
|
1559
|
-
continue;
|
|
1526
|
+
removeInlineStyleInRange(range, cssPropKebab, options) {
|
|
1527
|
+
const shouldRestoreSelection = options?.restoreSelection !== false;
|
|
1528
|
+
this.isolateRangeFromStyleCarriers(range, cssPropKebab);
|
|
1529
|
+
const editor = this.editorRef.nativeElement;
|
|
1530
|
+
const selAfter = window.getSelection();
|
|
1531
|
+
const effectiveRange = selAfter && selAfter.rangeCount > 0 ? selAfter.getRangeAt(0) : range;
|
|
1532
|
+
const common = effectiveRange.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
|
|
1533
|
+
? effectiveRange.commonAncestorContainer
|
|
1534
|
+
: (effectiveRange.commonAncestorContainer.parentElement || editor);
|
|
1535
|
+
const affected = [];
|
|
1536
|
+
const intersects = (node) => {
|
|
1560
1537
|
try {
|
|
1561
|
-
|
|
1562
|
-
nodes.push(n);
|
|
1538
|
+
return effectiveRange.intersectsNode ? effectiveRange.intersectsNode(node) : true;
|
|
1563
1539
|
}
|
|
1564
|
-
catch {
|
|
1565
|
-
|
|
1566
|
-
if (nodes.length === 0)
|
|
1567
|
-
return false;
|
|
1568
|
-
return nodes.every(t => this.isNodeItalic(t.parentElement));
|
|
1569
|
-
}
|
|
1570
|
-
computeUnderlineActiveForRange(range) {
|
|
1571
|
-
const root = this.isRangeInEditor(range) ? this.editorRef.nativeElement : range.commonAncestorContainer;
|
|
1572
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1573
|
-
const nodes = [];
|
|
1574
|
-
while (walker.nextNode()) {
|
|
1575
|
-
const n = walker.currentNode;
|
|
1576
|
-
if (!n.data.trim())
|
|
1577
|
-
continue;
|
|
1578
|
-
try {
|
|
1579
|
-
if (range.intersectsNode(n))
|
|
1580
|
-
nodes.push(n);
|
|
1540
|
+
catch {
|
|
1541
|
+
return false;
|
|
1581
1542
|
}
|
|
1582
|
-
|
|
1543
|
+
};
|
|
1544
|
+
const matchesEl = (el) => this.isStyleCarrier(el, cssPropKebab);
|
|
1545
|
+
console.log('[RTE-material] removeInlineStyleInRange:start', {
|
|
1546
|
+
cssPropKebab,
|
|
1547
|
+
rangeCollapsed: effectiveRange.collapsed,
|
|
1548
|
+
commonTag: common.tagName,
|
|
1549
|
+
commonHasStyle: common.style ? common.style.getPropertyValue?.(cssPropKebab) : undefined
|
|
1550
|
+
});
|
|
1551
|
+
if (common instanceof HTMLElement && matchesEl(common) && intersects(common)) {
|
|
1552
|
+
affected.push(common);
|
|
1583
1553
|
}
|
|
1584
|
-
if (nodes.length === 0)
|
|
1585
|
-
return false;
|
|
1586
|
-
return nodes.every(t => this.isNodeUnderline(t.parentElement));
|
|
1587
|
-
}
|
|
1588
|
-
removeInlineStyleInRange(range, cssPropKebab) {
|
|
1589
|
-
const editor = this.editorRef.nativeElement;
|
|
1590
|
-
const common = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE ? range.commonAncestorContainer : range.commonAncestorContainer.parentElement || editor;
|
|
1591
|
-
const affected = [];
|
|
1592
1554
|
const walker = document.createTreeWalker(common, NodeFilter.SHOW_ELEMENT);
|
|
1593
1555
|
while (walker.nextNode()) {
|
|
1594
1556
|
const el = walker.currentNode;
|
|
1595
|
-
|
|
1596
|
-
if (el.tagName === 'SPAN' && el.style && el.style.getPropertyValue(cssPropKebab))
|
|
1597
|
-
matches = true;
|
|
1598
|
-
if (cssPropKebab === 'font-weight' && (el.tagName === 'B' || el.tagName === 'STRONG'))
|
|
1599
|
-
matches = true;
|
|
1600
|
-
if (cssPropKebab === 'font-style' && (el.tagName === 'I' || el.tagName === 'EM'))
|
|
1601
|
-
matches = true;
|
|
1602
|
-
if (cssPropKebab === 'text-decoration' && el.tagName === 'U')
|
|
1603
|
-
matches = true;
|
|
1604
|
-
if (!matches)
|
|
1557
|
+
if (!matchesEl(el))
|
|
1605
1558
|
continue;
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1559
|
+
if (intersects(el)) {
|
|
1560
|
+
affected.push(el);
|
|
1561
|
+
console.log('[RTE-material] removeInlineStyleInRange:match', el.tagName, el.getAttribute('style'));
|
|
1609
1562
|
}
|
|
1610
|
-
catch { }
|
|
1611
1563
|
}
|
|
1564
|
+
let removedTags = 0;
|
|
1565
|
+
let removedProps = 0;
|
|
1612
1566
|
for (const el of affected) {
|
|
1613
1567
|
if (el.tagName === 'B' || el.tagName === 'STRONG' || el.tagName === 'I' || el.tagName === 'EM' || el.tagName === 'U') {
|
|
1568
|
+
console.log('[RTE-material] unwrap tag', el.tagName);
|
|
1614
1569
|
while (el.firstChild)
|
|
1615
1570
|
el.parentNode?.insertBefore(el.firstChild, el);
|
|
1616
1571
|
el.remove();
|
|
1572
|
+
removedTags++;
|
|
1617
1573
|
continue;
|
|
1618
1574
|
}
|
|
1575
|
+
console.log('[RTE-material] remove style', cssPropKebab, 'from', el.tagName, el.getAttribute('style'));
|
|
1619
1576
|
el.style.removeProperty(cssPropKebab);
|
|
1577
|
+
if (!el.style.length) {
|
|
1578
|
+
console.log('[RTE-material] removeInlineStyleInRange: style attribute now empty, removing attribute', el);
|
|
1579
|
+
el.removeAttribute('style');
|
|
1580
|
+
}
|
|
1581
|
+
removedProps++;
|
|
1620
1582
|
if (!el.getAttribute('style')) {
|
|
1583
|
+
console.log('[RTE-material] unwrap empty styled span', el.tagName);
|
|
1621
1584
|
while (el.firstChild)
|
|
1622
1585
|
el.parentNode?.insertBefore(el.firstChild, el);
|
|
1623
1586
|
el.remove();
|
|
1624
1587
|
}
|
|
1625
1588
|
}
|
|
1626
|
-
this.
|
|
1589
|
+
this.pruneDanglingStyleCarriers(common, cssPropKebab);
|
|
1590
|
+
console.log('[RTE-material] removeInlineStyleInRange:done', { affected: affected.length, removedTags, removedProps });
|
|
1591
|
+
let resultRange = null;
|
|
1592
|
+
if (!options?.suppressEmit) {
|
|
1593
|
+
this.emitValue();
|
|
1594
|
+
resultRange = null;
|
|
1595
|
+
}
|
|
1596
|
+
else {
|
|
1597
|
+
const sel = window.getSelection();
|
|
1598
|
+
if (sel && sel.rangeCount > 0) {
|
|
1599
|
+
resultRange = sel.getRangeAt(0).cloneRange();
|
|
1600
|
+
}
|
|
1601
|
+
else if (shouldRestoreSelection) {
|
|
1602
|
+
resultRange = effectiveRange.cloneRange();
|
|
1603
|
+
}
|
|
1604
|
+
if (shouldRestoreSelection && resultRange) {
|
|
1605
|
+
const selRestore = window.getSelection();
|
|
1606
|
+
if (selRestore) {
|
|
1607
|
+
selRestore.removeAllRanges();
|
|
1608
|
+
selRestore.addRange(resultRange);
|
|
1609
|
+
this.saveSelectionFromRange(resultRange);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
return resultRange;
|
|
1614
|
+
}
|
|
1615
|
+
pruneDanglingStyleCarriers(root, cssPropKebab) {
|
|
1616
|
+
const toRemove = [];
|
|
1617
|
+
if (root instanceof HTMLElement && this.isStyleCarrier(root, cssPropKebab) && !this.elementHasVisibleContent(root)) {
|
|
1618
|
+
toRemove.push(root);
|
|
1619
|
+
}
|
|
1620
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
1621
|
+
while (walker.nextNode()) {
|
|
1622
|
+
const el = walker.currentNode;
|
|
1623
|
+
if (!this.isStyleCarrier(el, cssPropKebab))
|
|
1624
|
+
continue;
|
|
1625
|
+
if (this.elementHasVisibleContent(el))
|
|
1626
|
+
continue;
|
|
1627
|
+
toRemove.push(el);
|
|
1628
|
+
}
|
|
1629
|
+
for (const el of toRemove) {
|
|
1630
|
+
console.log('[RTE-material] pruneDanglingStyleCarriers: removing empty carrier', { tag: el.tagName, style: el.getAttribute('style') });
|
|
1631
|
+
if (el.tagName === 'B' || el.tagName === 'STRONG' || el.tagName === 'I' || el.tagName === 'EM' || el.tagName === 'U') {
|
|
1632
|
+
el.remove();
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
el.remove();
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
elementHasVisibleContent(node) {
|
|
1639
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1640
|
+
const text = node.data;
|
|
1641
|
+
for (let i = 0; i < text.length; i++) {
|
|
1642
|
+
const ch = text[i];
|
|
1643
|
+
if (ch === '\u00a0')
|
|
1644
|
+
return true;
|
|
1645
|
+
if (!/\s/.test(ch) && ch !== '\u200b' && ch !== '\u200c' && ch !== '\u200d' && ch !== '\ufeff') {
|
|
1646
|
+
return true;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
return false;
|
|
1650
|
+
}
|
|
1651
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1652
|
+
const el = node;
|
|
1653
|
+
if (el.tagName === 'BR')
|
|
1654
|
+
return true;
|
|
1655
|
+
for (const child of Array.from(node.childNodes)) {
|
|
1656
|
+
if (this.elementHasVisibleContent(child))
|
|
1657
|
+
return true;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
return false;
|
|
1661
|
+
}
|
|
1662
|
+
isolateRangeFromStyleCarriers(range, cssPropKebab) {
|
|
1663
|
+
let carrier = this.findCarrierContainingRange(range, cssPropKebab);
|
|
1664
|
+
while (carrier) {
|
|
1665
|
+
this.splitCarrierAroundSelection(range, carrier);
|
|
1666
|
+
carrier = this.findCarrierContainingRange(range, cssPropKebab);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
selectionHasStyle(range, cssPropKebab) {
|
|
1670
|
+
const editor = this.editorRef.nativeElement;
|
|
1671
|
+
const common = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
|
|
1672
|
+
? range.commonAncestorContainer
|
|
1673
|
+
: (range.commonAncestorContainer.parentElement || editor);
|
|
1674
|
+
const intersects = (node) => this.intersectsRange(range, node);
|
|
1675
|
+
if (common instanceof HTMLElement && this.isStyleCarrier(common, cssPropKebab) && intersects(common)) {
|
|
1676
|
+
return true;
|
|
1677
|
+
}
|
|
1678
|
+
const walker = document.createTreeWalker(common, NodeFilter.SHOW_ELEMENT);
|
|
1679
|
+
while (walker.nextNode()) {
|
|
1680
|
+
const el = walker.currentNode;
|
|
1681
|
+
if (!this.isStyleCarrier(el, cssPropKebab))
|
|
1682
|
+
continue;
|
|
1683
|
+
if (intersects(el))
|
|
1684
|
+
return true;
|
|
1685
|
+
}
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
findCarrierContainingRange(range, cssPropKebab) {
|
|
1689
|
+
let el = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
|
|
1690
|
+
? range.commonAncestorContainer
|
|
1691
|
+
: range.commonAncestorContainer.parentElement;
|
|
1692
|
+
const editor = this.editorRef.nativeElement;
|
|
1693
|
+
while (el && el !== editor) {
|
|
1694
|
+
if (this.isStyleCarrier(el, cssPropKebab)) {
|
|
1695
|
+
const sc = range.startContainer;
|
|
1696
|
+
const ec = range.endContainer;
|
|
1697
|
+
if (el.contains(sc) && el.contains(ec))
|
|
1698
|
+
return el;
|
|
1699
|
+
}
|
|
1700
|
+
el = el.parentElement;
|
|
1701
|
+
}
|
|
1702
|
+
return null;
|
|
1703
|
+
}
|
|
1704
|
+
isStyleCarrier(el, cssPropKebab) {
|
|
1705
|
+
const tag = el.tagName;
|
|
1706
|
+
if (cssPropKebab === 'font-weight') {
|
|
1707
|
+
if (tag === 'B' || tag === 'STRONG')
|
|
1708
|
+
return true;
|
|
1709
|
+
const fw = el.style?.fontWeight || '';
|
|
1710
|
+
return !!fw && fw !== 'normal' && fw !== '400';
|
|
1711
|
+
}
|
|
1712
|
+
if (cssPropKebab === 'font-style') {
|
|
1713
|
+
if (tag === 'I' || tag === 'EM')
|
|
1714
|
+
return true;
|
|
1715
|
+
const fs = el.style?.fontStyle || '';
|
|
1716
|
+
return !!fs && fs !== 'normal';
|
|
1717
|
+
}
|
|
1718
|
+
if (cssPropKebab === 'text-decoration') {
|
|
1719
|
+
if (tag === 'U')
|
|
1720
|
+
return true;
|
|
1721
|
+
const td = el.style?.textDecoration || el.style?.textDecorationLine || '';
|
|
1722
|
+
return typeof td === 'string' && td.includes('underline');
|
|
1723
|
+
}
|
|
1724
|
+
const inline = el.style?.getPropertyValue(cssPropKebab) || '';
|
|
1725
|
+
if (!inline)
|
|
1726
|
+
return false;
|
|
1727
|
+
if (cssPropKebab === 'color') {
|
|
1728
|
+
return inline !== 'inherit' && inline !== 'initial';
|
|
1729
|
+
}
|
|
1730
|
+
if (cssPropKebab === 'background-color') {
|
|
1731
|
+
return inline !== 'transparent' && inline !== 'initial';
|
|
1732
|
+
}
|
|
1733
|
+
return true;
|
|
1734
|
+
}
|
|
1735
|
+
splitCarrierAroundSelection(range, carrier) {
|
|
1736
|
+
const parent = carrier.parentNode;
|
|
1737
|
+
if (!parent)
|
|
1738
|
+
return;
|
|
1739
|
+
const rightRange = document.createRange();
|
|
1740
|
+
rightRange.setStart(range.endContainer, range.endOffset);
|
|
1741
|
+
try {
|
|
1742
|
+
rightRange.setEnd(carrier, carrier.childNodes.length);
|
|
1743
|
+
}
|
|
1744
|
+
catch { }
|
|
1745
|
+
const rightFrag = rightRange.extractContents();
|
|
1746
|
+
const leftRange = document.createRange();
|
|
1747
|
+
leftRange.setStart(carrier, 0);
|
|
1748
|
+
try {
|
|
1749
|
+
leftRange.setEnd(range.startContainer, range.startOffset);
|
|
1750
|
+
}
|
|
1751
|
+
catch { }
|
|
1752
|
+
const leftFrag = leftRange.extractContents();
|
|
1753
|
+
if (leftFrag.childNodes.length) {
|
|
1754
|
+
const leftClone = carrier.cloneNode(false);
|
|
1755
|
+
leftClone.appendChild(leftFrag);
|
|
1756
|
+
parent.insertBefore(leftClone, carrier);
|
|
1757
|
+
}
|
|
1758
|
+
let insertAfter = carrier;
|
|
1759
|
+
if (rightFrag.childNodes.length) {
|
|
1760
|
+
const rightClone = carrier.cloneNode(false);
|
|
1761
|
+
rightClone.appendChild(rightFrag);
|
|
1762
|
+
parent.insertBefore(rightClone, carrier.nextSibling);
|
|
1763
|
+
insertAfter = rightClone;
|
|
1764
|
+
}
|
|
1765
|
+
while (carrier.firstChild)
|
|
1766
|
+
parent.insertBefore(carrier.firstChild, insertAfter);
|
|
1767
|
+
carrier.remove();
|
|
1627
1768
|
}
|
|
1628
1769
|
toggleBold() {
|
|
1629
1770
|
this.focusEditor();
|
|
@@ -1635,45 +1776,11 @@ class StackchRichtextEditorMaterial {
|
|
|
1635
1776
|
if (range.collapsed)
|
|
1636
1777
|
this.expandRangeToWord(range);
|
|
1637
1778
|
const anyActive = this.computeBoldAnyForRange(range);
|
|
1638
|
-
const blocks = this.getIntersectingBlocks(range);
|
|
1639
|
-
const multiBlock = blocks.length > 1;
|
|
1640
1779
|
if (anyActive) {
|
|
1641
|
-
|
|
1642
|
-
for (const b of blocks)
|
|
1643
|
-
b.style.removeProperty('font-weight');
|
|
1644
|
-
this.emitValue();
|
|
1645
|
-
this.updateInlineStates();
|
|
1646
|
-
this.takeSnapshot('toggle-bold-blocks');
|
|
1647
|
-
return;
|
|
1648
|
-
}
|
|
1649
|
-
// Preferred: split bold ancestor around selection so we keep surrounding text intact
|
|
1650
|
-
const changed = this.deselectBoldBySplitting(range);
|
|
1651
|
-
if (changed) {
|
|
1652
|
-
this.cleanupEmptyBoldSpans(this.editorRef.nativeElement);
|
|
1653
|
-
this.emitValue();
|
|
1654
|
-
}
|
|
1655
|
-
else {
|
|
1656
|
-
// Fallback: previous neutralization approach for complex cases
|
|
1657
|
-
const normal = this.wrapSelectionInline('fontWeight', '400');
|
|
1658
|
-
if (normal) {
|
|
1659
|
-
this.stripBoldWithin(normal);
|
|
1660
|
-
this.liftOutOfBoldAncestors(normal);
|
|
1661
|
-
this.cleanupEmptyBoldSpans(this.editorRef.nativeElement);
|
|
1662
|
-
this.maybeUnwrapNormalSpan(normal);
|
|
1663
|
-
this.emitValue();
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1780
|
+
this.removeInlineStyleInRange(range, 'font-weight');
|
|
1666
1781
|
}
|
|
1667
1782
|
else {
|
|
1668
|
-
|
|
1669
|
-
for (const b of blocks)
|
|
1670
|
-
b.style.fontWeight = 'bold';
|
|
1671
|
-
this.emitValue();
|
|
1672
|
-
this.updateInlineStates();
|
|
1673
|
-
this.takeSnapshot('toggle-bold-blocks');
|
|
1674
|
-
return;
|
|
1675
|
-
}
|
|
1676
|
-
this.applyInlineStyleSmart('fontWeight', 'bold');
|
|
1783
|
+
this.applySelectionStyles({ fontWeight: 'bold' });
|
|
1677
1784
|
}
|
|
1678
1785
|
this.updateInlineStates();
|
|
1679
1786
|
this.takeSnapshot('toggle-bold');
|
|
@@ -1689,18 +1796,10 @@ class StackchRichtextEditorMaterial {
|
|
|
1689
1796
|
this.expandRangeToWord(range);
|
|
1690
1797
|
const anyActive = this.computeItalicAnyForRange(range);
|
|
1691
1798
|
if (anyActive) {
|
|
1692
|
-
|
|
1693
|
-
if (changed) {
|
|
1694
|
-
this.cleanupEmptyItalicSpans(this.editorRef.nativeElement);
|
|
1695
|
-
this.emitValue();
|
|
1696
|
-
}
|
|
1697
|
-
else {
|
|
1698
|
-
this.removeInlineStyleInRange(range, 'font-style');
|
|
1699
|
-
this.applyInlineStyle('fontStyle', 'normal');
|
|
1700
|
-
}
|
|
1799
|
+
this.removeInlineStyleInRange(range, 'font-style');
|
|
1701
1800
|
}
|
|
1702
1801
|
else {
|
|
1703
|
-
this.
|
|
1802
|
+
this.applySelectionStyles({ fontStyle: 'italic' });
|
|
1704
1803
|
}
|
|
1705
1804
|
this.updateInlineStates();
|
|
1706
1805
|
this.takeSnapshot('toggle-italic');
|
|
@@ -1716,18 +1815,10 @@ class StackchRichtextEditorMaterial {
|
|
|
1716
1815
|
this.expandRangeToWord(range);
|
|
1717
1816
|
const anyActive = this.computeUnderlineAnyForRange(range);
|
|
1718
1817
|
if (anyActive) {
|
|
1719
|
-
|
|
1720
|
-
if (changed) {
|
|
1721
|
-
this.cleanupEmptyUnderlineSpans(this.editorRef.nativeElement);
|
|
1722
|
-
this.emitValue();
|
|
1723
|
-
}
|
|
1724
|
-
else {
|
|
1725
|
-
this.removeInlineStyleInRange(range, 'text-decoration');
|
|
1726
|
-
this.applyInlineStyle('textDecoration', 'none');
|
|
1727
|
-
}
|
|
1818
|
+
this.removeInlineStyleInRange(range, 'text-decoration');
|
|
1728
1819
|
}
|
|
1729
1820
|
else {
|
|
1730
|
-
this.
|
|
1821
|
+
this.applySelectionStyles({ textDecoration: 'underline' });
|
|
1731
1822
|
}
|
|
1732
1823
|
this.updateInlineStates();
|
|
1733
1824
|
this.takeSnapshot('toggle-underline');
|
|
@@ -2073,7 +2164,7 @@ class StackchRichtextEditorMaterial {
|
|
|
2073
2164
|
}
|
|
2074
2165
|
}
|
|
2075
2166
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: StackchRichtextEditorMaterial, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
2076
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.4", type: StackchRichtextEditorMaterial, isStandalone: true, selector: "stackch-richtext-editor-material", inputs: { placeholder: "placeholder", showToolbar: "showToolbar", fonts: "fonts", fontSizes: "fontSizes", height: "height", minHeight: "minHeight", maxHeight: "maxHeight", disabled: "disabled", config: "config" }, outputs: { valueChange: "valueChange" }, host: { listeners: { "document:selectionchange": "onSelectionChange()", "document:click": "closeMenus()", "keydown": "onKeydown($event)" } }, providers: [
|
|
2167
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.4", type: StackchRichtextEditorMaterial, isStandalone: true, selector: "stackch-richtext-editor-material", 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)" } }, providers: [
|
|
2077
2168
|
{
|
|
2078
2169
|
provide: NG_VALUE_ACCESSOR,
|
|
2079
2170
|
useExisting: forwardRef(() => StackchRichtextEditorMaterial),
|
|
@@ -2108,6 +2199,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImpor
|
|
|
2108
2199
|
type: Input
|
|
2109
2200
|
}], valueChange: [{
|
|
2110
2201
|
type: Output
|
|
2202
|
+
}], metricsChange: [{
|
|
2203
|
+
type: Output
|
|
2111
2204
|
}], editorRef: [{
|
|
2112
2205
|
type: ViewChild,
|
|
2113
2206
|
args: ['editor', { static: true }]
|