@ship-ui/core 0.22.12 → 0.22.15

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 (72) hide show
  1. package/README.md +46 -0
  2. package/assets/mcp/components.json +579 -2
  3. package/bin/src/scanner.zig +9 -18
  4. package/fesm2022/ship-ui-core-sh-form-field-experimental.mjs +17 -2
  5. package/fesm2022/ship-ui-core-sh-form-field-experimental.mjs.map +1 -1
  6. package/fesm2022/ship-ui-core-ship-a11y-keybindings.mjs +433 -0
  7. package/fesm2022/ship-ui-core-ship-a11y-keybindings.mjs.map +1 -0
  8. package/fesm2022/ship-ui-core-ship-accordion.mjs +1 -0
  9. package/fesm2022/ship-ui-core-ship-accordion.mjs.map +1 -1
  10. package/fesm2022/ship-ui-core-ship-alert.mjs +3 -2
  11. package/fesm2022/ship-ui-core-ship-alert.mjs.map +1 -1
  12. package/fesm2022/ship-ui-core-ship-blueprint.mjs +14 -9
  13. package/fesm2022/ship-ui-core-ship-blueprint.mjs.map +1 -1
  14. package/fesm2022/ship-ui-core-ship-checkbox.mjs +16 -14
  15. package/fesm2022/ship-ui-core-ship-checkbox.mjs.map +1 -1
  16. package/fesm2022/ship-ui-core-ship-color-picker.mjs +3 -1
  17. package/fesm2022/ship-ui-core-ship-color-picker.mjs.map +1 -1
  18. package/fesm2022/ship-ui-core-ship-datepicker.mjs +51 -29
  19. package/fesm2022/ship-ui-core-ship-datepicker.mjs.map +1 -1
  20. package/fesm2022/ship-ui-core-ship-dialog.mjs +10 -5
  21. package/fesm2022/ship-ui-core-ship-dialog.mjs.map +1 -1
  22. package/fesm2022/ship-ui-core-ship-divider.mjs +4 -2
  23. package/fesm2022/ship-ui-core-ship-divider.mjs.map +1 -1
  24. package/fesm2022/ship-ui-core-ship-editor.mjs +2673 -0
  25. package/fesm2022/ship-ui-core-ship-editor.mjs.map +1 -0
  26. package/fesm2022/ship-ui-core-ship-icon.mjs +2 -2
  27. package/fesm2022/ship-ui-core-ship-icon.mjs.map +1 -1
  28. package/fesm2022/ship-ui-core-ship-list.mjs +4 -2
  29. package/fesm2022/ship-ui-core-ship-list.mjs.map +1 -1
  30. package/fesm2022/ship-ui-core-ship-menu.mjs +8 -5
  31. package/fesm2022/ship-ui-core-ship-menu.mjs.map +1 -1
  32. package/fesm2022/ship-ui-core-ship-popover.mjs +10 -5
  33. package/fesm2022/ship-ui-core-ship-popover.mjs.map +1 -1
  34. package/fesm2022/ship-ui-core-ship-progress-bar.mjs +5 -1
  35. package/fesm2022/ship-ui-core-ship-progress-bar.mjs.map +1 -1
  36. package/fesm2022/ship-ui-core-ship-radio.mjs +16 -14
  37. package/fesm2022/ship-ui-core-ship-radio.mjs.map +1 -1
  38. package/fesm2022/ship-ui-core-ship-select.mjs +9 -9
  39. package/fesm2022/ship-ui-core-ship-select.mjs.map +1 -1
  40. package/fesm2022/ship-ui-core-ship-sidenav.mjs +2 -2
  41. package/fesm2022/ship-ui-core-ship-sidenav.mjs.map +1 -1
  42. package/fesm2022/ship-ui-core-ship-spinner.mjs +3 -1
  43. package/fesm2022/ship-ui-core-ship-spinner.mjs.map +1 -1
  44. package/fesm2022/ship-ui-core-ship-spotlight.mjs +77 -24
  45. package/fesm2022/ship-ui-core-ship-spotlight.mjs.map +1 -1
  46. package/fesm2022/ship-ui-core-ship-table.mjs +139 -139
  47. package/fesm2022/ship-ui-core-ship-table.mjs.map +1 -1
  48. package/fesm2022/ship-ui-core-ship-theme-toggle.mjs +2 -2
  49. package/fesm2022/ship-ui-core-ship-theme-toggle.mjs.map +1 -1
  50. package/fesm2022/ship-ui-core-ship-toggle-card.mjs +24 -3
  51. package/fesm2022/ship-ui-core-ship-toggle-card.mjs.map +1 -1
  52. package/fesm2022/ship-ui-core-ship-toggle.mjs +16 -14
  53. package/fesm2022/ship-ui-core-ship-toggle.mjs.map +1 -1
  54. package/fesm2022/ship-ui-core-ship-tree.mjs +2 -2
  55. package/fesm2022/ship-ui-core-ship-tree.mjs.map +1 -1
  56. package/fesm2022/ship-ui-core-ship-virtual-scroll.mjs +2 -2
  57. package/fesm2022/ship-ui-core-ship-virtual-scroll.mjs.map +1 -1
  58. package/fesm2022/ship-ui-core.mjs +36 -23
  59. package/fesm2022/ship-ui-core.mjs.map +1 -1
  60. package/package.json +33 -2
  61. package/types/ship-ui-core-sh-form-field-experimental.d.ts +2 -0
  62. package/types/ship-ui-core-ship-a11y-keybindings.d.ts +102 -0
  63. package/types/ship-ui-core-ship-blueprint.d.ts +1 -1
  64. package/types/ship-ui-core-ship-checkbox.d.ts +2 -1
  65. package/types/ship-ui-core-ship-editor.d.ts +168 -0
  66. package/types/ship-ui-core-ship-radio.d.ts +2 -1
  67. package/types/ship-ui-core-ship-spotlight.d.ts +1 -1
  68. package/types/ship-ui-core-ship-table.d.ts +2 -0
  69. package/types/ship-ui-core-ship-toggle-card.d.ts +1 -0
  70. package/types/ship-ui-core-ship-toggle.d.ts +2 -1
  71. package/types/ship-ui-core.d.ts +3 -0
  72. package/bin/ship-fg-scanner +0 -0
