@aquera/nile-elements 0.1.67-beta-1.2 → 0.1.67-beta-1.3

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.
Files changed (25) hide show
  1. package/demo/index.html +9 -9
  2. package/dist/index.js +31 -29
  3. package/dist/nile-rich-text-editor/nile-rich-text-editor.cjs.js +1 -1
  4. package/dist/nile-rich-text-editor/nile-rich-text-editor.cjs.js.map +1 -1
  5. package/dist/nile-rich-text-editor/nile-rich-text-editor.css.cjs.js +1 -1
  6. package/dist/nile-rich-text-editor/nile-rich-text-editor.css.cjs.js.map +1 -1
  7. package/dist/nile-rich-text-editor/nile-rich-text-editor.css.esm.js +3 -1
  8. package/dist/nile-rich-text-editor/nile-rich-text-editor.esm.js +1 -1
  9. package/dist/nile-rich-text-editor/utils.cjs.js +1 -1
  10. package/dist/nile-rich-text-editor/utils.cjs.js.map +1 -1
  11. package/dist/nile-rich-text-editor/utils.esm.js +1 -1
  12. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.css.js +3 -1
  13. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.css.js.map +1 -1
  14. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.d.ts +1 -0
  15. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.js +64 -8
  16. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.js.map +1 -1
  17. package/dist/src/nile-rich-text-editor/utils.d.ts +2 -2
  18. package/dist/src/nile-rich-text-editor/utils.js +181 -27
  19. package/dist/src/nile-rich-text-editor/utils.js.map +1 -1
  20. package/dist/tsconfig.tsbuildinfo +1 -1
  21. package/package.json +1 -1
  22. package/src/nile-rich-text-editor/nile-rich-text-editor.css.ts +3 -1
  23. package/src/nile-rich-text-editor/nile-rich-text-editor.ts +89 -11
  24. package/src/nile-rich-text-editor/utils.ts +248 -26
  25. package/vscode-html-custom-data.json +1 -1
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Webcomponent nile-elements following open-wc recommendations",
4
4
  "license": "MIT",
5
5
  "author": "nile-elements",
6
- "version": "0.1.67-beta-1.2",
6
+ "version": "0.1.67-beta-1.3",
7
7
  "main": "dist/src/index.js",
8
8
  "type": "module",
9
9
  "module": "dist/src/index.js",
