@aquera/nile-elements 0.1.67-beta-1.5 → 0.1.67-beta-1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/demo/index.html +13 -6
- package/dist/index.js +51 -48
- package/dist/nile-rich-text-editor/nile-rich-text-editor.cjs.js +1 -1
- package/dist/nile-rich-text-editor/nile-rich-text-editor.cjs.js.map +1 -1
- package/dist/nile-rich-text-editor/nile-rich-text-editor.css.cjs.js +1 -1
- package/dist/nile-rich-text-editor/nile-rich-text-editor.css.cjs.js.map +1 -1
- package/dist/nile-rich-text-editor/nile-rich-text-editor.css.esm.js +11 -8
- package/dist/nile-rich-text-editor/nile-rich-text-editor.esm.js +1 -1
- package/dist/nile-rich-text-editor/nile-rte-select.cjs.js +1 -1
- package/dist/nile-rich-text-editor/nile-rte-select.cjs.js.map +1 -1
- package/dist/nile-rich-text-editor/nile-rte-select.esm.js +39 -39
- package/dist/nile-rich-text-editor/utils.cjs.js.map +1 -1
- package/dist/src/nile-rich-text-editor/nile-rich-text-editor.css.js +11 -8
- package/dist/src/nile-rich-text-editor/nile-rich-text-editor.css.js.map +1 -1
- package/dist/src/nile-rich-text-editor/nile-rich-text-editor.js +33 -25
- package/dist/src/nile-rich-text-editor/nile-rich-text-editor.js.map +1 -1
- package/dist/src/nile-rich-text-editor/nile-rte-select.js +62 -57
- package/dist/src/nile-rich-text-editor/nile-rte-select.js.map +1 -1
- package/dist/src/nile-rich-text-editor/rte-utils/content.d.ts +2 -0
- package/dist/src/nile-rich-text-editor/rte-utils/content.js +25 -0
- package/dist/src/nile-rich-text-editor/rte-utils/content.js.map +1 -0
- package/dist/src/nile-rich-text-editor/rte-utils/css.d.ts +1 -0
- package/dist/src/nile-rich-text-editor/rte-utils/css.js +9 -0
- package/dist/src/nile-rich-text-editor/rte-utils/css.js.map +1 -0
- package/dist/src/nile-rich-text-editor/rte-utils/dom.d.ts +2 -0
- package/dist/src/nile-rich-text-editor/rte-utils/dom.js +48 -0
- package/dist/src/nile-rich-text-editor/rte-utils/dom.js.map +1 -0
- package/dist/src/nile-rich-text-editor/rte-utils/formatting.d.ts +2 -0
- package/dist/src/nile-rich-text-editor/rte-utils/formatting.js +69 -0
- package/dist/src/nile-rich-text-editor/rte-utils/formatting.js.map +1 -0
- package/dist/src/nile-rich-text-editor/rte-utils/keys.d.ts +2 -0
- package/dist/src/nile-rich-text-editor/rte-utils/keys.js +38 -0
- package/dist/src/nile-rich-text-editor/rte-utils/keys.js.map +1 -0
- package/dist/src/nile-rich-text-editor/rte-utils/lists.d.ts +2 -0
- package/dist/src/nile-rich-text-editor/rte-utils/lists.js +28 -0
- package/dist/src/nile-rich-text-editor/rte-utils/lists.js.map +1 -0
- package/dist/src/nile-rich-text-editor/rte-utils/selection.d.ts +17 -0
- package/dist/src/nile-rich-text-editor/rte-utils/selection.js +39 -0
- package/dist/src/nile-rich-text-editor/rte-utils/selection.js.map +1 -0
- package/dist/src/nile-rich-text-editor/rte-utils/toolbar.d.ts +28 -0
- package/dist/src/nile-rich-text-editor/rte-utils/toolbar.js +161 -0
- package/dist/src/nile-rich-text-editor/rte-utils/toolbar.js.map +1 -0
- package/dist/src/nile-rich-text-editor/rte-utils/toolbarState.d.ts +13 -0
- package/dist/src/nile-rich-text-editor/rte-utils/toolbarState.js +119 -0
- package/dist/src/nile-rich-text-editor/rte-utils/toolbarState.js.map +1 -0
- package/dist/src/nile-rich-text-editor/rte-utils/vars.d.ts +1 -0
- package/dist/src/nile-rich-text-editor/rte-utils/vars.js +14 -0
- package/dist/src/nile-rich-text-editor/rte-utils/vars.js.map +1 -0
- package/dist/src/nile-rich-text-editor/utils.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/nile-rich-text-editor/nile-rich-text-editor.css.ts +11 -8
- package/src/nile-rich-text-editor/nile-rich-text-editor.ts +46 -26
- package/src/nile-rich-text-editor/nile-rte-select.ts +178 -173
- package/src/nile-rich-text-editor/utils.ts +342 -341
@@ -1,189 +1,189 @@
|
|
1
1
|
// src/nile-rich-text-editor/utils.ts
|
2
2
|
export function closestBlock(node: Node | null, root: HTMLElement): HTMLElement | null {
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
while (node && node !== root) {
|
4
|
+
if (node instanceof HTMLElement) {
|
5
|
+
const display = getComputedStyle(node).display;
|
6
|
+
if (node.tagName.match(/^(P|DIV|H1|H2|H3|H4|H5|H6|LI)$/) || display === 'block' || display === 'list-item') {
|
7
|
+
return node;
|
8
|
+
}
|
8
9
|
}
|
10
|
+
node = node?.parentNode || null;
|
9
11
|
}
|
10
|
-
|
12
|
+
return root;
|
11
13
|
}
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
while (n && !(n instanceof HTMLElement)) n = n.parentNode as Node | null;
|
17
|
-
return n as HTMLElement | null;
|
18
|
-
}
|
19
|
-
|
20
|
-
export function rgbToHex(rgb: string): string {
|
21
|
-
const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
|
22
|
-
if (!m) return '#000000';
|
23
|
-
const r = Number(m[1]).toString(16).padStart(2,'0');
|
24
|
-
const g = Number(m[2]).toString(16).padStart(2,'0');
|
25
|
-
const b = Number(m[3]).toString(16).padStart(2,'0');
|
26
|
-
return `#${r}${g}${b}`;
|
27
|
-
}
|
28
|
-
|
29
|
-
export function unwrap(node: HTMLElement) {
|
30
|
-
const p = node.parentNode; if (!p) return;
|
31
|
-
while (node.firstChild) p.insertBefore(node.firstChild, node);
|
32
|
-
p.removeChild(node);
|
33
|
-
}
|
34
|
-
|
35
|
-
export function surroundInline(range: Range, tag: string, attrs?: Record<string,string>) {
|
36
|
-
const wrap = document.createElement(tag);
|
37
|
-
if (attrs) Object.entries(attrs).forEach(([k,v]) => wrap.setAttribute(k, v));
|
38
|
-
try { range.surroundContents(wrap); }
|
39
|
-
catch {
|
40
|
-
const frag = range.extractContents();
|
41
|
-
wrap.appendChild(frag);
|
42
|
-
range.insertNode(wrap);
|
14
|
+
|
15
|
+
export function nearestElement(n: Node | null): HTMLElement | null {
|
16
|
+
while (n && !(n instanceof HTMLElement)) n = n.parentNode as Node | null;
|
17
|
+
return n as HTMLElement | null;
|
43
18
|
}
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
const elm = document.createElement(tag);
|
53
|
-
if (attrs) Object.entries(attrs).forEach(([k,v]) => elm.setAttribute(k,v));
|
54
|
-
elm.appendChild(document.createTextNode('\u200b'));
|
55
|
-
range.insertNode(elm);
|
56
|
-
const r = document.createRange();
|
57
|
-
r.setStart(elm.firstChild!, 1);
|
58
|
-
r.collapse(true);
|
59
|
-
sel.removeAllRanges(); sel.addRange(r);
|
60
|
-
return;
|
19
|
+
|
20
|
+
export function rgbToHex(rgb: string): string {
|
21
|
+
const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
|
22
|
+
if (!m) return '#000000';
|
23
|
+
const r = Number(m[1]).toString(16).padStart(2,'0');
|
24
|
+
const g = Number(m[2]).toString(16).padStart(2,'0');
|
25
|
+
const b = Number(m[3]).toString(16).padStart(2,'0');
|
26
|
+
return `#${r}${g}${b}`;
|
61
27
|
}
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
n = n.parentNode;
|
28
|
+
|
29
|
+
export function unwrap(node: HTMLElement) {
|
30
|
+
const p = node.parentNode; if (!p) return;
|
31
|
+
while (node.firstChild) p.insertBefore(node.firstChild, node);
|
32
|
+
p.removeChild(node);
|
68
33
|
}
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
range.
|
99
|
-
|
34
|
+
|
35
|
+
export function surroundInline(range: Range, tag: string, attrs?: Record<string,string>) {
|
36
|
+
const wrap = document.createElement(tag);
|
37
|
+
if (attrs) Object.entries(attrs).forEach(([k,v]) => wrap.setAttribute(k, v));
|
38
|
+
try { range.surroundContents(wrap); }
|
39
|
+
catch {
|
40
|
+
const frag = range.extractContents();
|
41
|
+
wrap.appendChild(frag);
|
42
|
+
range.insertNode(wrap);
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
export function toggleInlineTag(root: HTMLElement, tag: 'strong'|'em'|'u'|'span', attrs?: Record<string,string>) {
|
47
|
+
const sel = document.getSelection();
|
48
|
+
if (!sel || sel.rangeCount === 0) return;
|
49
|
+
const range = sel.getRangeAt(0);
|
50
|
+
|
51
|
+
if (range.collapsed) {
|
52
|
+
const elm = document.createElement(tag);
|
53
|
+
if (attrs) Object.entries(attrs).forEach(([k,v]) => elm.setAttribute(k,v));
|
54
|
+
elm.appendChild(document.createTextNode('\u200b'));
|
55
|
+
range.insertNode(elm);
|
56
|
+
const r = document.createRange();
|
57
|
+
r.setStart(elm.firstChild!, 1);
|
58
|
+
r.collapse(true);
|
59
|
+
sel.removeAllRanges(); sel.addRange(r);
|
60
|
+
return;
|
61
|
+
}
|
62
|
+
|
63
|
+
let n: Node | null = range.startContainer;
|
64
|
+
let target: HTMLElement | null = null;
|
65
|
+
while (n && n !== root) {
|
66
|
+
if (n instanceof HTMLElement && n.tagName.toLowerCase() === tag) { target = n; break; }
|
67
|
+
n = n.parentNode;
|
68
|
+
}
|
69
|
+
if (target) unwrap(target); else surroundInline(range, tag, attrs);
|
70
|
+
}
|
71
|
+
|
72
|
+
export function setBlockTag(root: HTMLElement, tag: 'p'|'h1'|'h2'|'h3') {
|
73
|
+
const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
|
74
|
+
const range = sel.getRangeAt(0);
|
75
|
+
const block = closestBlock(range.startContainer, root); if (!block) return;
|
76
|
+
if (block.tagName.toLowerCase() === tag) return;
|
77
|
+
const nb = document.createElement(tag);
|
78
|
+
while (block.firstChild) nb.appendChild(block.firstChild);
|
79
|
+
block.replaceWith(nb);
|
80
|
+
const r = document.createRange(); r.selectNodeContents(nb); r.collapse(true);
|
100
81
|
sel.removeAllRanges(); sel.addRange(r);
|
101
|
-
return;
|
102
82
|
}
|
103
|
-
|
104
|
-
|
105
|
-
|
83
|
+
|
84
|
+
export function setAlignment(root: HTMLElement, align: 'left'|'center'|'right'|'justify') {
|
85
|
+
const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
|
86
|
+
const range = sel.getRangeAt(0);
|
87
|
+
const block = closestBlock(range.startContainer, root); if (!block) return;
|
88
|
+
block.style.textAlign = align === 'justify' ? 'justify' : align;
|
89
|
+
}
|
90
|
+
|
91
|
+
export function setFontFamily(root: HTMLElement, family: string) {
|
92
|
+
const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
|
93
|
+
const range = sel.getRangeAt(0);
|
94
|
+
if (range.collapsed) {
|
95
|
+
const span = document.createElement('span');
|
96
|
+
span.style.fontFamily = family;
|
97
|
+
span.appendChild(document.createTextNode('\u200b'));
|
98
|
+
range.insertNode(span);
|
99
|
+
const r = document.createRange(); r.setStart(span.firstChild!, 1); r.collapse(true);
|
100
|
+
sel.removeAllRanges(); sel.addRange(r);
|
101
|
+
return;
|
102
|
+
}
|
103
|
+
surroundInline(range, 'span', { style: `font-family:${family}` });
|
104
|
+
}
|
106
105
|
|
106
|
+
|
107
107
|
function enclosingStyledSpan(
|
108
|
-
editor: HTMLElement,
|
109
|
-
node: Node | null,
|
110
|
-
dataAttr: 'data-rte-color' | 'data-rte-bg'
|
108
|
+
editor: HTMLElement,
|
109
|
+
node: Node | null,
|
110
|
+
dataAttr: 'data-rte-color' | 'data-rte-bg'
|
111
111
|
): HTMLSpanElement | null {
|
112
|
-
while (node && node !== editor) {
|
113
|
-
|
114
|
-
|
112
|
+
while (node && node !== editor) {
|
113
|
+
if (node instanceof HTMLSpanElement && node.hasAttribute(dataAttr)) {
|
114
|
+
return node;
|
115
|
+
}
|
116
|
+
node = node.parentNode;
|
115
117
|
}
|
116
|
-
|
117
|
-
}
|
118
|
-
return null;
|
118
|
+
return null;
|
119
119
|
}
|
120
120
|
|
121
|
-
|
121
|
+
|
122
122
|
function applyInlineStyle(
|
123
|
-
editor: HTMLElement,
|
124
|
-
cssProp: 'color' | 'backgroundColor',
|
125
|
-
value: string,
|
126
|
-
dataAttr: 'data-rte-color' | 'data-rte-bg'
|
123
|
+
editor: HTMLElement,
|
124
|
+
cssProp: 'color' | 'backgroundColor',
|
125
|
+
value: string,
|
126
|
+
dataAttr: 'data-rte-color' | 'data-rte-bg'
|
127
127
|
) {
|
128
|
-
const sel = window.getSelection();
|
129
|
-
if (!sel || sel.rangeCount === 0) return;
|
130
|
-
const r0 = sel.getRangeAt(0);
|
131
|
-
if (!editor.contains(r0.commonAncestorContainer)) return;
|
132
|
-
|
133
|
-
const range = r0.cloneRange();
|
134
|
-
|
135
|
-
|
136
|
-
if (range.collapsed) {
|
137
|
-
const enclosing = enclosingStyledSpan(editor, range.startContainer, dataAttr);
|
138
|
-
if (enclosing) {
|
139
|
-
(enclosing.style as any)[cssProp] = value;
|
140
|
-
mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
|
141
|
-
return;
|
142
|
-
}
|
128
|
+
const sel = window.getSelection();
|
129
|
+
if (!sel || sel.rangeCount === 0) return;
|
130
|
+
const r0 = sel.getRangeAt(0);
|
131
|
+
if (!editor.contains(r0.commonAncestorContainer)) return;
|
143
132
|
|
144
|
-
const
|
145
|
-
s.setAttribute(dataAttr, '1');
|
146
|
-
(s.style as any)[cssProp] = value;
|
147
|
-
s.appendChild(document.createTextNode('\u200B'));
|
148
|
-
range.insertNode(s);
|
133
|
+
const range = r0.cloneRange();
|
149
134
|
|
135
|
+
|
136
|
+
if (range.collapsed) {
|
137
|
+
const enclosing = enclosingStyledSpan(editor, range.startContainer, dataAttr);
|
138
|
+
if (enclosing) {
|
139
|
+
(enclosing.style as any)[cssProp] = value;
|
140
|
+
mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
|
141
|
+
return;
|
142
|
+
}
|
150
143
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
144
|
+
const s = document.createElement('span');
|
145
|
+
s.setAttribute(dataAttr, '1');
|
146
|
+
(s.style as any)[cssProp] = value;
|
147
|
+
s.appendChild(document.createTextNode('\u200B'));
|
148
|
+
range.insertNode(s);
|
155
149
|
|
156
|
-
|
157
|
-
|
158
|
-
|
150
|
+
|
151
|
+
const caret = document.createRange();
|
152
|
+
caret.setStart(s.firstChild!, 1);
|
153
|
+
caret.collapse(true);
|
154
|
+
sel.removeAllRanges(); sel.addRange(caret);
|
159
155
|
|
156
|
+
mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
|
157
|
+
return;
|
158
|
+
}
|
160
159
|
|
161
|
-
const leftEdge = enclosingStyledSpan(editor, range.startContainer, dataAttr);
|
162
|
-
const rightEdge = enclosingStyledSpan(editor, range.endContainer, dataAttr);
|
163
|
-
if (leftEdge && leftEdge === rightEdge) {
|
164
160
|
|
165
|
-
|
166
|
-
|
167
|
-
|
161
|
+
const leftEdge = enclosingStyledSpan(editor, range.startContainer, dataAttr);
|
162
|
+
const rightEdge = enclosingStyledSpan(editor, range.endContainer, dataAttr);
|
163
|
+
if (leftEdge && leftEdge === rightEdge) {
|
168
164
|
|
169
|
-
|
170
|
-
leftEdge
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
165
|
+
if (rangeCoversWholeNode(range, leftEdge)) {
|
166
|
+
(leftEdge.style as any)[cssProp] = value;
|
167
|
+
} else {
|
168
|
+
|
169
|
+
const mid = splitAndRecolorWithinSpan(
|
170
|
+
leftEdge,
|
171
|
+
range,
|
172
|
+
dataAttr,
|
173
|
+
cssProp,
|
174
|
+
value
|
175
|
+
);
|
176
|
+
|
177
|
+
|
178
|
+
const sel = window.getSelection();
|
179
|
+
const r = document.createRange();
|
180
|
+
r.selectNodeContents(mid);
|
181
|
+
sel?.removeAllRanges();
|
182
|
+
sel?.addRange(r);
|
183
|
+
}
|
184
|
+
mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
|
185
|
+
return;
|
183
186
|
}
|
184
|
-
mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
|
185
|
-
return;
|
186
|
-
}
|
187
187
|
|
188
188
|
|
189
189
|
const commonEl = (() => {
|
@@ -192,227 +192,228 @@ if (leftEdge && leftEdge === rightEdge) {
|
|
192
192
|
return n as HTMLElement | null;
|
193
193
|
})();
|
194
194
|
|
195
|
-
const walker = document.createTreeWalker(
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
195
|
+
const walker = document.createTreeWalker(
|
196
|
+
commonEl || editor,
|
197
|
+
NodeFilter.SHOW_TEXT,
|
198
|
+
{
|
199
|
+
acceptNode: (n) => {
|
200
|
+
if (!n.nodeValue || !n.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
|
201
|
+
const nodeRange = document.createRange();
|
202
|
+
nodeRange.selectNodeContents(n);
|
203
|
+
const intersects =
|
204
|
+
range.compareBoundaryPoints(Range.END_TO_START, nodeRange) < 0 &&
|
205
|
+
range.compareBoundaryPoints(Range.START_TO_END, nodeRange) > 0;
|
206
|
+
return intersects ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
|
207
|
+
}
|
208
|
+
}
|
209
|
+
);
|
210
|
+
|
211
|
+
const toProcess: Text[] = [];
|
212
|
+
let t: Node | null;
|
213
|
+
while ((t = walker.nextNode())) toProcess.push(t as Text);
|
214
|
+
|
215
|
+
toProcess.forEach((text) => {
|
216
|
+
|
217
|
+
let start = 0, end = text.length;
|
218
|
+
if (text === range.startContainer) start = range.startOffset;
|
219
|
+
if (text === range.endContainer) end = range.endOffset;
|
220
|
+
if (start > 0) text = text.splitText(start);
|
221
|
+
if (end < text.length) text.splitText(end);
|
222
|
+
|
223
|
+
// If this slice already sits in a styled span → update it, don’t nest.
|
224
|
+
const existing = enclosingStyledSpan(editor, text, dataAttr);
|
225
|
+
if (existing) {
|
226
|
+
(existing.style as any)[cssProp] = value;
|
227
|
+
return;
|
207
228
|
}
|
208
|
-
}
|
209
|
-
);
|
210
|
-
|
211
|
-
const toProcess: Text[] = [];
|
212
|
-
let t: Node | null;
|
213
|
-
while ((t = walker.nextNode())) toProcess.push(t as Text);
|
214
|
-
|
215
|
-
toProcess.forEach((text) => {
|
216
|
-
|
217
|
-
let start = 0, end = text.length;
|
218
|
-
if (text === range.startContainer) start = range.startOffset;
|
219
|
-
if (text === range.endContainer) end = range.endOffset;
|
220
|
-
if (start > 0) text = text.splitText(start);
|
221
|
-
if (end < text.length) text.splitText(end);
|
222
|
-
|
223
|
-
// If this slice already sits in a styled span → update it, don’t nest.
|
224
|
-
const existing = enclosingStyledSpan(editor, text, dataAttr);
|
225
|
-
if (existing) {
|
226
|
-
(existing.style as any)[cssProp] = value;
|
227
|
-
return;
|
228
|
-
}
|
229
229
|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
});
|
230
|
+
// Create a new, single-purpose span
|
231
|
+
const span = document.createElement('span');
|
232
|
+
span.setAttribute(dataAttr, '1');
|
233
|
+
(span.style as any)[cssProp] = value;
|
234
|
+
const parent = text.parentElement!;
|
235
|
+
parent.replaceChild(span, text);
|
236
|
+
span.appendChild(text);
|
237
|
+
});
|
238
238
|
|
239
|
-
mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
|
239
|
+
mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
|
240
240
|
|
241
|
-
// restore selection
|
242
|
-
sel.removeAllRanges(); sel.addRange(range);
|
241
|
+
// restore selection
|
242
|
+
sel.removeAllRanges(); sel.addRange(range);
|
243
243
|
}
|
244
244
|
|
245
|
-
// Is the range covering the entire node's contents?
|
245
|
+
// Is the range covering the entire node's contents?
|
246
246
|
function rangeCoversWholeNode(range: Range, node: Node): boolean {
|
247
|
-
const all = document.createRange();
|
248
|
-
all.selectNodeContents(node);
|
249
|
-
return (
|
250
|
-
|
251
|
-
|
252
|
-
);
|
247
|
+
const all = document.createRange();
|
248
|
+
all.selectNodeContents(node);
|
249
|
+
return (
|
250
|
+
range.compareBoundaryPoints(Range.START_TO_START, all) <= 0 &&
|
251
|
+
range.compareBoundaryPoints(Range.END_TO_END, all) >= 0
|
252
|
+
);
|
253
253
|
}
|
254
254
|
|
255
255
|
function hasRangeContent(r: Range): boolean {
|
256
|
-
if (r.collapsed) return false;
|
257
|
-
const text = r.cloneContents().textContent || '';
|
258
|
-
return text.length > 0;
|
256
|
+
if (r.collapsed) return false;
|
257
|
+
const text = r.cloneContents().textContent || '';
|
258
|
+
return text.length > 0;
|
259
259
|
}
|
260
260
|
|
261
261
|
// Split one styled span into [left][middle][right]; recolor only middle
|
262
262
|
function splitAndRecolorWithinSpan(
|
263
|
-
span: HTMLSpanElement,
|
264
|
-
range: Range,
|
265
|
-
dataAttr: 'data-rte-color' | 'data-rte-bg',
|
266
|
-
cssProp: 'color' | 'backgroundColor',
|
267
|
-
newValue: string
|
263
|
+
span: HTMLSpanElement,
|
264
|
+
range: Range,
|
265
|
+
dataAttr: 'data-rte-color' | 'data-rte-bg',
|
266
|
+
cssProp: 'color' | 'backgroundColor',
|
267
|
+
newValue: string
|
268
268
|
): HTMLSpanElement {
|
269
|
-
const oldValue = (span.style as any)[cssProp];
|
270
|
-
|
271
|
-
const left = document.createRange();
|
272
|
-
left.setStart(span, 0);
|
273
|
-
left.setEnd(range.startContainer, range.startOffset);
|
274
|
-
|
275
|
-
const right = document.createRange();
|
276
|
-
right.setStart(range.endContainer, range.endOffset);
|
277
|
-
right.setEnd(span, span.childNodes.length);
|
278
|
-
|
279
|
-
// Build replacement fragment
|
280
|
-
const frag = document.createDocumentFragment();
|
281
|
-
|
282
|
-
// helper to make a styled clone shell
|
283
|
-
const makeShell = (val: string) => {
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
};
|
289
|
-
|
290
|
-
if (hasRangeContent(left)) {
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
}
|
269
|
+
const oldValue = (span.style as any)[cssProp];
|
270
|
+
|
271
|
+
const left = document.createRange();
|
272
|
+
left.setStart(span, 0);
|
273
|
+
left.setEnd(range.startContainer, range.startOffset);
|
274
|
+
|
275
|
+
const right = document.createRange();
|
276
|
+
right.setStart(range.endContainer, range.endOffset);
|
277
|
+
right.setEnd(span, span.childNodes.length);
|
278
|
+
|
279
|
+
// Build replacement fragment
|
280
|
+
const frag = document.createDocumentFragment();
|
281
|
+
|
282
|
+
// helper to make a styled clone shell
|
283
|
+
const makeShell = (val: string) => {
|
284
|
+
const s = document.createElement('span');
|
285
|
+
s.setAttribute(dataAttr, '1');
|
286
|
+
(s.style as any)[cssProp] = val;
|
287
|
+
return s;
|
288
|
+
};
|
289
|
+
|
290
|
+
if (hasRangeContent(left)) {
|
291
|
+
const sLeft = makeShell(oldValue);
|
292
|
+
sLeft.appendChild(left.cloneContents());
|
293
|
+
frag.appendChild(sLeft);
|
294
|
+
}
|
295
295
|
|
296
|
-
const mid = makeShell(newValue);
|
297
|
-
mid.appendChild(range.cloneContents());
|
298
|
-
frag.appendChild(mid);
|
296
|
+
const mid = makeShell(newValue);
|
297
|
+
mid.appendChild(range.cloneContents());
|
298
|
+
frag.appendChild(mid);
|
299
299
|
|
300
|
-
if (hasRangeContent(right)) {
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
}
|
300
|
+
if (hasRangeContent(right)) {
|
301
|
+
const sRight = makeShell(oldValue);
|
302
|
+
sRight.appendChild(right.cloneContents());
|
303
|
+
frag.appendChild(sRight);
|
304
|
+
}
|
305
305
|
|
306
|
-
// Replace original span
|
307
|
-
span.replaceWith(frag);
|
308
|
-
return mid; // return the middle span so caller can restore selection
|
306
|
+
// Replace original span
|
307
|
+
span.replaceWith(frag);
|
308
|
+
return mid; // return the middle span so caller can restore selection
|
309
309
|
}
|
310
310
|
|
311
311
|
|
312
312
|
function mergeAdjacentStyledSpans(
|
313
|
-
root: HTMLElement,
|
314
|
-
dataAttr: 'data-rte-color' | 'data-rte-bg',
|
315
|
-
cssProp: 'color' | 'backgroundColor'
|
313
|
+
root: HTMLElement,
|
314
|
+
dataAttr: 'data-rte-color' | 'data-rte-bg',
|
315
|
+
cssProp: 'color' | 'backgroundColor'
|
316
316
|
) {
|
317
|
-
const spans = Array.from(root.querySelectorAll<HTMLSpanElement>(`span[${dataAttr}]`));
|
317
|
+
const spans = Array.from(root.querySelectorAll<HTMLSpanElement>(`span[${dataAttr}]`));
|
318
318
|
|
319
|
-
const valOf = (el: HTMLElement) => (el.style as any)[cssProp];
|
319
|
+
const valOf = (el: HTMLElement) => (el.style as any)[cssProp];
|
320
320
|
|
321
|
-
spans.forEach((s) => {
|
321
|
+
spans.forEach((s) => {
|
322
|
+
|
323
|
+
const nested = Array.from(s.querySelectorAll<HTMLSpanElement>(`span[${dataAttr}]`));
|
324
|
+
nested.forEach((child) => {
|
325
|
+
if (valOf(child) === valOf(s)) {
|
326
|
+
while (child.firstChild) s.insertBefore(child.firstChild, child);
|
327
|
+
child.remove();
|
328
|
+
}
|
329
|
+
});
|
330
|
+
|
331
|
+
|
332
|
+
const prev = s.previousSibling;
|
333
|
+
if (prev instanceof HTMLSpanElement &&
|
334
|
+
prev.hasAttribute(dataAttr) &&
|
335
|
+
valOf(prev) === valOf(s)) {
|
336
|
+
while (s.firstChild) prev.appendChild(s.firstChild);
|
337
|
+
s.remove();
|
338
|
+
return; // s is gone, next checks not needed
|
339
|
+
}
|
322
340
|
|
323
|
-
|
324
|
-
|
325
|
-
if (
|
326
|
-
|
327
|
-
|
341
|
+
// 3) Merge with next sibling if identical
|
342
|
+
const next = s.nextSibling;
|
343
|
+
if (next instanceof HTMLSpanElement &&
|
344
|
+
next.hasAttribute(dataAttr) &&
|
345
|
+
valOf(next) === valOf(s)) {
|
346
|
+
while (next.firstChild) s.appendChild(next.firstChild);
|
347
|
+
next.remove();
|
328
348
|
}
|
329
349
|
});
|
330
|
-
|
331
|
-
|
332
|
-
const prev = s.previousSibling;
|
333
|
-
if (prev instanceof HTMLSpanElement &&
|
334
|
-
prev.hasAttribute(dataAttr) &&
|
335
|
-
valOf(prev) === valOf(s)) {
|
336
|
-
while (s.firstChild) prev.appendChild(s.firstChild);
|
337
|
-
s.remove();
|
338
|
-
return; // s is gone, next checks not needed
|
339
|
-
}
|
340
|
-
|
341
|
-
// 3) Merge with next sibling if identical
|
342
|
-
const next = s.nextSibling;
|
343
|
-
if (next instanceof HTMLSpanElement &&
|
344
|
-
next.hasAttribute(dataAttr) &&
|
345
|
-
valOf(next) === valOf(s)) {
|
346
|
-
while (next.firstChild) s.appendChild(next.firstChild);
|
347
|
-
next.remove();
|
348
|
-
}
|
349
|
-
});
|
350
350
|
}
|
351
351
|
|
352
352
|
|
353
353
|
export function setForeColor(editor: HTMLElement, hex: string) {
|
354
|
-
applyInlineStyle(editor, 'color', hex, 'data-rte-color');
|
354
|
+
applyInlineStyle(editor, 'color', hex, 'data-rte-color');
|
355
355
|
}
|
356
356
|
export function setBackColor(editor: HTMLElement, hex: string) {
|
357
|
-
applyInlineStyle(editor, 'backgroundColor', hex, 'data-rte-bg');
|
357
|
+
applyInlineStyle(editor, 'backgroundColor', hex, 'data-rte-bg');
|
358
358
|
}
|
359
359
|
|
360
360
|
|
361
|
-
export function toggleList(root: HTMLElement, kind: 'ul'|'ol') {
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
361
|
+
export function toggleList(root: HTMLElement, kind: 'ul'|'ol') {
|
362
|
+
const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
|
363
|
+
const range = sel.getRangeAt(0);
|
364
|
+
const block = closestBlock(range.startContainer, root); if (!block) return;
|
365
|
+
|
366
|
+
let li = block.closest('li');
|
367
|
+
if (li) {
|
368
|
+
const list = li.closest('ul,ol') as HTMLElement | null;
|
369
|
+
if (!list) return;
|
370
|
+
if (list.tagName.toLowerCase() === kind) {
|
371
|
+
const parent = list.parentElement!;
|
372
|
+
const frag = document.createDocumentFragment();
|
373
|
+
for (const child of Array.from(list.children)) {
|
374
|
+
if (child.tagName.toLowerCase() === 'li') {
|
375
|
+
const p = document.createElement('p');
|
376
|
+
while (child.firstChild) p.appendChild(child.firstChild);
|
377
|
+
frag.appendChild(p);
|
378
|
+
}
|
378
379
|
}
|
380
|
+
parent.replaceChild(frag, list);
|
381
|
+
} else {
|
382
|
+
const newList = document.createElement(kind);
|
383
|
+
while (list.firstChild) newList.appendChild(list.firstChild);
|
384
|
+
list.replaceWith(newList);
|
379
385
|
}
|
380
|
-
|
381
|
-
} else {
|
382
|
-
const newList = document.createElement(kind);
|
383
|
-
while (list.firstChild) newList.appendChild(list.firstChild);
|
384
|
-
list.replaceWith(newList);
|
386
|
+
return;
|
385
387
|
}
|
386
|
-
|
388
|
+
|
389
|
+
const list = document.createElement(kind);
|
390
|
+
const item = document.createElement('li'); list.appendChild(item);
|
391
|
+
while (block.firstChild) item.appendChild(block.firstChild);
|
392
|
+
block.replaceWith(list);
|
387
393
|
}
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
const link = document.createElement('a'); link.href = url;
|
410
|
-
|
411
|
-
|
412
|
-
return;
|
394
|
+
|
395
|
+
export function insertOrEditLink(root: HTMLElement, href?: string) {
|
396
|
+
const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
|
397
|
+
const range = sel.getRangeAt(0);
|
398
|
+
|
399
|
+
let n: Node | null = range.startContainer;
|
400
|
+
let a: HTMLAnchorElement | null = null;
|
401
|
+
while (n && n !== root) { if (n instanceof HTMLAnchorElement) { a = n; break; } n = n.parentNode; }
|
402
|
+
|
403
|
+
const url = href ?? (typeof window !== 'undefined' ? window.prompt('Enter URL', a?.href || 'https://') || '' : '');
|
404
|
+
if (!url) return;
|
405
|
+
|
406
|
+
if (a) { a.href = url; return; }
|
407
|
+
|
408
|
+
if (range.collapsed) {
|
409
|
+
const link = document.createElement('a'); link.href = url; link.textContent = url; range.insertNode(link);
|
410
|
+
const r = document.createRange(); r.setStartAfter(link); r.collapse(true);
|
411
|
+
sel.removeAllRanges(); sel.addRange(r);
|
412
|
+
return;
|
413
|
+
}
|
414
|
+
|
415
|
+
const link = document.createElement('a'); link.href = url;
|
416
|
+
try { range.surroundContents(link); }
|
417
|
+
catch { const frag = range.extractContents(); link.appendChild(frag); range.insertNode(link); }
|
413
418
|
}
|
414
|
-
|
415
|
-
const link = document.createElement('a'); link.href = url;
|
416
|
-
try { range.surroundContents(link); }
|
417
|
-
catch { const frag = range.extractContents(); link.appendChild(frag); range.insertNode(link); }
|
418
|
-
}
|
419
|
+
|