@aquera/nile-elements 1.9.5 → 1.9.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.
Files changed (67) hide show
  1. package/README.md +6 -0
  2. package/dist/index.cjs.js +1 -1
  3. package/dist/index.esm.js +1 -1
  4. package/dist/index.js +470 -298
  5. package/dist/nile-chip/nile-chip.css.cjs.js +1 -1
  6. package/dist/nile-chip/nile-chip.css.cjs.js.map +1 -1
  7. package/dist/nile-chip/nile-chip.css.esm.js +8 -1
  8. package/dist/nile-code-editor/nile-code-editor.cjs.js +1 -1
  9. package/dist/nile-code-editor/nile-code-editor.cjs.js.map +1 -1
  10. package/dist/nile-code-editor/nile-code-editor.esm.js +3 -3
  11. package/dist/nile-hero/nile-hero.cjs.js +1 -1
  12. package/dist/nile-hero/nile-hero.cjs.js.map +1 -1
  13. package/dist/nile-hero/nile-hero.css.cjs.js +1 -1
  14. package/dist/nile-hero/nile-hero.css.cjs.js.map +1 -1
  15. package/dist/nile-hero/nile-hero.css.esm.js +14 -14
  16. package/dist/nile-hero/nile-hero.esm.js +1 -1
  17. package/dist/nile-hero-header/nile-hero-header.cjs.js +1 -1
  18. package/dist/nile-hero-header/nile-hero-header.cjs.js.map +1 -1
  19. package/dist/nile-hero-header/nile-hero-header.css.cjs.js +1 -1
  20. package/dist/nile-hero-header/nile-hero-header.css.cjs.js.map +1 -1
  21. package/dist/nile-hero-header/nile-hero-header.css.esm.js +11 -11
  22. package/dist/nile-hero-header/nile-hero-header.esm.js +3 -3
  23. package/dist/nile-rich-text-editor/index.cjs.js +1 -1
  24. package/dist/nile-rich-text-editor/index.esm.js +1 -1
  25. package/dist/nile-rich-text-editor/nile-rich-text-editor.cjs.js +1 -1
  26. package/dist/nile-rich-text-editor/nile-rich-text-editor.cjs.js.map +1 -1
  27. package/dist/nile-rich-text-editor/nile-rich-text-editor.css.cjs.js +1 -1
  28. package/dist/nile-rich-text-editor/nile-rich-text-editor.css.cjs.js.map +1 -1
  29. package/dist/nile-rich-text-editor/nile-rich-text-editor.css.esm.js +70 -0
  30. package/dist/nile-rich-text-editor/nile-rich-text-editor.esm.js +1 -1
  31. package/dist/nile-rich-text-editor/nile-rte-image.cjs.js +3 -0
  32. package/dist/nile-rich-text-editor/nile-rte-image.cjs.js.map +1 -0
  33. package/dist/nile-rich-text-editor/nile-rte-image.esm.js +96 -0
  34. package/dist/src/nile-chip/nile-chip.css.js +8 -1
  35. package/dist/src/nile-chip/nile-chip.css.js.map +1 -1
  36. package/dist/src/nile-code-editor/nile-code-editor.js +3 -3
  37. package/dist/src/nile-code-editor/nile-code-editor.js.map +1 -1
  38. package/dist/src/nile-hero/nile-hero.css.js +12 -12
  39. package/dist/src/nile-hero/nile-hero.css.js.map +1 -1
  40. package/dist/src/nile-hero/nile-hero.js +1 -1
  41. package/dist/src/nile-hero/nile-hero.js.map +1 -1
  42. package/dist/src/nile-hero-header/nile-hero-header.css.js +9 -9
  43. package/dist/src/nile-hero-header/nile-hero-header.css.js.map +1 -1
  44. package/dist/src/nile-hero-header/nile-hero-header.js +1 -1
  45. package/dist/src/nile-hero-header/nile-hero-header.js.map +1 -1
  46. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.css.js +70 -0
  47. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.css.js.map +1 -1
  48. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.d.ts +3 -0
  49. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.js +81 -3
  50. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.js.map +1 -1
  51. package/dist/src/nile-rich-text-editor/nile-rte-image.d.ts +87 -0
  52. package/dist/src/nile-rich-text-editor/nile-rte-image.js +1073 -0
  53. package/dist/src/nile-rich-text-editor/nile-rte-image.js.map +1 -0
  54. package/dist/src/version.js +1 -1
  55. package/dist/src/version.js.map +1 -1
  56. package/dist/tsconfig.tsbuildinfo +1 -1
  57. package/package.json +1 -1
  58. package/src/nile-chip/nile-chip.css.ts +8 -1
  59. package/src/nile-code-editor/nile-code-editor.ts +3 -3
  60. package/src/nile-hero/nile-hero.css.ts +12 -12
  61. package/src/nile-hero/nile-hero.ts +1 -1
  62. package/src/nile-hero-header/nile-hero-header.css.ts +9 -9
  63. package/src/nile-hero-header/nile-hero-header.ts +1 -1
  64. package/src/nile-rich-text-editor/nile-rich-text-editor.css.ts +70 -0
  65. package/src/nile-rich-text-editor/nile-rich-text-editor.ts +89 -3
  66. package/src/nile-rich-text-editor/nile-rte-image.ts +1132 -0
  67. package/vscode-html-custom-data.json +41 -1