@@ -42,7 +42,9 @@ nile-rte-toolbar-item > nile-button::part(base) {
42
42
  .editor h5 { font-size:0.83em }
43
43
  .editor h6 { font-size:0.67em }
44
44
 
45
- .editor { min-height:160px; padding:12px; border:1px solid #e5e7eb; border-radius:0 0 8px 8px; background:#fff; outline:none; }
45
+ .editor { min-height:160px; padding:12px; border:1px solid #e5e7eb; border-radius:0 0 8px 8px; background:#fff; outline:none; white-space: pre-wrap;
46
+ tab-size: 4;
47
+ -moz-tab-size: 4; }
46
48
  nile-rte-preview { display:block; margin-top:10px; padding:10px; border:1px dashed #cbd5e1; border-radius:8px; background:#fafafa; }
47
49
 
48
50
  .rte-color-trigger {
@@ -159,7 +159,7 @@ private bgSwatchEl: HTMLElement | null = null;
159
159
  private ensureEditor() {
160
160
  this.editorEl = this.querySelector('.editor') as HTMLElement;
161
161
  if (!this.editorEl) {
162
- const editor = document.createElement('div');
162
+ const editor = document.createElement('article');
163
163
  editor.className = 'editor';
164
164
  editor.setAttribute('contenteditable','true');
165
165
  if (this.toolbarEl?.nextSibling) {
@@ -178,14 +178,65 @@ private bgSwatchEl: HTMLElement | null = null;
178
178
  }
179
179
 
180
180
  private wireEditor() {
181
- // Input & selection preservation
182
181
  this.editorEl.addEventListener('input', () => {
183
182
  this.ensureAtLeastOneParagraph();
184
183
  this.updateContent();
185
184
  });
186
185
  this.editorEl.addEventListener('mouseup', () => this.saveSelection());
187
- this.editorEl.addEventListener('keyup', () => this.saveSelection());
186
+ this.editorEl.addEventListener('keyup', () => this.saveSelection());
187
+ this.editorEl.addEventListener('keydown', this.onEditorKeydown);
188
188
  }
189
+
190
+
191
+ private onEditorKeydown = (e: KeyboardEvent) => {
192
+ if (e.key !== 'Tab') return;
193
+
194
+ e.preventDefault();
195
+ this.focusAndRestore();
196
+
197
+ const sel = window.getSelection();
198
+ if (!sel || sel.rangeCount === 0) return;
199
+ const range = sel.getRangeAt(0);
200
+
201
+
202
+ if (e.shiftKey) {
203
+ if (range.collapsed && range.startContainer.nodeType === Node.TEXT_NODE) {
204
+ const t = range.startContainer as Text;
205
+ const off = range.startOffset;
206
+ const before = t.data.slice(0, off);
207
+
208
+
209
+ const removed = before.replace(/(\t|[ \u00a0]{2})$/, '');
210
+ if (removed.length !== before.length) {
211
+ t.data = removed + t.data.slice(off);
212
+ const r = document.createRange();
213
+ r.setStart(t, removed.length);
214
+ r.collapse(true);
215
+ sel.removeAllRanges();
216
+ sel.addRange(r);
217
+ this.updateContent();
218
+ this.updateToolbarState();
219
+ }
220
+ }
221
+ return;
222
+ }
223
+
224
+
225
+ range.deleteContents();
226
+ const tabNode = document.createTextNode('\t');
227
+ range.insertNode(tabNode);
228
+
229
+
230
+ const r = document.createRange();
231
+ r.setStartAfter(tabNode);
232
+ r.collapse(true);
233
+ sel.removeAllRanges();
234
+ sel.addRange(r);
235
+
236
+ this.updateContent();
237
+ this.updateToolbarState();
238
+ };
239
+
189
240
 
190
241
  private wireAuthoredToolbar(tb: HTMLElement) {
191
242
 
@@ -239,7 +290,6 @@ private bgSwatchEl: HTMLElement | null = null;
239
290
  }
240
291
  child.innerHTML = '';
241
292
  } else {
242
- // Author provided custom content (could be a <nile-icon> already)
243
293
  btn.innerHTML = child.innerHTML;
244
294
  child.innerHTML = '';
245
295
  }
@@ -252,12 +302,12 @@ private bgSwatchEl: HTMLElement | null = null;
252
302
  btn.addEventListener('mousedown', e => e.preventDefault());
253
303
  btn.addEventListener('click', () => this.onToolbarCommand(cmd));
254
304
 
255
- // Register for active reflection
305
+
256
306
  const arr = this.buttonMap.get(cmd) ?? [];
257
307
  arr.push(btn);
258
308
  this.buttonMap.set(cmd, arr);
259
309
 
260
- return; // done with this child
310
+ return;
261
311
  }
262
312
  if (tag === 'nile-rte-select') {
263
313
  const type = child.getAttribute('type') || '';
@@ -621,15 +671,43 @@ private ensureAtLeastOneParagraph() {
621
671
  private updateContent() {
622
672
  if (!this.editorEl) return;
623
673
  this.ensureAtLeastOneParagraph();
624
- this.content = this.editorEl.innerHTML;
625
-
626
- // live preview on the authored node (kept in place)
674
+
675
+ // Clone content for safe manipulation
676
+ const clone = this.editorEl.cloneNode(true) as HTMLElement;
677
+
678
+ // Walk the original DOM and the clone in parallel
679
+ const origWalker = document.createTreeWalker(this.editorEl, NodeFilter.SHOW_ELEMENT);
680
+ const cloneWalker = document.createTreeWalker(clone, NodeFilter.SHOW_ELEMENT);
681
+
682
+ while (origWalker.nextNode() && cloneWalker.nextNode()) {
683
+ const origEl = origWalker.currentNode as HTMLElement;
684
+ const cloneEl = cloneWalker.currentNode as HTMLElement;
685
+ const computed = window.getComputedStyle(origEl);
686
+
687
+ // Dump *all* computed styles into a single inline style attribute
688
+ const cssText = Array.from(computed)
689
+ .map(prop => `${prop}:${computed.getPropertyValue(prop)}`)
690
+ .join(';');
691
+
692
+ cloneEl.setAttribute('style', cssText);
693
+ }
694
+
695
+ // Store the fully inlined HTML
696
+ this.content = clone.innerHTML;
697
+
698
+ // Live preview updates
627
699
  if (this.previewEl) this.previewEl.innerHTML = this.content;
628
-
700
+
701
+ // Emit event with full inline styles
629
702
  this.dispatchEvent(new CustomEvent('content-changed', {
630
- detail: { content: this.content }, bubbles: true, composed: true
703
+ detail: { content: this.content },
704
+ bubbles: true,
705
+ composed: true
631
706
  }));
632
707
  }
708
+
709
+
710
+
633
711
  }
634
712
 
635
713
  declare global {
@@ -103,39 +103,261 @@ export function closestBlock(node: Node | null, root: HTMLElement): HTMLElement
103
103
  surroundInline(range, 'span', { style: `font-family:${family}` });
104
104
  }
105
105
 
106
- export function setBackColor(rootEl: HTMLElement, color: string) {
106
+
107
+ function enclosingStyledSpan(
108
+ editor: HTMLElement,
109
+ node: Node | null,
110
+ dataAttr: 'data-rte-color' | 'data-rte-bg'
111
+ ): HTMLSpanElement | null {
112
+ while (node && node !== editor) {
113
+ if (node instanceof HTMLSpanElement && node.hasAttribute(dataAttr)) {
114
+ return node;
115
+ }
116
+ node = node.parentNode;
117
+ }
118
+ return null;
119
+ }
120
+
121
+
122
+ function applyInlineStyle(
123
+ editor: HTMLElement,
124
+ cssProp: 'color' | 'backgroundColor',
125
+ value: string,
126
+ dataAttr: 'data-rte-color' | 'data-rte-bg'
127
+ ) {
107
128
  const sel = window.getSelection();
108
129
  if (!sel || sel.rangeCount === 0) return;
109
- const range = sel.getRangeAt(0);
110
- if (!rootEl.contains(range.commonAncestorContainer) || range.collapsed) return;
111
-
112
- const span = document.createElement('span');
113
- span.style.backgroundColor = color;
114
- span.appendChild(range.extractContents());
115
- range.insertNode(span);
116
- const after = document.createRange();
117
- after.setStartAfter(span);
118
- after.collapse(true);
119
- sel.removeAllRanges();
120
- sel.addRange(after);
121
- }
130
+ const r0 = sel.getRangeAt(0);
131
+ if (!editor.contains(r0.commonAncestorContainer)) return;
122
132
 
123
-
124
- export function setForeColor(root: HTMLElement, color: string) {
125
- const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
126
- const range = sel.getRangeAt(0);
127
- if (range.collapsed) {
128
- const span = document.createElement('span');
129
- span.style.color = color;
130
- span.appendChild(document.createTextNode('\u200b'));
131
- range.insertNode(span);
132
- const r = document.createRange(); r.setStart(span.firstChild!, 1); r.collapse(true);
133
- sel.removeAllRanges(); sel.addRange(r);
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);
134
141
  return;
135
142
  }
136
- surroundInline(range, 'span', { style: `color:${color}` });
143
+
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);
149
+
150
+
151
+ const caret = document.createRange();
152
+ caret.setStart(s.firstChild!, 1);
153
+ caret.collapse(true);
154
+ sel.removeAllRanges(); sel.addRange(caret);
155
+
156
+ mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
157
+ return;
137
158
  }
159
+
160
+
161
+ const leftEdge = enclosingStyledSpan(editor, range.startContainer, dataAttr);
162
+ const rightEdge = enclosingStyledSpan(editor, range.endContainer, dataAttr);
163
+ if (leftEdge && leftEdge === rightEdge) {
164
+
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
+ );
138
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;
186
+ }
187
+
188
+
189
+ const commonEl = (() => {
190
+ let n: Node | null = range.commonAncestorContainer;
191
+ while (n && !(n instanceof HTMLElement)) n = n.parentNode;
192
+ return n as HTMLElement | null;
193
+ })();
194
+
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;
228
+ }
229
+
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
+
239
+ mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
240
+
241
+ // restore selection
242
+ sel.removeAllRanges(); sel.addRange(range);
243
+ }
244
+
245
+ // Is the range covering the entire node's contents?
246
+ function rangeCoversWholeNode(range: Range, node: Node): boolean {
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
+ }
254
+
255
+ function hasRangeContent(r: Range): boolean {
256
+ if (r.collapsed) return false;
257
+ const text = r.cloneContents().textContent || '';
258
+ return text.length > 0;
259
+ }
260
+
261
+ // Split one styled span into [left][middle][right]; recolor only middle
262
+ function splitAndRecolorWithinSpan(
263
+ span: HTMLSpanElement,
264
+ range: Range,
265
+ dataAttr: 'data-rte-color' | 'data-rte-bg',
266
+ cssProp: 'color' | 'backgroundColor',
267
+ newValue: string
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
+ 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
+
296
+ const mid = makeShell(newValue);
297
+ mid.appendChild(range.cloneContents());
298
+ frag.appendChild(mid);
299
+
300
+ if (hasRangeContent(right)) {
301
+ const sRight = makeShell(oldValue);
302
+ sRight.appendChild(right.cloneContents());
303
+ frag.appendChild(sRight);
304
+ }
305
+
306
+ // Replace original span
307
+ span.replaceWith(frag);
308
+ return mid; // return the middle span so caller can restore selection
309
+ }
310
+
311
+
312
+ function mergeAdjacentStyledSpans(
313
+ root: HTMLElement,
314
+ dataAttr: 'data-rte-color' | 'data-rte-bg',
315
+ cssProp: 'color' | 'backgroundColor'
316
+ ) {
317
+ const spans = Array.from(root.querySelectorAll<HTMLSpanElement>(`span[${dataAttr}]`));
318
+
319
+ const valOf = (el: HTMLElement) => (el.style as any)[cssProp];
320
+
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
+ }
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
+ }
351
+
352
+
353
+ export function setForeColor(editor: HTMLElement, hex: string) {
354
+ applyInlineStyle(editor, 'color', hex, 'data-rte-color');
355
+ }
356
+ export function setBackColor(editor: HTMLElement, hex: string) {
357
+ applyInlineStyle(editor, 'backgroundColor', hex, 'data-rte-bg');
358
+ }
359
+
360
+
139
361
  export function toggleList(root: HTMLElement, kind: 'ul'|'ol') {
140
362
  const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
141
363
  const range = sel.getRangeAt(0);
@@ -2820,7 +2820,7 @@
2820
2820
  },