@@ -0,0 +1,2673 @@
1
+ import { DOCUMENT, isPlatformBrowser } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { forwardRef, inject, PLATFORM_ID, ElementRef, viewChild, model, input, signal, computed, output, effect, HostListener, ChangeDetectionStrategy, ViewEncapsulation, Component } from '@angular/core';
4
+ import { NG_VALUE_ACCESSOR } from '@angular/forms';
5
+ import { shipComponentClasses, ShipTooltip } from '@ship-ui/core';
6
+ import { ShipIcon } from '@ship-ui/core/ship-icon';
7
+ import { ShipMenu } from '@ship-ui/core/ship-menu';
8
+ import { ShipA11yKeybindingsService } from '@ship-ui/core/ship-a11y-keybindings';
9
+
10
+ // Value Accessor Provider
11
+ const SHIP_EDITOR_VALUE_ACCESSOR = {
12
+ provide: NG_VALUE_ACCESSOR,
13
+ useExisting: forwardRef(() => ShipEditor),
14
+ multi: true,
15
+ };
16
+ class ShipEditor {
17
+ #document;
18
+ #platformId;
19
+ #isBrowser;
20
+ #elementRef;
21
+ #keybindings;
22
+ get #doc() {
23
+ return this.#document;
24
+ }
25
+ #lastValueWrittenFromDOM;
26
+ #lastEditorElement;
27
+ #historyStack;
28
+ #historyIndex;
29
+ #maxHistorySize;
30
+ #isInternalDOMUpdate;
31
+ #typingTimeout;
32
+ // Image layout overlay signals
33
+ #selectedImage;
34
+ // Selection backup for inserting links/images when modal is open
35
+ #savedRange;
36
+ // Track outer changes
37
+ #isWriting;
38
+ #lastFormat;
39
+ constructor() {
40
+ this.#document = inject(DOCUMENT);
41
+ this.#platformId = inject(PLATFORM_ID);
42
+ this.#isBrowser = isPlatformBrowser(this.#platformId);
43
+ this.#elementRef = inject(ElementRef);
44
+ this.#keybindings = inject(ShipA11yKeybindingsService);
45
+ // Elements
46
+ this.editorRef = viewChild('editorRef', /* @ts-ignore */
47
+ ...(ngDevMode ? [{ debugName: "editorRef" }] : /* istanbul ignore next */ []));
48
+ this.codeEditorRef = viewChild('codeEditorRef', /* @ts-ignore */
49
+ ...(ngDevMode ? [{ debugName: "codeEditorRef" }] : /* istanbul ignore next */ []));
50
+ this.uploadBtn = viewChild('uploadBtn', /* @ts-ignore */
51
+ ...(ngDevMode ? [{ debugName: "uploadBtn" }] : /* istanbul ignore next */ []));
52
+ this.imageInput = viewChild('imageInput', /* @ts-ignore */
53
+ ...(ngDevMode ? [{ debugName: "imageInput" }] : /* istanbul ignore next */ []));
54
+ this.linkInput = viewChild('linkInput', /* @ts-ignore */
55
+ ...(ngDevMode ? [{ debugName: "linkInput" }] : /* istanbul ignore next */ []));
56
+ // Configuration inputs & model signals
57
+ this.value = model('', /* @ts-ignore */
58
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
59
+ this.format = input('html', /* @ts-ignore */
60
+ ...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
61
+ this.placeholder = input('Type your content here...', /* @ts-ignore */
62
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
63
+ this.readonly = input(false, /* @ts-ignore */
64
+ ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
65
+ this.toolbar = input(true, /* @ts-ignore */
66
+ ...(ngDevMode ? [{ debugName: "toolbar" }] : /* istanbul ignore next */ []));
67
+ this.color = input(null, /* @ts-ignore */
68
+ ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
69
+ this.variant = input('base', /* @ts-ignore */
70
+ ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
71
+ this.customCommands = input([], /* @ts-ignore */
72
+ ...(ngDevMode ? [{ debugName: "customCommands" }] : /* istanbul ignore next */ []));
73
+ // Slash commands state signals
74
+ this.showSlashMenu = signal(false, /* @ts-ignore */
75
+ ...(ngDevMode ? [{ debugName: "showSlashMenu" }] : /* istanbul ignore next */ []));
76
+ this.slashSearchQuery = signal('', /* @ts-ignore */
77
+ ...(ngDevMode ? [{ debugName: "slashSearchQuery" }] : /* istanbul ignore next */ []));
78
+ this.slashMenuTop = signal(0, /* @ts-ignore */
79
+ ...(ngDevMode ? [{ debugName: "slashMenuTop" }] : /* istanbul ignore next */ []));
80
+ this.slashMenuLeft = signal(0, /* @ts-ignore */
81
+ ...(ngDevMode ? [{ debugName: "slashMenuLeft" }] : /* istanbul ignore next */ []));
82
+ this.#lastValueWrittenFromDOM = undefined;
83
+ this.#lastEditorElement = null;
84
+ this.#historyStack = [];
85
+ this.#historyIndex = -1;
86
+ this.#maxHistorySize = 100;
87
+ this.#isInternalDOMUpdate = false;
88
+ this.defaultCommands = computed(() => [
89
+ {
90
+ id: 'paragraph',
91
+ label: 'Normal Text',
92
+ icon: 'paragraph',
93
+ description: 'Start writing with plain text',
94
+ action: (editor) => editor.selectBlockType('p'),
95
+ },
96
+ {
97
+ id: 'h1',
98
+ label: 'Heading 1',
99
+ icon: 'text-h-one',
100
+ description: 'Big section heading',
101
+ action: (editor) => editor.selectBlockType('h1'),
102
+ },
103
+ {
104
+ id: 'h2',
105
+ label: 'Heading 2',
106
+ icon: 'text-h-two',
107
+ description: 'Medium section heading',
108
+ action: (editor) => editor.selectBlockType('h2'),
109
+ },
110
+ {
111
+ id: 'h3',
112
+ label: 'Heading 3',
113
+ icon: 'text-h-three',
114
+ description: 'Small section heading',
115
+ action: (editor) => editor.selectBlockType('h3'),
116
+ },
117
+ {
118
+ id: 'bullet-list',
119
+ label: 'Bullet List',
120
+ icon: 'list-bullets',
121
+ description: 'Create a simple bullet list',
122
+ action: (editor) => editor.formatText('insertUnorderedList'),
123
+ },
124
+ {
125
+ id: 'ordered-list',
126
+ label: 'Numbered List',
127
+ icon: 'list-numbers',
128
+ description: 'Create a list with numbering',
129
+ action: (editor) => editor.formatText('insertOrderedList'),
130
+ },
131
+ {
132
+ id: 'quote',
133
+ label: 'Quote',
134
+ icon: 'quotes',
135
+ description: 'Capture a quote',
136
+ action: (editor) => editor.selectBlockType('blockquote'),
137
+ },
138
+ {
139
+ id: 'code-block',
140
+ label: 'Code Block',
141
+ icon: 'code',
142
+ description: 'Write code snippets',
143
+ action: (editor) => editor.selectBlockType('pre'),
144
+ },
145
+ {
146
+ id: 'image',
147
+ label: 'Image',
148
+ icon: 'image',
149
+ description: 'Insert an image from URL or upload',
150
+ action: (editor) => editor.openImageModal(),
151
+ },
152
+ {
153
+ id: 'link',
154
+ label: 'Link',
155
+ icon: 'link',
156
+ description: 'Insert a hyperlink',
157
+ action: (editor) => editor.openLinkModal(),
158
+ },
159
+ ], /* @ts-ignore */
160
+ ...(ngDevMode ? [{ debugName: "defaultCommands" }] : /* istanbul ignore next */ []));
161
+ this.slashCommands = input(true, /* @ts-ignore */
162
+ ...(ngDevMode ? [{ debugName: "slashCommands" }] : /* istanbul ignore next */ []));
163
+ this.allCommands = computed(() => [...this.defaultCommands(), ...this.customCommands()], /* @ts-ignore */
164
+ ...(ngDevMode ? [{ debugName: "allCommands" }] : /* istanbul ignore next */ []));
165
+ this.enabledCommands = computed(() => {
166
+ const sc = this.slashCommands();
167
+ if (sc === false)
168
+ return [];
169
+ if (Array.isArray(sc)) {
170
+ return this.allCommands().filter((cmd) => sc.includes(cmd.id));
171
+ }
172
+ return this.allCommands();
173
+ }, /* @ts-ignore */
174
+ ...(ngDevMode ? [{ debugName: "enabledCommands" }] : /* istanbul ignore next */ []));
175
+ this.hasSlashCommands = computed(() => !this.readonly() && this.enabledCommands().length > 0, /* @ts-ignore */
176
+ ...(ngDevMode ? [{ debugName: "hasSlashCommands" }] : /* istanbul ignore next */ []));
177
+ this.filteredCommands = computed(() => {
178
+ const query = this.slashSearchQuery().toLowerCase().trim();
179
+ if (!query)
180
+ return this.enabledCommands();
181
+ return this.enabledCommands().filter((cmd) => cmd.label.toLowerCase().includes(query) ||
182
+ (cmd.description && cmd.description.toLowerCase().includes(query)) ||
183
+ cmd.id.toLowerCase().includes(query));
184
+ }, /* @ts-ignore */
185
+ ...(ngDevMode ? [{ debugName: "filteredCommands" }] : /* istanbul ignore next */ []));
186
+ // Toolbar group configuration inputs
187
+ this.showFormats = input(true, /* @ts-ignore */
188
+ ...(ngDevMode ? [{ debugName: "showFormats" }] : /* istanbul ignore next */ []));
189
+ this.showBlocks = input(true, /* @ts-ignore */
190
+ ...(ngDevMode ? [{ debugName: "showBlocks" }] : /* istanbul ignore next */ []));
191
+ this.showLists = input(true, /* @ts-ignore */
192
+ ...(ngDevMode ? [{ debugName: "showLists" }] : /* istanbul ignore next */ []));
193
+ this.showAlignments = input(true, /* @ts-ignore */
194
+ ...(ngDevMode ? [{ debugName: "showAlignments" }] : /* istanbul ignore next */ []));
195
+ this.showInsertions = input(true, /* @ts-ignore */
196
+ ...(ngDevMode ? [{ debugName: "showInsertions" }] : /* istanbul ignore next */ []));
197
+ this.showHistory = input(true, /* @ts-ignore */
198
+ ...(ngDevMode ? [{ debugName: "showHistory" }] : /* istanbul ignore next */ []));
199
+ // File upload output hook & configuration
200
+ this.customUpload = input(false, /* @ts-ignore */
201
+ ...(ngDevMode ? [{ debugName: "customUpload" }] : /* istanbul ignore next */ []));
202
+ this.imageUploadEnabled = input(true, /* @ts-ignore */
203
+ ...(ngDevMode ? [{ debugName: "imageUploadEnabled" }] : /* istanbul ignore next */ []));
204
+ this.imageUpload = output();
205
+ // Image layout overlay signals
206
+ this.#selectedImage = signal(null, /* @ts-ignore */
207
+ ...(ngDevMode ? [{ debugName: "#selectedImage" }] : /* istanbul ignore next */ []));
208
+ this.imgToolbarTop = signal(0, /* @ts-ignore */
209
+ ...(ngDevMode ? [{ debugName: "imgToolbarTop" }] : /* istanbul ignore next */ []));
210
+ this.imgToolbarLeft = signal(0, /* @ts-ignore */
211
+ ...(ngDevMode ? [{ debugName: "imgToolbarLeft" }] : /* istanbul ignore next */ []));
212
+ this.imgMode = signal('content', /* @ts-ignore */
213
+ ...(ngDevMode ? [{ debugName: "imgMode" }] : /* istanbul ignore next */ []));
214
+ this.imgSize = signal('auto', /* @ts-ignore */
215
+ ...(ngDevMode ? [{ debugName: "imgSize" }] : /* istanbul ignore next */ []));
216
+ // Local state signals
217
+ this.viewMode = signal('design', /* @ts-ignore */
218
+ ...(ngDevMode ? [{ debugName: "viewMode" }] : /* istanbul ignore next */ []));
219
+ this.isFocused = signal(false, /* @ts-ignore */
220
+ ...(ngDevMode ? [{ debugName: "isFocused" }] : /* istanbul ignore next */ []));
221
+ this.showLinkModal = signal(false, /* @ts-ignore */
222
+ ...(ngDevMode ? [{ debugName: "showLinkModal" }] : /* istanbul ignore next */ []));
223
+ this.showImageModal = signal(false, /* @ts-ignore */
224
+ ...(ngDevMode ? [{ debugName: "showImageModal" }] : /* istanbul ignore next */ []));
225
+ this.rawCodeValue = signal('', /* @ts-ignore */
226
+ ...(ngDevMode ? [{ debugName: "rawCodeValue" }] : /* istanbul ignore next */ []));
227
+ this.showBlockMenu = signal(false, /* @ts-ignore */
228
+ ...(ngDevMode ? [{ debugName: "showBlockMenu" }] : /* istanbul ignore next */ []));
229
+ // Toolbar active states
230
+ this.isBold = signal(false, /* @ts-ignore */
231
+ ...(ngDevMode ? [{ debugName: "isBold" }] : /* istanbul ignore next */ []));
232
+ this.isItalic = signal(false, /* @ts-ignore */
233
+ ...(ngDevMode ? [{ debugName: "isItalic" }] : /* istanbul ignore next */ []));
234
+ this.isUnderline = signal(false, /* @ts-ignore */
235
+ ...(ngDevMode ? [{ debugName: "isUnderline" }] : /* istanbul ignore next */ []));
236
+ this.isStrike = signal(false, /* @ts-ignore */
237
+ ...(ngDevMode ? [{ debugName: "isStrike" }] : /* istanbul ignore next */ []));
238
+ this.align = signal('left', /* @ts-ignore */
239
+ ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
240
+ this.activeBlock = signal('p', /* @ts-ignore */
241
+ ...(ngDevMode ? [{ debugName: "activeBlock" }] : /* istanbul ignore next */ []));
242
+ this.canUndo = signal(false, /* @ts-ignore */
243
+ ...(ngDevMode ? [{ debugName: "canUndo" }] : /* istanbul ignore next */ []));
244
+ this.canRedo = signal(false, /* @ts-ignore */
245
+ ...(ngDevMode ? [{ debugName: "canRedo" }] : /* istanbul ignore next */ []));
246
+ // Selection backup for inserting links/images when modal is open
247
+ this.#savedRange = null;
248
+ // Text metrics signals
249
+ this.charCount = signal(0, /* @ts-ignore */
250
+ ...(ngDevMode ? [{ debugName: "charCount" }] : /* istanbul ignore next */ []));
251
+ this.wordCount = signal(0, /* @ts-ignore */
252
+ ...(ngDevMode ? [{ debugName: "wordCount" }] : /* istanbul ignore next */ []));
253
+ // Form accessors callbacks
254
+ this.onChange = () => { };
255
+ this.onTouched = () => { };
256
+ // Host CSS classes resolver
257
+ this.hostClasses = shipComponentClasses('editor', {
258
+ color: this.color,
259
+ variant: this.variant,
260
+ readonly: this.readonly,
261
+ });
262
+ // Track outer changes
263
+ this.#isWriting = false;
264
+ this.#lastFormat = null;
265
+ // --- IMAGE FILE HANDLING & LAYOUT OVERLAYS ---
266
+ this.selectedImage = this.#selectedImage.asReadonly();
267
+ // Auto-focus link dialog input when opened
268
+ effect(() => {
269
+ if (this.showLinkModal()) {
270
+ const linkInput = this.linkInput();
271
+ if (linkInput) {
272
+ linkInput.nativeElement.focus();
273
+ }
274
+ }
275
+ });
276
+ // Auto-focus image dialog elements when opened based on configuration
277
+ effect(() => {
278
+ if (this.showImageModal()) {
279
+ const uploadBtn = this.uploadBtn();
280
+ const imageInput = this.imageInput();
281
+ if (this.imageUploadEnabled()) {
282
+ if (uploadBtn) {
283
+ uploadBtn.nativeElement.focus();
284
+ }
285
+ }
286
+ else {
287
+ if (imageInput) {
288
+ imageInput.nativeElement.focus();
289
+ }
290
+ }
291
+ }
292
+ });
293
+ // Keep DOM inside editor in sync with value model changes reactively (when written from parent)
294
+ effect(() => {
295
+ const val = this.value();
296
+ const editor = this.editorRef()?.nativeElement;
297
+ // If the DOM element itself has changed (re-created during HMR), we must reset
298
+ // lastValueWrittenFromDOM to force content initialization onto the new element.
299
+ if (editor && editor !== this.#lastEditorElement) {
300
+ this.#lastEditorElement = editor;
301
+ this.#lastValueWrittenFromDOM = undefined;
302
+ }
303
+ if (val === this.#lastValueWrittenFromDOM)
304
+ return;
305
+ this.#syncModelToDOM(val);
306
+ });
307
+ // React to format changes by converting the model value to the new format reactively
308
+ effect(() => {
309
+ const fmt = this.format();
310
+ const prev = this.#lastFormat;
311
+ this.#lastFormat = fmt;
312
+ if (prev !== null && prev !== fmt) {
313
+ const val = this.value();
314
+ // 1. Convert current value in prev format to HTML
315
+ let html = '';
316
+ if (prev === 'html' && typeof val === 'string') {
317
+ html = val;
318
+ }
319
+ else if (prev === 'markdown' && typeof val === 'string') {
320
+ html = this.#markdownToHTML(val);
321
+ }
322
+ else if (prev === 'json' && Array.isArray(val)) {
323
+ html = this.#jsonToHTML(val);
324
+ }
325
+ // 2. Convert HTML to the new format
326
+ let newValue = '';
327
+ if (fmt === 'html') {
328
+ newValue = html;
329
+ }
330
+ else if (fmt === 'markdown') {
331
+ newValue = this.#htmlToMarkdown(html);
332
+ }
333
+ else if (fmt === 'json') {
334
+ newValue = this.#htmlToJSON(html);
335
+ }
336
+ // 3. Update the model and DOM
337
+ this.#isWriting = true;
338
+ this.value.set(newValue);
339
+ this.#lastValueWrittenFromDOM = newValue;
340
+ this.onChange(newValue);
341
+ if (fmt === 'markdown' && typeof newValue === 'string') {
342
+ this.rawCodeValue.set(newValue);
343
+ }
344
+ else if (fmt === 'json' && Array.isArray(newValue)) {
345
+ this.rawCodeValue.set(JSON.stringify(newValue, null, 2));
346
+ }
347
+ else {
348
+ this.rawCodeValue.set(html);
349
+ }
350
+ const editor = this.editorRef()?.nativeElement;
351
+ if (editor) {
352
+ const normalizedHTML = html === '<p><br></p>' ? '' : html;
353
+ const currentNormalized = editor.innerHTML === '<p><br></p>' ? '' : editor.innerHTML;
354
+ if (normalizedHTML !== currentNormalized) {
355
+ editor.innerHTML = html;
356
+ }
357
+ this.#ensureImagesFocusable();
358
+ }
359
+ this.#isWriting = false;
360
+ }
361
+ });
362
+ // Recalculate metrics whenever raw content updates
363
+ effect(() => {
364
+ const val = this.value();
365
+ const fmt = this.format();
366
+ let html = '';
367
+ if (!val) {
368
+ html = '';
369
+ }
370
+ else if (typeof val === 'string') {
371
+ if (fmt === 'markdown') {
372
+ html = this.#markdownToHTML(val);
373
+ }
374
+ else {
375
+ html = val;
376
+ }
377
+ }
378
+ else if (Array.isArray(val)) {
379
+ html = this.#jsonToHTML(val);
380
+ }
381
+ const temp = this.#document.createElement('div');
382
+ temp.innerHTML = html;
383
+ const text = (temp.textContent || '').replace(/\u00a0/g, ' ').trim();
384
+ this.charCount.set(text.length);
385
+ this.wordCount.set(text === '' ? 0 : text.split(/\s+/).filter((w) => w.length > 0).length);
386
+ });
387
+ // Reinitialize toolbar tabindexes when visibility inputs change
388
+ effect(() => {
389
+ this.showFormats();
390
+ this.showBlocks();
391
+ this.showLists();
392
+ this.showAlignments();
393
+ this.showInsertions();
394
+ this.showHistory();
395
+ this.toolbar();
396
+ this.readonly();
397
+ this.#initializeToolbarTabindexes();
398
+ });
399
+ }
400
+ ngOnInit() {
401
+ // Empty hook
402
+ }
403
+ ngAfterViewInit() {
404
+ this.#syncModelToDOM(this.value());
405
+ }
406
+ ngOnDestroy() {
407
+ this.#savedRange = null;
408
+ if (this.#typingTimeout) {
409
+ clearTimeout(this.#typingTimeout);
410
+ }
411
+ }
412
+ // --- CONTROL VALUE ACCESSOR ---
413
+ writeValue(obj) {
414
+ this.#isWriting = true;
415
+ this.value.set(obj);
416
+ this.#syncModelToDOM(obj);
417
+ this.#isWriting = false;
418
+ }
419
+ registerOnChange(fn) {
420
+ this.onChange = fn;
421
+ }
422
+ registerOnTouched(fn) {
423
+ this.onTouched = fn;
424
+ }
425
+ setDisabledState(isDisabled) {
426
+ // In Angular forms, this matches readonly
427
+ }
428
+ // --- MODEL TO DOM SYNCING ---
429
+ #syncModelToDOM(val) {
430
+ let html = '';
431
+ if (val === null || val === undefined || val === '') {
432
+ html = '<p><br></p>';
433
+ }
434
+ else if (typeof val === 'string') {
435
+ if (this.format() === 'markdown') {
436
+ html = this.#markdownToHTML(val);
437
+ }
438
+ else {
439
+ html = val;
440
+ }
441
+ }
442
+ else if (Array.isArray(val)) {
443
+ html = this.#jsonToHTML(val);
444
+ }
445
+ else {
446
+ html = '<p><br></p>';
447
+ }
448
+ const isNewValue = val !== this.#lastValueWrittenFromDOM;
449
+ // Set code editor value
450
+ if (this.format() === 'markdown' && typeof val === 'string') {
451
+ this.rawCodeValue.set(val);
452
+ }
453
+ else if (this.format() === 'json' && Array.isArray(val)) {
454
+ this.rawCodeValue.set(JSON.stringify(val, null, 2));
455
+ }
456
+ else {
457
+ this.rawCodeValue.set(html);
458
+ }
459
+ // Update WYSIWYG editor surface if in design mode
460
+ const editor = this.editorRef()?.nativeElement;
461
+ if (editor) {
462
+ this.#lastValueWrittenFromDOM = val;
463
+ // Avoid resetting selection if html content is equivalent
464
+ const normalizedHTML = html === '<p><br></p>' ? '' : html;
465
+ const currentNormalized = editor.innerHTML === '<p><br></p>' ? '' : editor.innerHTML;
466
+ if (normalizedHTML !== currentNormalized) {
467
+ editor.innerHTML = html;
468
+ }
469
+ this.#ensureImagesFocusable();
470
+ if (isNewValue) {
471
+ this.#historyStack = [];
472
+ this.#historyIndex = -1;
473
+ }
474
+ if (this.#historyStack.length === 0) {
475
+ this.#saveHistory();
476
+ }
477
+ }
478
+ this.#updateHistoryStates();
479
+ }
480
+ // --- DOM TO MODEL SYNCING ---
481
+ #updateValueFromDOM() {
482
+ const editor = this.editorRef()?.nativeElement;
483
+ if (!editor)
484
+ return;
485
+ let html = editor.innerHTML;
486
+ // Normalize empty editor contents to empty string or basic paragraph
487
+ if (html === '' || html === '<br>' || html === '<p><br></p>') {
488
+ html = '';
489
+ }
490
+ this.#isWriting = true;
491
+ const currentFormat = this.format();
492
+ if (currentFormat === 'html') {
493
+ this.value.set(html);
494
+ this.#lastValueWrittenFromDOM = html;
495
+ this.onChange(html);
496
+ }
497
+ else if (currentFormat === 'markdown') {
498
+ const md = this.#htmlToMarkdown(html);
499
+ this.value.set(md);
500
+ this.#lastValueWrittenFromDOM = md;
501
+ this.onChange(md);
502
+ this.rawCodeValue.set(md);
503
+ }
504
+ else if (currentFormat === 'json') {
505
+ const json = this.#htmlToJSON(html);
506
+ this.value.set(json);
507
+ this.#lastValueWrittenFromDOM = json;
508
+ this.onChange(json);
509
+ this.rawCodeValue.set(JSON.stringify(json, null, 2));
510
+ }
511
+ this.#isWriting = false;
512
+ }
513
+ // --- COMPONENT HANDLERS ---
514
+ onDOMInput() {
515
+ this.#ensureImagesFocusable();
516
+ this.#updateValueFromDOM();
517
+ // Debounce history save on typing (500ms) or save immediately on word/sentence boundaries
518
+ const selection = window.getSelection();
519
+ let saveImmediately = false;
520
+ if (selection && selection.rangeCount > 0) {
521
+ const range = selection.getRangeAt(0);
522
+ const textNode = range.startContainer;
523
+ if (textNode.nodeType === Node.TEXT_NODE) {
524
+ const char = textNode.textContent?.charAt(range.startOffset - 1);
525
+ if (char && /[\s.,!?;/]/.test(char)) {
526
+ saveImmediately = true;
527
+ }
528
+ }
529
+ }
530
+ if (this.#typingTimeout) {
531
+ clearTimeout(this.#typingTimeout);
532
+ }
533
+ if (saveImmediately) {
534
+ this.#saveHistory();
535
+ }
536
+ else {
537
+ this.#typingTimeout = setTimeout(() => {
538
+ this.#saveHistory();
539
+ }, 500);
540
+ }
541
+ }
542
+ onDOMBlur() {
543
+ this.isFocused.set(false);
544
+ this.onTouched();
545
+ }
546
+ onCodeInput(event) {
547
+ const textarea = event.target;
548
+ this.rawCodeValue.set(textarea.value);
549
+ }
550
+ onCodeBlur(event) {
551
+ this.isFocused.set(false);
552
+ const textarea = event.target;
553
+ const codeVal = textarea.value;
554
+ this.#isWriting = true;
555
+ const currentFormat = this.format();
556
+ if (currentFormat === 'html') {
557
+ this.value.set(codeVal);
558
+ this.onChange(codeVal);
559
+ }
560
+ else if (currentFormat === 'markdown') {
561
+ this.value.set(codeVal);
562
+ this.onChange(codeVal);
563
+ }
564
+ else if (currentFormat === 'json') {
565
+ try {
566
+ const parsed = JSON.parse(codeVal);
567
+ this.value.set(parsed);
568
+ this.onChange(parsed);
569
+ }
570
+ catch (e) {
571
+ // invalid JSON, do not update model, let user fix it
572
+ }
573
+ }
574
+ this.#isWriting = false;
575
+ this.onTouched();
576
+ }
577
+ // --- SELECTION STATE & COMMANDS ---
578
+ formatText(command, value = '') {
579
+ if (this.readonly())
580
+ return;
581
+ if (this.viewMode() === 'code')
582
+ return;
583
+ const cmd = command.toLowerCase();
584
+ if (cmd === 'bold') {
585
+ this.applyInlineStyle('strong');
586
+ }
587
+ else if (cmd === 'italic') {
588
+ this.applyInlineStyle('em');
589
+ }
590
+ else if (cmd === 'underline') {
591
+ this.applyInlineStyle('u');
592
+ }
593
+ else if (cmd === 'strikethrough') {
594
+ this.applyInlineStyle('s');
595
+ }
596
+ else if (cmd === 'undo') {
597
+ this.undo();
598
+ }
599
+ else if (cmd === 'redo') {
600
+ this.redo();
601
+ }
602
+ else if (cmd === 'insertunorderedlist') {
603
+ this.toggleList('ul');
604
+ }
605
+ else if (cmd === 'insertorderedlist') {
606
+ this.toggleList('ol');
607
+ }
608
+ else if (cmd === 'inserthorizontalrule') {
609
+ this.insertHorizontalRule();
610
+ }
611
+ else if (cmd === 'removeformat') {
612
+ this.removeFormat();
613
+ }
614
+ else if (cmd === 'justifyleft') {
615
+ this.setAlign('left');
616
+ }
617
+ else if (cmd === 'justifycenter') {
618
+ this.setAlign('center');
619
+ }
620
+ else if (cmd === 'justifyright') {
621
+ this.setAlign('right');
622
+ }
623
+ else if (cmd === 'formatblock') {
624
+ this.setBlockType(value);
625
+ }
626
+ }
627
+ applyInlineStyle(tag) {
628
+ if (this.readonly() || this.viewMode() === 'code')
629
+ return;
630
+ this.#restoreSelection();
631
+ const editor = this.editorRef()?.nativeElement;
632
+ const selection = window.getSelection();
633
+ if (!selection || selection.rangeCount === 0 || selection.isCollapsed)
634
+ return;
635
+ const range = selection.getRangeAt(0);
636
+ if (!editor?.contains(range.commonAncestorContainer))
637
+ return;
638
+ this.#saveHistory();
639
+ this.#isInternalDOMUpdate = true;
640
+ const textNodes = getTextNodesInRange(range);
641
+ if (textNodes.length === 0) {
642
+ this.#isInternalDOMUpdate = false;
643
+ return;
644
+ }
645
+ const tagName = tag.toUpperCase();
646
+ const allWrapped = textNodes.every((node) => isNodeWrappedInTag(node, tag, editor));
647
+ let finalStartNode = range.startContainer;
648
+ let finalStartOffset = range.startOffset;
649
+ let finalEndNode = range.endContainer;
650
+ let finalEndOffset = range.endOffset;
651
+ if (allWrapped) {
652
+ const elementsToUnwrap = new Set();
653
+ for (const node of textNodes) {
654
+ let current = node.parentNode;
655
+ while (current && current !== editor) {
656
+ if (current.nodeType === Node.ELEMENT_NODE && current.tagName === tagName) {
657
+ elementsToUnwrap.add(current);
658
+ }
659
+ current = current.parentNode;
660
+ }
661
+ }
662
+ for (const el of elementsToUnwrap) {
663
+ const parent = el.parentNode;
664
+ if (parent) {
665
+ while (el.firstChild) {
666
+ parent.insertBefore(el.firstChild, el);
667
+ }
668
+ parent.removeChild(el);
669
+ }
670
+ }
671
+ }
672
+ else {
673
+ for (const node of textNodes) {
674
+ const isStart = node === range.startContainer;
675
+ const isEnd = node === range.endContainer;
676
+ let start = isStart ? range.startOffset : 0;
677
+ let end = isEnd ? range.endOffset : node.textContent.length;
678
+ if (start >= end)
679
+ continue;
680
+ let targetNode = node;
681
+ if (isStart && start > 0) {
682
+ targetNode = node.splitText(start);
683
+ if (isEnd) {
684
+ finalEndNode = targetNode;
685
+ finalEndOffset = end - start;
686
+ }
687
+ finalStartNode = targetNode;
688
+ finalStartOffset = 0;
689
+ end = end - start;
690
+ start = 0;
691
+ }
692
+ if (isEnd && finalEndOffset < targetNode.textContent.length) {
693
+ targetNode.splitText(finalEndOffset);
694
+ finalEndNode = targetNode;
695
+ finalEndOffset = targetNode.textContent.length;
696
+ end = targetNode.textContent.length;
697
+ }
698
+ if (!isNodeWrappedInTag(targetNode, tag, editor)) {
699
+ const element = node.ownerDocument.createElement(tag);
700
+ targetNode.parentNode?.insertBefore(element, targetNode);
701
+ element.appendChild(targetNode);
702
+ }
703
+ }
704
+ }
705
+ const newRange = this.#document.createRange();
706
+ try {
707
+ newRange.setStart(finalStartNode, finalStartOffset);
708
+ newRange.setEnd(finalEndNode, finalEndOffset);
709
+ selection.removeAllRanges();
710
+ selection.addRange(newRange);
711
+ }
712
+ catch (e) { }
713
+ this.#updateValueFromDOM();
714
+ this.#saveHistory();
715
+ this.#isInternalDOMUpdate = false;
716
+ this.onSelectionChange();
717
+ }
718
+ toggleLink(url) {
719
+ if (this.readonly() || this.viewMode() === 'code')
720
+ return;
721
+ this.#restoreSelection();
722
+ const editor = this.editorRef()?.nativeElement;
723
+ const selection = window.getSelection();
724
+ if (!selection || selection.rangeCount === 0)
725
+ return;
726
+ const range = selection.getRangeAt(0);
727
+ if (!editor?.contains(range.commonAncestorContainer))
728
+ return;
729
+ this.#saveHistory();
730
+ this.#isInternalDOMUpdate = true;
731
+ let existingLink = null;
732
+ let node = range.commonAncestorContainer;
733
+ while (node && node !== editor) {
734
+ if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'A') {
735
+ existingLink = node;
736
+ break;
737
+ }
738
+ node = node.parentNode;
739
+ }
740
+ if (existingLink) {
741
+ if (!url) {
742
+ const parent = existingLink.parentNode;
743
+ if (parent) {
744
+ while (existingLink.firstChild) {
745
+ parent.insertBefore(existingLink.firstChild, existingLink);
746
+ }
747
+ parent.removeChild(existingLink);
748
+ }
749
+ }
750
+ else {
751
+ existingLink.href = url;
752
+ }
753
+ }
754
+ else if (url && !selection.isCollapsed) {
755
+ const link = this.#document.createElement('a');
756
+ link.href = url;
757
+ link.target = '_blank';
758
+ try {
759
+ link.appendChild(range.extractContents());
760
+ range.insertNode(link);
761
+ const newRange = this.#document.createRange();
762
+ newRange.selectNodeContents(link);
763
+ selection.removeAllRanges();
764
+ selection.addRange(newRange);
765
+ }
766
+ catch (e) {
767
+ // Fallback
768
+ }
769
+ }
770
+ this.#updateValueFromDOM();
771
+ this.#saveHistory();
772
+ this.#isInternalDOMUpdate = false;
773
+ this.onSelectionChange();
774
+ }
775
+ setBlockType(tag) {
776
+ if (this.readonly() || this.viewMode() === 'code')
777
+ return;
778
+ this.#restoreSelection();
779
+ const editor = this.editorRef()?.nativeElement;
780
+ const selection = window.getSelection();
781
+ if (!selection || selection.rangeCount === 0)
782
+ return;
783
+ const range = selection.getRangeAt(0);
784
+ if (!editor?.contains(range.commonAncestorContainer))
785
+ return;
786
+ this.#saveHistory();
787
+ this.#isInternalDOMUpdate = true;
788
+ let blockNode = null;
789
+ let node = range.commonAncestorContainer;
790
+ while (node && node !== editor) {
791
+ if (node.nodeType === Node.ELEMENT_NODE) {
792
+ const el = node;
793
+ if (el.parentElement === editor) {
794
+ blockNode = el;
795
+ break;
796
+ }
797
+ }
798
+ node = node.parentNode;
799
+ }
800
+ if (!blockNode) {
801
+ node = range.commonAncestorContainer;
802
+ while (node && node !== editor) {
803
+ if (node.nodeType === Node.ELEMENT_NODE) {
804
+ const el = node;
805
+ const tagName = el.tagName.toLowerCase();
806
+ if (['p', 'h1', 'h2', 'h3', 'blockquote', 'pre', 'ul', 'ol', 'li'].includes(tagName)) {
807
+ blockNode = el;
808
+ break;
809
+ }
810
+ }
811
+ node = node.parentNode;
812
+ }
813
+ }
814
+ if (blockNode) {
815
+ const newBlock = this.#document.createElement(tag);
816
+ newBlock.innerHTML = blockNode.innerHTML;
817
+ if (blockNode.className) {
818
+ newBlock.className = blockNode.className;
819
+ }
820
+ if (tag === 'pre') {
821
+ newBlock.innerHTML = `<code>${blockNode.innerText || blockNode.innerHTML}</code>`;
822
+ }
823
+ else if (blockNode.tagName.toLowerCase() === 'pre' && tag !== 'pre') {
824
+ const codeEl = blockNode.querySelector('code');
825
+ if (codeEl) {
826
+ newBlock.innerHTML = codeEl.innerHTML;
827
+ }
828
+ }
829
+ // Ensure empty block has placeholder <br> so caret is visible and focusable
830
+ if (!newBlock.innerHTML || newBlock.innerHTML === '<br>') {
831
+ newBlock.innerHTML = '<br>';
832
+ }
833
+ const parent = blockNode.parentNode;
834
+ if (parent) {
835
+ parent.replaceChild(newBlock, blockNode);
836
+ try {
837
+ const newRange = this.#document.createRange();
838
+ if (newBlock.textContent?.trim() === '') {
839
+ newRange.setStart(newBlock, 0);
840
+ newRange.collapse(true);
841
+ }
842
+ else {
843
+ newRange.selectNodeContents(newBlock);
844
+ newRange.collapse(false);
845
+ }
846
+ selection.removeAllRanges();
847
+ selection.addRange(newRange);
848
+ }
849
+ catch (e) { }
850
+ }
851
+ }
852
+ else {
853
+ const newBlock = this.#document.createElement(tag);
854
+ newBlock.appendChild(range.extractContents());
855
+ range.insertNode(newBlock);
856
+ }
857
+ this.#updateValueFromDOM();
858
+ this.#saveHistory();
859
+ this.#isInternalDOMUpdate = false;
860
+ this.onSelectionChange();
861
+ }
862
+ toggleList(listType) {
863
+ if (this.readonly() || this.viewMode() === 'code')
864
+ return;
865
+ this.#restoreSelection();
866
+ const editor = this.editorRef()?.nativeElement;
867
+ const selection = window.getSelection();
868
+ if (!selection || selection.rangeCount === 0)
869
+ return;
870
+ const range = selection.getRangeAt(0);
871
+ if (!editor?.contains(range.commonAncestorContainer))
872
+ return;
873
+ this.#saveHistory();
874
+ this.#isInternalDOMUpdate = true;
875
+ let listNode = null;
876
+ let listTagName = '';
877
+ let node = range.commonAncestorContainer;
878
+ while (node && node !== editor) {
879
+ if (node.nodeType === Node.ELEMENT_NODE) {
880
+ const el = node;
881
+ const tag = el.tagName.toLowerCase();
882
+ if (tag === 'ul' || tag === 'ol') {
883
+ listNode = el;
884
+ listTagName = tag;
885
+ break;
886
+ }
887
+ }
888
+ node = node.parentNode;
889
+ }
890
+ if (listNode) {
891
+ if (listTagName === listType) {
892
+ const parent = listNode.parentNode;
893
+ if (parent) {
894
+ const items = Array.from(listNode.querySelectorAll('li'));
895
+ let lastP = null;
896
+ items.forEach((item) => {
897
+ const p = this.#document.createElement('p');
898
+ p.innerHTML = item.innerHTML;
899
+ if (!p.innerHTML || p.innerHTML === '<br>') {
900
+ p.innerHTML = '<br>';
901
+ }
902
+ parent.insertBefore(p, listNode);
903
+ lastP = p;
904
+ });
905
+ parent.removeChild(listNode);
906
+ if (lastP) {
907
+ try {
908
+ const newRange = this.#document.createRange();
909
+ if (lastP.textContent?.trim() === '') {
910
+ newRange.setStart(lastP, 0);
911
+ newRange.collapse(true);
912
+ }
913
+ else {
914
+ newRange.selectNodeContents(lastP);
915
+ newRange.collapse(false);
916
+ }
917
+ selection.removeAllRanges();
918
+ selection.addRange(newRange);
919
+ }
920
+ catch (e) { }
921
+ }
922
+ }
923
+ }
924
+ else {
925
+ const newList = this.#document.createElement(listType);
926
+ newList.innerHTML = listNode.innerHTML;
927
+ const parent = listNode.parentNode;
928
+ if (parent) {
929
+ parent.replaceChild(newList, listNode);
930
+ const firstItem = newList.querySelector('li');
931
+ if (firstItem) {
932
+ try {
933
+ const newRange = this.#document.createRange();
934
+ if (firstItem.textContent?.trim() === '') {
935
+ newRange.setStart(firstItem, 0);
936
+ newRange.collapse(true);
937
+ }
938
+ else {
939
+ newRange.selectNodeContents(firstItem);
940
+ newRange.collapse(false);
941
+ }
942
+ selection.removeAllRanges();
943
+ selection.addRange(newRange);
944
+ }
945
+ catch (e) { }
946
+ }
947
+ }
948
+ }
949
+ }
950
+ else {
951
+ let blockNode = null;
952
+ let node = range.commonAncestorContainer;
953
+ while (node && node !== editor) {
954
+ if (node.nodeType === Node.ELEMENT_NODE) {
955
+ const el = node;
956
+ if (el.parentElement === editor) {
957
+ blockNode = el;
958
+ break;
959
+ }
960
+ }
961
+ node = node.parentNode;
962
+ }
963
+ if (blockNode) {
964
+ const newList = this.#document.createElement(listType);
965
+ const item = this.#document.createElement('li');
966
+ item.innerHTML = blockNode.innerHTML;
967
+ if (!item.innerHTML || item.innerHTML === '<br>') {
968
+ item.innerHTML = '<br>';
969
+ }
970
+ newList.appendChild(item);
971
+ const parent = blockNode.parentNode;
972
+ if (parent) {
973
+ parent.replaceChild(newList, blockNode);
974
+ try {
975
+ const newRange = this.#document.createRange();
976
+ if (item.textContent?.trim() === '') {
977
+ newRange.setStart(item, 0);
978
+ newRange.collapse(true);
979
+ }
980
+ else {
981
+ newRange.selectNodeContents(item);
982
+ newRange.collapse(false);
983
+ }
984
+ selection.removeAllRanges();
985
+ selection.addRange(newRange);
986
+ }
987
+ catch (e) { }
988
+ }
989
+ }
990
+ }
991
+ this.#updateValueFromDOM();
992
+ this.#saveHistory();
993
+ this.#isInternalDOMUpdate = false;
994
+ this.onSelectionChange();
995
+ }
996
+ insertHorizontalRule() {
997
+ if (this.readonly() || this.viewMode() === 'code')
998
+ return;
999
+ this.#restoreSelection();
1000
+ const editor = this.editorRef()?.nativeElement;
1001
+ if (!editor)
1002
+ return;
1003
+ const selection = window.getSelection();
1004
+ if (!selection || selection.rangeCount === 0)
1005
+ return;
1006
+ const range = selection.getRangeAt(0);
1007
+ if (!editor.contains(range.commonAncestorContainer))
1008
+ return;
1009
+ this.#saveHistory();
1010
+ this.#isInternalDOMUpdate = true;
1011
+ const hr = this.#document.createElement('hr');
1012
+ range.deleteContents();
1013
+ range.insertNode(hr);
1014
+ const p = this.#document.createElement('p');
1015
+ p.innerHTML = '<br>';
1016
+ if (hr.nextSibling) {
1017
+ hr.parentNode?.insertBefore(p, hr.nextSibling);
1018
+ }
1019
+ else {
1020
+ hr.parentNode?.appendChild(p);
1021
+ }
1022
+ const newRange = this.#document.createRange();
1023
+ newRange.setStart(p, 0);
1024
+ newRange.collapse(true);
1025
+ selection.removeAllRanges();
1026
+ selection.addRange(newRange);
1027
+ this.#updateValueFromDOM();
1028
+ this.#saveHistory();
1029
+ this.#isInternalDOMUpdate = false;
1030
+ this.onSelectionChange();
1031
+ }
1032
+ setAlign(direction) {
1033
+ if (this.readonly() || this.viewMode() === 'code')
1034
+ return;
1035
+ this.#restoreSelection();
1036
+ const editor = this.editorRef()?.nativeElement;
1037
+ if (!editor)
1038
+ return;
1039
+ const selection = window.getSelection();
1040
+ if (!selection || selection.rangeCount === 0)
1041
+ return;
1042
+ const range = selection.getRangeAt(0);
1043
+ if (!editor.contains(range.commonAncestorContainer))
1044
+ return;
1045
+ this.#saveHistory();
1046
+ this.#isInternalDOMUpdate = true;
1047
+ let blockNode = null;
1048
+ let node = range.commonAncestorContainer;
1049
+ while (node && node !== editor) {
1050
+ if (node.nodeType === Node.ELEMENT_NODE) {
1051
+ const el = node;
1052
+ const tag = el.tagName.toLowerCase();
1053
+ if (['p', 'h1', 'h2', 'h3', 'blockquote', 'pre', 'li'].includes(tag)) {
1054
+ blockNode = el;
1055
+ break;
1056
+ }
1057
+ }
1058
+ node = node.parentNode;
1059
+ }
1060
+ if (blockNode) {
1061
+ blockNode.style.textAlign = direction;
1062
+ }
1063
+ this.#updateValueFromDOM();
1064
+ this.#saveHistory();
1065
+ this.#isInternalDOMUpdate = false;
1066
+ this.onSelectionChange();
1067
+ }
1068
+ removeFormat() {
1069
+ if (this.readonly() || this.viewMode() === 'code')
1070
+ return;
1071
+ this.#restoreSelection();
1072
+ const editor = this.editorRef()?.nativeElement;
1073
+ if (!editor)
1074
+ return;
1075
+ const selection = window.getSelection();
1076
+ if (!selection || selection.rangeCount === 0 || selection.isCollapsed)
1077
+ return;
1078
+ const range = selection.getRangeAt(0);
1079
+ if (!editor.contains(range.commonAncestorContainer))
1080
+ return;
1081
+ this.#saveHistory();
1082
+ this.#isInternalDOMUpdate = true;
1083
+ const fragment = range.extractContents();
1084
+ const stripTags = ['strong', 'em', 'u', 's', 'a', 'b', 'i', 'span'];
1085
+ const container = this.#document.createElement('div');
1086
+ container.appendChild(fragment);
1087
+ stripTags.forEach((tag) => {
1088
+ const els = Array.from(container.querySelectorAll(tag));
1089
+ els.forEach((el) => {
1090
+ const parent = el.parentNode;
1091
+ if (parent) {
1092
+ while (el.firstChild) {
1093
+ parent.insertBefore(el.firstChild, el);
1094
+ }
1095
+ parent.removeChild(el);
1096
+ }
1097
+ });
1098
+ });
1099
+ range.insertNode(container.firstChild || container);
1100
+ this.#updateValueFromDOM();
1101
+ this.#saveHistory();
1102
+ this.#isInternalDOMUpdate = false;
1103
+ this.onSelectionChange();
1104
+ }
1105
+ undo() {
1106
+ if (this.#historyIndex <= 0)
1107
+ return;
1108
+ this.#historyIndex--;
1109
+ this.#restoreHistoryState(this.#historyStack[this.#historyIndex]);
1110
+ }
1111
+ redo() {
1112
+ if (this.#historyIndex >= this.#historyStack.length - 1)
1113
+ return;
1114
+ this.#historyIndex++;
1115
+ this.#restoreHistoryState(this.#historyStack[this.#historyIndex]);
1116
+ }
1117
+ #saveHistory() {
1118
+ if (!this.#isBrowser)
1119
+ return;
1120
+ const editor = this.editorRef()?.nativeElement;
1121
+ if (!editor)
1122
+ return;
1123
+ const html = editor.innerHTML;
1124
+ if (this.#historyIndex >= 0 && this.#historyStack[this.#historyIndex].html === html) {
1125
+ return;
1126
+ }
1127
+ let caret = null;
1128
+ const selection = window.getSelection();
1129
+ if (selection && selection.rangeCount > 0) {
1130
+ const range = selection.getRangeAt(0);
1131
+ if (editor.contains(range.commonAncestorContainer)) {
1132
+ caret = {
1133
+ startPath: getNodePath(editor, range.startContainer),
1134
+ startOffset: range.startOffset,
1135
+ endPath: getNodePath(editor, range.endContainer),
1136
+ endOffset: range.endOffset,
1137
+ };
1138
+ }
1139
+ }
1140
+ if (this.#historyIndex < this.#historyStack.length - 1) {
1141
+ this.#historyStack = this.#historyStack.slice(0, this.#historyIndex + 1);
1142
+ }
1143
+ this.#historyStack.push({ html, caret });
1144
+ if (this.#historyStack.length > this.#maxHistorySize) {
1145
+ this.#historyStack.shift();
1146
+ }
1147
+ else {
1148
+ this.#historyIndex++;
1149
+ }
1150
+ this.#updateHistoryStates();
1151
+ }
1152
+ #restoreHistoryState(state) {
1153
+ const editor = this.editorRef()?.nativeElement;
1154
+ if (!editor)
1155
+ return;
1156
+ this.#isWriting = true;
1157
+ editor.innerHTML = state.html;
1158
+ this.#ensureImagesFocusable();
1159
+ this.#updateValueFromDOM();
1160
+ this.#isWriting = false;
1161
+ if (state.caret) {
1162
+ const startNode = getNodeByPath(editor, state.caret.startPath);
1163
+ const endNode = getNodeByPath(editor, state.caret.endPath);
1164
+ const selection = window.getSelection();
1165
+ if (selection) {
1166
+ try {
1167
+ const range = this.#document.createRange();
1168
+ range.setStart(startNode, state.caret.startOffset);
1169
+ range.setEnd(endNode, state.caret.endOffset);
1170
+ selection.removeAllRanges();
1171
+ selection.addRange(range);
1172
+ }
1173
+ catch (e) {
1174
+ // Fallback
1175
+ }
1176
+ }
1177
+ }
1178
+ this.#updateHistoryStates();
1179
+ }
1180
+ toggleViewMode() {
1181
+ const nextMode = this.viewMode() === 'design' ? 'code' : 'design';
1182
+ this.viewMode.set(nextMode);
1183
+ // Give time to render then sync and focus
1184
+ setTimeout(() => {
1185
+ if (nextMode === 'design') {
1186
+ const editor = this.editorRef()?.nativeElement;
1187
+ if (editor) {
1188
+ this.#syncModelToDOM(this.value());
1189
+ editor.focus();
1190
+ }
1191
+ }
1192
+ else {
1193
+ const codeEditor = this.codeEditorRef()?.nativeElement;
1194
+ if (codeEditor) {
1195
+ codeEditor.focus();
1196
+ }
1197
+ }
1198
+ this.#updateHistoryStates();
1199
+ });
1200
+ }
1201
+ #updateHistoryStates() {
1202
+ if (!this.#isBrowser || this.viewMode() === 'code') {
1203
+ this.canUndo.set(false);
1204
+ this.canRedo.set(false);
1205
+ return;
1206
+ }
1207
+ this.canUndo.set(this.#historyIndex > 0);
1208
+ this.canRedo.set(this.#historyIndex < this.#historyStack.length - 1);
1209
+ }
1210
+ #restoreSelection() {
1211
+ if (!this.#isBrowser)
1212
+ return;
1213
+ const selection = window.getSelection();
1214
+ const editor = this.editorRef()?.nativeElement;
1215
+ if (selection && this.#savedRange && editor) {
1216
+ let isInside = false;
1217
+ if (selection.rangeCount > 0) {
1218
+ const range = selection.getRangeAt(0);
1219
+ if (editor.contains(range.commonAncestorContainer)) {
1220
+ isInside = true;
1221
+ }
1222
+ }
1223
+ if (!isInside) {
1224
+ editor.focus();
1225
+ try {
1226
+ selection.removeAllRanges();
1227
+ selection.addRange(this.#savedRange);
1228
+ }
1229
+ catch (e) { }
1230
+ }
1231
+ }
1232
+ }
1233
+ onSelectionChange() {
1234
+ if (!this.#isBrowser)
1235
+ return;
1236
+ this.#updateHistoryStates();
1237
+ if (this.readonly() || this.viewMode() === 'code')
1238
+ return;
1239
+ const selection = window.getSelection();
1240
+ if (!selection || selection.rangeCount === 0)
1241
+ return;
1242
+ const range = selection.getRangeAt(0);
1243
+ const editorEl = this.editorRef()?.nativeElement;
1244
+ if (!editorEl || !editorEl.contains(range.commonAncestorContainer)) {
1245
+ return;
1246
+ }
1247
+ // Save selection range for modal operations
1248
+ this.#savedRange = range.cloneRange();
1249
+ // DOM tree traversal for formatting active states
1250
+ let current = range.commonAncestorContainer;
1251
+ let bold = false;
1252
+ let italic = false;
1253
+ let underline = false;
1254
+ let strike = false;
1255
+ let textAlign = 'left';
1256
+ while (current && current !== editorEl) {
1257
+ if (current.nodeType === Node.ELEMENT_NODE) {
1258
+ const el = current;
1259
+ const tag = el.tagName.toLowerCase();
1260
+ if (tag === 'strong' || tag === 'b')
1261
+ bold = true;
1262
+ if (tag === 'em' || tag === 'i')
1263
+ italic = true;
1264
+ if (tag === 'u')
1265
+ underline = true;
1266
+ if (tag === 's' || tag === 'del')
1267
+ strike = true;
1268
+ if (['p', 'h1', 'h2', 'h3', 'blockquote', 'pre', 'li'].includes(tag)) {
1269
+ const alignValue = el.style.textAlign || window.getComputedStyle(el).textAlign;
1270
+ if (alignValue === 'center')
1271
+ textAlign = 'center';
1272
+ else if (alignValue === 'right')
1273
+ textAlign = 'right';
1274
+ else
1275
+ textAlign = 'left';
1276
+ }
1277
+ }
1278
+ current = current.parentNode;
1279
+ }
1280
+ this.isBold.set(bold);
1281
+ this.isItalic.set(italic);
1282
+ this.isUnderline.set(underline);
1283
+ this.isStrike.set(strike);
1284
+ this.align.set(textAlign);
1285
+ // Active block traverse
1286
+ let node = selection.anchorNode;
1287
+ let blockType = 'p';
1288
+ while (node && node !== editorEl) {
1289
+ if (node.nodeType === Node.ELEMENT_NODE) {
1290
+ const tag = node.tagName.toLowerCase();
1291
+ if (['h1', 'h2', 'h3', 'blockquote', 'pre', 'ul', 'ol', 'li'].includes(tag)) {
1292
+ blockType = tag;
1293
+ break;
1294
+ }
1295
+ }
1296
+ node = node.parentNode;
1297
+ }
1298
+ this.activeBlock.set(blockType);
1299
+ // Track active block class for paragraph placeholders
1300
+ const activeBlocks = editorEl.querySelectorAll('.sh-editor-active-block');
1301
+ activeBlocks.forEach((b) => b.classList.remove('sh-editor-active-block'));
1302
+ let activeNode = selection.anchorNode;
1303
+ if (activeNode) {
1304
+ if (activeNode.nodeType === Node.TEXT_NODE) {
1305
+ activeNode = activeNode.parentElement;
1306
+ }
1307
+ while (activeNode && activeNode !== editorEl) {
1308
+ if (activeNode.parentElement === editorEl) {
1309
+ activeNode.classList.add('sh-editor-active-block');
1310
+ break;
1311
+ }
1312
+ activeNode = activeNode.parentElement;
1313
+ }
1314
+ }
1315
+ }
1316
+ // --- MODALS (LINK & IMAGE) ---
1317
+ openLinkModal() {
1318
+ // Back up current selection before focus shifts to modal
1319
+ const selection = window.getSelection();
1320
+ if (selection && selection.rangeCount > 0) {
1321
+ this.#savedRange = selection.getRangeAt(0).cloneRange();
1322
+ }
1323
+ this.showLinkModal.set(true);
1324
+ }
1325
+ applyLink(url) {
1326
+ this.showLinkModal.set(false);
1327
+ if (!url)
1328
+ return;
1329
+ const editor = this.editorRef()?.nativeElement;
1330
+ if (!editor)
1331
+ return;
1332
+ // Restore selection range
1333
+ const selection = window.getSelection();
1334
+ if (selection && this.#savedRange) {
1335
+ selection.removeAllRanges();
1336
+ selection.addRange(this.#savedRange);
1337
+ }
1338
+ editor.focus();
1339
+ this.toggleLink(url);
1340
+ }
1341
+ openImageModal() {
1342
+ const selection = window.getSelection();
1343
+ if (selection && selection.rangeCount > 0) {
1344
+ this.#savedRange = selection.getRangeAt(0).cloneRange();
1345
+ }
1346
+ this.showImageModal.set(true);
1347
+ }
1348
+ applyImage(url) {
1349
+ this.showImageModal.set(false);
1350
+ this.insertImage(url);
1351
+ }
1352
+ onComponentFocusIn(event) {
1353
+ if (!this.#isBrowser)
1354
+ return;
1355
+ const target = event.target;
1356
+ if (target && target.tagName === 'IMG') {
1357
+ this.#selectedImage.set(target);
1358
+ const className = target.className || '';
1359
+ if (className.includes('sh-editor-img-theater')) {
1360
+ this.imgMode.set('theater');
1361
+ }
1362
+ else if (className.includes('sh-editor-img-float')) {
1363
+ this.imgMode.set('float');
1364
+ }
1365
+ else if (className.includes('sh-editor-img-custom') || className.includes('sh-editor-img-auto')) {
1366
+ this.imgMode.set('custom');
1367
+ }
1368
+ else {
1369
+ this.imgMode.set('content');
1370
+ }
1371
+ if (className.includes('sh-editor-img-size-small')) {
1372
+ this.imgSize.set('small');
1373
+ }
1374
+ else if (className.includes('sh-editor-img-size-medium')) {
1375
+ this.imgSize.set('medium');
1376
+ }
1377
+ else if (className.includes('sh-editor-img-size-large')) {
1378
+ this.imgSize.set('large');
1379
+ }
1380
+ else {
1381
+ this.imgSize.set('auto');
1382
+ }
1383
+ this.updateImgToolbarPosition();
1384
+ }
1385
+ }
1386
+ onComponentClick(event) {
1387
+ if (!this.#isBrowser)
1388
+ return;
1389
+ const target = event.target;
1390
+ if (target && !target.closest('.sh-editor-dropdown')) {
1391
+ this.showBlockMenu.set(false);
1392
+ }
1393
+ if (target && target.tagName === 'IMG') {
1394
+ target.focus();
1395
+ const selection = window.getSelection();
1396
+ if (selection) {
1397
+ const range = this.#document.createRange();
1398
+ range.selectNode(target);
1399
+ selection.removeAllRanges();
1400
+ selection.addRange(range);
1401
+ }
1402
+ this.#selectedImage.set(target);
1403
+ const className = target.className || '';
1404
+ if (className.includes('sh-editor-img-theater')) {
1405
+ this.imgMode.set('theater');
1406
+ }
1407
+ else if (className.includes('sh-editor-img-float')) {
1408
+ this.imgMode.set('float');
1409
+ }
1410
+ else if (className.includes('sh-editor-img-custom') || className.includes('sh-editor-img-auto')) {
1411
+ this.imgMode.set('custom');
1412
+ }
1413
+ else {
1414
+ this.imgMode.set('content');
1415
+ }
1416
+ if (className.includes('sh-editor-img-size-small')) {
1417
+ this.imgSize.set('small');
1418
+ }
1419
+ else if (className.includes('sh-editor-img-size-medium')) {
1420
+ this.imgSize.set('medium');
1421
+ }
1422
+ else if (className.includes('sh-editor-img-size-large')) {
1423
+ this.imgSize.set('large');
1424
+ }
1425
+ else {
1426
+ this.imgSize.set('auto');
1427
+ }
1428
+ this.updateImgToolbarPosition();
1429
+ }
1430
+ else {
1431
+ // If clicking inside image toolbar itself, don't dismiss
1432
+ if (target && target.closest('.sh-editor-img-toolbar')) {
1433
+ return;
1434
+ }
1435
+ this.#selectedImage.set(null);
1436
+ }
1437
+ }
1438
+ onKeyDown(event) {
1439
+ if (!this.#isBrowser)
1440
+ return;
1441
+ if (this.readonly())
1442
+ return;
1443
+ if (this.viewMode() === 'design') {
1444
+ const selection = window.getSelection();
1445
+ if (selection && selection.rangeCount > 0) {
1446
+ const range = selection.getRangeAt(0);
1447
+ const anchorNode = selection.anchorNode;
1448
+ if (anchorNode) {
1449
+ const editorEl = this.editorRef()?.nativeElement;
1450
+ if (editorEl) {
1451
+ let currentBlock = null;
1452
+ if (anchorNode.nodeType === Node.TEXT_NODE) {
1453
+ currentBlock = anchorNode.parentElement;
1454
+ }
1455
+ else {
1456
+ currentBlock = anchorNode;
1457
+ }
1458
+ // Traverse up to find blockquote, pre, or li
1459
+ let blockquoteEl = null;
1460
+ let preEl = null;
1461
+ let liEl = null;
1462
+ let node = currentBlock;
1463
+ while (node && node !== editorEl) {
1464
+ const tagName = node.tagName.toLowerCase();
1465
+ if (tagName === 'blockquote') {
1466
+ blockquoteEl = node;
1467
+ }
1468
+ else if (tagName === 'pre') {
1469
+ preEl = node;
1470
+ }
1471
+ else if (tagName === 'li') {
1472
+ liEl = node;
1473
+ }
1474
+ node = node.parentElement;
1475
+ }
1476
+ // 1. Exit block with Ctrl+Enter or Cmd+Enter
1477
+ if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
1478
+ const targetBlock = preEl || blockquoteEl || liEl;
1479
+ if (targetBlock) {
1480
+ event.preventDefault();
1481
+ const p = this.#document.createElement('p');
1482
+ p.innerHTML = '<br>';
1483
+ // If we are in a list item, we should insert the paragraph after the parent list (ul/ol)
1484
+ let insertAfterNode = targetBlock;
1485
+ if (liEl) {
1486
+ const listParent = liEl.closest('ul, ol');
1487
+ if (listParent && editorEl.contains(listParent)) {
1488
+ insertAfterNode = listParent;
1489
+ }
1490
+ }
1491
+ insertAfterNode.parentNode?.insertBefore(p, insertAfterNode.nextSibling);
1492
+ // Focus the new paragraph
1493
+ const newRange = this.#document.createRange();
1494
+ newRange.setStart(p, 0);
1495
+ newRange.collapse(true);
1496
+ selection.removeAllRanges();
1497
+ selection.addRange(newRange);
1498
+ p.focus();
1499
+ this.#updateValueFromDOM();
1500
+ return;
1501
+ }
1502
+ }
1503
+ // 2. Double enter on empty blockquote or list item
1504
+ if (event.key === 'Enter' && !event.shiftKey && !event.ctrlKey && !event.metaKey) {
1505
+ const targetBlock = blockquoteEl || liEl;
1506
+ if (targetBlock) {
1507
+ const text = targetBlock.textContent?.trim() || '';
1508
+ if (text === '') {
1509
+ event.preventDefault();
1510
+ const p = this.#document.createElement('p');
1511
+ p.innerHTML = '<br>';
1512
+ if (liEl) {
1513
+ const listParent = liEl.closest('ul, ol');
1514
+ if (listParent) {
1515
+ if (listParent.children.length <= 1) {
1516
+ listParent.parentNode?.replaceChild(p, listParent);
1517
+ }
1518
+ else {
1519
+ listParent.parentNode?.insertBefore(p, listParent.nextSibling);
1520
+ liEl.remove();
1521
+ }
1522
+ }
1523
+ }
1524
+ else {
1525
+ targetBlock.parentNode?.replaceChild(p, targetBlock);
1526
+ }
1527
+ // Focus new paragraph
1528
+ const newRange = this.#document.createRange();
1529
+ newRange.setStart(p, 0);
1530
+ newRange.collapse(true);
1531
+ selection.removeAllRanges();
1532
+ selection.addRange(newRange);
1533
+ p.focus();
1534
+ this.#updateValueFromDOM();
1535
+ return;
1536
+ }
1537
+ }
1538
+ }
1539
+ // 3. Enter inside pre (code block) to insert newline instead of splitting tag
1540
+ if (event.key === 'Enter' && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
1541
+ if (preEl) {
1542
+ event.preventDefault();
1543
+ // Insert a newline character at current caret position
1544
+ const selection = window.getSelection();
1545
+ if (selection && selection.rangeCount > 0) {
1546
+ const range = selection.getRangeAt(0);
1547
+ const textNode = this.#document.createTextNode('\n');
1548
+ range.insertNode(textNode);
1549
+ // Move caret after the new line character
1550
+ range.setStartAfter(textNode);
1551
+ range.setEndAfter(textNode);
1552
+ selection.removeAllRanges();
1553
+ selection.addRange(range);
1554
+ this.#updateValueFromDOM();
1555
+ }
1556
+ return;
1557
+ }
1558
+ }
1559
+ }
1560
+ }
1561
+ }
1562
+ }
1563
+ const img = this.#selectedImage();
1564
+ if (img && (event.key === 'Backspace' || event.key === 'Delete')) {
1565
+ const activeEl = this.#document.activeElement;
1566
+ if (activeEl === img ||
1567
+ window.getSelection()?.anchorNode === img ||
1568
+ window.getSelection()?.focusNode === img ||
1569
+ window.getSelection()?.anchorNode?.contains(img)) {
1570
+ event.preventDefault();
1571
+ this.deleteImage();
1572
+ }
1573
+ }
1574
+ }
1575
+ onKeyUp(event) {
1576
+ if (!this.#isBrowser)
1577
+ return;
1578
+ if (this.readonly() || this.viewMode() === 'code')
1579
+ return;
1580
+ const sc = this.slashCommands();
1581
+ if (sc === false || (Array.isArray(sc) && sc.length === 0)) {
1582
+ this.showSlashMenu.set(false);
1583
+ return;
1584
+ }
1585
+ const selection = window.getSelection();
1586
+ if (!selection || selection.rangeCount === 0) {
1587
+ this.showSlashMenu.set(false);
1588
+ return;
1589
+ }
1590
+ const range = selection.getRangeAt(0);
1591
+ const textNode = range.startContainer;
1592
+ const editorEl = this.editorRef()?.nativeElement;
1593
+ if (!editorEl || !editorEl.contains(textNode)) {
1594
+ this.showSlashMenu.set(false);
1595
+ return;
1596
+ }
1597
+ if (textNode.nodeType === Node.TEXT_NODE) {
1598
+ const text = textNode.textContent || '';
1599
+ const offset = range.startOffset;
1600
+ const textBeforeCaret = text.substring(0, offset);
1601
+ const match = textBeforeCaret.match(/(?:^|\s)\/([a-zA-Z0-9_-]*)$/);
1602
+ if (match) {
1603
+ this.showSlashMenu.set(true);
1604
+ this.slashSearchQuery.set(match[1]);
1605
+ this.#updateSlashMenuPosition(range);
1606
+ return;
1607
+ }
1608
+ }
1609
+ this.showSlashMenu.set(false);
1610
+ }
1611
+ executeCommand(cmd) {
1612
+ this.#restoreSelection();
1613
+ const selection = window.getSelection();
1614
+ if (selection && selection.rangeCount > 0) {
1615
+ const range = selection.getRangeAt(0);
1616
+ const textNode = range.startContainer;
1617
+ if (textNode.nodeType === Node.TEXT_NODE) {
1618
+ const text = textNode.textContent || '';
1619
+ const offset = range.startOffset;
1620
+ const textBeforeCaret = text.substring(0, offset);
1621
+ const match = textBeforeCaret.match(/(?:^|\s)\/([a-zA-Z0-9_-]*)$/);
1622
+ if (match) {
1623
+ const slashIndex = textBeforeCaret.lastIndexOf('/');
1624
+ if (slashIndex !== -1) {
1625
+ range.setStart(textNode, slashIndex);
1626
+ range.setEnd(textNode, offset);
1627
+ selection.removeAllRanges();
1628
+ selection.addRange(range);
1629
+ range.deleteContents();
1630
+ }
1631
+ }
1632
+ }
1633
+ }
1634
+ this.showSlashMenu.set(false);
1635
+ cmd.action(this);
1636
+ }
1637
+ #updateSlashMenuPosition(range) {
1638
+ const editorEl = this.editorRef()?.nativeElement;
1639
+ if (!editorEl)
1640
+ return;
1641
+ const editorRect = editorEl.getBoundingClientRect();
1642
+ const rect = range.getBoundingClientRect();
1643
+ let top = 0;
1644
+ let left = 0;
1645
+ if (rect.top === 0 && rect.left === 0) {
1646
+ let parentNode = range.startContainer;
1647
+ if (parentNode.nodeType === Node.TEXT_NODE) {
1648
+ parentNode = parentNode.parentElement;
1649
+ }
1650
+ const parentRect = parentNode.getBoundingClientRect();
1651
+ top = parentRect.bottom - editorRect.top + editorEl.scrollTop + 4;
1652
+ left = parentRect.left - editorRect.left;
1653
+ }
1654
+ else {
1655
+ top = rect.bottom - editorRect.top + editorEl.scrollTop + 4;
1656
+ left = rect.left - editorRect.left;
1657
+ }
1658
+ this.slashMenuTop.set(top);
1659
+ this.slashMenuLeft.set(left);
1660
+ }
1661
+ getBlockLabel() {
1662
+ const block = this.activeBlock();
1663
+ if (block === 'h1')
1664
+ return 'Heading 1';
1665
+ if (block === 'h2')
1666
+ return 'Heading 2';
1667
+ if (block === 'h3')
1668
+ return 'Heading 3';
1669
+ if (block === 'blockquote')
1670
+ return 'Quote';
1671
+ if (block === 'pre')
1672
+ return 'Code Block';
1673
+ return 'Normal text';
1674
+ }
1675
+ toggleBlockMenu() {
1676
+ if (this.readonly())
1677
+ return;
1678
+ this.showBlockMenu.update((v) => !v);
1679
+ }
1680
+ selectBlockType(tag) {
1681
+ this.setBlockType(tag);
1682
+ this.showBlockMenu.set(false);
1683
+ }
1684
+ onToolbarKeyDown(event) {
1685
+ if (!this.#isBrowser)
1686
+ return;
1687
+ const target = event.target;
1688
+ const toolbarEl = target.closest('.sh-editor-toolbar');
1689
+ if (!toolbarEl)
1690
+ return;
1691
+ const items = Array.from(toolbarEl.querySelectorAll('.sh-editor-btn, .sh-editor-dropdown-trigger')).filter((el) => {
1692
+ const btn = el;
1693
+ return !btn.disabled && el.getAttribute('disabled') === null;
1694
+ });
1695
+ if (items.length === 0)
1696
+ return;
1697
+ const currentIndex = items.indexOf(target);
1698
+ if (currentIndex === -1)
1699
+ return;
1700
+ const isGroupJump = event.ctrlKey || event.altKey || event.metaKey;
1701
+ if (this.#keybindings.matches(event, 'editor-toolbar.next')) {
1702
+ event.preventDefault();
1703
+ if (isGroupJump) {
1704
+ const currentGroup = target.closest('.sh-editor-toolbar-group');
1705
+ if (currentGroup) {
1706
+ const groups = Array.from(toolbarEl.querySelectorAll('.sh-editor-toolbar-group'));
1707
+ const groupIndex = groups.indexOf(currentGroup);
1708
+ if (groupIndex !== -1) {
1709
+ for (let i = 1; i <= groups.length; i++) {
1710
+ const nextGroupIndex = (groupIndex + i) % groups.length;
1711
+ const groupItems = Array.from(groups[nextGroupIndex].querySelectorAll('.sh-editor-btn, .sh-editor-dropdown-trigger')).filter((el) => {
1712
+ const btn = el;
1713
+ return !btn.disabled && el.getAttribute('disabled') === null && el.offsetWidth > 0;
1714
+ });
1715
+ if (groupItems.length > 0) {
1716
+ groupItems[0].focus();
1717
+ break;
1718
+ }
1719
+ }
1720
+ }
1721
+ }
1722
+ }
1723
+ else {
1724
+ const nextIndex = (currentIndex + 1) % items.length;
1725
+ items[nextIndex].focus();
1726
+ }
1727
+ }
1728
+ else if (this.#keybindings.matches(event, 'editor-toolbar.prev')) {
1729
+ event.preventDefault();
1730
+ if (isGroupJump) {
1731
+ const currentGroup = target.closest('.sh-editor-toolbar-group');
1732
+ if (currentGroup) {
1733
+ const groups = Array.from(toolbarEl.querySelectorAll('.sh-editor-toolbar-group'));
1734
+ const groupIndex = groups.indexOf(currentGroup);
1735
+ if (groupIndex !== -1) {
1736
+ for (let i = 1; i <= groups.length; i++) {
1737
+ const prevGroupIndex = (groupIndex - i + groups.length) % groups.length;
1738
+ const groupItems = Array.from(groups[prevGroupIndex].querySelectorAll('.sh-editor-btn, .sh-editor-dropdown-trigger')).filter((el) => {
1739
+ const btn = el;
1740
+ return !btn.disabled && el.getAttribute('disabled') === null && el.offsetWidth > 0;
1741
+ });
1742
+ if (groupItems.length > 0) {
1743
+ groupItems[0].focus();
1744
+ break;
1745
+ }
1746
+ }
1747
+ }
1748
+ }
1749
+ }
1750
+ else {
1751
+ const nextIndex = (currentIndex - 1 + items.length) % items.length;
1752
+ items[nextIndex].focus();
1753
+ }
1754
+ }
1755
+ else if (this.#keybindings.matches(event, 'editor-toolbar.home')) {
1756
+ event.preventDefault();
1757
+ items[0].focus();
1758
+ }
1759
+ else if (this.#keybindings.matches(event, 'editor-toolbar.end')) {
1760
+ event.preventDefault();
1761
+ items[items.length - 1].focus();
1762
+ }
1763
+ }
1764
+ onToolbarFocusIn(event) {
1765
+ if (!this.#isBrowser)
1766
+ return;
1767
+ const target = event.target;
1768
+ const toolbarEl = target.closest('.sh-editor-toolbar');
1769
+ if (!toolbarEl)
1770
+ return;
1771
+ const items = Array.from(toolbarEl.querySelectorAll('.sh-editor-btn, .sh-editor-dropdown-trigger')).filter((el) => {
1772
+ const btn = el;
1773
+ return !btn.disabled && el.getAttribute('disabled') === null;
1774
+ });
1775
+ items.forEach((item) => {
1776
+ if (item === target) {
1777
+ item.setAttribute('tabindex', '0');
1778
+ }
1779
+ else {
1780
+ item.setAttribute('tabindex', '-1');
1781
+ }
1782
+ });
1783
+ }
1784
+ #initializeToolbarTabindexes() {
1785
+ if (!this.#isBrowser)
1786
+ return;
1787
+ setTimeout(() => {
1788
+ const toolbarEl = this.editorRef()?.nativeElement?.parentElement?.querySelector('.sh-editor-toolbar');
1789
+ if (!toolbarEl)
1790
+ return;
1791
+ const items = Array.from(toolbarEl.querySelectorAll('.sh-editor-btn, .sh-editor-dropdown-trigger')).filter((el) => {
1792
+ const btn = el;
1793
+ return !btn.disabled && el.getAttribute('disabled') === null;
1794
+ });
1795
+ items.forEach((item, idx) => {
1796
+ item.setAttribute('tabindex', idx === 0 ? '0' : '-1');
1797
+ });
1798
+ });
1799
+ }
1800
+ #ensureImagesFocusable() {
1801
+ const editor = this.editorRef()?.nativeElement;
1802
+ if (!editor)
1803
+ return;
1804
+ const imgs = editor.querySelectorAll('img');
1805
+ Array.from(imgs).forEach((img) => {
1806
+ if (!img.hasAttribute('tabindex')) {
1807
+ img.setAttribute('tabindex', '0');
1808
+ }
1809
+ });
1810
+ }
1811
+ onComponentScroll(event) {
1812
+ if (this.#selectedImage()) {
1813
+ this.updateImgToolbarPosition();
1814
+ }
1815
+ }
1816
+ onWindowResize() {
1817
+ if (this.#selectedImage()) {
1818
+ this.updateImgToolbarPosition();
1819
+ }
1820
+ }
1821
+ updateImgToolbarPosition() {
1822
+ const img = this.#selectedImage();
1823
+ const editorEl = this.editorRef()?.nativeElement;
1824
+ if (!img || !editorEl)
1825
+ return;
1826
+ const imgRect = img.getBoundingClientRect();
1827
+ const editorRect = editorEl.getBoundingClientRect();
1828
+ // Position toolbar just above the top-left edge of the image
1829
+ const top = imgRect.top - editorRect.top - 55 + editorEl.scrollTop;
1830
+ const left = imgRect.left - editorRect.left;
1831
+ this.imgToolbarTop.set(top);
1832
+ this.imgToolbarLeft.set(left);
1833
+ }
1834
+ setImageMode(mode) {
1835
+ const img = this.#selectedImage();
1836
+ if (!img)
1837
+ return;
1838
+ img.classList.remove('sh-editor-img-content', 'sh-editor-img-theater', 'sh-editor-img-float', 'sh-editor-img-custom', 'sh-editor-img-auto');
1839
+ img.classList.add(`sh-editor-img-${mode}`);
1840
+ this.imgMode.set(mode);
1841
+ // If switching to content or theater (which do not support sizes), strip size classes
1842
+ if (mode === 'content' || mode === 'theater') {
1843
+ img.classList.remove('sh-editor-img-size-auto', 'sh-editor-img-size-small', 'sh-editor-img-size-medium', 'sh-editor-img-size-large');
1844
+ this.imgSize.set('auto');
1845
+ }
1846
+ // Reposition overlay after CSS layout transition
1847
+ setTimeout(() => this.updateImgToolbarPosition(), 50);
1848
+ this.#updateValueFromDOM();
1849
+ }
1850
+ setImageSize(size) {
1851
+ const img = this.#selectedImage();
1852
+ if (!img)
1853
+ return;
1854
+ img.classList.remove('sh-editor-img-size-auto', 'sh-editor-img-size-small', 'sh-editor-img-size-medium', 'sh-editor-img-size-large');
1855
+ img.classList.add(`sh-editor-img-size-${size}`);
1856
+ this.imgSize.set(size);
1857
+ // Reposition overlay after CSS layout transition
1858
+ setTimeout(() => this.updateImgToolbarPosition(), 50);
1859
+ this.#updateValueFromDOM();
1860
+ }
1861
+ deleteImage() {
1862
+ const img = this.#selectedImage();
1863
+ if (img) {
1864
+ img.remove();
1865
+ this.#selectedImage.set(null);
1866
+ this.#updateValueFromDOM();
1867
+ }
1868
+ }
1869
+ onFileSelected(event) {
1870
+ const input = event.target;
1871
+ if (input.files && input.files.length > 0) {
1872
+ this.#handleImageUpload(input.files[0]);
1873
+ this.showImageModal.set(false);
1874
+ }
1875
+ }
1876
+ onDrop(event) {
1877
+ if (!this.#isBrowser)
1878
+ return;
1879
+ const files = event.dataTransfer?.files;
1880
+ if (files && files.length > 0) {
1881
+ const file = files[0];
1882
+ if (file.type.startsWith('image/')) {
1883
+ event.preventDefault();
1884
+ this.#handleImageUpload(file);
1885
+ }
1886
+ }
1887
+ }
1888
+ onPaste(event) {
1889
+ if (!this.#isBrowser)
1890
+ return;
1891
+ const items = event.clipboardData?.items;
1892
+ if (items) {
1893
+ for (let i = 0; i < items.length; i++) {
1894
+ if (items[i].type.indexOf('image') !== -1) {
1895
+ const file = items[i].getAsFile();
1896
+ if (file) {
1897
+ event.preventDefault();
1898
+ this.#handleImageUpload(file);
1899
+ }
1900
+ }
1901
+ }
1902
+ }
1903
+ }
1904
+ #handleImageUpload(file) {
1905
+ if (this.customUpload()) {
1906
+ this.imageUpload.emit(file);
1907
+ }
1908
+ else {
1909
+ const reader = new FileReader();
1910
+ reader.onload = (e) => {
1911
+ const base64Url = e.target?.result;
1912
+ this.insertImage(base64Url);
1913
+ };
1914
+ reader.readAsDataURL(file);
1915
+ }
1916
+ }
1917
+ insertImage(url) {
1918
+ if (!url)
1919
+ return;
1920
+ const editor = this.editorRef()?.nativeElement;
1921
+ if (!editor)
1922
+ return;
1923
+ // Restore selection range
1924
+ const selection = window.getSelection();
1925
+ if (selection && this.#savedRange) {
1926
+ selection.removeAllRanges();
1927
+ selection.addRange(this.#savedRange);
1928
+ }
1929
+ editor.focus();
1930
+ // Insert image markup with default content layout class
1931
+ const imgHtml = `<img src="${url}" class="sh-editor-img-content" alt="Image">`;
1932
+ const range = selection?.getRangeAt(0);
1933
+ if (range) {
1934
+ this.#saveHistory();
1935
+ this.#isInternalDOMUpdate = true;
1936
+ range.deleteContents();
1937
+ const el = this.#document.createElement('div');
1938
+ el.innerHTML = imgHtml;
1939
+ const child = el.firstChild;
1940
+ if (child) {
1941
+ range.insertNode(child);
1942
+ const newRange = this.#document.createRange();
1943
+ newRange.setStartAfter(child);
1944
+ newRange.collapse(true);
1945
+ selection?.removeAllRanges();
1946
+ selection?.addRange(newRange);
1947
+ }
1948
+ this.#updateValueFromDOM();
1949
+ this.#saveHistory();
1950
+ this.#isInternalDOMUpdate = false;
1951
+ this.onSelectionChange();
1952
+ }
1953
+ }
1954
+ // --- PUBLIC API METHODS ---
1955
+ getHTML() {
1956
+ const editor = this.editorRef()?.nativeElement;
1957
+ if (editor) {
1958
+ return editor.innerHTML;
1959
+ }
1960
+ // Fallback if not rendered
1961
+ const val = this.value();
1962
+ if (typeof val === 'string') {
1963
+ return this.format() === 'markdown' ? this.#markdownToHTML(val) : val;
1964
+ }
1965
+ if (Array.isArray(val)) {
1966
+ return this.#jsonToHTML(val);
1967
+ }
1968
+ return '';
1969
+ }
1970
+ getMarkdown() {
1971
+ const html = this.getHTML();
1972
+ return this.#htmlToMarkdown(html);
1973
+ }
1974
+ getJSON() {
1975
+ const html = this.getHTML();
1976
+ return this.#htmlToJSON(html);
1977
+ }
1978
+ setHTML(html) {
1979
+ this.#isWriting = true;
1980
+ const currentFormat = this.format();
1981
+ if (currentFormat === 'html') {
1982
+ this.value.set(html);
1983
+ this.onChange(html);
1984
+ }
1985
+ else if (currentFormat === 'markdown') {
1986
+ const md = this.#htmlToMarkdown(html);
1987
+ this.value.set(md);
1988
+ this.onChange(md);
1989
+ }
1990
+ else if (currentFormat === 'json') {
1991
+ const json = this.#htmlToJSON(html);
1992
+ this.value.set(json);
1993
+ this.onChange(json);
1994
+ }
1995
+ this.#syncModelToDOM(this.value());
1996
+ this.#isWriting = false;
1997
+ }
1998
+ setMarkdown(md) {
1999
+ this.#isWriting = true;
2000
+ const currentFormat = this.format();
2001
+ if (currentFormat === 'markdown') {
2002
+ this.value.set(md);
2003
+ this.onChange(md);
2004
+ }
2005
+ else if (currentFormat === 'html') {
2006
+ const html = this.#markdownToHTML(md);
2007
+ this.value.set(html);
2008
+ this.onChange(html);
2009
+ }
2010
+ else if (currentFormat === 'json') {
2011
+ const html = this.#markdownToHTML(md);
2012
+ const json = this.#htmlToJSON(html);
2013
+ this.value.set(json);
2014
+ this.onChange(json);
2015
+ }
2016
+ this.#syncModelToDOM(this.value());
2017
+ this.#isWriting = false;
2018
+ }
2019
+ setJSON(json) {
2020
+ this.#isWriting = true;
2021
+ const currentFormat = this.format();
2022
+ if (currentFormat === 'json') {
2023
+ this.value.set(json);
2024
+ this.onChange(json);
2025
+ }
2026
+ else if (currentFormat === 'html') {
2027
+ const html = this.#jsonToHTML(json);
2028
+ this.value.set(html);
2029
+ this.onChange(html);
2030
+ }
2031
+ else if (currentFormat === 'markdown') {
2032
+ const html = this.#jsonToHTML(json);
2033
+ const md = this.#htmlToMarkdown(html);
2034
+ this.value.set(md);
2035
+ this.onChange(md);
2036
+ }
2037
+ this.#syncModelToDOM(this.value());
2038
+ this.#isWriting = false;
2039
+ }
2040
+ clear() {
2041
+ this.value.set('');
2042
+ this.#lastValueWrittenFromDOM = '';
2043
+ this.rawCodeValue.set('');
2044
+ this.onChange('');
2045
+ const editor = this.editorRef()?.nativeElement;
2046
+ if (editor) {
2047
+ editor.innerHTML = '<p><br></p>';
2048
+ }
2049
+ }
2050
+ // --- PARSERS & SERIALIZERS ---
2051
+ #escapeHTML(str) {
2052
+ return str
2053
+ .replace(/&/g, '&amp;')
2054
+ .replace(/</g, '&lt;')
2055
+ .replace(/>/g, '&gt;')
2056
+ .replace(/"/g, '&quot;')
2057
+ .replace(/'/g, '&#039;');
2058
+ }
2059
+ // JSON => HTML Converter
2060
+ #jsonToHTML(doc) {
2061
+ if (!doc || !Array.isArray(doc))
2062
+ return '';
2063
+ return doc
2064
+ .map((block) => {
2065
+ const alignStyle = block.attrs?.align ? ` style="text-align: ${block.attrs.align};"` : '';
2066
+ switch (block.type) {
2067
+ case 'paragraph': {
2068
+ const contentHtml = this.#inlineToHTML(block.content || []);
2069
+ return `<p${alignStyle}>${contentHtml || '<br>'}</p>`;
2070
+ }
2071
+ case 'heading': {
2072
+ const level = block.attrs?.level || 1;
2073
+ const contentHtml = this.#inlineToHTML(block.content || []);
2074
+ return `<h${level}${alignStyle}>${contentHtml || '<br>'}</h${level}>`;
2075
+ }
2076
+ case 'quote': {
2077
+ const contentHtml = this.#inlineToHTML(block.content || []);
2078
+ return `<blockquote>${contentHtml || '<br>'}</blockquote>`;
2079
+ }
2080
+ case 'code-block': {
2081
+ const codeText = (block.content || []).map((node) => node.text || '').join('');
2082
+ const langClass = block.attrs?.language ? ` class="language-${block.attrs.language}"` : '';
2083
+ return `<pre><code${langClass}>${this.#escapeHTML(codeText)}</code></pre>`;
2084
+ }
2085
+ case 'bullet-list': {
2086
+ const itemsHtml = (block.content || [])
2087
+ .map((item) => `<li>${this.#inlineToHTML(item.content || [])}</li>`)
2088
+ .join('');
2089
+ return `<ul>${itemsHtml}</ul>`;
2090
+ }
2091
+ case 'ordered-list': {
2092
+ const itemsHtml = (block.content || [])
2093
+ .map((item) => `<li>${this.#inlineToHTML(item.content || [])}</li>`)
2094
+ .join('');
2095
+ return `<ol>${itemsHtml}</ol>`;
2096
+ }
2097
+ case 'hr':
2098
+ return '<hr>';
2099
+ case 'image': {
2100
+ const src = block.attrs?.src || '';
2101
+ const alt = block.attrs?.alt || '';
2102
+ const mode = block.attrs?.mode || 'content';
2103
+ const size = block.attrs?.size || 'auto';
2104
+ if (mode === 'content' || mode === 'theater') {
2105
+ return `<img src="${src}" alt="${alt}" class="sh-editor-img-${mode}">`;
2106
+ }
2107
+ return `<img src="${src}" alt="${alt}" class="sh-editor-img-${mode} sh-editor-img-size-${size}">`;
2108
+ }
2109
+ default:
2110
+ return '';
2111
+ }
2112
+ })
2113
+ .join('');
2114
+ }
2115
+ #inlineToHTML(nodes) {
2116
+ if (!nodes || !Array.isArray(nodes))
2117
+ return '';
2118
+ return nodes
2119
+ .map((node) => {
2120
+ let text = this.#escapeHTML(node.text || '');
2121
+ if (!node.marks)
2122
+ return text;
2123
+ node.marks.forEach((mark) => {
2124
+ if (mark.type === 'bold') {
2125
+ text = `<strong>${text}</strong>`;
2126
+ }
2127
+ else if (mark.type === 'italic') {
2128
+ text = `<em>${text}</em>`;
2129
+ }
2130
+ else if (mark.type === 'underline') {
2131
+ text = `<u>${text}</u>`;
2132
+ }
2133
+ else if (mark.type === 'strike') {
2134
+ text = `<s>${text}</s>`;
2135
+ }
2136
+ else if (mark.type === 'code') {
2137
+ text = `<code>${text}</code>`;
2138
+ }
2139
+ else if (mark.type === 'link') {
2140
+ const href = mark.attrs?.href || '';
2141
+ const target = mark.attrs?.target ? ` target="${mark.attrs.target}"` : '';
2142
+ text = `<a href="${href}"${target}>${text}</a>`;
2143
+ }
2144
+ });
2145
+ return text;
2146
+ })
2147
+ .join('');
2148
+ }
2149
+ // HTML => JSON Converter
2150
+ #htmlToJSON(html) {
2151
+ const doc = [];
2152
+ const temp = this.#document.createElement('div');
2153
+ temp.innerHTML = html;
2154
+ const children = Array.from(temp.childNodes);
2155
+ for (const node of children) {
2156
+ if (node.nodeType === Node.ELEMENT_NODE) {
2157
+ const el = node;
2158
+ const tagName = el.tagName.toLowerCase();
2159
+ if (['p', 'div'].includes(tagName)) {
2160
+ doc.push({
2161
+ type: 'paragraph',
2162
+ attrs: { align: this.#getAlign(el) },
2163
+ content: this.#parseInlineNodes(el),
2164
+ });
2165
+ }
2166
+ else if (/^h[1-6]$/.test(tagName)) {
2167
+ const level = parseInt(tagName.charAt(1), 10);
2168
+ doc.push({
2169
+ type: 'heading',
2170
+ attrs: { level, align: this.#getAlign(el) },
2171
+ content: this.#parseInlineNodes(el),
2172
+ });
2173
+ }
2174
+ else if (tagName === 'blockquote') {
2175
+ doc.push({
2176
+ type: 'quote',
2177
+ content: this.#parseInlineNodes(el),
2178
+ });
2179
+ }
2180
+ else if (tagName === 'pre') {
2181
+ const codeEl = el.querySelector('code');
2182
+ const lang = codeEl?.getAttribute('class')?.replace('language-', '') || '';
2183
+ doc.push({
2184
+ type: 'code-block',
2185
+ attrs: { language: lang },
2186
+ content: [{ type: 'text', text: codeEl ? codeEl.textContent || '' : el.textContent || '' }],
2187
+ });
2188
+ }
2189
+ else if (tagName === 'ul') {
2190
+ doc.push({
2191
+ type: 'bullet-list',
2192
+ content: this.#parseListItems(el),
2193
+ });
2194
+ }
2195
+ else if (tagName === 'ol') {
2196
+ doc.push({
2197
+ type: 'ordered-list',
2198
+ content: this.#parseListItems(el),
2199
+ });
2200
+ }
2201
+ else if (tagName === 'hr') {
2202
+ doc.push({ type: 'hr' });
2203
+ }
2204
+ else if (tagName === 'img') {
2205
+ const className = el.getAttribute('class') || '';
2206
+ let mode = 'content';
2207
+ if (className.includes('sh-editor-img-theater'))
2208
+ mode = 'theater';
2209
+ else if (className.includes('sh-editor-img-float'))
2210
+ mode = 'float';
2211
+ else if (className.includes('sh-editor-img-custom') || className.includes('sh-editor-img-auto'))
2212
+ mode = 'custom';
2213
+ let size = 'auto';
2214
+ if (className.includes('sh-editor-img-size-small'))
2215
+ size = 'small';
2216
+ else if (className.includes('sh-editor-img-size-medium'))
2217
+ size = 'medium';
2218
+ else if (className.includes('sh-editor-img-size-large'))
2219
+ size = 'large';
2220
+ doc.push({
2221
+ type: 'image',
2222
+ attrs: {
2223
+ src: el.getAttribute('src') || '',
2224
+ alt: el.getAttribute('alt') || '',
2225
+ mode,
2226
+ size,
2227
+ },
2228
+ });
2229
+ }
2230
+ else {
2231
+ doc.push({
2232
+ type: 'paragraph',
2233
+ content: this.#parseInlineNodes(el),
2234
+ });
2235
+ }
2236
+ }
2237
+ else if (node.nodeType === Node.TEXT_NODE) {
2238
+ const text = node.textContent?.trim();
2239
+ if (text) {
2240
+ doc.push({
2241
+ type: 'paragraph',
2242
+ content: [{ type: 'text', text }],
2243
+ });
2244
+ }
2245
+ }
2246
+ }
2247
+ return doc;
2248
+ }
2249
+ #getAlign(el) {
2250
+ const align = el.style.textAlign || el.getAttribute('align');
2251
+ if (align === 'center' || align === 'right' || align === 'left') {
2252
+ return align;
2253
+ }
2254
+ return undefined;
2255
+ }
2256
+ #parseListItems(listEl) {
2257
+ const items = [];
2258
+ for (const child of Array.from(listEl.childNodes)) {
2259
+ if (child.nodeType === Node.ELEMENT_NODE && child.tagName.toLowerCase() === 'li') {
2260
+ items.push({
2261
+ type: 'list-item',
2262
+ content: this.#parseInlineNodes(child),
2263
+ });
2264
+ }
2265
+ }
2266
+ return items;
2267
+ }
2268
+ #parseInlineNodes(parentEl) {
2269
+ const inlineNodes = [];
2270
+ const traverse = (node, marks) => {
2271
+ if (node.nodeType === Node.TEXT_NODE) {
2272
+ const text = node.textContent || '';
2273
+ if (text) {
2274
+ inlineNodes.push({
2275
+ type: 'text',
2276
+ text,
2277
+ marks: marks.length > 0 ? [...marks] : undefined,
2278
+ });
2279
+ }
2280
+ }
2281
+ else if (node.nodeType === Node.ELEMENT_NODE) {
2282
+ const el = node;
2283
+ const tagName = el.tagName.toLowerCase();
2284
+ const currentMarks = [...marks];
2285
+ if (['strong', 'b'].includes(tagName)) {
2286
+ currentMarks.push({ type: 'bold' });
2287
+ }
2288
+ else if (['em', 'i'].includes(tagName)) {
2289
+ currentMarks.push({ type: 'italic' });
2290
+ }
2291
+ else if (tagName === 'u') {
2292
+ currentMarks.push({ type: 'underline' });
2293
+ }
2294
+ else if (['strike', 's', 'del'].includes(tagName)) {
2295
+ currentMarks.push({ type: 'strike' });
2296
+ }
2297
+ else if (tagName === 'code') {
2298
+ currentMarks.push({ type: 'code' });
2299
+ }
2300
+ else if (tagName === 'a') {
2301
+ currentMarks.push({
2302
+ type: 'link',
2303
+ attrs: {
2304
+ href: el.getAttribute('href') || '',
2305
+ target: el.getAttribute('target') || undefined,
2306
+ },
2307
+ });
2308
+ }
2309
+ for (const child of Array.from(el.childNodes)) {
2310
+ traverse(child, currentMarks);
2311
+ }
2312
+ }
2313
+ };
2314
+ for (const child of Array.from(parentEl.childNodes)) {
2315
+ traverse(child, []);
2316
+ }
2317
+ return inlineNodes;
2318
+ }
2319
+ // HTML => Markdown Converter
2320
+ #htmlToMarkdown(html) {
2321
+ const temp = this.#document.createElement('div');
2322
+ temp.innerHTML = html;
2323
+ let markdown = '';
2324
+ const children = Array.from(temp.childNodes);
2325
+ for (const node of children) {
2326
+ if (node.nodeType === Node.ELEMENT_NODE) {
2327
+ const el = node;
2328
+ const tagName = el.tagName.toLowerCase();
2329
+ if (['p', 'div'].includes(tagName)) {
2330
+ const inlineMd = this.#elementToMarkdownInline(el);
2331
+ if (inlineMd.trim()) {
2332
+ markdown += inlineMd + '\n\n';
2333
+ }
2334
+ }
2335
+ else if (/^h[1-6]$/.test(tagName)) {
2336
+ const level = parseInt(tagName.charAt(1), 10);
2337
+ const prefix = '#'.repeat(level) + ' ';
2338
+ markdown += prefix + this.#elementToMarkdownInline(el) + '\n\n';
2339
+ }
2340
+ else if (tagName === 'blockquote') {
2341
+ const text = this.#elementToMarkdownInline(el);
2342
+ markdown += '> ' + text.replace(/\n/g, '\n> ') + '\n\n';
2343
+ }
2344
+ else if (tagName === 'pre') {
2345
+ const codeEl = el.querySelector('code');
2346
+ const lang = codeEl?.getAttribute('class')?.replace('language-', '') || '';
2347
+ const codeText = codeEl ? codeEl.textContent || '' : el.textContent || '';
2348
+ markdown += '```' + lang + '\n' + codeText.trim() + '\n```\n\n';
2349
+ }
2350
+ else if (tagName === 'ul') {
2351
+ const items = Array.from(el.querySelectorAll(':scope > li'));
2352
+ items.forEach((li) => {
2353
+ markdown += '* ' + this.#elementToMarkdownInline(li) + '\n';
2354
+ });
2355
+ markdown += '\n';
2356
+ }
2357
+ else if (tagName === 'ol') {
2358
+ const items = Array.from(el.querySelectorAll(':scope > li'));
2359
+ items.forEach((li, idx) => {
2360
+ markdown += `${idx + 1}. ` + this.#elementToMarkdownInline(li) + '\n';
2361
+ });
2362
+ markdown += '\n';
2363
+ }
2364
+ else if (tagName === 'hr') {
2365
+ markdown += '---\n\n';
2366
+ }
2367
+ else if (tagName === 'img') {
2368
+ const src = el.getAttribute('src') || '';
2369
+ const alt = el.getAttribute('alt') || '';
2370
+ markdown += `![${alt}](${src})\n\n`;
2371
+ }
2372
+ else {
2373
+ const inlineMd = this.#elementToMarkdownInline(el);
2374
+ if (inlineMd.trim()) {
2375
+ markdown += inlineMd + '\n\n';
2376
+ }
2377
+ }
2378
+ }
2379
+ else if (node.nodeType === Node.TEXT_NODE) {
2380
+ const text = node.textContent?.trim();
2381
+ if (text) {
2382
+ markdown += text + '\n\n';
2383
+ }
2384
+ }
2385
+ }
2386
+ return markdown.trim();
2387
+ }
2388
+ #elementToMarkdownInline(parentEl) {
2389
+ let md = '';
2390
+ const traverse = (node) => {
2391
+ if (node.nodeType === Node.TEXT_NODE) {
2392
+ md += node.textContent || '';
2393
+ }
2394
+ else if (node.nodeType === Node.ELEMENT_NODE) {
2395
+ const el = node;
2396
+ const tagName = el.tagName.toLowerCase();
2397
+ let prefix = '';
2398
+ let suffix = '';
2399
+ if (['strong', 'b'].includes(tagName)) {
2400
+ prefix = '**';
2401
+ suffix = '**';
2402
+ }
2403
+ else if (['em', 'i'].includes(tagName)) {
2404
+ prefix = '*';
2405
+ suffix = '*';
2406
+ }
2407
+ else if (tagName === 'u') {
2408
+ prefix = '<u>';
2409
+ suffix = '</u>';
2410
+ }
2411
+ else if (['strike', 's', 'del'].includes(tagName)) {
2412
+ prefix = '~~';
2413
+ suffix = '~~';
2414
+ }
2415
+ else if (tagName === 'code') {
2416
+ prefix = '`';
2417
+ suffix = '`';
2418
+ }
2419
+ else if (tagName === 'a') {
2420
+ prefix = '[';
2421
+ suffix = `](${el.getAttribute('href') || ''})`;
2422
+ }
2423
+ else if (tagName === 'br') {
2424
+ md += '\n';
2425
+ return;
2426
+ }
2427
+ md += prefix;
2428
+ for (const child of Array.from(el.childNodes)) {
2429
+ traverse(child);
2430
+ }
2431
+ md += suffix;
2432
+ }
2433
+ };
2434
+ for (const child of Array.from(parentEl.childNodes)) {
2435
+ traverse(child);
2436
+ }
2437
+ return md;
2438
+ }
2439
+ // Markdown => HTML Converter
2440
+ #markdownToHTML(markdown) {
2441
+ if (!markdown)
2442
+ return '';
2443
+ const blocks = markdown.split(/\n\s*\n/);
2444
+ let html = '';
2445
+ let inList = false;
2446
+ let listType = null;
2447
+ for (let block of blocks) {
2448
+ block = block.trim();
2449
+ if (!block)
2450
+ continue;
2451
+ const isBullet = /^[*-]\s+/.test(block);
2452
+ const isOrdered = /^\d+\.\s+/.test(block);
2453
+ if (inList && !isBullet && !isOrdered) {
2454
+ html += listType === 'ul' ? '</ul>' : '</ol>';
2455
+ inList = false;
2456
+ listType = null;
2457
+ }
2458
+ if (block.startsWith('```')) {
2459
+ const lines = block.split('\n');
2460
+ const firstLine = lines[0];
2461
+ const lang = firstLine.replace('```', '').trim();
2462
+ const codeLines = lines.slice(1, lines.length - (lines[lines.length - 1] === '```' ? 1 : 0));
2463
+ const codeText = codeLines.join('\n');
2464
+ const langClass = lang ? ` class="language-${lang}"` : '';
2465
+ html += `<pre><code${langClass}>${this.#escapeHTML(codeText)}</code></pre>`;
2466
+ }
2467
+ else if (block.startsWith('#')) {
2468
+ const match = block.match(/^(#{1,6})\s+(.*)$/);
2469
+ if (match) {
2470
+ const level = match[1].length;
2471
+ const content = this.#parseInlineMarkdown(match[2]);
2472
+ html += `<h${level}>${content}</h${level}>`;
2473
+ }
2474
+ else {
2475
+ html += `<p>${this.#parseInlineMarkdown(block)}</p>`;
2476
+ }
2477
+ }
2478
+ else if (block.startsWith('>')) {
2479
+ const lines = block.split('\n').map((line) => line.replace(/^>\s?/, ''));
2480
+ const content = this.#parseInlineMarkdown(lines.join('<br>'));
2481
+ html += `<blockquote>${content}</blockquote>`;
2482
+ }
2483
+ else if (block === '---') {
2484
+ html += '<hr>';
2485
+ }
2486
+ else if (isBullet) {
2487
+ if (!inList || listType !== 'ul') {
2488
+ if (inList) {
2489
+ html += listType === 'ul' ? '</ul>' : '</ol>';
2490
+ }
2491
+ html += '<ul>';
2492
+ inList = true;
2493
+ listType = 'ul';
2494
+ }
2495
+ const lines = block.split('\n');
2496
+ lines.forEach((line) => {
2497
+ const itemText = line.replace(/^[*-]\s+/, '');
2498
+ html += `<li>${this.#parseInlineMarkdown(itemText)}</li>`;
2499
+ });
2500
+ }
2501
+ else if (isOrdered) {
2502
+ if (!inList || listType !== 'ol') {
2503
+ if (inList) {
2504
+ html += listType === 'ul' ? '</ul>' : '</ol>';
2505
+ }
2506
+ html += '<ol>';
2507
+ inList = true;
2508
+ listType = 'ol';
2509
+ }
2510
+ const lines = block.split('\n');
2511
+ lines.forEach((line) => {
2512
+ const itemText = line.replace(/^\d+\.\s+/, '');
2513
+ html += `<li>${this.#parseInlineMarkdown(itemText)}</li>`;
2514
+ });
2515
+ }
2516
+ else if (block.startsWith('![') && block.includes('](')) {
2517
+ const match = block.match(/^!\[(.*?)\]\((.*?)\)$/);
2518
+ if (match) {
2519
+ html += `<img src="${match[2]}" alt="${match[1]}">`;
2520
+ }
2521
+ else {
2522
+ html += `<p>${this.#parseInlineMarkdown(block)}</p>`;
2523
+ }
2524
+ }
2525
+ else {
2526
+ html += `<p>${this.#parseInlineMarkdown(block)}</p>`;
2527
+ }
2528
+ }
2529
+ if (inList) {
2530
+ html += listType === 'ul' ? '</ul>' : '</ol>';
2531
+ }
2532
+ return html;
2533
+ }
2534
+ #parseInlineMarkdown(text) {
2535
+ let html = this.#escapeHTML(text);
2536
+ // Bold
2537
+ html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
2538
+ html = html.replace(/__(.*?)__/g, '<strong>$1</strong>');
2539
+ // Italic
2540
+ html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
2541
+ html = html.replace(/_(.*?)_/g, '<em>$1</em>');
2542
+ // Underline (un-escape decoded <u> tags)
2543
+ html = html.replace(/&lt;u&gt;(.*?)&lt;\/u&gt;/g, '<u>$1</u>');
2544
+ // Strike
2545
+ html = html.replace(/~~(.*?)~~/g, '<s>$1</s>');
2546
+ // Code
2547
+ html = html.replace(/`(.*?)`/g, '<code>$1</code>');
2548
+ // Images
2549
+ html = html.replace(/!\[(.*?)\]\((.*?)\)/g, '<img src="$2" alt="$1">');
2550
+ // Links
2551
+ html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2">$1</a>');
2552
+ return html;
2553
+ }
2554
+ // Helper to extract plain text from JSON doc for metrics
2555
+ #getJSONText(doc) {
2556
+ return doc
2557
+ .map((block) => {
2558
+ if (['paragraph', 'heading', 'quote', 'list-item'].includes(block.type)) {
2559
+ const content = block.content;
2560
+ return content ? content.map((node) => node.text || '').join('') : '';
2561
+ }
2562
+ if (['bullet-list', 'ordered-list'].includes(block.type)) {
2563
+ const content = block.content;
2564
+ return content ? this.#getJSONText(content) : '';
2565
+ }
2566
+ if (block.type === 'code-block') {
2567
+ const content = block.content;
2568
+ return content ? content.map((node) => node.text || '').join('') : '';
2569
+ }
2570
+ return '';
2571
+ })
2572
+ .join(' ');
2573
+ }
2574
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: ShipEditor, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2575
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", type: ShipEditor, isStandalone: true, selector: "sh-editor", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, customCommands: { classPropertyName: "customCommands", publicName: "customCommands", isSignal: true, isRequired: false, transformFunction: null }, slashCommands: { classPropertyName: "slashCommands", publicName: "slashCommands", isSignal: true, isRequired: false, transformFunction: null }, showFormats: { classPropertyName: "showFormats", publicName: "showFormats", isSignal: true, isRequired: false, transformFunction: null }, showBlocks: { classPropertyName: "showBlocks", publicName: "showBlocks", isSignal: true, isRequired: false, transformFunction: null }, showLists: { classPropertyName: "showLists", publicName: "showLists", isSignal: true, isRequired: false, transformFunction: null }, showAlignments: { classPropertyName: "showAlignments", publicName: "showAlignments", isSignal: true, isRequired: false, transformFunction: null }, showInsertions: { classPropertyName: "showInsertions", publicName: "showInsertions", isSignal: true, isRequired: false, transformFunction: null }, showHistory: { classPropertyName: "showHistory", publicName: "showHistory", isSignal: true, isRequired: false, transformFunction: null }, customUpload: { classPropertyName: "customUpload", publicName: "customUpload", isSignal: true, isRequired: false, transformFunction: null }, imageUploadEnabled: { classPropertyName: "imageUploadEnabled", publicName: "imageUploadEnabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", imageUpload: "imageUpload" }, host: { listeners: { "document:selectionchange": "onSelectionChange()", "focusin": "onComponentFocusIn($event)", "click": "onComponentClick($event)", "keydown": "onKeyDown($event)", "keyup": "onKeyUp($event)", "scroll": "onComponentScroll($event)", "window:resize": "onWindowResize()", "drop": "onDrop($event)", "paste": "onPaste($event)" }, properties: { "class": "hostClasses()", "class.sh-editor-readonly": "readonly()", "class.sh-editor-focused": "isFocused()" } }, providers: [SHIP_EDITOR_VALUE_ACCESSOR], viewQueries: [{ propertyName: "editorRef", first: true, predicate: ["editorRef"], descendants: true, isSignal: true }, { propertyName: "codeEditorRef", first: true, predicate: ["codeEditorRef"], descendants: true, isSignal: true }, { propertyName: "uploadBtn", first: true, predicate: ["uploadBtn"], descendants: true, isSignal: true }, { propertyName: "imageInput", first: true, predicate: ["imageInput"], descendants: true, isSignal: true }, { propertyName: "linkInput", first: true, predicate: ["linkInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"sh-editor-container\">\n <!-- Editor Toolbar -->\n @if (toolbar() && !readonly()) {\n <div\n class=\"sh-editor-toolbar\"\n role=\"toolbar\"\n aria-label=\"Rich Text Editor Formatting Toolbar\"\n (keydown)=\"onToolbarKeyDown($event)\"\n (focusin)=\"onToolbarFocusIn($event)\">\n <!-- Text formatting group -->\n @if (showFormats()) {\n <div class=\"sh-editor-toolbar-group\" role=\"group\" aria-label=\"Text formatting\">\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"isBold()\"\n [attr.aria-pressed]=\"isBold()\"\n (click)=\"formatText('bold')\"\n shTooltip=\"Bold\">\n <sh-icon>text-b</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"isItalic()\"\n [attr.aria-pressed]=\"isItalic()\"\n (click)=\"formatText('italic')\"\n shTooltip=\"Italic\">\n <sh-icon>text-italic</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"isUnderline()\"\n [attr.aria-pressed]=\"isUnderline()\"\n (click)=\"formatText('underline')\"\n shTooltip=\"Underline\">\n <sh-icon>text-underline</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"isStrike()\"\n [attr.aria-pressed]=\"isStrike()\"\n (click)=\"formatText('strikeThrough')\"\n shTooltip=\"Strikethrough\">\n <sh-icon>text-strikethrough</sh-icon>\n </button>\n <button type=\"button\" class=\"sh-editor-btn\" (click)=\"formatText('removeFormat')\" shTooltip=\"Clear Formatting\">\n <sh-icon>eraser</sh-icon>\n </button>\n </div>\n }\n\n <!-- Headings & Block types Dropdown -->\n @if (showBlocks()) {\n <div class=\"sh-editor-toolbar-group\" role=\"group\" aria-label=\"Text styles\">\n <sh-menu [(isOpen)]=\"showBlockMenu\" [openIndicator]=\"true\" class=\"sh-editor-dropdown\">\n <button trigger class=\"sh-editor-dropdown-trigger\" aria-label=\"Text Style\" shTooltip=\"Text Style\">\n <span class=\"sh-editor-dropdown-label\">{{ getBlockLabel() }}</span>\n </button>\n <div menu>\n <button (click)=\"selectBlockType('p')\" [class.active]=\"activeBlock() === 'p'\">\n <sh-icon>paragraph</sh-icon>\n <span>Normal text</span>\n </button>\n <button (click)=\"selectBlockType('h1')\" [class.active]=\"activeBlock() === 'h1'\">\n <sh-icon>text-h-one</sh-icon>\n <span>Heading 1</span>\n </button>\n <button (click)=\"selectBlockType('h2')\" [class.active]=\"activeBlock() === 'h2'\">\n <sh-icon>text-h-two</sh-icon>\n <span>Heading 2</span>\n </button>\n <button (click)=\"selectBlockType('h3')\" [class.active]=\"activeBlock() === 'h3'\">\n <sh-icon>text-h-three</sh-icon>\n <span>Heading 3</span>\n </button>\n <button (click)=\"selectBlockType('blockquote')\" [class.active]=\"activeBlock() === 'blockquote'\">\n <sh-icon>quotes</sh-icon>\n <span>Quote</span>\n </button>\n <button (click)=\"selectBlockType('pre')\" [class.active]=\"activeBlock() === 'pre'\">\n <sh-icon>code</sh-icon>\n\n <span>Code Block</span>\n </button>\n </div>\n </sh-menu>\n </div>\n }\n\n <!-- Lists -->\n @if (showLists()) {\n <div class=\"sh-editor-toolbar-group\" role=\"group\" aria-label=\"Lists\">\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"activeBlock() === 'ul' || activeBlock() === 'li'\"\n [attr.aria-pressed]=\"activeBlock() === 'ul' || activeBlock() === 'li'\"\n (click)=\"formatText('insertUnorderedList')\"\n shTooltip=\"Bullet List\">\n <sh-icon>list-bullets</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"activeBlock() === 'ol'\"\n [attr.aria-pressed]=\"activeBlock() === 'ol'\"\n (click)=\"formatText('insertOrderedList')\"\n shTooltip=\"Numbered List\">\n <sh-icon>list-numbers</sh-icon>\n </button>\n </div>\n }\n\n <!-- Alignments -->\n @if (showAlignments()) {\n <div class=\"sh-editor-toolbar-group\" role=\"group\" aria-label=\"Text alignment\">\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"align() === 'left'\"\n [attr.aria-pressed]=\"align() === 'left'\"\n (click)=\"formatText('justifyLeft')\"\n shTooltip=\"Align Left\">\n <sh-icon>text-align-left</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"align() === 'center'\"\n [attr.aria-pressed]=\"align() === 'center'\"\n (click)=\"formatText('justifyCenter')\"\n shTooltip=\"Align Center\">\n <sh-icon>text-align-center</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"align() === 'right'\"\n [attr.aria-pressed]=\"align() === 'right'\"\n (click)=\"formatText('justifyRight')\"\n shTooltip=\"Align Right\">\n <sh-icon>text-align-right</sh-icon>\n </button>\n </div>\n }\n\n <!-- Links, images and lines -->\n @if (showInsertions()) {\n <div class=\"sh-editor-toolbar-group\" role=\"group\" aria-label=\"Insert elements\">\n <button type=\"button\" class=\"sh-editor-btn\" (click)=\"openLinkModal()\" shTooltip=\"Insert Link\">\n <sh-icon>link</sh-icon>\n </button>\n <button type=\"button\" class=\"sh-editor-btn\" (click)=\"openImageModal()\" shTooltip=\"Insert Image\">\n <sh-icon>image</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n (click)=\"formatText('insertHorizontalRule')\"\n shTooltip=\"Horizontal Line\">\n <sh-icon>minus</sh-icon>\n </button>\n </div>\n }\n\n <!-- Configured custom commands group -->\n @if (customCommands().length > 0) {\n <div class=\"sh-editor-toolbar-group\" role=\"group\" aria-label=\"Custom actions\">\n @for (cmd of customCommands(); track cmd.id) {\n <button type=\"button\" class=\"sh-editor-btn\" (click)=\"executeCommand(cmd)\" [shTooltip]=\"cmd.label\">\n <sh-icon>{{ cmd.icon }}</sh-icon>\n </button>\n }\n </div>\n }\n\n <!-- Custom projected buttons -->\n <div class=\"sh-editor-toolbar-group sh-editor-toolbar-extra\" role=\"group\" aria-label=\"Extra actions\">\n <ng-content />\n </div>\n\n <div class=\"spacer\"></div>\n\n <!-- Undo, Redo and Source View -->\n @if (showHistory()) {\n <div class=\"sh-editor-toolbar-group\" role=\"group\" aria-label=\"History and view\">\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [disabled]=\"!canUndo()\"\n (click)=\"formatText('undo')\"\n shTooltip=\"Undo\">\n <sh-icon>arrow-u-up-left</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [disabled]=\"!canRedo()\"\n (click)=\"formatText('redo')\"\n shTooltip=\"Redo\">\n <sh-icon>arrow-u-up-right</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn view-toggle\"\n [class.active]=\"viewMode() === 'code'\"\n [attr.aria-pressed]=\"viewMode() === 'code'\"\n (click)=\"toggleViewMode()\"\n shTooltip=\"Toggle Code View\">\n @if (viewMode() === 'design') {\n <sh-icon>terminal</sh-icon>\n } @else {\n <sh-icon>article</sh-icon>\n }\n </button>\n </div>\n }\n </div>\n }\n\n <!-- Simple Dialogs for Link and Image Inputs -->\n @if (showLinkModal()) {\n <div class=\"sh-editor-modal\">\n <div class=\"sh-editor-modal-backdrop\" (click)=\"showLinkModal.set(false)\"></div>\n <div class=\"sh-editor-modal-content\">\n <h4>Insert Link</h4>\n <input\n #linkInput\n type=\"text\"\n placeholder=\"https://example.com\"\n class=\"sh-editor-input\"\n (keyup.enter)=\"applyLink(linkInput.value)\"\n autofocus />\n <div class=\"sh-editor-modal-actions\">\n <button type=\"button\" class=\"sh-editor-modal-btn cancel\" (click)=\"showLinkModal.set(false)\">Cancel</button>\n <button type=\"button\" class=\"sh-editor-modal-btn confirm\" (click)=\"applyLink(linkInput.value)\">Insert</button>\n </div>\n </div>\n </div>\n }\n\n @if (showImageModal()) {\n <div class=\"sh-editor-modal\">\n <div class=\"sh-editor-modal-backdrop\" (click)=\"showImageModal.set(false)\"></div>\n <div class=\"sh-editor-modal-content\">\n <h4>Insert Image</h4>\n\n @if (imageUploadEnabled()) {\n <input #fileInput type=\"file\" accept=\"image/*\" style=\"display: none\" (change)=\"onFileSelected($event)\" />\n <button #uploadBtn type=\"button\" class=\"sh-editor-upload-btn\" (click)=\"fileInput.click()\">\n <sh-icon>upload-simple</sh-icon>\n Choose local file...\n </button>\n\n <div class=\"sh-editor-modal-divider\"><span>or insert from URL</span></div>\n }\n\n <input\n #imageInput\n type=\"text\"\n placeholder=\"https://example.com/image.jpg\"\n class=\"sh-editor-input\"\n (keyup.enter)=\"applyImage(imageInput.value)\" />\n <div class=\"sh-editor-modal-actions\">\n <button type=\"button\" class=\"sh-editor-modal-btn cancel\" (click)=\"showImageModal.set(false)\">Cancel</button>\n <button type=\"button\" class=\"sh-editor-modal-btn confirm\" (click)=\"applyImage(imageInput.value)\">\n Insert\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Image layout options floating toolbar -->\n @if (selectedImage() && !readonly()) {\n <div class=\"sh-editor-img-toolbar\" [style.top.px]=\"imgToolbarTop()\" [style.left.px]=\"imgToolbarLeft()\">\n <button\n type=\"button\"\n class=\"sh-editor-img-toolbar-btn\"\n [class.active]=\"imgMode() === 'content'\"\n (click)=\"setImageMode('content')\">\n Content\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-img-toolbar-btn\"\n [class.active]=\"imgMode() === 'theater'\"\n (click)=\"setImageMode('theater')\">\n Theater\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-img-toolbar-btn\"\n [class.active]=\"imgMode() === 'float'\"\n (click)=\"setImageMode('float')\">\n Float\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-img-toolbar-btn\"\n [class.active]=\"imgMode() === 'custom'\"\n (click)=\"setImageMode('custom')\">\n Custom\n </button>\n\n @if (imgMode() === 'float' || imgMode() === 'custom') {\n <div class=\"sh-editor-img-toolbar-divider\"></div>\n\n <button\n type=\"button\"\n class=\"sh-editor-img-toolbar-btn\"\n [class.active]=\"imgSize() === 'auto'\"\n (click)=\"setImageSize('auto')\">\n Auto\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-img-toolbar-btn\"\n [class.active]=\"imgSize() === 'small'\"\n (click)=\"setImageSize('small')\">\n Small\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-img-toolbar-btn\"\n [class.active]=\"imgSize() === 'medium'\"\n (click)=\"setImageSize('medium')\">\n Medium\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-img-toolbar-btn\"\n [class.active]=\"imgSize() === 'large'\"\n (click)=\"setImageSize('large')\">\n Large\n </button>\n }\n\n <div class=\"sh-editor-img-toolbar-divider\"></div>\n\n <button type=\"button\" class=\"sh-editor-img-toolbar-btn danger\" (click)=\"deleteImage()\" shTooltip=\"Delete Image\">\n <sh-icon>trash</sh-icon>\n </button>\n </div>\n }\n\n <!-- Slash Commands floating menu using design system sh-menu component -->\n @if (filteredCommands().length > 0 && !readonly()) {\n <sh-menu\n #slashMenu\n [(isOpen)]=\"showSlashMenu\"\n [searchable]=\"false\"\n [openIndicator]=\"false\"\n class=\"sh-editor-slash-menu-container\"\n [style.top.px]=\"slashMenuTop()\"\n [style.left.px]=\"slashMenuLeft()\">\n <div trigger class=\"sh-editor-slash-anchor\"></div>\n <div menu>\n @for (cmd of filteredCommands(); track cmd.id; let idx = $index) {\n <button\n type=\"button\"\n class=\"sh-editor-slash-item\"\n [class.active]=\"idx === slashMenu.activeOptionIndex()\"\n [attr.aria-selected]=\"idx === slashMenu.activeOptionIndex() ? 'true' : 'false'\"\n (click)=\"executeCommand(cmd)\"\n (mouseenter)=\"slashMenu.activeOptionIndex.set(idx)\">\n <div class=\"sh-editor-slash-item-icon\">\n <sh-icon>{{ cmd.icon }}</sh-icon>\n </div>\n <div class=\"sh-editor-slash-item-content\">\n <div class=\"sh-editor-slash-item-label\">{{ cmd.label }}</div>\n @if (cmd.description) {\n <div class=\"sh-editor-slash-item-desc\">{{ cmd.description }}</div>\n }\n </div>\n </button>\n }\n </div>\n </sh-menu>\n }\n\n <!-- Body / Editor Surface -->\n <div class=\"sh-editor-body\">\n @if (viewMode() === 'design') {\n <div\n #editorRef\n class=\"sh-editor-content\"\n [class.has-slash-commands]=\"hasSlashCommands()\"\n [attr.contenteditable]=\"readonly() ? 'false' : 'true'\"\n [attr.placeholder]=\"placeholder()\"\n role=\"textbox\"\n aria-multiline=\"true\"\n (input)=\"onDOMInput()\"\n (blur)=\"onDOMBlur()\"\n (focus)=\"isFocused.set(true)\"></div>\n } @else {\n <textarea\n #codeEditorRef\n class=\"sh-editor-code\"\n [attr.readonly]=\"readonly() ? '' : null\"\n [value]=\"rawCodeValue()\"\n (input)=\"onCodeInput($event)\"\n (blur)=\"onCodeBlur($event)\"\n (focus)=\"isFocused.set(true)\"></textarea>\n }\n </div>\n\n <!-- Footer / Status Bar -->\n <div class=\"sh-editor-footer\">\n <div class=\"word-count\">{{ wordCount() }} words, {{ charCount() }} characters</div>\n <div class=\"spacer\"></div>\n <div class=\"storage-format\">Storage: {{ format().toUpperCase() }}</div>\n </div>\n</div>\n", styles: ["sh-editor{--editor-bg: var(--base-1);--editor-code-bg: var(--base-2);--editor-border-color: var(--base-4);--editor-toolbar-bg: rgb(from var(--base-1) r g b / 70%);--editor-toolbar-border: var(--base-4);--editor-btn-hover: var(--base-3);--editor-btn-active-bg: var(--primary-3);--editor-btn-active-c: var(--primary-11);--editor-footer-bg: var(--base-2);--editor-p: 1rem 1.25rem;--editor-min-h: 13.75rem;--editor-border-focus: var(--primary-8);--editor-shadow-focus: 0 0 0 3px var(--primary-3);display:block;width:100%;margin-bottom:1rem}sh-editor .sh-editor-container{position:relative;display:flex;flex-direction:column;background-color:var(--editor-bg);border:var(--border-10);border-color:var(--editor-border-color);border-radius:var(--shape-3);box-shadow:var(--box-shadow-10);overflow:hidden;transition:border-color .2s ease,box-shadow .2s ease}sh-editor.sh-editor-focused .sh-editor-container{border-color:var(--editor-border-focus);box-shadow:var(--editor-shadow-focus)}sh-editor.sh-editor-readonly .sh-editor-content{cursor:not-allowed}sh-editor .sh-editor-toolbar{position:sticky;top:0;z-index:10;display:flex;flex-wrap:wrap;align-items:center;gap:.25rem;padding:.375rem .5rem;background-color:var(--editor-toolbar-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--editor-toolbar-border);border-top-left-radius:calc(var(--shape-3) - 1px);border-top-right-radius:calc(var(--shape-3) - 1px)}sh-editor .sh-editor-toolbar-group{display:flex;align-items:center;gap:.125rem;padding-right:.375rem;border-right:1px solid var(--editor-toolbar-border)}sh-editor .sh-editor-toolbar-group:last-child,sh-editor .sh-editor-toolbar-group.view-toggle{border-right:none}sh-editor .sh-editor-btn{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;padding:0;background:transparent;border:none;border-radius:var(--shape-1);color:var(--base-11);cursor:pointer;font-size:1rem;transition:all .15s ease}sh-editor .sh-editor-btn:hover:not(:disabled){background-color:var(--editor-btn-hover);color:var(--base-12)}sh-editor .sh-editor-btn:disabled{opacity:.35;cursor:not-allowed}sh-editor .sh-editor-btn.active{background-color:var(--editor-btn-active-bg);color:var(--editor-btn-active-c);font-weight:700}sh-editor .sh-editor-btn i{font-style:normal;line-height:1}sh-editor .sh-editor-body{position:relative;display:flex;flex-direction:column;min-height:var(--editor-min-h)}sh-editor .sh-editor-content{flex:1 1 auto;padding:var(--editor-p);min-height:var(--editor-min-h);outline:none;color:var(--base-12);font:var(--paragraph-20);line-height:1.6;word-break:break-word}sh-editor .sh-editor-content p{margin-top:0;margin-bottom:.75rem}sh-editor .sh-editor-content p:last-child{margin-bottom:0}sh-editor .sh-editor-content h1,sh-editor .sh-editor-content h2,sh-editor .sh-editor-content h3{color:var(--base-12);font-weight:700;margin-top:1rem;margin-bottom:.5rem;line-height:1.3}sh-editor .sh-editor-content h1{font:var(--title-30B)}sh-editor .sh-editor-content h2{font:var(--title-20B)}sh-editor .sh-editor-content h3{font:var(--title-10B)}sh-editor .sh-editor-content blockquote{margin:1rem 0;padding:.5rem 1rem;border-left:4px solid var(--primary-8);background-color:var(--base-2);color:var(--base-11);border-radius:0 var(--shape-2) var(--shape-2) 0;font-style:italic}sh-editor .sh-editor-content pre{background-color:var(--editor-code-bg);border-radius:var(--shape-2);padding:.75rem 1rem;margin:1rem 0;overflow-x:auto}sh-editor .sh-editor-content pre code{font:var(--code-20);background:transparent;padding:0;border-radius:0}sh-editor .sh-editor-content code{font:var(--code-20);background-color:var(--base-2);padding:.125rem .375rem;border-radius:var(--shape-1);color:var(--primary-11)}sh-editor .sh-editor-content ul,sh-editor .sh-editor-content ol{margin-top:0;margin-bottom:1rem;padding-left:1.5rem}sh-editor .sh-editor-content li{margin-bottom:.25rem}sh-editor .sh-editor-content hr{border:none;border-top:1px solid var(--editor-border-color);margin:1.5rem 0}sh-editor .sh-editor-content img{height:auto;max-width:100%;transition:all .2s ease;border:var(--border-10);border-color:var(--base-3);cursor:pointer}sh-editor .sh-editor-content img:hover{outline:2px solid var(--primary-7)}sh-editor .sh-editor-content img:focus{outline:2px solid var(--primary-8)}sh-editor .sh-editor-content img.sh-editor-img-content{display:block;margin:1rem auto;box-sizing:border-box;border-radius:var(--shape-2)}sh-editor .sh-editor-content img.sh-editor-img-content.sh-editor-img-size-small{width:15rem;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-content.sh-editor-img-size-medium{width:30rem;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-content.sh-editor-img-size-large{width:calc(100% - 3rem);max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-content:not([class*=sh-editor-img-size-]){width:calc(100% - 3rem);max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-theater{display:block;width:calc(100% + 2.5rem);max-width:none!important;margin:1rem -1.25rem;padding:0;border-radius:0;border-left:none;border-right:none}sh-editor .sh-editor-content img.sh-editor-img-float{float:left;margin:.5rem 1rem .5rem 0;border-radius:var(--shape-2)}sh-editor .sh-editor-content img.sh-editor-img-float.sh-editor-img-size-auto{width:auto;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-float.sh-editor-img-size-small{width:15rem;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-float.sh-editor-img-size-medium{width:30rem;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-float.sh-editor-img-size-large{width:100%;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-float:not([class*=sh-editor-img-size-]){width:auto;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-custom,sh-editor .sh-editor-content img.sh-editor-img-auto{display:inline-block;margin:.75rem 0;border-radius:var(--shape-2)}sh-editor .sh-editor-content img.sh-editor-img-custom.sh-editor-img-size-auto,sh-editor .sh-editor-content img.sh-editor-img-auto.sh-editor-img-size-auto{width:auto;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-custom.sh-editor-img-size-small,sh-editor .sh-editor-content img.sh-editor-img-auto.sh-editor-img-size-small{width:15rem;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-custom.sh-editor-img-size-medium,sh-editor .sh-editor-content img.sh-editor-img-auto.sh-editor-img-size-medium{width:30rem;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-custom.sh-editor-img-size-large,sh-editor .sh-editor-content img.sh-editor-img-auto.sh-editor-img-size-large{width:100%;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-custom:not([class*=sh-editor-img-size-]),sh-editor .sh-editor-content img.sh-editor-img-auto:not([class*=sh-editor-img-size-]){width:auto;max-width:100%!important}sh-editor .sh-editor-content a{color:var(--primary-9);text-decoration:underline;cursor:pointer}sh-editor .sh-editor-content a:hover{color:var(--primary-11)}sh-editor .sh-editor-content:empty:before{content:attr(placeholder);color:var(--base-8);font-style:italic;pointer-events:none;display:block}sh-editor .sh-editor-content p{position:relative}sh-editor .sh-editor-content.has-slash-commands p.sh-editor-active-block:empty:before,sh-editor .sh-editor-content.has-slash-commands p.sh-editor-active-block:has(>br:only-child):before{content:\"Type '/' for commands...\";position:absolute;left:0;top:0;pointer-events:none;color:var(--base-6);font-style:normal;font-weight:400;opacity:.6}sh-editor .sh-editor-code{flex:1 1 auto;width:100%;min-height:var(--editor-min-h);padding:var(--editor-p);background-color:var(--editor-code-bg);color:var(--base-12);font:var(--code-20);border:none;resize:vertical;outline:none;font-family:monospace;line-height:1.5}sh-editor .sh-editor-footer{display:flex;align-items:center;padding:.375rem .75rem;background-color:var(--editor-footer-bg);border-top:1px solid var(--editor-border-color);font:var(--paragraph-10);color:var(--base-8);border-bottom-left-radius:calc(var(--shape-3) - 1px);border-bottom-right-radius:calc(var(--shape-3) - 1px)}sh-editor .sh-editor-modal{position:absolute;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;padding:1rem}sh-editor .sh-editor-modal-backdrop{position:absolute;inset:0;background-color:rgb(from var(--base-1) r g b/40%);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}sh-editor .sh-editor-modal-content{position:relative;z-index:101;width:100%;max-width:22.5rem;padding:1.25rem;background-color:var(--base-1);border:var(--border-10);border-color:var(--base-4);border-radius:var(--shape-3);box-shadow:var(--box-shadow-40)}sh-editor .sh-editor-modal-content h4{margin-top:0;margin-bottom:.75rem;font:var(--title-10B);color:var(--base-12)}sh-editor .sh-editor-input{width:100%;padding:.5rem .75rem;background-color:var(--base-2);border:var(--border-10);border-color:var(--base-4);border-radius:var(--shape-2);color:var(--base-12);font:var(--paragraph-20);outline:none;margin-bottom:1rem;transition:border-color .15s ease}sh-editor .sh-editor-input:focus{border-color:var(--primary-8)}sh-editor .sh-editor-modal-actions{display:flex;justify-content:flex-end;gap:.5rem}sh-editor .sh-editor-modal-btn{padding:.375rem .75rem;border-radius:var(--shape-2);font:var(--paragraph-20);cursor:pointer;border:none;transition:all .15s ease}sh-editor .sh-editor-modal-btn.cancel{background-color:transparent;color:var(--base-11)}sh-editor .sh-editor-modal-btn.cancel:hover{background-color:var(--base-3)}sh-editor .sh-editor-modal-btn.confirm{background-color:var(--primary-9);color:var(--primary-1)}sh-editor .sh-editor-modal-btn.confirm:hover{background-color:var(--primary-10)}sh-editor .sh-editor-img-toolbar{position:absolute;z-index:50;display:flex;align-items:center;gap:.25rem;padding:.25rem .375rem;background-color:rgb(from var(--base-1) r g b/85%);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:var(--border-10);border-color:var(--base-4);border-radius:var(--shape-2);box-shadow:var(--box-shadow-20);pointer-events:auto;transition:opacity .2s ease,transform .2s ease}sh-editor .sh-editor-img-toolbar-btn{display:inline-flex;align-items:center;justify-content:center;padding:.25rem .5rem;background:transparent;border:none;border-radius:var(--shape-1);color:var(--base-11);cursor:pointer;font:var(--paragraph-10);font-weight:500;transition:all .15s ease}sh-editor .sh-editor-img-toolbar-btn:hover{background-color:var(--editor-btn-hover);color:var(--base-12)}sh-editor .sh-editor-img-toolbar-btn.active{background-color:var(--editor-btn-active-bg);color:var(--editor-btn-active-c)}sh-editor .sh-editor-img-toolbar-btn.danger{color:var(--error-9)}sh-editor .sh-editor-img-toolbar-btn.danger:hover{background-color:var(--error-3);color:var(--error-11)}sh-editor .sh-editor-img-toolbar-divider{width:1px;height:1rem;background-color:var(--editor-toolbar-border);margin:0 .125rem}sh-editor .sh-editor-upload-btn{display:flex;align-items:center;justify-content:center;gap:.5rem;width:100%;padding:.625rem 1rem;background-color:var(--base-2);border:1px dashed var(--editor-border-color);border-radius:var(--shape-2);color:var(--base-11);font:var(--paragraph-20);cursor:pointer;transition:all .15s ease;margin-bottom:.75rem}sh-editor .sh-editor-upload-btn:hover{background-color:var(--base-3);border-color:var(--primary-7);color:var(--base-12)}sh-editor .sh-editor-upload-btn sh-icon{font-size:1.125rem}sh-editor .sh-editor-modal-divider{display:flex;align-items:center;text-align:center;color:var(--base-8);font:var(--paragraph-10);margin-bottom:.75rem;width:100%}sh-editor .sh-editor-modal-divider:before,sh-editor .sh-editor-modal-divider:after{content:\"\";flex:1;border-bottom:1px solid var(--editor-border-color)}sh-editor .sh-editor-modal-divider:not(:empty):before{margin-right:.5rem}sh-editor .sh-editor-modal-divider:not(:empty):after{margin-left:.5rem}sh-editor .sh-editor-dropdown{position:relative;display:inline-block}sh-editor .sh-editor-dropdown-trigger{display:inline-flex;align-items:center;gap:.375rem;height:1.75rem;padding:0 .5rem;background:transparent;border:none;border-radius:var(--shape-1);color:var(--base-11);cursor:pointer;font:var(--paragraph-20);font-weight:600;transition:all .15s ease;outline:none}sh-editor .sh-editor-dropdown-trigger:hover{background-color:var(--editor-btn-hover);color:var(--base-12)}sh-editor .sh-editor-dropdown-trigger:focus{outline:2px solid var(--primary-8)}sh-editor .sh-editor-dropdown-label{white-space:nowrap}sh-editor .sh-editor-slash-item{display:flex;align-items:center;gap:.625rem;width:100%;padding:.375rem .625rem;background:transparent;border:none;border-radius:var(--shape-2);text-align:left;cursor:pointer;transition:all .1s ease;outline:none}sh-editor .sh-editor-slash-item.active,sh-editor .sh-editor-slash-item:hover{background-color:var(--editor-btn-hover)}sh-editor .sh-editor-slash-item.active .sh-editor-slash-item-icon,sh-editor .sh-editor-slash-item:hover .sh-editor-slash-item-icon{background-color:var(--editor-btn-active-bg);color:var(--editor-btn-active-c)}sh-editor .sh-editor-slash-item .sh-editor-slash-item-icon{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;border-radius:var(--shape-1);background-color:var(--base-3);color:var(--base-11);font-size:1rem;transition:all .15s ease;flex-shrink:0}sh-editor .sh-editor-slash-item .sh-editor-slash-item-content{display:flex;flex-direction:column;overflow:hidden}sh-editor .sh-editor-slash-item .sh-editor-slash-item-label{font:var(--paragraph-20);font-weight:600;color:var(--base-12);line-height:1.2}sh-editor .sh-editor-slash-item .sh-editor-slash-item-desc{font:var(--paragraph-10);color:var(--base-8);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:.125rem}\n"], dependencies: [{ kind: "directive", type: ShipTooltip, selector: "[shTooltip]", inputs: ["shTooltip"] }, { kind: "component", type: ShipIcon, selector: "sh-icon", inputs: ["color", "size"] }, { kind: "component", type: ShipMenu, selector: "sh-menu", inputs: ["asMultiLayer", "openIndicator", "disabled", "customOptionElementSelectors", "keepClickedOptionActive", "closeOnClick", "isOpen", "searchable"], outputs: ["isOpenChange", "closed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
2576
+ }
2577
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: ShipEditor, decorators: [{
2578
+ type: Component,
2579
+ args: [{ selector: 'sh-editor', standalone: true, encapsulation: ViewEncapsulation.None, imports: [ShipTooltip, ShipIcon, ShipMenu], providers: [SHIP_EDITOR_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush, host: {
2580
+ '[class]': 'hostClasses()',
2581
+ '[class.sh-editor-readonly]': 'readonly()',
2582
+ '[class.sh-editor-focused]': 'isFocused()',
2583
+ }, template: "<div class=\"sh-editor-container\">\n <!-- Editor Toolbar -->\n @if (toolbar() && !readonly()) {\n <div\n class=\"sh-editor-toolbar\"\n role=\"toolbar\"\n aria-label=\"Rich Text Editor Formatting Toolbar\"\n (keydown)=\"onToolbarKeyDown($event)\"\n (focusin)=\"onToolbarFocusIn($event)\">\n <!-- Text formatting group -->\n @if (showFormats()) {\n <div class=\"sh-editor-toolbar-group\" role=\"group\" aria-label=\"Text formatting\">\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"isBold()\"\n [attr.aria-pressed]=\"isBold()\"\n (click)=\"formatText('bold')\"\n shTooltip=\"Bold\">\n <sh-icon>text-b</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"isItalic()\"\n [attr.aria-pressed]=\"isItalic()\"\n (click)=\"formatText('italic')\"\n shTooltip=\"Italic\">\n <sh-icon>text-italic</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"isUnderline()\"\n [attr.aria-pressed]=\"isUnderline()\"\n (click)=\"formatText('underline')\"\n shTooltip=\"Underline\">\n <sh-icon>text-underline</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"isStrike()\"\n [attr.aria-pressed]=\"isStrike()\"\n (click)=\"formatText('strikeThrough')\"\n shTooltip=\"Strikethrough\">\n <sh-icon>text-strikethrough</sh-icon>\n </button>\n <button type=\"button\" class=\"sh-editor-btn\" (click)=\"formatText('removeFormat')\" shTooltip=\"Clear Formatting\">\n <sh-icon>eraser</sh-icon>\n </button>\n </div>\n }\n\n <!-- Headings & Block types Dropdown -->\n @if (showBlocks()) {\n <div class=\"sh-editor-toolbar-group\" role=\"group\" aria-label=\"Text styles\">\n <sh-menu [(isOpen)]=\"showBlockMenu\" [openIndicator]=\"true\" class=\"sh-editor-dropdown\">\n <button trigger class=\"sh-editor-dropdown-trigger\" aria-label=\"Text Style\" shTooltip=\"Text Style\">\n <span class=\"sh-editor-dropdown-label\">{{ getBlockLabel() }}</span>\n </button>\n <div menu>\n <button (click)=\"selectBlockType('p')\" [class.active]=\"activeBlock() === 'p'\">\n <sh-icon>paragraph</sh-icon>\n <span>Normal text</span>\n </button>\n <button (click)=\"selectBlockType('h1')\" [class.active]=\"activeBlock() === 'h1'\">\n <sh-icon>text-h-one</sh-icon>\n <span>Heading 1</span>\n </button>\n <button (click)=\"selectBlockType('h2')\" [class.active]=\"activeBlock() === 'h2'\">\n <sh-icon>text-h-two</sh-icon>\n <span>Heading 2</span>\n </button>\n <button (click)=\"selectBlockType('h3')\" [class.active]=\"activeBlock() === 'h3'\">\n <sh-icon>text-h-three</sh-icon>\n <span>Heading 3</span>\n </button>\n <button (click)=\"selectBlockType('blockquote')\" [class.active]=\"activeBlock() === 'blockquote'\">\n <sh-icon>quotes</sh-icon>\n <span>Quote</span>\n </button>\n <button (click)=\"selectBlockType('pre')\" [class.active]=\"activeBlock() === 'pre'\">\n <sh-icon>code</sh-icon>\n\n <span>Code Block</span>\n </button>\n </div>\n </sh-menu>\n </div>\n }\n\n <!-- Lists -->\n @if (showLists()) {\n <div class=\"sh-editor-toolbar-group\" role=\"group\" aria-label=\"Lists\">\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"activeBlock() === 'ul' || activeBlock() === 'li'\"\n [attr.aria-pressed]=\"activeBlock() === 'ul' || activeBlock() === 'li'\"\n (click)=\"formatText('insertUnorderedList')\"\n shTooltip=\"Bullet List\">\n <sh-icon>list-bullets</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"activeBlock() === 'ol'\"\n [attr.aria-pressed]=\"activeBlock() === 'ol'\"\n (click)=\"formatText('insertOrderedList')\"\n shTooltip=\"Numbered List\">\n <sh-icon>list-numbers</sh-icon>\n </button>\n </div>\n }\n\n <!-- Alignments -->\n @if (showAlignments()) {\n <div class=\"sh-editor-toolbar-group\" role=\"group\" aria-label=\"Text alignment\">\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"align() === 'left'\"\n [attr.aria-pressed]=\"align() === 'left'\"\n (click)=\"formatText('justifyLeft')\"\n shTooltip=\"Align Left\">\n <sh-icon>text-align-left</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"align() === 'center'\"\n [attr.aria-pressed]=\"align() === 'center'\"\n (click)=\"formatText('justifyCenter')\"\n shTooltip=\"Align Center\">\n <sh-icon>text-align-center</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [class.active]=\"align() === 'right'\"\n [attr.aria-pressed]=\"align() === 'right'\"\n (click)=\"formatText('justifyRight')\"\n shTooltip=\"Align Right\">\n <sh-icon>text-align-right</sh-icon>\n </button>\n </div>\n }\n\n <!-- Links, images and lines -->\n @if (showInsertions()) {\n <div class=\"sh-editor-toolbar-group\" role=\"group\" aria-label=\"Insert elements\">\n <button type=\"button\" class=\"sh-editor-btn\" (click)=\"openLinkModal()\" shTooltip=\"Insert Link\">\n <sh-icon>link</sh-icon>\n </button>\n <button type=\"button\" class=\"sh-editor-btn\" (click)=\"openImageModal()\" shTooltip=\"Insert Image\">\n <sh-icon>image</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n (click)=\"formatText('insertHorizontalRule')\"\n shTooltip=\"Horizontal Line\">\n <sh-icon>minus</sh-icon>\n </button>\n </div>\n }\n\n <!-- Configured custom commands group -->\n @if (customCommands().length > 0) {\n <div class=\"sh-editor-toolbar-group\" role=\"group\" aria-label=\"Custom actions\">\n @for (cmd of customCommands(); track cmd.id) {\n <button type=\"button\" class=\"sh-editor-btn\" (click)=\"executeCommand(cmd)\" [shTooltip]=\"cmd.label\">\n <sh-icon>{{ cmd.icon }}</sh-icon>\n </button>\n }\n </div>\n }\n\n <!-- Custom projected buttons -->\n <div class=\"sh-editor-toolbar-group sh-editor-toolbar-extra\" role=\"group\" aria-label=\"Extra actions\">\n <ng-content />\n </div>\n\n <div class=\"spacer\"></div>\n\n <!-- Undo, Redo and Source View -->\n @if (showHistory()) {\n <div class=\"sh-editor-toolbar-group\" role=\"group\" aria-label=\"History and view\">\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [disabled]=\"!canUndo()\"\n (click)=\"formatText('undo')\"\n shTooltip=\"Undo\">\n <sh-icon>arrow-u-up-left</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn\"\n [disabled]=\"!canRedo()\"\n (click)=\"formatText('redo')\"\n shTooltip=\"Redo\">\n <sh-icon>arrow-u-up-right</sh-icon>\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-btn view-toggle\"\n [class.active]=\"viewMode() === 'code'\"\n [attr.aria-pressed]=\"viewMode() === 'code'\"\n (click)=\"toggleViewMode()\"\n shTooltip=\"Toggle Code View\">\n @if (viewMode() === 'design') {\n <sh-icon>terminal</sh-icon>\n } @else {\n <sh-icon>article</sh-icon>\n }\n </button>\n </div>\n }\n </div>\n }\n\n <!-- Simple Dialogs for Link and Image Inputs -->\n @if (showLinkModal()) {\n <div class=\"sh-editor-modal\">\n <div class=\"sh-editor-modal-backdrop\" (click)=\"showLinkModal.set(false)\"></div>\n <div class=\"sh-editor-modal-content\">\n <h4>Insert Link</h4>\n <input\n #linkInput\n type=\"text\"\n placeholder=\"https://example.com\"\n class=\"sh-editor-input\"\n (keyup.enter)=\"applyLink(linkInput.value)\"\n autofocus />\n <div class=\"sh-editor-modal-actions\">\n <button type=\"button\" class=\"sh-editor-modal-btn cancel\" (click)=\"showLinkModal.set(false)\">Cancel</button>\n <button type=\"button\" class=\"sh-editor-modal-btn confirm\" (click)=\"applyLink(linkInput.value)\">Insert</button>\n </div>\n </div>\n </div>\n }\n\n @if (showImageModal()) {\n <div class=\"sh-editor-modal\">\n <div class=\"sh-editor-modal-backdrop\" (click)=\"showImageModal.set(false)\"></div>\n <div class=\"sh-editor-modal-content\">\n <h4>Insert Image</h4>\n\n @if (imageUploadEnabled()) {\n <input #fileInput type=\"file\" accept=\"image/*\" style=\"display: none\" (change)=\"onFileSelected($event)\" />\n <button #uploadBtn type=\"button\" class=\"sh-editor-upload-btn\" (click)=\"fileInput.click()\">\n <sh-icon>upload-simple</sh-icon>\n Choose local file...\n </button>\n\n <div class=\"sh-editor-modal-divider\"><span>or insert from URL</span></div>\n }\n\n <input\n #imageInput\n type=\"text\"\n placeholder=\"https://example.com/image.jpg\"\n class=\"sh-editor-input\"\n (keyup.enter)=\"applyImage(imageInput.value)\" />\n <div class=\"sh-editor-modal-actions\">\n <button type=\"button\" class=\"sh-editor-modal-btn cancel\" (click)=\"showImageModal.set(false)\">Cancel</button>\n <button type=\"button\" class=\"sh-editor-modal-btn confirm\" (click)=\"applyImage(imageInput.value)\">\n Insert\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Image layout options floating toolbar -->\n @if (selectedImage() && !readonly()) {\n <div class=\"sh-editor-img-toolbar\" [style.top.px]=\"imgToolbarTop()\" [style.left.px]=\"imgToolbarLeft()\">\n <button\n type=\"button\"\n class=\"sh-editor-img-toolbar-btn\"\n [class.active]=\"imgMode() === 'content'\"\n (click)=\"setImageMode('content')\">\n Content\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-img-toolbar-btn\"\n [class.active]=\"imgMode() === 'theater'\"\n (click)=\"setImageMode('theater')\">\n Theater\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-img-toolbar-btn\"\n [class.active]=\"imgMode() === 'float'\"\n (click)=\"setImageMode('float')\">\n Float\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-img-toolbar-btn\"\n [class.active]=\"imgMode() === 'custom'\"\n (click)=\"setImageMode('custom')\">\n Custom\n </button>\n\n @if (imgMode() === 'float' || imgMode() === 'custom') {\n <div class=\"sh-editor-img-toolbar-divider\"></div>\n\n <button\n type=\"button\"\n class=\"sh-editor-img-toolbar-btn\"\n [class.active]=\"imgSize() === 'auto'\"\n (click)=\"setImageSize('auto')\">\n Auto\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-img-toolbar-btn\"\n [class.active]=\"imgSize() === 'small'\"\n (click)=\"setImageSize('small')\">\n Small\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-img-toolbar-btn\"\n [class.active]=\"imgSize() === 'medium'\"\n (click)=\"setImageSize('medium')\">\n Medium\n </button>\n <button\n type=\"button\"\n class=\"sh-editor-img-toolbar-btn\"\n [class.active]=\"imgSize() === 'large'\"\n (click)=\"setImageSize('large')\">\n Large\n </button>\n }\n\n <div class=\"sh-editor-img-toolbar-divider\"></div>\n\n <button type=\"button\" class=\"sh-editor-img-toolbar-btn danger\" (click)=\"deleteImage()\" shTooltip=\"Delete Image\">\n <sh-icon>trash</sh-icon>\n </button>\n </div>\n }\n\n <!-- Slash Commands floating menu using design system sh-menu component -->\n @if (filteredCommands().length > 0 && !readonly()) {\n <sh-menu\n #slashMenu\n [(isOpen)]=\"showSlashMenu\"\n [searchable]=\"false\"\n [openIndicator]=\"false\"\n class=\"sh-editor-slash-menu-container\"\n [style.top.px]=\"slashMenuTop()\"\n [style.left.px]=\"slashMenuLeft()\">\n <div trigger class=\"sh-editor-slash-anchor\"></div>\n <div menu>\n @for (cmd of filteredCommands(); track cmd.id; let idx = $index) {\n <button\n type=\"button\"\n class=\"sh-editor-slash-item\"\n [class.active]=\"idx === slashMenu.activeOptionIndex()\"\n [attr.aria-selected]=\"idx === slashMenu.activeOptionIndex() ? 'true' : 'false'\"\n (click)=\"executeCommand(cmd)\"\n (mouseenter)=\"slashMenu.activeOptionIndex.set(idx)\">\n <div class=\"sh-editor-slash-item-icon\">\n <sh-icon>{{ cmd.icon }}</sh-icon>\n </div>\n <div class=\"sh-editor-slash-item-content\">\n <div class=\"sh-editor-slash-item-label\">{{ cmd.label }}</div>\n @if (cmd.description) {\n <div class=\"sh-editor-slash-item-desc\">{{ cmd.description }}</div>\n }\n </div>\n </button>\n }\n </div>\n </sh-menu>\n }\n\n <!-- Body / Editor Surface -->\n <div class=\"sh-editor-body\">\n @if (viewMode() === 'design') {\n <div\n #editorRef\n class=\"sh-editor-content\"\n [class.has-slash-commands]=\"hasSlashCommands()\"\n [attr.contenteditable]=\"readonly() ? 'false' : 'true'\"\n [attr.placeholder]=\"placeholder()\"\n role=\"textbox\"\n aria-multiline=\"true\"\n (input)=\"onDOMInput()\"\n (blur)=\"onDOMBlur()\"\n (focus)=\"isFocused.set(true)\"></div>\n } @else {\n <textarea\n #codeEditorRef\n class=\"sh-editor-code\"\n [attr.readonly]=\"readonly() ? '' : null\"\n [value]=\"rawCodeValue()\"\n (input)=\"onCodeInput($event)\"\n (blur)=\"onCodeBlur($event)\"\n (focus)=\"isFocused.set(true)\"></textarea>\n }\n </div>\n\n <!-- Footer / Status Bar -->\n <div class=\"sh-editor-footer\">\n <div class=\"word-count\">{{ wordCount() }} words, {{ charCount() }} characters</div>\n <div class=\"spacer\"></div>\n <div class=\"storage-format\">Storage: {{ format().toUpperCase() }}</div>\n </div>\n</div>\n", styles: ["sh-editor{--editor-bg: var(--base-1);--editor-code-bg: var(--base-2);--editor-border-color: var(--base-4);--editor-toolbar-bg: rgb(from var(--base-1) r g b / 70%);--editor-toolbar-border: var(--base-4);--editor-btn-hover: var(--base-3);--editor-btn-active-bg: var(--primary-3);--editor-btn-active-c: var(--primary-11);--editor-footer-bg: var(--base-2);--editor-p: 1rem 1.25rem;--editor-min-h: 13.75rem;--editor-border-focus: var(--primary-8);--editor-shadow-focus: 0 0 0 3px var(--primary-3);display:block;width:100%;margin-bottom:1rem}sh-editor .sh-editor-container{position:relative;display:flex;flex-direction:column;background-color:var(--editor-bg);border:var(--border-10);border-color:var(--editor-border-color);border-radius:var(--shape-3);box-shadow:var(--box-shadow-10);overflow:hidden;transition:border-color .2s ease,box-shadow .2s ease}sh-editor.sh-editor-focused .sh-editor-container{border-color:var(--editor-border-focus);box-shadow:var(--editor-shadow-focus)}sh-editor.sh-editor-readonly .sh-editor-content{cursor:not-allowed}sh-editor .sh-editor-toolbar{position:sticky;top:0;z-index:10;display:flex;flex-wrap:wrap;align-items:center;gap:.25rem;padding:.375rem .5rem;background-color:var(--editor-toolbar-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--editor-toolbar-border);border-top-left-radius:calc(var(--shape-3) - 1px);border-top-right-radius:calc(var(--shape-3) - 1px)}sh-editor .sh-editor-toolbar-group{display:flex;align-items:center;gap:.125rem;padding-right:.375rem;border-right:1px solid var(--editor-toolbar-border)}sh-editor .sh-editor-toolbar-group:last-child,sh-editor .sh-editor-toolbar-group.view-toggle{border-right:none}sh-editor .sh-editor-btn{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;padding:0;background:transparent;border:none;border-radius:var(--shape-1);color:var(--base-11);cursor:pointer;font-size:1rem;transition:all .15s ease}sh-editor .sh-editor-btn:hover:not(:disabled){background-color:var(--editor-btn-hover);color:var(--base-12)}sh-editor .sh-editor-btn:disabled{opacity:.35;cursor:not-allowed}sh-editor .sh-editor-btn.active{background-color:var(--editor-btn-active-bg);color:var(--editor-btn-active-c);font-weight:700}sh-editor .sh-editor-btn i{font-style:normal;line-height:1}sh-editor .sh-editor-body{position:relative;display:flex;flex-direction:column;min-height:var(--editor-min-h)}sh-editor .sh-editor-content{flex:1 1 auto;padding:var(--editor-p);min-height:var(--editor-min-h);outline:none;color:var(--base-12);font:var(--paragraph-20);line-height:1.6;word-break:break-word}sh-editor .sh-editor-content p{margin-top:0;margin-bottom:.75rem}sh-editor .sh-editor-content p:last-child{margin-bottom:0}sh-editor .sh-editor-content h1,sh-editor .sh-editor-content h2,sh-editor .sh-editor-content h3{color:var(--base-12);font-weight:700;margin-top:1rem;margin-bottom:.5rem;line-height:1.3}sh-editor .sh-editor-content h1{font:var(--title-30B)}sh-editor .sh-editor-content h2{font:var(--title-20B)}sh-editor .sh-editor-content h3{font:var(--title-10B)}sh-editor .sh-editor-content blockquote{margin:1rem 0;padding:.5rem 1rem;border-left:4px solid var(--primary-8);background-color:var(--base-2);color:var(--base-11);border-radius:0 var(--shape-2) var(--shape-2) 0;font-style:italic}sh-editor .sh-editor-content pre{background-color:var(--editor-code-bg);border-radius:var(--shape-2);padding:.75rem 1rem;margin:1rem 0;overflow-x:auto}sh-editor .sh-editor-content pre code{font:var(--code-20);background:transparent;padding:0;border-radius:0}sh-editor .sh-editor-content code{font:var(--code-20);background-color:var(--base-2);padding:.125rem .375rem;border-radius:var(--shape-1);color:var(--primary-11)}sh-editor .sh-editor-content ul,sh-editor .sh-editor-content ol{margin-top:0;margin-bottom:1rem;padding-left:1.5rem}sh-editor .sh-editor-content li{margin-bottom:.25rem}sh-editor .sh-editor-content hr{border:none;border-top:1px solid var(--editor-border-color);margin:1.5rem 0}sh-editor .sh-editor-content img{height:auto;max-width:100%;transition:all .2s ease;border:var(--border-10);border-color:var(--base-3);cursor:pointer}sh-editor .sh-editor-content img:hover{outline:2px solid var(--primary-7)}sh-editor .sh-editor-content img:focus{outline:2px solid var(--primary-8)}sh-editor .sh-editor-content img.sh-editor-img-content{display:block;margin:1rem auto;box-sizing:border-box;border-radius:var(--shape-2)}sh-editor .sh-editor-content img.sh-editor-img-content.sh-editor-img-size-small{width:15rem;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-content.sh-editor-img-size-medium{width:30rem;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-content.sh-editor-img-size-large{width:calc(100% - 3rem);max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-content:not([class*=sh-editor-img-size-]){width:calc(100% - 3rem);max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-theater{display:block;width:calc(100% + 2.5rem);max-width:none!important;margin:1rem -1.25rem;padding:0;border-radius:0;border-left:none;border-right:none}sh-editor .sh-editor-content img.sh-editor-img-float{float:left;margin:.5rem 1rem .5rem 0;border-radius:var(--shape-2)}sh-editor .sh-editor-content img.sh-editor-img-float.sh-editor-img-size-auto{width:auto;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-float.sh-editor-img-size-small{width:15rem;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-float.sh-editor-img-size-medium{width:30rem;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-float.sh-editor-img-size-large{width:100%;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-float:not([class*=sh-editor-img-size-]){width:auto;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-custom,sh-editor .sh-editor-content img.sh-editor-img-auto{display:inline-block;margin:.75rem 0;border-radius:var(--shape-2)}sh-editor .sh-editor-content img.sh-editor-img-custom.sh-editor-img-size-auto,sh-editor .sh-editor-content img.sh-editor-img-auto.sh-editor-img-size-auto{width:auto;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-custom.sh-editor-img-size-small,sh-editor .sh-editor-content img.sh-editor-img-auto.sh-editor-img-size-small{width:15rem;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-custom.sh-editor-img-size-medium,sh-editor .sh-editor-content img.sh-editor-img-auto.sh-editor-img-size-medium{width:30rem;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-custom.sh-editor-img-size-large,sh-editor .sh-editor-content img.sh-editor-img-auto.sh-editor-img-size-large{width:100%;max-width:100%!important}sh-editor .sh-editor-content img.sh-editor-img-custom:not([class*=sh-editor-img-size-]),sh-editor .sh-editor-content img.sh-editor-img-auto:not([class*=sh-editor-img-size-]){width:auto;max-width:100%!important}sh-editor .sh-editor-content a{color:var(--primary-9);text-decoration:underline;cursor:pointer}sh-editor .sh-editor-content a:hover{color:var(--primary-11)}sh-editor .sh-editor-content:empty:before{content:attr(placeholder);color:var(--base-8);font-style:italic;pointer-events:none;display:block}sh-editor .sh-editor-content p{position:relative}sh-editor .sh-editor-content.has-slash-commands p.sh-editor-active-block:empty:before,sh-editor .sh-editor-content.has-slash-commands p.sh-editor-active-block:has(>br:only-child):before{content:\"Type '/' for commands...\";position:absolute;left:0;top:0;pointer-events:none;color:var(--base-6);font-style:normal;font-weight:400;opacity:.6}sh-editor .sh-editor-code{flex:1 1 auto;width:100%;min-height:var(--editor-min-h);padding:var(--editor-p);background-color:var(--editor-code-bg);color:var(--base-12);font:var(--code-20);border:none;resize:vertical;outline:none;font-family:monospace;line-height:1.5}sh-editor .sh-editor-footer{display:flex;align-items:center;padding:.375rem .75rem;background-color:var(--editor-footer-bg);border-top:1px solid var(--editor-border-color);font:var(--paragraph-10);color:var(--base-8);border-bottom-left-radius:calc(var(--shape-3) - 1px);border-bottom-right-radius:calc(var(--shape-3) - 1px)}sh-editor .sh-editor-modal{position:absolute;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;padding:1rem}sh-editor .sh-editor-modal-backdrop{position:absolute;inset:0;background-color:rgb(from var(--base-1) r g b/40%);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}sh-editor .sh-editor-modal-content{position:relative;z-index:101;width:100%;max-width:22.5rem;padding:1.25rem;background-color:var(--base-1);border:var(--border-10);border-color:var(--base-4);border-radius:var(--shape-3);box-shadow:var(--box-shadow-40)}sh-editor .sh-editor-modal-content h4{margin-top:0;margin-bottom:.75rem;font:var(--title-10B);color:var(--base-12)}sh-editor .sh-editor-input{width:100%;padding:.5rem .75rem;background-color:var(--base-2);border:var(--border-10);border-color:var(--base-4);border-radius:var(--shape-2);color:var(--base-12);font:var(--paragraph-20);outline:none;margin-bottom:1rem;transition:border-color .15s ease}sh-editor .sh-editor-input:focus{border-color:var(--primary-8)}sh-editor .sh-editor-modal-actions{display:flex;justify-content:flex-end;gap:.5rem}sh-editor .sh-editor-modal-btn{padding:.375rem .75rem;border-radius:var(--shape-2);font:var(--paragraph-20);cursor:pointer;border:none;transition:all .15s ease}sh-editor .sh-editor-modal-btn.cancel{background-color:transparent;color:var(--base-11)}sh-editor .sh-editor-modal-btn.cancel:hover{background-color:var(--base-3)}sh-editor .sh-editor-modal-btn.confirm{background-color:var(--primary-9);color:var(--primary-1)}sh-editor .sh-editor-modal-btn.confirm:hover{background-color:var(--primary-10)}sh-editor .sh-editor-img-toolbar{position:absolute;z-index:50;display:flex;align-items:center;gap:.25rem;padding:.25rem .375rem;background-color:rgb(from var(--base-1) r g b/85%);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:var(--border-10);border-color:var(--base-4);border-radius:var(--shape-2);box-shadow:var(--box-shadow-20);pointer-events:auto;transition:opacity .2s ease,transform .2s ease}sh-editor .sh-editor-img-toolbar-btn{display:inline-flex;align-items:center;justify-content:center;padding:.25rem .5rem;background:transparent;border:none;border-radius:var(--shape-1);color:var(--base-11);cursor:pointer;font:var(--paragraph-10);font-weight:500;transition:all .15s ease}sh-editor .sh-editor-img-toolbar-btn:hover{background-color:var(--editor-btn-hover);color:var(--base-12)}sh-editor .sh-editor-img-toolbar-btn.active{background-color:var(--editor-btn-active-bg);color:var(--editor-btn-active-c)}sh-editor .sh-editor-img-toolbar-btn.danger{color:var(--error-9)}sh-editor .sh-editor-img-toolbar-btn.danger:hover{background-color:var(--error-3);color:var(--error-11)}sh-editor .sh-editor-img-toolbar-divider{width:1px;height:1rem;background-color:var(--editor-toolbar-border);margin:0 .125rem}sh-editor .sh-editor-upload-btn{display:flex;align-items:center;justify-content:center;gap:.5rem;width:100%;padding:.625rem 1rem;background-color:var(--base-2);border:1px dashed var(--editor-border-color);border-radius:var(--shape-2);color:var(--base-11);font:var(--paragraph-20);cursor:pointer;transition:all .15s ease;margin-bottom:.75rem}sh-editor .sh-editor-upload-btn:hover{background-color:var(--base-3);border-color:var(--primary-7);color:var(--base-12)}sh-editor .sh-editor-upload-btn sh-icon{font-size:1.125rem}sh-editor .sh-editor-modal-divider{display:flex;align-items:center;text-align:center;color:var(--base-8);font:var(--paragraph-10);margin-bottom:.75rem;width:100%}sh-editor .sh-editor-modal-divider:before,sh-editor .sh-editor-modal-divider:after{content:\"\";flex:1;border-bottom:1px solid var(--editor-border-color)}sh-editor .sh-editor-modal-divider:not(:empty):before{margin-right:.5rem}sh-editor .sh-editor-modal-divider:not(:empty):after{margin-left:.5rem}sh-editor .sh-editor-dropdown{position:relative;display:inline-block}sh-editor .sh-editor-dropdown-trigger{display:inline-flex;align-items:center;gap:.375rem;height:1.75rem;padding:0 .5rem;background:transparent;border:none;border-radius:var(--shape-1);color:var(--base-11);cursor:pointer;font:var(--paragraph-20);font-weight:600;transition:all .15s ease;outline:none}sh-editor .sh-editor-dropdown-trigger:hover{background-color:var(--editor-btn-hover);color:var(--base-12)}sh-editor .sh-editor-dropdown-trigger:focus{outline:2px solid var(--primary-8)}sh-editor .sh-editor-dropdown-label{white-space:nowrap}sh-editor .sh-editor-slash-item{display:flex;align-items:center;gap:.625rem;width:100%;padding:.375rem .625rem;background:transparent;border:none;border-radius:var(--shape-2);text-align:left;cursor:pointer;transition:all .1s ease;outline:none}sh-editor .sh-editor-slash-item.active,sh-editor .sh-editor-slash-item:hover{background-color:var(--editor-btn-hover)}sh-editor .sh-editor-slash-item.active .sh-editor-slash-item-icon,sh-editor .sh-editor-slash-item:hover .sh-editor-slash-item-icon{background-color:var(--editor-btn-active-bg);color:var(--editor-btn-active-c)}sh-editor .sh-editor-slash-item .sh-editor-slash-item-icon{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;border-radius:var(--shape-1);background-color:var(--base-3);color:var(--base-11);font-size:1rem;transition:all .15s ease;flex-shrink:0}sh-editor .sh-editor-slash-item .sh-editor-slash-item-content{display:flex;flex-direction:column;overflow:hidden}sh-editor .sh-editor-slash-item .sh-editor-slash-item-label{font:var(--paragraph-20);font-weight:600;color:var(--base-12);line-height:1.2}sh-editor .sh-editor-slash-item .sh-editor-slash-item-desc{font:var(--paragraph-10);color:var(--base-8);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:.125rem}\n"] }]
2584
+ }], ctorParameters: () => [], propDecorators: { editorRef: [{ type: i0.ViewChild, args: ['editorRef', { isSignal: true }] }], codeEditorRef: [{ type: i0.ViewChild, args: ['codeEditorRef', { isSignal: true }] }], uploadBtn: [{ type: i0.ViewChild, args: ['uploadBtn', { isSignal: true }] }], imageInput: [{ type: i0.ViewChild, args: ['imageInput', { isSignal: true }] }], linkInput: [{ type: i0.ViewChild, args: ['linkInput', { isSignal: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], toolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbar", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], customCommands: [{ type: i0.Input, args: [{ isSignal: true, alias: "customCommands", required: false }] }], slashCommands: [{ type: i0.Input, args: [{ isSignal: true, alias: "slashCommands", required: false }] }], showFormats: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFormats", required: false }] }], showBlocks: [{ type: i0.Input, args: [{ isSignal: true, alias: "showBlocks", required: false }] }], showLists: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLists", required: false }] }], showAlignments: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAlignments", required: false }] }], showInsertions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showInsertions", required: false }] }], showHistory: [{ type: i0.Input, args: [{ isSignal: true, alias: "showHistory", required: false }] }], customUpload: [{ type: i0.Input, args: [{ isSignal: true, alias: "customUpload", required: false }] }], imageUploadEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "imageUploadEnabled", required: false }] }], imageUpload: [{ type: i0.Output, args: ["imageUpload"] }], onSelectionChange: [{
2585
+ type: HostListener,
2586
+ args: ['document:selectionchange']
2587
+ }], onComponentFocusIn: [{
2588
+ type: HostListener,
2589
+ args: ['focusin', ['$event']]
2590
+ }], onComponentClick: [{
2591
+ type: HostListener,
2592
+ args: ['click', ['$event']]
2593
+ }], onKeyDown: [{
2594
+ type: HostListener,
2595
+ args: ['keydown', ['$event']]
2596
+ }], onKeyUp: [{
2597
+ type: HostListener,
2598
+ args: ['keyup', ['$event']]
2599
+ }], onComponentScroll: [{
2600
+ type: HostListener,
2601
+ args: ['scroll', ['$event']]
2602
+ }], onWindowResize: [{
2603
+ type: HostListener,
2604
+ args: ['window:resize']
2605
+ }], onDrop: [{
2606
+ type: HostListener,
2607
+ args: ['drop', ['$event']]
2608
+ }], onPaste: [{
2609
+ type: HostListener,
2610
+ args: ['paste', ['$event']]
2611
+ }] } });
2612
+ function getNodePath(parent, node) {
2613
+ const path = [];
2614
+ let current = node;
2615
+ while (current && current !== parent) {
2616
+ const parentNode = current.parentNode;
2617
+ if (!parentNode)
2618
+ break;
2619
+ const siblingIndex = Array.prototype.indexOf.call(parentNode.childNodes, current);
2620
+ if (siblingIndex === -1)
2621
+ break;
2622
+ path.unshift(siblingIndex);
2623
+ current = parentNode;
2624
+ }
2625
+ return path;
2626
+ }
2627
+ function getNodeByPath(parent, path) {
2628
+ let current = parent;
2629
+ for (const index of path) {
2630
+ if (current.childNodes[index]) {
2631
+ current = current.childNodes[index];
2632
+ }
2633
+ else {
2634
+ break;
2635
+ }
2636
+ }
2637
+ return current;
2638
+ }
2639
+ function isNodeWrappedInTag(node, tag, limit) {
2640
+ const tagName = tag.toUpperCase();
2641
+ let current = node.parentNode;
2642
+ while (current && current !== limit) {
2643
+ if (current.nodeType === Node.ELEMENT_NODE && current.tagName === tagName) {
2644
+ return true;
2645
+ }
2646
+ current = current.parentNode;
2647
+ }
2648
+ return false;
2649
+ }
2650
+ function getTextNodesInRange(range) {
2651
+ const textNodes = [];
2652
+ const commonAncestor = range.commonAncestorContainer;
2653
+ if (commonAncestor.nodeType === Node.TEXT_NODE) {
2654
+ textNodes.push(commonAncestor);
2655
+ return textNodes;
2656
+ }
2657
+ const walker = commonAncestor.ownerDocument.createTreeWalker(commonAncestor, NodeFilter.SHOW_TEXT);
2658
+ let node = walker.nextNode();
2659
+ while (node) {
2660
+ if (range.intersectsNode(node)) {
2661
+ textNodes.push(node);
2662
+ }
2663
+ node = walker.nextNode();
2664
+ }
2665
+ return textNodes;
2666
+ }
2667
+
2668
+ /**
2669
+ * Generated bundle index. Do not edit.
2670
+ */
2671
+
2672
+ export { ShipEditor };
2673
+ //# sourceMappingURL=ship-ui-core-ship-editor.mjs.map