@@ -0,0 +1,1073 @@
1
+ import { __decorate } from "tslib";
2
+ import { LitElement, html } from 'lit';
3
+ import { customElement, property, state } from 'lit/decorators.js';
4
+ let NileRteImage = class NileRteImage extends LitElement {
5
+ constructor() {
6
+ super(...arguments);
7
+ this.disabled = false;
8
+ this.label = 'Image';
9
+ this.placeholder = 'Paste image link here';
10
+ this.altPlaceholder = 'Describe the image';
11
+ this.captionPlaceholder = 'Add a caption (optional)';
12
+ this.maxFileSize = 2 * 1024 * 1024;
13
+ this.uploadFromComputer = false;
14
+ this.srcValue = '';
15
+ this.altValue = '';
16
+ this.captionValue = '';
17
+ this.hasActiveImage = false;
18
+ this.uploadedFileName = '';
19
+ this.errorMessage = '';
20
+ this.popoverStyle = '';
21
+ this.selectionRange = null;
22
+ this.activeFigure = null;
23
+ this.ignoreBlur = false;
24
+ this.isApplying = false;
25
+ this.selectedFigure = null;
26
+ this.resizeOverlay = null;
27
+ this.dragStartX = 0;
28
+ this.dragStartWidth = 0;
29
+ this.dragDir = '';
30
+ this.isDragging = false;
31
+ this.trackingRaf = 0;
32
+ this.onEditorInput = () => {
33
+ // content edits can remove or reflow the figure
34
+ if (this.selectedFigure && !this.selectedFigure.isConnected) {
35
+ this.deselectImage();
36
+ }
37
+ else {
38
+ this.updateOverlayPosition();
39
+ }
40
+ };
41
+ this.onEditorImagePaste = (e) => {
42
+ if (this.disabled)
43
+ return;
44
+ const items = Array.from(e.clipboardData?.items ?? []);
45
+ const imageFiles = items
46
+ .filter(i => i.kind === 'file' && i.type.startsWith('image/'))
47
+ .map(i => i.getAsFile())
48
+ .filter((f) => !!f);
49
+ if (!imageFiles.length)
50
+ return;
51
+ e.preventDefault();
52
+ e.stopPropagation();
53
+ for (const file of imageFiles) {
54
+ if (this.maxFileSize > 0 && file.size > this.maxFileSize) {
55
+ console.warn(`nile-rte-image: pasted image "${file.name}" skipped — exceeds the ${this.maxFileSize} byte limit`);
56
+ continue;
57
+ }
58
+ const reader = new FileReader();
59
+ reader.onload = () => {
60
+ this.insertPastedImage(String(reader.result || ''), file.name.replace(/\.[^.]+$/, ''));
61
+ };
62
+ reader.readAsDataURL(file);
63
+ }
64
+ };
65
+ this.onScroll = () => {
66
+ this.closePopover();
67
+ };
68
+ this.onEditorImageClick = (e) => {
69
+ if (this.disabled)
70
+ return;
71
+ const target = e.target;
72
+ const figure = target.closest?.('figure.rte-image');
73
+ if (!figure || !this.editorEl.contains(figure)) {
74
+ this.deselectImage();
75
+ return;
76
+ }
77
+ e.preventDefault();
78
+ this.selectImage(figure);
79
+ };
80
+ this.onEditorImageDblClick = (e) => {
81
+ if (this.disabled)
82
+ return;
83
+ const target = e.target;
84
+ const figure = target.closest?.('figure.rte-image');
85
+ if (!figure || !this.editorEl.contains(figure))
86
+ return;
87
+ e.preventDefault();
88
+ this.openEditFor(figure);
89
+ };
90
+ this.onDocKeydown = (e) => {
91
+ if (!this.selectedFigure)
92
+ return;
93
+ if (e.key === 'Escape') {
94
+ this.deselectImage();
95
+ return;
96
+ }
97
+ if (e.key !== 'Backspace' && e.key !== 'Delete')
98
+ return;
99
+ // don't hijack deletions typed into inputs (e.g. the edit popover)
100
+ const target = e.target;
101
+ if (target?.closest?.('input, textarea, nile-input, nile-popover'))
102
+ return;
103
+ e.preventDefault();
104
+ e.stopPropagation();
105
+ const figure = this.selectedFigure;
106
+ this.deselectImage();
107
+ const parent = figure.parentNode;
108
+ figure.remove();
109
+ if (parent instanceof HTMLElement && !parent.hasChildNodes()) {
110
+ parent.innerHTML = '<br>';
111
+ }
112
+ this.emit('');
113
+ };
114
+ this.onSelectedImageLoad = () => {
115
+ this.updateOverlayPosition();
116
+ };
117
+ this.onDocMousedown = (e) => {
118
+ const target = e.target;
119
+ if (this.resizeOverlay?.contains(target))
120
+ return;
121
+ if (this.selectedFigure?.contains(target))
122
+ return;
123
+ // clicks on our own UI (toolbar image button, popover) must not clear the
124
+ // selection, otherwise pressing the toolbar button while an image is
125
+ // selected would drop into insert mode instead of edit mode
126
+ if (this.contains(target))
127
+ return;
128
+ this.deselectImage();
129
+ };
130
+ this.onAlignClick = (e) => {
131
+ if (!this.selectedFigure)
132
+ return;
133
+ const dir = e.currentTarget.dataset.dir || 'left';
134
+ if (dir === 'left') {
135
+ this.selectedFigure.removeAttribute('data-align');
136
+ }
137
+ else {
138
+ this.selectedFigure.setAttribute('data-align', dir);
139
+ }
140
+ this.syncAlignBar();
141
+ this.updateOverlayPosition();
142
+ const img = this.selectedFigure.querySelector('img');
143
+ this.emit(img?.getAttribute('src') || '');
144
+ };
145
+ this.onHandleMousedown = (e) => {
146
+ if (this.disabled || !this.selectedFigure)
147
+ return;
148
+ e.preventDefault();
149
+ e.stopPropagation();
150
+ const img = this.selectedFigure.querySelector('img');
151
+ if (!img)
152
+ return;
153
+ // a full-width image can't be hand-resized — dragging implies a custom
154
+ // width, so drop back to default alignment first
155
+ if (this.selectedFigure.getAttribute('data-align') === 'justify') {
156
+ this.selectedFigure.removeAttribute('data-align');
157
+ this.syncAlignBar();
158
+ }
159
+ this.isDragging = true;
160
+ this.dragDir = e.currentTarget.dataset.dir || 'se';
161
+ this.dragStartX = e.clientX;
162
+ this.dragStartWidth = img.getBoundingClientRect().width;
163
+ document.addEventListener('mousemove', this.onDragMove);
164
+ document.addEventListener('mouseup', this.onDragEnd);
165
+ };
166
+ this.onDragMove = (e) => {
167
+ if (!this.isDragging || !this.selectedFigure)
168
+ return;
169
+ const img = this.selectedFigure.querySelector('img');
170
+ if (!img)
171
+ return;
172
+ const dx = e.clientX - this.dragStartX;
173
+ const growsRight = this.dragDir === 'ne' || this.dragDir === 'se';
174
+ let width = this.dragStartWidth + (growsRight ? dx : -dx);
175
+ const maxWidth = this.getEditorContentWidth();
176
+ width = Math.max(48, Math.min(width, maxWidth));
177
+ img.setAttribute('width', String(Math.round(width)));
178
+ this.updateOverlayPosition();
179
+ };
180
+ this.onDragEnd = () => {
181
+ if (!this.isDragging)
182
+ return;
183
+ this.isDragging = false;
184
+ document.removeEventListener('mousemove', this.onDragMove);
185
+ document.removeEventListener('mouseup', this.onDragEnd);
186
+ this.updateOverlayPosition();
187
+ const img = this.selectedFigure?.querySelector('img');
188
+ this.dispatchEvent(new CustomEvent('nile-image-changed', {
189
+ detail: {
190
+ src: img?.getAttribute('src') || '',
191
+ alt: img?.getAttribute('alt') || '',
192
+ caption: this.selectedFigure?.querySelector('figcaption')?.textContent || '',
193
+ width: img ? Number(img.getAttribute('width')) || null : null,
194
+ },
195
+ bubbles: true,
196
+ composed: true,
197
+ }));
198
+ };
199
+ this.onOpen = () => {
200
+ if (this.disabled)
201
+ return;
202
+ if (this.selectedFigure) {
203
+ this.openEditFor(this.selectedFigure, this.getToolbarButtonRect());
204
+ return;
205
+ }
206
+ const sel = document.getSelection();
207
+ if (sel &&
208
+ sel.rangeCount > 0 &&
209
+ this.editorEl.contains(sel.getRangeAt(0).commonAncestorContainer)) {
210
+ this.selectionRange = sel.getRangeAt(0).cloneRange();
211
+ }
212
+ else {
213
+ const range = document.createRange();
214
+ range.selectNodeContents(this.editorEl);
215
+ range.collapse(false);
216
+ this.selectionRange = range;
217
+ }
218
+ this.activeFigure = null;
219
+ this.hasActiveImage = false;
220
+ this.srcValue = '';
221
+ this.altValue = '';
222
+ this.captionValue = '';
223
+ this.uploadedFileName = '';
224
+ this.errorMessage = '';
225
+ this.positionPopover(this.getToolbarButtonRect());
226
+ this.openPopover();
227
+ requestAnimationFrame(() => {
228
+ const input = this.querySelector('nile-input');
229
+ input?.focus();
230
+ });
231
+ };
232
+ this.onInputKeydown = (e) => {
233
+ if (e.key !== 'Escape' && e.key !== 'Enter')
234
+ return;
235
+ e.preventDefault();
236
+ e.stopPropagation();
237
+ if (e.key === 'Escape') {
238
+ this.closePopover();
239
+ return;
240
+ }
241
+ this.isApplying = true;
242
+ try {
243
+ this.applyImage();
244
+ }
245
+ finally {
246
+ this.isApplying = false;
247
+ }
248
+ };
249
+ this.onInputBlur = () => {
250
+ if (this.ignoreBlur || this.isApplying)
251
+ return;
252
+ requestAnimationFrame(() => {
253
+ if (this.ignoreBlur || this.isApplying)
254
+ return;
255
+ const pop = this.querySelector('nile-popover');
256
+ if (pop?.matches(':focus-within'))
257
+ return;
258
+ this.closePopover();
259
+ });
260
+ };
261
+ this.onSrcInput = (e) => {
262
+ this.srcValue = e.detail.value;
263
+ this.uploadedFileName = '';
264
+ this.errorMessage = '';
265
+ };
266
+ this.onAltInput = (e) => {
267
+ this.altValue = e.detail.value;
268
+ };
269
+ this.onCaptionInput = (e) => {
270
+ this.captionValue = e.detail.value;
271
+ };
272
+ this.onPopoverMousedown = () => {
273
+ this.ignoreBlur = true;
274
+ };
275
+ this.onPopoverMouseup = () => {
276
+ this.ignoreBlur = false;
277
+ };
278
+ this.onToolbarButtonMousedown = (e) => {
279
+ e.preventDefault();
280
+ };
281
+ this.onUploadClick = () => {
282
+ // keep the popover open while the OS file dialog steals focus
283
+ this.ignoreBlur = true;
284
+ const fileInput = this.querySelector('input[type="file"]');
285
+ fileInput?.click();
286
+ window.addEventListener('focus', () => {
287
+ setTimeout(() => {
288
+ this.ignoreBlur = false;
289
+ }, 200);
290
+ }, { once: true });
291
+ };
292
+ this.onFileChange = (e) => {
293
+ const input = e.target;
294
+ const file = input.files?.[0];
295
+ input.value = '';
296
+ if (!file)
297
+ return;
298
+ if (!file.type.startsWith('image/')) {
299
+ this.errorMessage = 'Only image files are supported';
300
+ return;
301
+ }
302
+ if (this.maxFileSize > 0 && file.size > this.maxFileSize) {
303
+ const mb = (this.maxFileSize / (1024 * 1024)).toFixed(1);
304
+ this.errorMessage = `File is too large. Maximum size is ${mb} MB`;
305
+ return;
306
+ }
307
+ const reader = new FileReader();
308
+ reader.onload = () => {
309
+ this.srcValue = String(reader.result || '');
310
+ this.uploadedFileName = file.name;
311
+ this.errorMessage = '';
312
+ if (!this.altValue) {
313
+ this.altValue = file.name.replace(/\.[^.]+$/, '');
314
+ }
315
+ };
316
+ reader.onerror = () => {
317
+ this.errorMessage = 'Could not read the selected file';
318
+ };
319
+ reader.readAsDataURL(file);
320
+ };
321
+ this.onApplyClick = () => {
322
+ this.isApplying = true;
323
+ this.applyImage();
324
+ this.isApplying = false;
325
+ this.ignoreBlur = false;
326
+ };
327
+ this.onRemoveClick = () => {
328
+ this.isApplying = true;
329
+ this.removeImage();
330
+ this.isApplying = false;
331
+ this.ignoreBlur = false;
332
+ };
333
+ }
334
+ createRenderRoot() {
335
+ return this;
336
+ }
337
+ connectedCallback() {
338
+ super.connectedCallback();
339
+ this.injectCss(`
340
+ nile-popover.rte-image-popover::part(popover) {
341
+ min-width: 340px;
342
+ max-width: 420px;
343
+ background: white;
344
+ border: 1px solid var(--nile-colors-neutral-400, var(--ng-componentcolors-utility-gray-400));
345
+ border-radius: 8px;
346
+ padding: 0;
347
+ gap: 0;
348
+ overflow: hidden;
349
+ height: auto;
350
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
351
+ }
352
+
353
+ .image-popup-wrap {
354
+ display: flex;
355
+ flex-direction: column;
356
+ gap: 12px;
357
+ padding: 12px;
358
+ }
359
+
360
+ .image-popup-wrap nile-input {
361
+ width: 100%;
362
+ }
363
+
364
+ .image-popup-wrap .image-upload-row {
365
+ display: flex;
366
+ align-items: center;
367
+ justify-content: center;
368
+ gap: 8px;
369
+ width: 100%;
370
+ }
371
+
372
+ .image-popup-wrap .image-upload-row nile-button {
373
+ flex-shrink: 0;
374
+ }
375
+
376
+ .image-popup-wrap .image-upload-filename {
377
+ font-size: 12px;
378
+ color: var(--nile-colors-neutral-600, var(--ng-colors-text-secondary));
379
+ overflow: hidden;
380
+ text-overflow: ellipsis;
381
+ white-space: nowrap;
382
+ min-width: 0;
383
+ }
384
+
385
+ .image-popup-wrap .image-error {
386
+ font-size: 12px;
387
+ color: var(--nile-colors-error-600, var(--ng-colors-text-error-primary-600));
388
+ }
389
+
390
+ .image-popup-wrap .image-divider-row {
391
+ display: flex;
392
+ align-items: center;
393
+ gap: 8px;
394
+ color: var(--nile-colors-neutral-500, var(--ng-colors-text-secondary));
395
+ font-size: 12px;
396
+ }
397
+
398
+ .image-popup-wrap .image-divider-row::before,
399
+ .image-popup-wrap .image-divider-row::after {
400
+ content: '';
401
+ flex: 1;
402
+ height: 1px;
403
+ background: var(--nile-colors-neutral-400, var(--ng-colors-border-neutral));
404
+ }
405
+
406
+ .image-popup-wrap .image-actions-row {
407
+ display: flex;
408
+ justify-content: flex-end;
409
+ gap: 8px;
410
+ width: 100%;
411
+ }
412
+
413
+ nile-button.rte-image-button::part(base) {
414
+ border: none;
415
+ height: 32px;
416
+ width: 32px;
417
+ }
418
+
419
+ nile-rte-image input[type='file'] {
420
+ display: none;
421
+ }
422
+
423
+ .rte-image-resize-overlay {
424
+ position: fixed;
425
+ z-index: 999;
426
+ pointer-events: none;
427
+ border: 2px solid var(--nile-colors-primary-500, var(--ng-componentcolors-utility-blue-500));
428
+ border-radius: var(--nile-radius-radius-lg, var(--ng-radius-md));
429
+ box-sizing: border-box;
430
+ }
431
+
432
+ .rte-image-resize-overlay .rte-resize-handle {
433
+ position: absolute;
434
+ width: 12px;
435
+ height: 12px;
436
+ background: var(--nile-colors-white-base, #fff);
437
+ border: 2px solid var(--nile-colors-primary-500, var(--ng-componentcolors-utility-blue-500));
438
+ border-radius: 50%;
439
+ pointer-events: auto;
440
+ box-sizing: border-box;
441
+ }
442
+
443
+ .rte-image-resize-overlay .rte-resize-handle[data-dir='nw'] { top: -7px; left: -7px; cursor: nwse-resize; }
444
+ .rte-image-resize-overlay .rte-resize-handle[data-dir='ne'] { top: -7px; right: -7px; cursor: nesw-resize; }
445
+ .rte-image-resize-overlay .rte-resize-handle[data-dir='sw'] { bottom: -7px; left: -7px; cursor: nesw-resize; }
446
+ .rte-image-resize-overlay .rte-resize-handle[data-dir='se'] { bottom: -7px; right: -7px; cursor: nwse-resize; }
447
+
448
+ .rte-image-resize-overlay .rte-image-align-bar {
449
+ position: absolute;
450
+ top: -44px;
451
+ left: 50%;
452
+ transform: translateX(-50%);
453
+ display: flex;
454
+ gap: 2px;
455
+ padding: 4px;
456
+ background: var(--nile-colors-white-base, #fff);
457
+ border: 1px solid var(--nile-colors-neutral-400, var(--ng-colors-border-neutral));
458
+ border-radius: 8px;
459
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
460
+ pointer-events: auto;
461
+ }
462
+
463
+ .rte-image-resize-overlay .rte-image-align-bar button {
464
+ display: flex;
465
+ align-items: center;
466
+ justify-content: center;
467
+ width: 28px;
468
+ height: 28px;
469
+ border: none;
470
+ background: transparent;
471
+ border-radius: 6px;
472
+ cursor: pointer;
473
+ padding: 0;
474
+ }
475
+
476
+ .rte-image-resize-overlay .rte-image-align-bar button:hover {
477
+ background: var(--nile-colors-neutral-200, var(--ng-componentcolors-utility-gray-200));
478
+ }
479
+
480
+ .rte-image-resize-overlay .rte-image-align-bar button[data-active] {
481
+ background: var(--nile-colors-primary-400, var(--ng-componentcolors-utility-blue-400));
482
+ }
483
+
484
+ .rte-image-resize-overlay .rte-image-align-bar button nile-icon {
485
+ pointer-events: none;
486
+ }
487
+
488
+ .rte-image-resize-overlay .rte-resize-size-label {
489
+ position: absolute;
490
+ bottom: -26px;
491
+ right: 0;
492
+ background: var(--nile-colors-dark-900, #101828);
493
+ color: #fff;
494
+ font-size: 11px;
495
+ line-height: 1;
496
+ padding: 4px 6px;
497
+ border-radius: 4px;
498
+ white-space: nowrap;
499
+ pointer-events: none;
500
+ }
501
+ `);
502
+ }
503
+ injectCss(cssText) {
504
+ if (this.querySelector('style[data-rte-image-style]'))
505
+ return;
506
+ const style = document.createElement('style');
507
+ style.setAttribute('data-rte-image-style', 'true');
508
+ style.textContent = cssText;
509
+ this.insertBefore(style, this.firstChild);
510
+ }
511
+ updated(changed) {
512
+ if (changed.has('editorEl')) {
513
+ const prev = changed.get('editorEl');
514
+ prev?.removeEventListener('click', this.onEditorImageClick);
515
+ prev?.removeEventListener('dblclick', this.onEditorImageDblClick);
516
+ prev?.removeEventListener('input', this.onEditorInput);
517
+ prev?.removeEventListener('paste', this.onEditorImagePaste);
518
+ this.editorEl?.addEventListener('click', this.onEditorImageClick);
519
+ this.editorEl?.addEventListener('dblclick', this.onEditorImageDblClick);
520
+ this.editorEl?.addEventListener('input', this.onEditorInput);
521
+ this.editorEl?.addEventListener('paste', this.onEditorImagePaste);
522
+ }
523
+ if (changed.has('disabled') && this.disabled) {
524
+ this.deselectImage();
525
+ }
526
+ }
527
+ disconnectedCallback() {
528
+ this.editorEl?.removeEventListener('click', this.onEditorImageClick);
529
+ this.editorEl?.removeEventListener('dblclick', this.onEditorImageDblClick);
530
+ this.editorEl?.removeEventListener('input', this.onEditorInput);
531
+ this.editorEl?.removeEventListener('paste', this.onEditorImagePaste);
532
+ window.removeEventListener('scroll', this.onScroll, true);
533
+ this.deselectImage();
534
+ super.disconnectedCallback();
535
+ }
536
+ insertPastedImage(src, alt) {
537
+ if (!src.startsWith('data:image/'))
538
+ return;
539
+ let range;
540
+ const sel = document.getSelection();
541
+ if (sel &&
542
+ sel.rangeCount > 0 &&
543
+ this.editorEl.contains(sel.getRangeAt(0).commonAncestorContainer)) {
544
+ range = sel.getRangeAt(0).cloneRange();
545
+ }
546
+ else {
547
+ range = document.createRange();
548
+ range.selectNodeContents(this.editorEl);
549
+ range.collapse(false);
550
+ }
551
+ const figure = this.buildFigure(src, alt);
552
+ this.insertFigureAt(range, figure);
553
+ this.emit(src);
554
+ }
555
+ isSafeSrc(src) {
556
+ return /^(https?:\/\/|data:image\/|blob:|\/)/i.test(src);
557
+ }
558
+ normalizeSrc(src) {
559
+ if (this.isSafeSrc(src))
560
+ return src;
561
+ if (/^[a-z][a-z0-9+.-]*:/i.test(src))
562
+ return '';
563
+ return `https://${src}`;
564
+ }
565
+ getSelectionRect() {
566
+ if (!this.selectionRange)
567
+ return null;
568
+ let rect = this.selectionRange.getBoundingClientRect();
569
+ if (rect.width === 0 && rect.height === 0) {
570
+ const span = document.createElement('span');
571
+ span.textContent = '\u200b';
572
+ const tempRange = this.selectionRange.cloneRange();
573
+ tempRange.insertNode(span);
574
+ rect = span.getBoundingClientRect();
575
+ span.remove();
576
+ }
577
+ return rect;
578
+ }
579
+ positionPopover(anchorRect) {
580
+ const rect = anchorRect ?? this.getSelectionRect();
581
+ if (!rect)
582
+ return;
583
+ const half = 170;
584
+ const centerX = Math.min(Math.max(rect.left + rect.width / 2, half + 8), window.innerWidth - half - 8);
585
+ this.popoverStyle = `
586
+ position: fixed;
587
+ top: ${rect.bottom + 8}px;
588
+ left: ${centerX}px;
589
+ transform: translateX(-50%);
590
+ z-index: 1000;
591
+ `;
592
+ }
593
+ openPopover() {
594
+ requestAnimationFrame(() => {
595
+ const pop = this.querySelector('nile-popover');
596
+ pop && (pop.isShow = true);
597
+ window.addEventListener('scroll', this.onScroll, true);
598
+ });
599
+ }
600
+ closePopover() {
601
+ const pop = this.querySelector('nile-popover');
602
+ pop && (pop.isShow = false);
603
+ window.removeEventListener('scroll', this.onScroll, true);
604
+ this.srcValue = '';
605
+ this.altValue = '';
606
+ this.captionValue = '';
607
+ this.uploadedFileName = '';
608
+ this.errorMessage = '';
609
+ this.selectionRange = null;
610
+ this.activeFigure = null;
611
+ this.hasActiveImage = false;
612
+ this.popoverStyle = '';
613
+ }
614
+ openEditFor(figure, anchorRect) {
615
+ const range = document.createRange();
616
+ range.selectNode(figure);
617
+ this.selectionRange = range;
618
+ this.activeFigure = figure;
619
+ this.hasActiveImage = true;
620
+ const img = figure.querySelector('img');
621
+ const figcaption = figure.querySelector('figcaption');
622
+ this.srcValue = img?.getAttribute('src') || '';
623
+ this.altValue = img?.getAttribute('alt') || '';
624
+ this.captionValue = figcaption?.textContent || '';
625
+ this.uploadedFileName = '';
626
+ this.errorMessage = '';
627
+ this.positionPopover(anchorRect ?? figure.getBoundingClientRect());
628
+ this.openPopover();
629
+ }
630
+ getToolbarButtonRect() {
631
+ const btn = this.querySelector('nile-button.rte-image-button');
632
+ return btn ? btn.getBoundingClientRect() : null;
633
+ }
634
+ // ---------- resize ----------
635
+ selectImage(figure) {
636
+ if (this.selectedFigure === figure) {
637
+ this.updateOverlayPosition();
638
+ return;
639
+ }
640
+ this.deselectImage();
641
+ this.selectedFigure = figure;
642
+ const img = figure.querySelector('img');
643
+ if (img && !img.complete) {
644
+ img.addEventListener('load', this.onSelectedImageLoad, { once: true });
645
+ }
646
+ this.buildOverlay();
647
+ this.syncAlignBar();
648
+ this.updateOverlayPosition();
649
+ this.startOverlayTracking();
650
+ document.addEventListener('mousedown', this.onDocMousedown, true);
651
+ document.addEventListener('keydown', this.onDocKeydown, true);
652
+ }
653
+ deselectImage() {
654
+ if (!this.selectedFigure && !this.resizeOverlay)
655
+ return;
656
+ this.selectedFigure = null;
657
+ this.resizeOverlay?.remove();
658
+ this.resizeOverlay = null;
659
+ this.isDragging = false;
660
+ this.stopOverlayTracking();
661
+ document.removeEventListener('mousedown', this.onDocMousedown, true);
662
+ document.removeEventListener('keydown', this.onDocKeydown, true);
663
+ document.removeEventListener('mousemove', this.onDragMove);
664
+ document.removeEventListener('mouseup', this.onDragEnd);
665
+ }
666
+ // re-sync the overlay to the image every frame while selected — this keeps
667
+ // it glued to the image through scrolling (any container), layout shifts,
668
+ // and image loads without needing to enumerate those events
669
+ startOverlayTracking() {
670
+ this.stopOverlayTracking();
671
+ const tick = () => {
672
+ if (!this.selectedFigure || !this.resizeOverlay)
673
+ return;
674
+ if (!this.selectedFigure.isConnected) {
675
+ this.deselectImage();
676
+ return;
677
+ }
678
+ this.updateOverlayPosition();
679
+ this.trackingRaf = requestAnimationFrame(tick);
680
+ };
681
+ this.trackingRaf = requestAnimationFrame(tick);
682
+ }
683
+ stopOverlayTracking() {
684
+ if (this.trackingRaf !== 0) {
685
+ cancelAnimationFrame(this.trackingRaf);
686
+ this.trackingRaf = 0;
687
+ }
688
+ }
689
+ buildOverlay() {
690
+ const overlay = document.createElement('div');
691
+ overlay.className = 'rte-image-resize-overlay';
692
+ for (const dir of ['nw', 'ne', 'sw', 'se']) {
693
+ const handle = document.createElement('div');
694
+ handle.className = 'rte-resize-handle';
695
+ handle.dataset.dir = dir;
696
+ handle.addEventListener('mousedown', this.onHandleMousedown);
697
+ overlay.appendChild(handle);
698
+ }
699
+ const label = document.createElement('span');
700
+ label.className = 'rte-resize-size-label';
701
+ overlay.appendChild(label);
702
+ const alignBar = document.createElement('div');
703
+ alignBar.className = 'rte-image-align-bar';
704
+ const alignments = [
705
+ { dir: 'left', icon: 'format_align_left', title: 'Align left' },
706
+ { dir: 'center', icon: 'format_align_middle', title: 'Align center' },
707
+ { dir: 'right', icon: 'format_align_right', title: 'Align right' },
708
+ { dir: 'justify', icon: 'format_align_justify', title: 'Full width' },
709
+ ];
710
+ for (const { dir, icon, title } of alignments) {
711
+ const btn = document.createElement('button');
712
+ btn.type = 'button';
713
+ btn.dataset.dir = dir;
714
+ btn.title = title;
715
+ btn.setAttribute('aria-label', title);
716
+ btn.innerHTML = `<nile-icon name="${icon}" size="18"></nile-icon>`;
717
+ btn.addEventListener('mousedown', e => e.preventDefault());
718
+ btn.addEventListener('click', this.onAlignClick);
719
+ alignBar.appendChild(btn);
720
+ }
721
+ overlay.appendChild(alignBar);
722
+ // append to <body>: a transformed/filtered ancestor (drawer, modal,
723
+ // animated panel) would otherwise become the containing block for
724
+ // position:fixed and the overlay would render far from the image
725
+ document.body.appendChild(overlay);
726
+ this.resizeOverlay = overlay;
727
+ }
728
+ updateOverlayPosition() {
729
+ if (!this.resizeOverlay || !this.selectedFigure)
730
+ return;
731
+ const img = this.selectedFigure.querySelector('img');
732
+ if (!img) {
733
+ this.deselectImage();
734
+ return;
735
+ }
736
+ const rect = img.getBoundingClientRect();
737
+ this.resizeOverlay.style.top = `${rect.top}px`;
738
+ this.resizeOverlay.style.left = `${rect.left}px`;
739
+ this.resizeOverlay.style.width = `${rect.width}px`;
740
+ this.resizeOverlay.style.height = `${rect.height}px`;
741
+ // keep the align bar reachable: above the image by default, flipped to
742
+ // just inside the top edge when the image touches the viewport top
743
+ const bar = this.resizeOverlay.querySelector('.rte-image-align-bar');
744
+ if (bar) {
745
+ if (rect.top < 56) {
746
+ bar.style.top = '8px';
747
+ }
748
+ else {
749
+ bar.style.top = '-44px';
750
+ }
751
+ }
752
+ const label = this.resizeOverlay.querySelector('.rte-resize-size-label');
753
+ if (label) {
754
+ label.textContent = `${Math.round(rect.width)} × ${Math.round(rect.height)}`;
755
+ label.style.display = this.isDragging ? '' : 'none';
756
+ }
757
+ }
758
+ syncAlignBar() {
759
+ if (!this.resizeOverlay || !this.selectedFigure)
760
+ return;
761
+ const current = this.selectedFigure.getAttribute('data-align') || 'left';
762
+ this.resizeOverlay
763
+ .querySelectorAll('.rte-image-align-bar button')
764
+ .forEach(btn => {
765
+ btn.toggleAttribute('data-active', btn.dataset.dir === current);
766
+ });
767
+ }
768
+ getEditorContentWidth() {
769
+ const style = getComputedStyle(this.editorEl);
770
+ return (this.editorEl.clientWidth -
771
+ parseFloat(style.paddingLeft || '0') -
772
+ parseFloat(style.paddingRight || '0') || Infinity);
773
+ }
774
+ buildFigure(src, alt = '', caption = '') {
775
+ const figure = document.createElement('figure');
776
+ figure.className = 'rte-image';
777
+ figure.setAttribute('contenteditable', 'false');
778
+ const img = document.createElement('img');
779
+ img.src = src;
780
+ if (alt.trim())
781
+ img.alt = alt.trim();
782
+ figure.appendChild(img);
783
+ if (caption.trim()) {
784
+ const figcaption = document.createElement('figcaption');
785
+ figcaption.textContent = caption.trim();
786
+ figure.appendChild(figcaption);
787
+ }
788
+ return figure;
789
+ }
790
+ insertFigureAt(insertRange, figure) {
791
+ const sel = document.getSelection();
792
+ if (!sel)
793
+ return;
794
+ // a <figure> is not valid inside <p>/<h*>, so place it after the
795
+ // block that holds the caret instead of splitting it at the range
796
+ let block = null;
797
+ let n = insertRange.startContainer;
798
+ while (n && n !== this.editorEl) {
799
+ if (n instanceof HTMLElement &&
800
+ /^(P|H[1-6]|UL|OL|BLOCKQUOTE|PRE|TABLE|FIGURE|DIV)$/.test(n.tagName)) {
801
+ block = n;
802
+ break;
803
+ }
804
+ n = n.parentNode;
805
+ }
806
+ // an empty paragraph (the default '<p><br></p>' of a fresh editor) would
807
+ // otherwise remain above the image as a blank gap — take its place instead
808
+ const isEmptyBlock = block &&
809
+ /^(P|H[1-6])$/.test(block.tagName) &&
810
+ (block.textContent ?? '').replace(/\u200B/g, '').trim() === '' &&
811
+ !block.querySelector('img');
812
+ if (block?.parentNode) {
813
+ if (isEmptyBlock) {
814
+ block.parentNode.insertBefore(figure, block);
815
+ block.remove();
816
+ }
817
+ else {
818
+ block.parentNode.insertBefore(figure, block.nextSibling);
819
+ }
820
+ }
821
+ else {
822
+ this.editorEl.appendChild(figure);
823
+ }
824
+ // keep a paragraph after the image so typing can continue below it
825
+ let after = figure.nextElementSibling;
826
+ if (!after) {
827
+ const p = document.createElement('p');
828
+ p.innerHTML = '<br>';
829
+ figure.parentNode?.insertBefore(p, figure.nextSibling);
830
+ after = p;
831
+ }
832
+ const range = document.createRange();
833
+ range.setStart(after, 0);
834
+ range.collapse(true);
835
+ sel.removeAllRanges();
836
+ sel.addRange(range);
837
+ }
838
+ applyImage() {
839
+ const raw = this.srcValue.trim();
840
+ if (!raw) {
841
+ if (this.uploadFromComputer) {
842
+ this.errorMessage = 'Enter an image link or upload a file';
843
+ }
844
+ return;
845
+ }
846
+ const src = this.normalizeSrc(raw);
847
+ if (!src) {
848
+ this.errorMessage = 'Enter a valid image link';
849
+ return;
850
+ }
851
+ if (this.activeFigure) {
852
+ const img = this.activeFigure.querySelector('img');
853
+ if (img) {
854
+ img.src = src;
855
+ if (this.altValue.trim()) {
856
+ img.alt = this.altValue.trim();
857
+ }
858
+ else {
859
+ img.removeAttribute('alt');
860
+ }
861
+ }
862
+ let figcaption = this.activeFigure.querySelector('figcaption');
863
+ if (this.captionValue.trim()) {
864
+ if (!figcaption) {
865
+ figcaption = document.createElement('figcaption');
866
+ this.activeFigure.appendChild(figcaption);
867
+ }
868
+ figcaption.textContent = this.captionValue.trim();
869
+ }
870
+ else {
871
+ figcaption?.remove();
872
+ }
873
+ this.emit(src);
874
+ this.closePopover();
875
+ requestAnimationFrame(() => this.updateOverlayPosition());
876
+ return;
877
+ }
878
+ if (!(this.selectionRange instanceof Range))
879
+ return;
880
+ this.editorEl.focus();
881
+ const sel = document.getSelection();
882
+ if (!sel)
883
+ return;
884
+ sel.removeAllRanges();
885
+ sel.addRange(this.selectionRange);
886
+ const figure = this.buildFigure(src, this.altValue, this.captionValue);
887
+ this.insertFigureAt(this.selectionRange, figure);
888
+ this.emit(src);
889
+ this.closePopover();
890
+ }
891
+ removeImage() {
892
+ if (!this.activeFigure)
893
+ return;
894
+ this.deselectImage();
895
+ const parent = this.activeFigure.parentNode;
896
+ this.activeFigure.remove();
897
+ if (parent instanceof HTMLElement && !parent.hasChildNodes()) {
898
+ parent.innerHTML = '<br>';
899
+ }
900
+ this.emit('');
901
+ this.closePopover();
902
+ }
903
+ emit(src) {
904
+ this.dispatchEvent(new CustomEvent('nile-image-changed', {
905
+ detail: {
906
+ src,
907
+ alt: this.altValue.trim(),
908
+ caption: this.captionValue.trim(),
909
+ },
910
+ bubbles: true,
911
+ composed: true,
912
+ }));
913
+ }
914
+ render() {
915
+ const iconColor = this.disabled
916
+ ? 'var(--nile-colors-neutral-500, var(--ng-colors-fg-disabled-subtle))'
917
+ : 'var(--nile-colors-dark-900, var(--ng-colors-text-primary-900))';
918
+ return html `
919
+ <nile-lite-tooltip content=${this.label}>
920
+ <nile-button
921
+ variant="tertiary"
922
+ size="small"
923
+ ?disabled=${this.disabled}
924
+ @mousedown=${this.onToolbarButtonMousedown}
925
+ @click=${this.onOpen}
926
+ class="rte-image-button"
927
+ >
928
+ <nile-glyph name="image" color=${iconColor}></nile-glyph>
929
+ </nile-button>
930
+ </nile-lite-tooltip>
931
+
932
+ <nile-popover
933
+ class="rte-image-popover"
934
+ .arrow=${false}
935
+ style=${this.popoverStyle}
936
+ @mousedown=${this.onPopoverMousedown}
937
+ @mouseup=${this.onPopoverMouseup}
938
+ >
939
+ <div class="image-popup-wrap">
940
+ <nile-input
941
+ label="Image URL"
942
+ placeholder=${this.placeholder}
943
+ .value=${this.srcValue}
944
+ @nile-input=${this.onSrcInput}
945
+ @keydown=${this.onInputKeydown}
946
+ @nile-blur=${this.onInputBlur}
947
+ ></nile-input>
948
+
949
+ ${this.uploadFromComputer
950
+ ? html `
951
+ <div class="image-divider-row" part="divider">or</div>
952
+
953
+ <div class="image-upload-row" part="upload-row">
954
+ <nile-button
955
+ part="upload-button"
956
+ size="small"
957
+ variant="secondary"
958
+ @click=${this.onUploadClick}
959
+ >
960
+ <nile-glyph name="upload" slot="prefix"></nile-glyph>
961
+ Upload from computer
962
+ </nile-button>
963
+ ${this.uploadedFileName
964
+ ? html `<span class="image-upload-filename" part="upload-filename"
965
+ >${this.uploadedFileName}</span
966
+ >`
967
+ : null}
968
+ </div>
969
+ <input
970
+ part="upload-input"
971
+ type="file"
972
+ accept="image/*"
973
+ @change=${this.onFileChange}
974
+ />
975
+ `
976
+ : null}
977
+
978
+ ${this.errorMessage
979
+ ? html `<span class="image-error" part="error">${this.errorMessage}</span>`
980
+ : null}
981
+
982
+ <nile-input
983
+ label="Alt text"
984
+ placeholder=${this.altPlaceholder}
985
+ .value=${this.altValue}
986
+ @nile-input=${this.onAltInput}
987
+ @keydown=${this.onInputKeydown}
988
+ @nile-blur=${this.onInputBlur}
989
+ part="alt-input"
990
+ ></nile-input>
991
+
992
+ <nile-input
993
+ label="Caption"
994
+ placeholder=${this.captionPlaceholder}
995
+ .value=${this.captionValue}
996
+ @nile-input=${this.onCaptionInput}
997
+ @keydown=${this.onInputKeydown}
998
+ @nile-blur=${this.onInputBlur}
999
+ part="caption-input"
1000
+ ></nile-input>
1001
+
1002
+ <div class="image-actions-row" part="actions-row">
1003
+ ${this.hasActiveImage
1004
+ ? html `
1005
+ <nile-button
1006
+ part="remove-button"
1007
+ size="small"
1008
+ variant="secondary"
1009
+ @click=${this.onRemoveClick}
1010
+ >
1011
+ Remove
1012
+ </nile-button>
1013
+ `
1014
+ : null}
1015
+ <nile-button part="apply-button" size="small" variant="primary" @click=${this.onApplyClick}>
1016
+ ${this.hasActiveImage ? 'Update' : 'Insert'}
1017
+ </nile-button>
1018
+ </div>
1019
+ </div>
1020
+ </nile-popover>
1021
+ `;
1022
+ }
1023
+ };
1024
+ __decorate([
1025
+ property({ attribute: false })
1026
+ ], NileRteImage.prototype, "editorEl", void 0);
1027
+ __decorate([
1028
+ property({ type: Boolean, reflect: true, attribute: true })
1029
+ ], NileRteImage.prototype, "disabled", void 0);
1030
+ __decorate([
1031
+ property({ type: String, reflect: true, attribute: true })
1032
+ ], NileRteImage.prototype, "label", void 0);
1033
+ __decorate([
1034
+ property({ type: String, reflect: true, attribute: true })
1035
+ ], NileRteImage.prototype, "placeholder", void 0);
1036
+ __decorate([
1037
+ property({ type: String, reflect: true, attribute: true })
1038
+ ], NileRteImage.prototype, "altPlaceholder", void 0);
1039
+ __decorate([
1040
+ property({ type: String, reflect: true, attribute: true })
1041
+ ], NileRteImage.prototype, "captionPlaceholder", void 0);
1042
+ __decorate([
1043
+ property({ type: Number, reflect: true, attribute: 'maxfilesize' })
1044
+ ], NileRteImage.prototype, "maxFileSize", void 0);
1045
+ __decorate([
1046
+ property({ type: Boolean, reflect: true, attribute: 'uploadfromcomputer' })
1047
+ ], NileRteImage.prototype, "uploadFromComputer", void 0);
1048
+ __decorate([
1049
+ state()
1050
+ ], NileRteImage.prototype, "srcValue", void 0);
1051
+ __decorate([
1052
+ state()
1053
+ ], NileRteImage.prototype, "altValue", void 0);
1054
+ __decorate([
1055
+ state()
1056
+ ], NileRteImage.prototype, "captionValue", void 0);
1057
+ __decorate([
1058
+ state()
1059
+ ], NileRteImage.prototype, "hasActiveImage", void 0);
1060
+ __decorate([
1061
+ state()
1062
+ ], NileRteImage.prototype, "uploadedFileName", void 0);
1063
+ __decorate([
1064
+ state()
1065
+ ], NileRteImage.prototype, "errorMessage", void 0);
1066
+ __decorate([
1067
+ state()
1068
+ ], NileRteImage.prototype, "popoverStyle", void 0);
1069
+ NileRteImage = __decorate([
1070
+ customElement('nile-rte-image')
1071
+ ], NileRteImage);
1072
+ export { NileRteImage };
1073
+ //# sourceMappingURL=nile-rte-image.js.map