2821
2821
  {
2822
2822
  "name": "nile-rich-text-editor",
2823
- "description": "Events:\n\n * `content-changed` {`CustomEvent<{ content: string; }>`} - \n\nAttributes:\n\n * `value` {`string`} - Initial HTML content\n\n * `mentions` - Optional mentions config (can also be on <nile-rte-mentions mentions=\"...\">)\n\nProperties:\n\n * `value` {`string`} - Initial HTML content\n\n * `mentions` - Optional mentions config (can also be on <nile-rte-mentions mentions=\"...\">)\n\n * `content` {`string`} - \n\n * `editorEl` {`HTMLElement`} - \n\n * `previewEl` {`HTMLElement | null`} - \n\n * `toolbarEl` {`HTMLElement | null`} - \n\n * `lastRange` {`Range | null`} - \n\n * `buttonMap` {`Map<string, HTMLElement[]>`} - \n\n * `headingSelect` {`HTMLSelectElement | null`} - \n\n * `fontSelect` {`HTMLSelectElement | null`} - \n\n * `colorInput` {`HTMLInputElement | null`} - \n\n * `bgColorInput` {`HTMLInputElement | null`} - \n\n * `colorSwatchEl` {`HTMLElement | null`} - \n\n * `bgSwatchEl` {`HTMLElement | null`} - \n\n * `mentionsEl` {`HTMLElement | null`} - \n\n * `onSelectionChange` - ",
2823
+ "description": "Events:\n\n * `content-changed` {`CustomEvent<{ content: string; }>`} - \n\nAttributes:\n\n * `value` {`string`} - Initial HTML content\n\n * `mentions` - Optional mentions config (can also be on <nile-rte-mentions mentions=\"...\">)\n\nProperties:\n\n * `value` {`string`} - Initial HTML content\n\n * `mentions` - Optional mentions config (can also be on <nile-rte-mentions mentions=\"...\">)\n\n * `content` {`string`} - \n\n * `editorEl` {`HTMLElement`} - \n\n * `previewEl` {`HTMLElement | null`} - \n\n * `toolbarEl` {`HTMLElement | null`} - \n\n * `lastRange` {`Range | null`} - \n\n * `buttonMap` {`Map<string, HTMLElement[]>`} - \n\n * `headingSelect` {`HTMLSelectElement | null`} - \n\n * `fontSelect` {`HTMLSelectElement | null`} - \n\n * `colorInput` {`HTMLInputElement | null`} - \n\n * `bgColorInput` {`HTMLInputElement | null`} - \n\n * `colorSwatchEl` {`HTMLElement | null`} - \n\n * `bgSwatchEl` {`HTMLElement | null`} - \n\n * `mentionsEl` {`HTMLElement | null`} - \n\n * `onEditorKeydown` - \n\n * `onSelectionChange` - ",
2824
2824
  "attributes": [
2825
2825
  {
2826
2826
  "name": "value",