@domternal/angular 0.2.0

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.
@@ -0,0 +1,1572 @@
1
+ import * as i0 from '@angular/core';
2
+ import { viewChild, input, output, signal, inject, NgZone, afterNextRender, effect, untracked, forwardRef, ViewEncapsulation, ChangeDetectionStrategy, Component, ElementRef, computed } from '@angular/core';
3
+ import { NG_VALUE_ACCESSOR } from '@angular/forms';
4
+ import { Document, Paragraph, Text, BaseKeymap, History, Editor, positionFloatingOnce, defaultIcons, ToolbarController, PluginKey, createBubbleMenuPlugin, createFloatingMenuPlugin } from '@domternal/core';
5
+ export { Editor } from '@domternal/core';
6
+ import { DomSanitizer } from '@angular/platform-browser';
7
+
8
+ const DEFAULT_EXTENSIONS = [Document, Paragraph, Text, BaseKeymap, History];
9
+ class DomternalEditorComponent {
10
+ // === Template ref ===
11
+ editorRef = viewChild.required('editorRef');
12
+ // === Inputs ===
13
+ extensions = input([], ...(ngDevMode ? [{ debugName: "extensions" }] : /* istanbul ignore next */ []));
14
+ content = input('', ...(ngDevMode ? [{ debugName: "content" }] : /* istanbul ignore next */ []));
15
+ editable = input(true, ...(ngDevMode ? [{ debugName: "editable" }] : /* istanbul ignore next */ []));
16
+ autofocus = input(false, ...(ngDevMode ? [{ debugName: "autofocus" }] : /* istanbul ignore next */ []));
17
+ outputFormat = input('html', ...(ngDevMode ? [{ debugName: "outputFormat" }] : /* istanbul ignore next */ []));
18
+ // === Outputs ===
19
+ editorCreated = output();
20
+ contentUpdated = output();
21
+ selectionChanged = output();
22
+ focusChanged = output();
23
+ blurChanged = output();
24
+ editorDestroyed = output();
25
+ // === Signals (read-only public state) ===
26
+ _htmlContent = signal('', ...(ngDevMode ? [{ debugName: "_htmlContent" }] : /* istanbul ignore next */ []));
27
+ _jsonContent = signal(null, ...(ngDevMode ? [{ debugName: "_jsonContent" }] : /* istanbul ignore next */ []));
28
+ _isEmpty = signal(true, ...(ngDevMode ? [{ debugName: "_isEmpty" }] : /* istanbul ignore next */ []));
29
+ _isFocused = signal(false, ...(ngDevMode ? [{ debugName: "_isFocused" }] : /* istanbul ignore next */ []));
30
+ // Candidate for linkedSignal(editable) once min Angular version is >=20
31
+ _isEditable = signal(true, ...(ngDevMode ? [{ debugName: "_isEditable" }] : /* istanbul ignore next */ []));
32
+ htmlContent = this._htmlContent.asReadonly();
33
+ jsonContent = this._jsonContent.asReadonly();
34
+ isEmpty = this._isEmpty.asReadonly();
35
+ isFocused = this._isFocused.asReadonly();
36
+ isEditable = this._isEditable.asReadonly();
37
+ // === Editor instance ===
38
+ _editor = null;
39
+ get editor() {
40
+ return this._editor;
41
+ }
42
+ // === ControlValueAccessor ===
43
+ onChange = () => { };
44
+ onTouched = () => { };
45
+ _pendingContent = null;
46
+ ngZone = inject(NgZone);
47
+ constructor() {
48
+ afterNextRender(() => {
49
+ this.createEditor();
50
+ });
51
+ // React to editable input changes
52
+ effect(() => {
53
+ const editable = this.editable();
54
+ if (!this._editor || this._editor.isDestroyed)
55
+ return;
56
+ untracked(() => {
57
+ this._editor.setEditable(editable);
58
+ this._isEditable.set(editable);
59
+ });
60
+ });
61
+ // React to content input changes
62
+ effect(() => {
63
+ const content = this.content();
64
+ const format = this.outputFormat();
65
+ if (!this._editor || this._editor.isDestroyed)
66
+ return;
67
+ untracked(() => {
68
+ const current = format === 'html'
69
+ ? this._editor.getHTML()
70
+ : JSON.stringify(this._editor.getJSON());
71
+ const incoming = format === 'html'
72
+ ? content
73
+ : JSON.stringify(content);
74
+ if (incoming !== current) {
75
+ this._editor.setContent(content, false);
76
+ }
77
+ });
78
+ });
79
+ // React to extensions input changes
80
+ effect(() => {
81
+ this.extensions(); // track the signal
82
+ if (!this._editor || this._editor.isDestroyed)
83
+ return;
84
+ untracked(() => {
85
+ this.recreateEditor();
86
+ });
87
+ });
88
+ }
89
+ // === Lifecycle ===
90
+ ngOnDestroy() {
91
+ if (this._editor && !this._editor.isDestroyed) {
92
+ this._editor.destroy();
93
+ this.editorDestroyed.emit();
94
+ }
95
+ this._editor = null;
96
+ }
97
+ // === ControlValueAccessor implementation ===
98
+ writeValue(value) {
99
+ if (!this._editor || this._editor.isDestroyed) {
100
+ this._pendingContent = value;
101
+ return;
102
+ }
103
+ // Compare current content to avoid unnecessary setContent (which resets cursor)
104
+ if (this.outputFormat() === 'html') {
105
+ if (value === this._editor.getHTML())
106
+ return;
107
+ }
108
+ else {
109
+ if (JSON.stringify(value) === JSON.stringify(this._editor.getJSON()))
110
+ return;
111
+ }
112
+ this._editor.setContent(value, false);
113
+ }
114
+ registerOnChange(fn) {
115
+ this.onChange = fn;
116
+ }
117
+ registerOnTouched(fn) {
118
+ this.onTouched = fn;
119
+ }
120
+ setDisabledState(isDisabled) {
121
+ this._isEditable.set(!isDisabled);
122
+ if (this._editor && !this._editor.isDestroyed) {
123
+ this._editor.setEditable(!isDisabled);
124
+ }
125
+ }
126
+ // === Private ===
127
+ recreateEditor() {
128
+ if (!this._editor || this._editor.isDestroyed)
129
+ return;
130
+ const currentContent = this._editor.getJSON();
131
+ this._editor.destroy();
132
+ this.editorDestroyed.emit();
133
+ this._pendingContent = currentContent;
134
+ this.createEditor();
135
+ }
136
+ createEditor() {
137
+ const initialContent = this._pendingContent ?? this.content();
138
+ this._pendingContent = null;
139
+ this._editor = new Editor({
140
+ element: this.editorRef().nativeElement,
141
+ extensions: [...DEFAULT_EXTENSIONS, ...this.extensions()],
142
+ content: initialContent,
143
+ editable: this.editable(),
144
+ autofocus: this.autofocus(),
145
+ });
146
+ this._isEditable.set(this.editable());
147
+ // Set initial signal values
148
+ this._htmlContent.set(this._editor.getHTML());
149
+ this._jsonContent.set(this._editor.getJSON());
150
+ this._isEmpty.set(this._editor.isEmpty);
151
+ this._editor.on('transaction', ({ transaction }) => {
152
+ this.ngZone.run(() => {
153
+ const ed = this._editor;
154
+ if (transaction.docChanged) {
155
+ const html = ed.getHTML();
156
+ this._htmlContent.set(html);
157
+ this._jsonContent.set(ed.getJSON());
158
+ this._isEmpty.set(ed.isEmpty);
159
+ this.contentUpdated.emit({ editor: ed });
160
+ const value = this.outputFormat() === 'html' ? html : ed.getJSON();
161
+ this.onChange(value);
162
+ }
163
+ if (!transaction.docChanged && transaction.selectionSet) {
164
+ this.selectionChanged.emit({ editor: ed });
165
+ }
166
+ });
167
+ });
168
+ this._editor.on('focus', ({ event }) => {
169
+ this.ngZone.run(() => {
170
+ this._isFocused.set(true);
171
+ this.focusChanged.emit({ editor: this._editor, event });
172
+ });
173
+ });
174
+ this._editor.on('blur', ({ event }) => {
175
+ this.ngZone.run(() => {
176
+ this._isFocused.set(false);
177
+ this.blurChanged.emit({ editor: this._editor, event });
178
+ this.onTouched();
179
+ });
180
+ });
181
+ // Emit editor created
182
+ this.ngZone.run(() => {
183
+ this.editorCreated.emit(this._editor);
184
+ });
185
+ }
186
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
187
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.5", type: DomternalEditorComponent, isStandalone: true, selector: "domternal-editor", inputs: { extensions: { classPropertyName: "extensions", publicName: "extensions", isSignal: true, isRequired: false, transformFunction: null }, content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, autofocus: { classPropertyName: "autofocus", publicName: "autofocus", isSignal: true, isRequired: false, transformFunction: null }, outputFormat: { classPropertyName: "outputFormat", publicName: "outputFormat", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { editorCreated: "editorCreated", contentUpdated: "contentUpdated", selectionChanged: "selectionChanged", focusChanged: "focusChanged", blurChanged: "blurChanged", editorDestroyed: "editorDestroyed" }, host: { classAttribute: "dm-editor" }, providers: [
188
+ {
189
+ provide: NG_VALUE_ACCESSOR,
190
+ useExisting: forwardRef(() => DomternalEditorComponent),
191
+ multi: true,
192
+ },
193
+ ], viewQueries: [{ propertyName: "editorRef", first: true, predicate: ["editorRef"], descendants: true, isSignal: true }], ngImport: i0, template: '<div #editorRef></div>', isInline: true, styles: [":host{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
194
+ }
195
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalEditorComponent, decorators: [{
196
+ type: Component,
197
+ args: [{ selector: 'domternal-editor', template: '<div #editorRef></div>', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: { class: 'dm-editor' }, providers: [
198
+ {
199
+ provide: NG_VALUE_ACCESSOR,
200
+ useExisting: forwardRef(() => DomternalEditorComponent),
201
+ multi: true,
202
+ },
203
+ ], styles: [":host{display:block}\n"] }]
204
+ }], ctorParameters: () => [], propDecorators: { editorRef: [{ type: i0.ViewChild, args: ['editorRef', { isSignal: true }] }], extensions: [{ type: i0.Input, args: [{ isSignal: true, alias: "extensions", required: false }] }], content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }], autofocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "autofocus", required: false }] }], outputFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "outputFormat", required: false }] }], editorCreated: [{ type: i0.Output, args: ["editorCreated"] }], contentUpdated: [{ type: i0.Output, args: ["contentUpdated"] }], selectionChanged: [{ type: i0.Output, args: ["selectionChanged"] }], focusChanged: [{ type: i0.Output, args: ["focusChanged"] }], blurChanged: [{ type: i0.Output, args: ["blurChanged"] }], editorDestroyed: [{ type: i0.Output, args: ["editorDestroyed"] }] } });
205
+
206
+ const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);
207
+ class DomternalToolbarComponent {
208
+ editor = input.required(...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
209
+ icons = input(null, ...(ngDevMode ? [{ debugName: "icons" }] : /* istanbul ignore next */ []));
210
+ layout = input(...(ngDevMode ? [undefined, { debugName: "layout" }] : /* istanbul ignore next */ []));
211
+ /** Exposed state signals for template */
212
+ groups = signal([], ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
213
+ focusedIndex = signal(0, ...(ngDevMode ? [{ debugName: "focusedIndex" }] : /* istanbul ignore next */ []));
214
+ openDropdown = signal(null, ...(ngDevMode ? [{ debugName: "openDropdown" }] : /* istanbul ignore next */ []));
215
+ /** Bumped on active state changes to trigger re-evaluation of isActive() */
216
+ activeVersion = signal(0, ...(ngDevMode ? [{ debugName: "activeVersion" }] : /* istanbul ignore next */ []));
217
+ controller = null;
218
+ clickOutsideHandler = null;
219
+ dismissOverlayHandler = null;
220
+ editorEl = null;
221
+ cleanupFloating = null;
222
+ ngZone = inject(NgZone);
223
+ elRef = inject(ElementRef);
224
+ sanitizer = inject(DomSanitizer);
225
+ /** SafeHtml cache — same reference returned for same key, prevents DOM churn */
226
+ htmlCache = new Map();
227
+ dropdownCaret = '<svg class="dm-dropdown-caret" width="10" height="10" viewBox="0 0 10 10"><path d="M2 4l3 3 3-3" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
228
+ constructor() {
229
+ effect(() => {
230
+ const editor = this.editor();
231
+ untracked(() => this.setupController(editor));
232
+ });
233
+ }
234
+ ngOnDestroy() {
235
+ this.destroyController();
236
+ }
237
+ // === Template helpers ===
238
+ isActive(name) {
239
+ this.activeVersion(); // subscribe to changes
240
+ return this.controller?.activeMap.get(name) ?? false;
241
+ }
242
+ isDisabled(name) {
243
+ this.activeVersion(); // subscribe to changes
244
+ return this.controller?.disabledMap.get(name) ?? false;
245
+ }
246
+ isDropdownActive(dropdown) {
247
+ if (dropdown.layout === 'grid')
248
+ return false; // color dropdowns use bar indicator instead
249
+ if (dropdown.dynamicLabel)
250
+ return false; // label text already communicates state
251
+ this.activeVersion(); // subscribe to changes
252
+ return dropdown.items.some((item) => this.controller?.activeMap.get(item.name) ?? false);
253
+ }
254
+ /** Returns trigger innerHTML: dynamic icon + caret (+ color indicator for grid dropdowns). */
255
+ getDropdownTriggerHtml(dropdown) {
256
+ this.activeVersion(); // subscribe to changes
257
+ const activeItem = dropdown.items.find((item) => this.controller?.activeMap.get(item.name));
258
+ if (dropdown.layout === 'grid') {
259
+ const color = activeItem?.color ?? dropdown.defaultIndicatorColor ?? null;
260
+ const key = `tr:${dropdown.icon}:${color ?? ''}`;
261
+ let cached = this.htmlCache.get(key);
262
+ if (!cached) {
263
+ let html = this.resolveIconSvg(dropdown.icon) + this.dropdownCaret;
264
+ if (color) {
265
+ html += `<span class="dm-toolbar-color-indicator" style="background-color: ${color}"></span>`;
266
+ }
267
+ cached = this.sanitizer.bypassSecurityTrustHtml(html);
268
+ this.htmlCache.set(key, cached);
269
+ }
270
+ return cached;
271
+ }
272
+ // Non-grid dropdown — show active sub-item's label as text
273
+ if (dropdown.dynamicLabel) {
274
+ if (activeItem)
275
+ return this.getCachedTriggerLabel(activeItem.label);
276
+ // Try reading CSS property from DOM (inline or computed depending on property)
277
+ if (dropdown.computedStyleProperty) {
278
+ let computed;
279
+ if (dropdown.computedStyleProperty === 'font-family') {
280
+ // Font-family: read ONLY inline style (explicit mark), not computed (browser default)
281
+ computed = this.getInlineStyleAtCursor(dropdown.computedStyleProperty);
282
+ if (computed) {
283
+ const first = computed.split(',')[0]?.replace(/['"]+/g, '').trim();
284
+ computed = first || null;
285
+ }
286
+ }
287
+ else {
288
+ computed = this.getComputedStyleAtCursor(dropdown.computedStyleProperty);
289
+ }
290
+ if (computed)
291
+ return this.getCachedTriggerLabel(computed);
292
+ }
293
+ if (dropdown.dynamicLabelFallback) {
294
+ return this.getCachedTriggerLabel(dropdown.dynamicLabelFallback);
295
+ }
296
+ return this.getCachedTriggerLabel(dropdown.icon, true);
297
+ }
298
+ const icon = dropdown.dynamicIcon && activeItem ? activeItem.icon : dropdown.icon;
299
+ return this.getCachedTriggerIcon(icon);
300
+ }
301
+ /**
302
+ * Returns 'true' when an emitEvent button's panel is open, null otherwise.
303
+ * Maps to [attr.aria-expanded] — null removes the attribute entirely.
304
+ */
305
+ getAriaExpanded(item) {
306
+ if (!item.emitEvent)
307
+ return null;
308
+ this.activeVersion(); // subscribe to changes
309
+ return this.controller?.expandedMap.get(item.name) ? 'true' : null;
310
+ }
311
+ getFlatIndex(name) {
312
+ return this.controller?.getFlatIndex(name) ?? -1;
313
+ }
314
+ getTooltip(item) {
315
+ if (item.shortcut) {
316
+ const parts = item.shortcut.split('-');
317
+ const mapped = parts.map(p => {
318
+ if (p === 'Mod')
319
+ return isMac ? '\u2318' : 'Ctrl';
320
+ if (p === 'Shift')
321
+ return isMac ? '\u21E7' : 'Shift';
322
+ if (p === 'Alt')
323
+ return isMac ? '\u2325' : 'Alt';
324
+ return p.toUpperCase();
325
+ });
326
+ const shortcut = isMac ? mapped.join('') : mapped.join('+');
327
+ return `${item.label} (${shortcut})`;
328
+ }
329
+ return item.label;
330
+ }
331
+ getCachedIcon(name) {
332
+ const key = `i:${name}`;
333
+ let cached = this.htmlCache.get(key);
334
+ if (!cached) {
335
+ cached = this.sanitizer.bypassSecurityTrustHtml(this.resolveIconSvg(name));
336
+ this.htmlCache.set(key, cached);
337
+ }
338
+ return cached;
339
+ }
340
+ getCachedTriggerLabel(label, isIcon) {
341
+ const key = `tl:${label}`;
342
+ let cached = this.htmlCache.get(key);
343
+ if (!cached) {
344
+ const content = isIcon ? this.resolveIconSvg(label) : label;
345
+ cached = this.sanitizer.bypassSecurityTrustHtml(`<span class="dm-toolbar-trigger-label">${content}</span>` + this.dropdownCaret);
346
+ this.htmlCache.set(key, cached);
347
+ }
348
+ return cached;
349
+ }
350
+ getCachedTriggerIcon(iconName) {
351
+ const key = `t:${iconName}`;
352
+ let cached = this.htmlCache.get(key);
353
+ if (!cached) {
354
+ cached = this.sanitizer.bypassSecurityTrustHtml(this.resolveIconSvg(iconName) + this.dropdownCaret);
355
+ this.htmlCache.set(key, cached);
356
+ }
357
+ return cached;
358
+ }
359
+ getCachedItemContent(iconName, label, displayMode) {
360
+ const mode = displayMode ?? 'icon-text';
361
+ const key = `dc:${iconName}:${label}:${mode}`;
362
+ let cached = this.htmlCache.get(key);
363
+ if (!cached) {
364
+ let html;
365
+ if (mode === 'text') {
366
+ html = label;
367
+ }
368
+ else if (mode === 'icon') {
369
+ html = this.resolveIconSvg(iconName);
370
+ }
371
+ else {
372
+ html = this.resolveIconSvg(iconName) + ' ' + label;
373
+ }
374
+ cached = this.sanitizer.bypassSecurityTrustHtml(html);
375
+ this.htmlCache.set(key, cached);
376
+ }
377
+ return cached;
378
+ }
379
+ asButton(item) {
380
+ return item;
381
+ }
382
+ asDropdown(item) {
383
+ return item;
384
+ }
385
+ getGridStyle(dropdown) {
386
+ return `--dm-palette-columns: ${String(dropdown.gridColumns ?? 10)}`;
387
+ }
388
+ // === Event handlers ===
389
+ onButtonClick(item, event) {
390
+ // Close any open dropdown when clicking a regular button
391
+ if (this.openDropdown()) {
392
+ this.cleanupFloating?.();
393
+ this.cleanupFloating = null;
394
+ this.controller?.closeDropdown();
395
+ this.syncState();
396
+ }
397
+ if (item.emitEvent) {
398
+ const anchor = event?.currentTarget;
399
+ // emitEvent is a dynamic string; cast needed to bypass strict EventEmitter<EditorEvents> typing
400
+ this.editor().emit(item.emitEvent, { anchorElement: anchor });
401
+ return;
402
+ }
403
+ this.controller?.executeCommand(item);
404
+ }
405
+ onDropdownToggle(dropdown) {
406
+ this.cleanupFloating?.();
407
+ this.cleanupFloating = null;
408
+ this.controller?.toggleDropdown(dropdown.name);
409
+ this.syncState();
410
+ // Position the panel after Angular renders it (tracks resize, not scroll)
411
+ if (this.openDropdown()) {
412
+ requestAnimationFrame(() => {
413
+ const trigger = this.elRef.nativeElement.querySelector(`[aria-expanded="true"]`);
414
+ const panel = trigger?.parentElement?.querySelector('.dm-toolbar-dropdown-panel');
415
+ if (trigger && panel) {
416
+ // List dropdowns align to trigger's left edge; grid/picker dropdowns center
417
+ const placement = dropdown.layout === 'grid' ? 'bottom' : 'bottom-start';
418
+ this.cleanupFloating = positionFloatingOnce(trigger, panel, {
419
+ placement,
420
+ offsetValue: 4,
421
+ });
422
+ }
423
+ });
424
+ }
425
+ }
426
+ onDropdownItemClick(item, event) {
427
+ // For emitEvent items, use the dropdown trigger as anchor (sub-item gets removed when dropdown closes)
428
+ let anchor;
429
+ if (item.emitEvent && event?.currentTarget) {
430
+ const wrapper = event.currentTarget.closest('.dm-toolbar-dropdown-wrapper');
431
+ anchor = wrapper?.querySelector('.dm-toolbar-dropdown-trigger');
432
+ }
433
+ this.cleanupFloating?.();
434
+ this.cleanupFloating = null;
435
+ this.controller?.closeDropdown();
436
+ if (item.emitEvent) {
437
+ this.editor().emit(item.emitEvent, { anchorElement: anchor });
438
+ }
439
+ else {
440
+ this.controller?.executeCommand(item);
441
+ }
442
+ }
443
+ onButtonFocus(name) {
444
+ const index = this.controller?.getFlatIndex(name) ?? -1;
445
+ if (index >= 0) {
446
+ this.controller?.setFocusedIndex(index);
447
+ }
448
+ }
449
+ onKeydown(event) {
450
+ if (!this.controller)
451
+ return;
452
+ switch (event.key) {
453
+ case 'ArrowRight':
454
+ event.preventDefault();
455
+ this.controller.navigateNext();
456
+ this.focusCurrentButton();
457
+ break;
458
+ case 'ArrowLeft':
459
+ event.preventDefault();
460
+ this.controller.navigatePrev();
461
+ this.focusCurrentButton();
462
+ break;
463
+ case 'Home':
464
+ event.preventDefault();
465
+ this.controller.navigateFirst();
466
+ this.focusCurrentButton();
467
+ break;
468
+ case 'End':
469
+ event.preventDefault();
470
+ this.controller.navigateLast();
471
+ this.focusCurrentButton();
472
+ break;
473
+ case 'Escape':
474
+ if (this.openDropdown()) {
475
+ event.preventDefault();
476
+ this.cleanupFloating?.();
477
+ this.cleanupFloating = null;
478
+ this.controller.closeDropdown();
479
+ this.syncState();
480
+ this.focusCurrentButton();
481
+ }
482
+ break;
483
+ }
484
+ }
485
+ // === Private ===
486
+ resolveIconSvg(name) {
487
+ const customIcons = this.icons();
488
+ if (customIcons) {
489
+ return customIcons[name] ?? '';
490
+ }
491
+ return defaultIcons[name] ?? '';
492
+ }
493
+ setupController(editor) {
494
+ this.destroyController();
495
+ this.controller = new ToolbarController(editor, () => this.ngZone.run(() => this.syncState()), this.layout());
496
+ this.controller.subscribe();
497
+ this.syncState();
498
+ // Click outside to close dropdown
499
+ this.clickOutsideHandler = (e) => {
500
+ if (this.openDropdown() && !this.elRef.nativeElement.contains(e.target)) {
501
+ this.cleanupFloating?.();
502
+ this.cleanupFloating = null;
503
+ this.controller?.closeDropdown();
504
+ this.ngZone.run(() => this.syncState());
505
+ }
506
+ };
507
+ document.addEventListener('mousedown', this.clickOutsideHandler);
508
+ // Listen for dismiss-overlays (e.g. table handle clicks that stopPropagation on mousedown)
509
+ this.editorEl = editor.view.dom.closest('.dm-editor');
510
+ if (this.editorEl) {
511
+ this.dismissOverlayHandler = () => {
512
+ if (this.openDropdown()) {
513
+ this.cleanupFloating?.();
514
+ this.cleanupFloating = null;
515
+ this.controller?.closeDropdown();
516
+ this.ngZone.run(() => this.syncState());
517
+ }
518
+ };
519
+ this.editorEl.addEventListener('dm:dismiss-overlays', this.dismissOverlayHandler);
520
+ }
521
+ }
522
+ destroyController() {
523
+ this.cleanupFloating?.();
524
+ this.cleanupFloating = null;
525
+ if (this.clickOutsideHandler) {
526
+ document.removeEventListener('mousedown', this.clickOutsideHandler);
527
+ this.clickOutsideHandler = null;
528
+ }
529
+ if (this.dismissOverlayHandler && this.editorEl) {
530
+ this.editorEl.removeEventListener('dm:dismiss-overlays', this.dismissOverlayHandler);
531
+ this.dismissOverlayHandler = null;
532
+ this.editorEl = null;
533
+ }
534
+ if (this.controller) {
535
+ this.controller.destroy();
536
+ this.controller = null;
537
+ }
538
+ }
539
+ syncState() {
540
+ if (!this.controller)
541
+ return;
542
+ // Only update groups if they actually changed (initial build or rebuild)
543
+ const controllerGroups = this.controller.groups;
544
+ if (this.groups().length !== controllerGroups.length) {
545
+ this.groups.set(controllerGroups);
546
+ }
547
+ this.focusedIndex.set(this.controller.focusedIndex);
548
+ this.openDropdown.set(this.controller.openDropdown);
549
+ // Bump version to trigger isActive() re-evaluation without creating new objects
550
+ this.activeVersion.update(v => v + 1);
551
+ }
552
+ getComputedStyleAtCursor(prop) {
553
+ try {
554
+ const { from } = this.editor().state.selection;
555
+ const resolved = this.editor().view.domAtPos(from);
556
+ const el = resolved.node instanceof HTMLElement
557
+ ? resolved.node
558
+ : resolved.node.parentElement;
559
+ if (!el)
560
+ return null;
561
+ // Prefer inline style (explicit mark) over computed style (inherited from heading, etc.)
562
+ return el.style.getPropertyValue(prop)
563
+ || window.getComputedStyle(el).getPropertyValue(prop)
564
+ || null;
565
+ }
566
+ catch {
567
+ return null;
568
+ }
569
+ }
570
+ /** Read only inline style — no computed fallback (used for font-family). */
571
+ getInlineStyleAtCursor(prop) {
572
+ try {
573
+ const { from } = this.editor().state.selection;
574
+ const resolved = this.editor().view.domAtPos(from);
575
+ const el = resolved.node instanceof HTMLElement
576
+ ? resolved.node
577
+ : resolved.node.parentElement;
578
+ if (!el)
579
+ return null;
580
+ return el.style.getPropertyValue(prop) || null;
581
+ }
582
+ catch {
583
+ return null;
584
+ }
585
+ }
586
+ focusCurrentButton() {
587
+ const idx = this.controller?.focusedIndex ?? 0;
588
+ const buttons = this.elRef.nativeElement.querySelectorAll('.dm-toolbar-button');
589
+ buttons[idx]?.focus();
590
+ }
591
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
592
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: DomternalToolbarComponent, isStandalone: true, selector: "domternal-toolbar", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null }, icons: { classPropertyName: "icons", publicName: "icons", isSignal: true, isRequired: false, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "toolbar" }, listeners: { "keydown": "onKeydown($event)" }, properties: { "attr.aria-label": "\"Editor formatting\"" }, classAttribute: "dm-toolbar" }, ngImport: i0, template: `
593
+ @for (group of groups(); track group.name; let gi = $index) {
594
+ @if (gi > 0) {
595
+ <div class="dm-toolbar-separator" role="separator"></div>
596
+ }
597
+ <div class="dm-toolbar-group" role="group" [attr.aria-label]="group.name || 'Tools'">
598
+ @for (item of group.items; track item.name) {
599
+ @if (item.type === 'button') {
600
+ <button
601
+ type="button"
602
+ class="dm-toolbar-button"
603
+ [class.dm-toolbar-button--active]="isActive(item.name)"
604
+ [attr.aria-pressed]="isActive(item.name)"
605
+ [attr.aria-expanded]="getAriaExpanded(asButton(item))"
606
+ [attr.aria-label]="asButton(item).label"
607
+ [title]="getTooltip(asButton(item))"
608
+ [tabindex]="getFlatIndex(item.name) === focusedIndex() ? 0 : -1"
609
+ [disabled]="isDisabled(item.name)"
610
+ [innerHTML]="getCachedIcon(asButton(item).icon)"
611
+ (mousedown)="$event.preventDefault()"
612
+ (click)="onButtonClick(asButton(item), $event)"
613
+ (focus)="onButtonFocus(item.name)"
614
+ ></button>
615
+ }
616
+ @if (item.type === 'dropdown') {
617
+ <div class="dm-toolbar-dropdown-wrapper">
618
+ <button
619
+ type="button"
620
+ class="dm-toolbar-button dm-toolbar-dropdown-trigger"
621
+ [class.dm-toolbar-button--active]="isDropdownActive(asDropdown(item))"
622
+ [attr.aria-expanded]="openDropdown() === asDropdown(item).name"
623
+ [attr.aria-haspopup]="'true'"
624
+ [attr.aria-label]="asDropdown(item).label"
625
+ [title]="asDropdown(item).label"
626
+ [tabindex]="getFlatIndex(item.name) === focusedIndex() ? 0 : -1"
627
+ [disabled]="isDisabled(asDropdown(item).name)"
628
+ [attr.data-dropdown]="asDropdown(item).name"
629
+ [innerHTML]="getDropdownTriggerHtml(asDropdown(item))"
630
+ (mousedown)="$event.preventDefault()"
631
+ (click)="onDropdownToggle(asDropdown(item))"
632
+ (focus)="onButtonFocus(item.name)"
633
+ ></button>
634
+ @if (openDropdown() === asDropdown(item).name) {
635
+ @if (asDropdown(item).layout === 'grid') {
636
+ <div class="dm-toolbar-dropdown-panel dm-color-palette" role="menu"
637
+ [attr.style]="getGridStyle(asDropdown(item))">
638
+ @for (sub of asDropdown(item).items; track sub.name) {
639
+ @if (sub.color) {
640
+ <button
641
+ type="button"
642
+ class="dm-color-swatch"
643
+ [class.dm-color-swatch--active]="isActive(sub.name)"
644
+ role="menuitem"
645
+ [attr.aria-label]="sub.label"
646
+ [title]="sub.label"
647
+ [style.background-color]="sub.color"
648
+ (mousedown)="$event.preventDefault()"
649
+ (click)="onDropdownItemClick(sub)"
650
+ ></button>
651
+ } @else {
652
+ <button
653
+ type="button"
654
+ class="dm-color-palette-reset"
655
+ role="menuitem"
656
+ [attr.aria-label]="sub.label"
657
+ [innerHTML]="getCachedItemContent(sub.icon, sub.label)"
658
+ (mousedown)="$event.preventDefault()"
659
+ (click)="onDropdownItemClick(sub)"
660
+ ></button>
661
+ }
662
+ }
663
+ </div>
664
+ } @else {
665
+ <div class="dm-toolbar-dropdown-panel" role="menu"
666
+ [attr.data-display-mode]="asDropdown(item).displayMode ?? null">
667
+ @for (sub of asDropdown(item).items; track sub.name) {
668
+ <button
669
+ type="button"
670
+ class="dm-toolbar-dropdown-item"
671
+ [class.dm-toolbar-dropdown-item--active]="isActive(sub.name)"
672
+ role="menuitem"
673
+ [attr.aria-label]="sub.label"
674
+ [attr.style]="sub.style ?? null"
675
+ [innerHTML]="getCachedItemContent(sub.icon, sub.label, asDropdown(item).displayMode)"
676
+ (mousedown)="$event.preventDefault()"
677
+ (click)="onDropdownItemClick(sub, $event)"
678
+ ></button>
679
+ }
680
+ </div>
681
+ }
682
+ }
683
+ </div>
684
+ }
685
+ }
686
+ </div>
687
+ }
688
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
689
+ }
690
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalToolbarComponent, decorators: [{
691
+ type: Component,
692
+ args: [{
693
+ selector: 'domternal-toolbar',
694
+ changeDetection: ChangeDetectionStrategy.OnPush,
695
+ encapsulation: ViewEncapsulation.None,
696
+ host: {
697
+ 'class': 'dm-toolbar',
698
+ 'role': 'toolbar',
699
+ '[attr.aria-label]': '"Editor formatting"',
700
+ '(keydown)': 'onKeydown($event)',
701
+ },
702
+ template: `
703
+ @for (group of groups(); track group.name; let gi = $index) {
704
+ @if (gi > 0) {
705
+ <div class="dm-toolbar-separator" role="separator"></div>
706
+ }
707
+ <div class="dm-toolbar-group" role="group" [attr.aria-label]="group.name || 'Tools'">
708
+ @for (item of group.items; track item.name) {
709
+ @if (item.type === 'button') {
710
+ <button
711
+ type="button"
712
+ class="dm-toolbar-button"
713
+ [class.dm-toolbar-button--active]="isActive(item.name)"
714
+ [attr.aria-pressed]="isActive(item.name)"
715
+ [attr.aria-expanded]="getAriaExpanded(asButton(item))"
716
+ [attr.aria-label]="asButton(item).label"
717
+ [title]="getTooltip(asButton(item))"
718
+ [tabindex]="getFlatIndex(item.name) === focusedIndex() ? 0 : -1"
719
+ [disabled]="isDisabled(item.name)"
720
+ [innerHTML]="getCachedIcon(asButton(item).icon)"
721
+ (mousedown)="$event.preventDefault()"
722
+ (click)="onButtonClick(asButton(item), $event)"
723
+ (focus)="onButtonFocus(item.name)"
724
+ ></button>
725
+ }
726
+ @if (item.type === 'dropdown') {
727
+ <div class="dm-toolbar-dropdown-wrapper">
728
+ <button
729
+ type="button"
730
+ class="dm-toolbar-button dm-toolbar-dropdown-trigger"
731
+ [class.dm-toolbar-button--active]="isDropdownActive(asDropdown(item))"
732
+ [attr.aria-expanded]="openDropdown() === asDropdown(item).name"
733
+ [attr.aria-haspopup]="'true'"
734
+ [attr.aria-label]="asDropdown(item).label"
735
+ [title]="asDropdown(item).label"
736
+ [tabindex]="getFlatIndex(item.name) === focusedIndex() ? 0 : -1"
737
+ [disabled]="isDisabled(asDropdown(item).name)"
738
+ [attr.data-dropdown]="asDropdown(item).name"
739
+ [innerHTML]="getDropdownTriggerHtml(asDropdown(item))"
740
+ (mousedown)="$event.preventDefault()"
741
+ (click)="onDropdownToggle(asDropdown(item))"
742
+ (focus)="onButtonFocus(item.name)"
743
+ ></button>
744
+ @if (openDropdown() === asDropdown(item).name) {
745
+ @if (asDropdown(item).layout === 'grid') {
746
+ <div class="dm-toolbar-dropdown-panel dm-color-palette" role="menu"
747
+ [attr.style]="getGridStyle(asDropdown(item))">
748
+ @for (sub of asDropdown(item).items; track sub.name) {
749
+ @if (sub.color) {
750
+ <button
751
+ type="button"
752
+ class="dm-color-swatch"
753
+ [class.dm-color-swatch--active]="isActive(sub.name)"
754
+ role="menuitem"
755
+ [attr.aria-label]="sub.label"
756
+ [title]="sub.label"
757
+ [style.background-color]="sub.color"
758
+ (mousedown)="$event.preventDefault()"
759
+ (click)="onDropdownItemClick(sub)"
760
+ ></button>
761
+ } @else {
762
+ <button
763
+ type="button"
764
+ class="dm-color-palette-reset"
765
+ role="menuitem"
766
+ [attr.aria-label]="sub.label"
767
+ [innerHTML]="getCachedItemContent(sub.icon, sub.label)"
768
+ (mousedown)="$event.preventDefault()"
769
+ (click)="onDropdownItemClick(sub)"
770
+ ></button>
771
+ }
772
+ }
773
+ </div>
774
+ } @else {
775
+ <div class="dm-toolbar-dropdown-panel" role="menu"
776
+ [attr.data-display-mode]="asDropdown(item).displayMode ?? null">
777
+ @for (sub of asDropdown(item).items; track sub.name) {
778
+ <button
779
+ type="button"
780
+ class="dm-toolbar-dropdown-item"
781
+ [class.dm-toolbar-dropdown-item--active]="isActive(sub.name)"
782
+ role="menuitem"
783
+ [attr.aria-label]="sub.label"
784
+ [attr.style]="sub.style ?? null"
785
+ [innerHTML]="getCachedItemContent(sub.icon, sub.label, asDropdown(item).displayMode)"
786
+ (mousedown)="$event.preventDefault()"
787
+ (click)="onDropdownItemClick(sub, $event)"
788
+ ></button>
789
+ }
790
+ </div>
791
+ }
792
+ }
793
+ </div>
794
+ }
795
+ }
796
+ </div>
797
+ }
798
+ `,
799
+ }]
800
+ }], ctorParameters: () => [], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "editor", required: true }] }], icons: [{ type: i0.Input, args: [{ isSignal: true, alias: "icons", required: false }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }] } });
801
+
802
+ /** Check if a resolved position is inside a table cell or header. */
803
+ function isInsideTableCell($pos) {
804
+ for (let d = $pos.depth; d > 0; d--) {
805
+ const name = $pos.node(d).type.name;
806
+ if (name === 'tableCell' || name === 'tableHeader')
807
+ return true;
808
+ }
809
+ return false;
810
+ }
811
+ class DomternalBubbleMenuComponent {
812
+ editor = input.required(...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
813
+ shouldShow = input(...(ngDevMode ? [undefined, { debugName: "shouldShow" }] : /* istanbul ignore next */ []));
814
+ placement = input('top', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
815
+ offset = input(8, ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
816
+ updateDelay = input(0, ...(ngDevMode ? [{ debugName: "updateDelay" }] : /* istanbul ignore next */ []));
817
+ /** Fixed item names (e.g. ['bold', 'italic', 'code']). Omit for auto mode (all format items). */
818
+ items = input(...(ngDevMode ? [undefined, { debugName: "items" }] : /* istanbul ignore next */ []));
819
+ /** Context-aware: map context names to item arrays, `true` for all valid items, or `null` to disable */
820
+ contexts = input(...(ngDevMode ? [undefined, { debugName: "contexts" }] : /* istanbul ignore next */ []));
821
+ /** Internal — updated on transactions. Not meant to be set from outside. */
822
+ resolvedItems = signal([], ...(ngDevMode ? [{ debugName: "resolvedItems" }] : /* istanbul ignore next */ []));
823
+ menuEl = viewChild.required('menuEl');
824
+ pluginKey;
825
+ sanitizer = inject(DomSanitizer);
826
+ ngZone = inject(NgZone);
827
+ activeVersion = signal(0, ...(ngDevMode ? [{ debugName: "activeVersion" }] : /* istanbul ignore next */ []));
828
+ itemMap = new Map();
829
+ activeMap = new Map();
830
+ disabledMap = new Map();
831
+ htmlCache = new Map();
832
+ bubbleDefaults = new Map();
833
+ transactionHandler = null;
834
+ constructor() {
835
+ this.pluginKey = new PluginKey('angularBubbleMenu-' + Math.random().toString(36).slice(2, 8));
836
+ afterNextRender(() => {
837
+ const editor = this.editor();
838
+ const ctxs = this.contexts();
839
+ let shouldShowFn = this.shouldShow();
840
+ if (!shouldShowFn) {
841
+ if (ctxs) {
842
+ shouldShowFn = ({ state }) => {
843
+ const context = this.detectContext(state.selection, ctxs);
844
+ if (!context)
845
+ return false;
846
+ if (context in ctxs) {
847
+ const val = ctxs[context];
848
+ if (val === null)
849
+ return false;
850
+ return val === true || (Array.isArray(val) && val.length > 0);
851
+ }
852
+ return this.bubbleDefaults.has(context);
853
+ };
854
+ }
855
+ else {
856
+ // Auto/items mode: show when any endpoint's parent allows marks
857
+ shouldShowFn = ({ state }) => {
858
+ if (state.selection.empty || state.selection.node)
859
+ return false;
860
+ if (isInsideTableCell(state.selection.$from))
861
+ return false;
862
+ return state.selection.$from.parent.type.spec.marks !== ''
863
+ || state.selection.$to.parent.type.spec.marks !== '';
864
+ };
865
+ }
866
+ }
867
+ const plugin = createBubbleMenuPlugin({
868
+ pluginKey: this.pluginKey,
869
+ editor,
870
+ element: this.menuEl().nativeElement,
871
+ shouldShow: shouldShowFn,
872
+ placement: this.placement(),
873
+ offset: this.offset(),
874
+ updateDelay: this.updateDelay(),
875
+ });
876
+ editor.registerPlugin(plugin);
877
+ this.setupItemTracking(editor);
878
+ });
879
+ }
880
+ ngOnDestroy() {
881
+ const editor = this.editor();
882
+ if (this.transactionHandler) {
883
+ editor.off('transaction', this.transactionHandler);
884
+ }
885
+ if (!editor.isDestroyed) {
886
+ editor.unregisterPlugin(this.pluginKey);
887
+ }
888
+ }
889
+ // === Template helpers ===
890
+ isItemActive(item) {
891
+ this.activeVersion();
892
+ return this.activeMap.get(item.name) ?? false;
893
+ }
894
+ isItemDisabled(item) {
895
+ this.activeVersion();
896
+ return this.disabledMap.get(item.name) ?? false;
897
+ }
898
+ getCachedIcon(name) {
899
+ let cached = this.htmlCache.get(name);
900
+ if (!cached) {
901
+ cached = this.sanitizer.bypassSecurityTrustHtml(defaultIcons[name] ?? '');
902
+ this.htmlCache.set(name, cached);
903
+ }
904
+ return cached;
905
+ }
906
+ executeCommand(item) {
907
+ if (item.emitEvent) {
908
+ // emitEvent is a dynamic string; cast needed to bypass strict EventEmitter<EditorEvents> typing
909
+ this.editor().emit(item.emitEvent, {});
910
+ return;
911
+ }
912
+ ToolbarController.executeItem(this.editor(), item);
913
+ }
914
+ // === Internal ===
915
+ buildItemMap(editor) {
916
+ this.itemMap.clear();
917
+ for (const item of editor.toolbarItems) {
918
+ if (item.type === 'button') {
919
+ this.itemMap.set(item.name, item);
920
+ }
921
+ else if (item.type === 'dropdown') {
922
+ for (const sub of item.items) {
923
+ this.itemMap.set(sub.name, sub);
924
+ }
925
+ }
926
+ }
927
+ }
928
+ resolveNames(names) {
929
+ const result = [];
930
+ let sepIdx = 0;
931
+ for (const name of names) {
932
+ if (name === '|') {
933
+ result.push({ type: 'separator', name: `sep-${sepIdx++}` });
934
+ }
935
+ else {
936
+ const item = this.itemMap.get(name);
937
+ if (item)
938
+ result.push(item);
939
+ }
940
+ }
941
+ return result;
942
+ }
943
+ getFormatItems() {
944
+ return Array.from(this.itemMap.values())
945
+ .filter(item => item.group === 'format')
946
+ .sort((a, b) => (b.priority ?? 100) - (a.priority ?? 100));
947
+ }
948
+ detectContext(selection, ctxs) {
949
+ // CellSelection (duck-type: has $anchorCell) — never show bubble menu
950
+ if ('$anchorCell' in selection)
951
+ return null;
952
+ if (selection.node)
953
+ return selection.node.type.name;
954
+ if (selection.empty)
955
+ return null;
956
+ // TextSelection inside a table cell → 'table' context.
957
+ // Cross-cell TextSelection (drag across cells) → hide bubble menu.
958
+ const fromCell = this.findCellNode(selection.$from);
959
+ if (fromCell) {
960
+ const toCell = this.findCellNode(selection.$to);
961
+ if (toCell && fromCell !== toCell)
962
+ return null;
963
+ return 'table';
964
+ }
965
+ const fromName = selection.$from.parent.type.name;
966
+ if (fromName in ctxs)
967
+ return fromName;
968
+ if ('text' in ctxs && selection.$from.parent.type.spec.marks !== '')
969
+ return 'text';
970
+ // Cross-block: also check $to so bold/italic is offered when any endpoint allows marks
971
+ const toName = selection.$to.parent.type.name;
972
+ if (toName in ctxs)
973
+ return toName;
974
+ if ('text' in ctxs && selection.$to.parent.type.spec.marks !== '')
975
+ return 'text';
976
+ return null;
977
+ }
978
+ findCellNode(pos) {
979
+ for (let d = pos.depth; d > 0; d--) {
980
+ const node = pos.node(d);
981
+ if (node.type.name === 'tableCell' || node.type.name === 'tableHeader') {
982
+ return node;
983
+ }
984
+ }
985
+ return null;
986
+ }
987
+ /** Filters out mark items that the context node type doesn't allow (e.g. bold on codeBlock) */
988
+ filterBySchema(editor, contextName, items) {
989
+ if (contextName === 'text' || contextName === 'table')
990
+ return items;
991
+ const schema = editor.state.schema;
992
+ if (!schema)
993
+ return items;
994
+ const nodeType = schema.nodes[contextName];
995
+ if (!nodeType)
996
+ return items;
997
+ return items.filter(item => {
998
+ const markName = typeof item.isActive === 'string' ? item.isActive : null;
999
+ if (!markName)
1000
+ return true;
1001
+ const markType = schema.marks?.[markName];
1002
+ if (!markType)
1003
+ return true;
1004
+ return nodeType.allowsMarkType(markType);
1005
+ });
1006
+ }
1007
+ buildBubbleDefaults(editor) {
1008
+ this.bubbleDefaults.clear();
1009
+ const byCtx = new Map();
1010
+ const addItem = (btn) => {
1011
+ const ctx = btn['bubbleMenu'];
1012
+ if (!ctx)
1013
+ return;
1014
+ let arr = byCtx.get(ctx);
1015
+ if (!arr) {
1016
+ arr = [];
1017
+ byCtx.set(ctx, arr);
1018
+ }
1019
+ arr.push(btn);
1020
+ };
1021
+ for (const item of editor.toolbarItems) {
1022
+ if (item.type === 'button')
1023
+ addItem(item);
1024
+ else if (item.type === 'dropdown') {
1025
+ for (const sub of item.items)
1026
+ addItem(sub);
1027
+ }
1028
+ }
1029
+ for (const [ctx, items] of byCtx) {
1030
+ items.sort((a, b) => (b.priority ?? 100) - (a.priority ?? 100));
1031
+ const result = [];
1032
+ let lastGroup;
1033
+ let sepIdx = 0;
1034
+ for (const item of items) {
1035
+ if (lastGroup !== undefined && item.group !== lastGroup) {
1036
+ result.push({ type: 'separator', name: `bsep-${sepIdx++}` });
1037
+ }
1038
+ result.push(item);
1039
+ lastGroup = item.group;
1040
+ }
1041
+ this.bubbleDefaults.set(ctx, result);
1042
+ }
1043
+ }
1044
+ setupItemTracking(editor) {
1045
+ this.buildItemMap(editor);
1046
+ this.buildBubbleDefaults(editor);
1047
+ if (this.contexts()) {
1048
+ this.updateContextItems(editor);
1049
+ }
1050
+ else if (this.items()) {
1051
+ this.resolvedItems.set(this.resolveNames(this.items()));
1052
+ }
1053
+ else {
1054
+ this.resolvedItems.set(this.resolveNames(['bold', 'italic', 'underline']));
1055
+ }
1056
+ this.transactionHandler = () => {
1057
+ this.ngZone.run(() => {
1058
+ if (this.contexts()) {
1059
+ this.updateContextItems(editor);
1060
+ }
1061
+ this.updateStates(editor);
1062
+ this.activeVersion.update(v => v + 1);
1063
+ });
1064
+ };
1065
+ editor.on('transaction', this.transactionHandler);
1066
+ this.updateStates(editor);
1067
+ }
1068
+ updateContextItems(editor) {
1069
+ const ctxs = this.contexts();
1070
+ const ctx = this.detectContext(editor.state.selection, ctxs);
1071
+ if (!ctx) {
1072
+ this.resolvedItems.set([]);
1073
+ return;
1074
+ }
1075
+ if (ctx in ctxs) {
1076
+ const val = ctxs[ctx];
1077
+ if (val === null || (Array.isArray(val) && val.length === 0)) {
1078
+ this.resolvedItems.set([]);
1079
+ return;
1080
+ }
1081
+ if (val === true) {
1082
+ this.resolvedItems.set(this.filterBySchema(editor, ctx, this.getFormatItems()));
1083
+ }
1084
+ else if (Array.isArray(val)) {
1085
+ const resolved = this.resolveNames(val);
1086
+ const buttons = resolved.filter((i) => i.type !== 'separator');
1087
+ const filtered = new Set(this.filterBySchema(editor, ctx, buttons).map(b => b.name));
1088
+ this.resolvedItems.set(resolved.filter(i => i.type === 'separator' || filtered.has(i.name)));
1089
+ }
1090
+ }
1091
+ else {
1092
+ this.resolvedItems.set(this.bubbleDefaults.get(ctx) ?? []);
1093
+ }
1094
+ }
1095
+ updateStates(editor) {
1096
+ let canProxy = null;
1097
+ try {
1098
+ canProxy = editor.can();
1099
+ }
1100
+ catch { }
1101
+ for (const item of this.resolvedItems()) {
1102
+ if (item.type === 'separator')
1103
+ continue;
1104
+ this.activeMap.set(item.name, ToolbarController.resolveActive(editor, item));
1105
+ try {
1106
+ const canCmd = canProxy?.[item.command];
1107
+ this.disabledMap.set(item.name, canCmd
1108
+ ? !(item.commandArgs?.length ? canCmd(...item.commandArgs) : canCmd())
1109
+ : false);
1110
+ }
1111
+ catch {
1112
+ this.disabledMap.set(item.name, false);
1113
+ }
1114
+ }
1115
+ }
1116
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1117
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: DomternalBubbleMenuComponent, isStandalone: true, selector: "domternal-bubble-menu", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null }, shouldShow: { classPropertyName: "shouldShow", publicName: "shouldShow", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, updateDelay: { classPropertyName: "updateDelay", publicName: "updateDelay", isSignal: true, isRequired: false, transformFunction: null }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, contexts: { classPropertyName: "contexts", publicName: "contexts", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "menuEl", first: true, predicate: ["menuEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
1118
+ <div #menuEl class="dm-bubble-menu">
1119
+ @for (item of resolvedItems(); track item.name) {
1120
+ @if (item.type === 'separator') {
1121
+ <span class="dm-toolbar-separator"></span>
1122
+ } @else {
1123
+ <button type="button" class="dm-toolbar-button"
1124
+ [class.dm-toolbar-button--active]="isItemActive(item)"
1125
+ [disabled]="isItemDisabled(item)"
1126
+ [title]="item.label"
1127
+ [innerHTML]="getCachedIcon(item.icon)"
1128
+ (mousedown)="$event.preventDefault()"
1129
+ (click)="executeCommand(item)"></button>
1130
+ }
1131
+ }
1132
+ <ng-content />
1133
+ </div>
1134
+ `, isInline: true, styles: [":host{display:contents}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1135
+ }
1136
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalBubbleMenuComponent, decorators: [{
1137
+ type: Component,
1138
+ args: [{ selector: 'domternal-bubble-menu', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
1139
+ <div #menuEl class="dm-bubble-menu">
1140
+ @for (item of resolvedItems(); track item.name) {
1141
+ @if (item.type === 'separator') {
1142
+ <span class="dm-toolbar-separator"></span>
1143
+ } @else {
1144
+ <button type="button" class="dm-toolbar-button"
1145
+ [class.dm-toolbar-button--active]="isItemActive(item)"
1146
+ [disabled]="isItemDisabled(item)"
1147
+ [title]="item.label"
1148
+ [innerHTML]="getCachedIcon(item.icon)"
1149
+ (mousedown)="$event.preventDefault()"
1150
+ (click)="executeCommand(item)"></button>
1151
+ }
1152
+ }
1153
+ <ng-content />
1154
+ </div>
1155
+ `, styles: [":host{display:contents}\n"] }]
1156
+ }], ctorParameters: () => [], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "editor", required: true }] }], shouldShow: [{ type: i0.Input, args: [{ isSignal: true, alias: "shouldShow", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], updateDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "updateDelay", required: false }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], contexts: [{ type: i0.Input, args: [{ isSignal: true, alias: "contexts", required: false }] }], menuEl: [{ type: i0.ViewChild, args: ['menuEl', { isSignal: true }] }] } });
1157
+
1158
+ class DomternalFloatingMenuComponent {
1159
+ editor = input.required(...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
1160
+ shouldShow = input(...(ngDevMode ? [undefined, { debugName: "shouldShow" }] : /* istanbul ignore next */ []));
1161
+ offset = input(0, ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
1162
+ menuEl = viewChild.required('menuEl');
1163
+ pluginKey;
1164
+ constructor() {
1165
+ // Unique key per instance — multiple floating menus on same page
1166
+ this.pluginKey = new PluginKey('angularFloatingMenu-' + Math.random().toString(36).slice(2, 8));
1167
+ afterNextRender(() => {
1168
+ const shouldShow = this.shouldShow();
1169
+ const plugin = createFloatingMenuPlugin({
1170
+ pluginKey: this.pluginKey,
1171
+ editor: this.editor(),
1172
+ element: this.menuEl().nativeElement,
1173
+ ...(shouldShow && { shouldShow }),
1174
+ offset: this.offset(),
1175
+ });
1176
+ this.editor().registerPlugin(plugin);
1177
+ });
1178
+ }
1179
+ ngOnDestroy() {
1180
+ const editor = this.editor();
1181
+ if (!editor.isDestroyed) {
1182
+ editor.unregisterPlugin(this.pluginKey);
1183
+ }
1184
+ }
1185
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalFloatingMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1186
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.5", type: DomternalFloatingMenuComponent, isStandalone: true, selector: "domternal-floating-menu", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null }, shouldShow: { classPropertyName: "shouldShow", publicName: "shouldShow", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "menuEl", first: true, predicate: ["menuEl"], descendants: true, isSignal: true }], ngImport: i0, template: '<div #menuEl class="dm-floating-menu"><ng-content /></div>', isInline: true, styles: [":host{display:contents}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1187
+ }
1188
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalFloatingMenuComponent, decorators: [{
1189
+ type: Component,
1190
+ args: [{ selector: 'domternal-floating-menu', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '<div #menuEl class="dm-floating-menu"><ng-content /></div>', styles: [":host{display:contents}\n"] }]
1191
+ }], ctorParameters: () => [], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "editor", required: true }] }], shouldShow: [{ type: i0.Input, args: [{ isSignal: true, alias: "shouldShow", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], menuEl: [{ type: i0.ViewChild, args: ['menuEl', { isSignal: true }] }] } });
1192
+
1193
+ const CATEGORY_ICONS = {
1194
+ 'Smileys & Emotion': '\u{1F600}',
1195
+ 'People & Body': '\u{1F44B}',
1196
+ 'Animals & Nature': '\u{1F431}',
1197
+ 'Food & Drink': '\u{1F355}',
1198
+ 'Travel & Places': '\u{1F697}',
1199
+ 'Activities': '\u{26BD}',
1200
+ 'Objects': '\u{1F4A1}',
1201
+ 'Symbols': '\u{1F523}',
1202
+ 'Flags': '\u{1F3C1}',
1203
+ };
1204
+ class DomternalEmojiPickerComponent {
1205
+ editor = input.required(...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
1206
+ emojis = input.required(...(ngDevMode ? [{ debugName: "emojis" }] : /* istanbul ignore next */ []));
1207
+ isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
1208
+ searchQuery = signal('', ...(ngDevMode ? [{ debugName: "searchQuery" }] : /* istanbul ignore next */ []));
1209
+ activeCategory = signal('', ...(ngDevMode ? [{ debugName: "activeCategory" }] : /* istanbul ignore next */ []));
1210
+ anchorEl = null;
1211
+ ngZone = inject(NgZone);
1212
+ elRef = inject(ElementRef);
1213
+ clickOutsideHandler = null;
1214
+ keydownHandler = null;
1215
+ eventHandler = null;
1216
+ cleanupFloating = null;
1217
+ categories = computed(() => {
1218
+ const map = new Map();
1219
+ for (const item of this.emojis()) {
1220
+ let list = map.get(item.group);
1221
+ if (!list) {
1222
+ list = [];
1223
+ map.set(item.group, list);
1224
+ }
1225
+ list.push(item);
1226
+ }
1227
+ return map;
1228
+ }, ...(ngDevMode ? [{ debugName: "categories" }] : /* istanbul ignore next */ []));
1229
+ categoryNames = computed(() => [...this.categories().keys()], ...(ngDevMode ? [{ debugName: "categoryNames" }] : /* istanbul ignore next */ []));
1230
+ filteredEmojis = computed(() => {
1231
+ const query = this.searchQuery().toLowerCase();
1232
+ if (!query)
1233
+ return [];
1234
+ const storage = this.getEmojiStorage();
1235
+ const searchFn = storage?.['searchEmoji'];
1236
+ if (searchFn) {
1237
+ return searchFn(query);
1238
+ }
1239
+ return this.emojis().filter((item) => item.name.includes(query) ||
1240
+ item.group.toLowerCase().includes(query));
1241
+ }, ...(ngDevMode ? [{ debugName: "filteredEmojis" }] : /* istanbul ignore next */ []));
1242
+ frequentlyUsed = computed(() => {
1243
+ // Re-evaluate when panel opens (isOpen changes)
1244
+ this.isOpen();
1245
+ const storage = this.getEmojiStorage();
1246
+ const getFreq = storage?.['getFrequentlyUsed'];
1247
+ if (!getFreq)
1248
+ return [];
1249
+ const names = getFreq();
1250
+ if (!names.length)
1251
+ return [];
1252
+ const nameMap = storage['_nameMap'];
1253
+ if (!nameMap)
1254
+ return [];
1255
+ return names.slice(0, 16).map((n) => nameMap.get(n)).filter(Boolean);
1256
+ }, ...(ngDevMode ? [{ debugName: "frequentlyUsed" }] : /* istanbul ignore next */ []));
1257
+ constructor() {
1258
+ effect(() => {
1259
+ const editor = this.editor();
1260
+ untracked(() => this.setupEventListener(editor));
1261
+ });
1262
+ }
1263
+ ngOnDestroy() {
1264
+ this.cleanup();
1265
+ }
1266
+ getCategory(cat) {
1267
+ return this.categories().get(cat) ?? [];
1268
+ }
1269
+ categoryIcon(cat) {
1270
+ return CATEGORY_ICONS[cat] ?? cat.charAt(0);
1271
+ }
1272
+ formatName(name) {
1273
+ return name.replace(/_/g, ' ');
1274
+ }
1275
+ onSearch(event) {
1276
+ const input = event.target;
1277
+ this.searchQuery.set(input.value);
1278
+ }
1279
+ selectEmoji(item) {
1280
+ const editor = this.editor();
1281
+ const cmd = editor.commands;
1282
+ if (cmd['insertEmoji']) {
1283
+ cmd['insertEmoji'](item.name);
1284
+ }
1285
+ this.close();
1286
+ }
1287
+ scrollToCategory(cat) {
1288
+ this.searchQuery.set('');
1289
+ this.activeCategory.set(cat);
1290
+ // Wait for search to clear and DOM to update
1291
+ requestAnimationFrame(() => {
1292
+ const grid = this.elRef.nativeElement.querySelector('.dm-emoji-picker-grid');
1293
+ if (!grid)
1294
+ return;
1295
+ const label = grid.querySelector(`[data-category="${cat}"]`);
1296
+ if (label) {
1297
+ // Use manual scrollTop instead of scrollIntoView to avoid scrolling the page
1298
+ grid.scrollTo({ top: label.offsetTop - grid.offsetTop, behavior: 'smooth' });
1299
+ }
1300
+ });
1301
+ }
1302
+ onGridScroll() {
1303
+ const grid = this.elRef.nativeElement.querySelector('.dm-emoji-picker-grid');
1304
+ if (!grid || this.searchQuery())
1305
+ return;
1306
+ const labels = Array.from(grid.querySelectorAll('.dm-emoji-picker-category-label[data-category]'));
1307
+ let currentCat = '';
1308
+ for (const label of labels) {
1309
+ if (label.offsetTop - grid.offsetTop <= grid.scrollTop + 20) {
1310
+ currentCat = label.getAttribute('data-category') ?? '';
1311
+ }
1312
+ }
1313
+ if (currentCat && currentCat !== this.activeCategory()) {
1314
+ this.activeCategory.set(currentCat);
1315
+ }
1316
+ }
1317
+ close() {
1318
+ this.cleanupFloating?.();
1319
+ this.cleanupFloating = null;
1320
+ this.isOpen.set(false);
1321
+ this.setStorageOpen(false);
1322
+ this.searchQuery.set('');
1323
+ this.anchorEl = null;
1324
+ this.removeGlobalListeners();
1325
+ this.editor().view.focus();
1326
+ }
1327
+ setupEventListener(editor) {
1328
+ this.cleanup();
1329
+ this.eventHandler = (...args) => {
1330
+ const data = args[0];
1331
+ this.ngZone.run(() => {
1332
+ if (this.isOpen()) {
1333
+ this.close();
1334
+ return;
1335
+ }
1336
+ this.anchorEl = data?.anchorElement ?? null;
1337
+ this.isOpen.set(true);
1338
+ this.setStorageOpen(true);
1339
+ this.searchQuery.set('');
1340
+ // Set initial active category
1341
+ const names = this.categoryNames();
1342
+ if (names.length > 0 && names[0]) {
1343
+ this.activeCategory.set(names[0]);
1344
+ }
1345
+ this.addGlobalListeners();
1346
+ // Position panel and focus search input after render
1347
+ requestAnimationFrame(() => {
1348
+ const panel = this.elRef.nativeElement.querySelector('.dm-emoji-picker');
1349
+ if (panel && this.anchorEl) {
1350
+ this.cleanupFloating?.();
1351
+ this.cleanupFloating = positionFloatingOnce(this.anchorEl, panel, {
1352
+ placement: 'bottom',
1353
+ offsetValue: 4,
1354
+ });
1355
+ }
1356
+ const input = this.elRef.nativeElement.querySelector('.dm-emoji-picker-search input');
1357
+ input?.focus();
1358
+ });
1359
+ });
1360
+ };
1361
+ editor.on('insertEmoji', this.eventHandler);
1362
+ }
1363
+ addGlobalListeners() {
1364
+ this.clickOutsideHandler = (e) => {
1365
+ const target = e.target;
1366
+ if (this.isOpen() &&
1367
+ !this.elRef.nativeElement.contains(target) &&
1368
+ target !== this.anchorEl &&
1369
+ !this.anchorEl?.contains(target)) {
1370
+ this.ngZone.run(() => this.close());
1371
+ }
1372
+ };
1373
+ document.addEventListener('mousedown', this.clickOutsideHandler);
1374
+ this.keydownHandler = (e) => {
1375
+ if (e.key === 'Escape' && this.isOpen()) {
1376
+ e.preventDefault();
1377
+ this.ngZone.run(() => this.close());
1378
+ }
1379
+ };
1380
+ document.addEventListener('keydown', this.keydownHandler);
1381
+ }
1382
+ removeGlobalListeners() {
1383
+ if (this.clickOutsideHandler) {
1384
+ document.removeEventListener('mousedown', this.clickOutsideHandler);
1385
+ this.clickOutsideHandler = null;
1386
+ }
1387
+ if (this.keydownHandler) {
1388
+ document.removeEventListener('keydown', this.keydownHandler);
1389
+ this.keydownHandler = null;
1390
+ }
1391
+ }
1392
+ setStorageOpen(open) {
1393
+ const storage = this.getEmojiStorage();
1394
+ if (storage)
1395
+ storage['isOpen'] = open;
1396
+ // Dispatch a no-op transaction so the toolbar's transaction handler
1397
+ // re-evaluates isActiveFn and shows the button as active/inactive.
1398
+ const editor = this.editor();
1399
+ editor.view.dispatch(editor.view.state.tr);
1400
+ }
1401
+ getEmojiStorage() {
1402
+ const storage = this.editor().storage;
1403
+ return storage['emoji'] ?? null;
1404
+ }
1405
+ cleanup() {
1406
+ this.removeGlobalListeners();
1407
+ if (this.eventHandler) {
1408
+ const editor = this.editor();
1409
+ editor.off('insertEmoji', this.eventHandler);
1410
+ this.eventHandler = null;
1411
+ }
1412
+ }
1413
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalEmojiPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1414
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: DomternalEmojiPickerComponent, isStandalone: true, selector: "domternal-emoji-picker", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null }, emojis: { classPropertyName: "emojis", publicName: "emojis", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "dm-emoji-picker-host" }, ngImport: i0, template: `
1415
+ @if (isOpen()) {
1416
+ <div class="dm-emoji-picker">
1417
+ <div class="dm-emoji-picker-search">
1418
+ <input
1419
+ #searchInput
1420
+ type="text"
1421
+ placeholder="Search emoji..."
1422
+ [value]="searchQuery()"
1423
+ (input)="onSearch($event)"
1424
+ (keydown.escape)="close()"
1425
+ />
1426
+ </div>
1427
+
1428
+ <div class="dm-emoji-picker-tabs" role="tablist">
1429
+ @for (cat of categoryNames(); track cat) {
1430
+ <button
1431
+ type="button"
1432
+ class="dm-emoji-picker-tab"
1433
+ [class.dm-emoji-picker-tab--active]="activeCategory() === cat"
1434
+ [title]="cat"
1435
+ (mousedown)="$event.preventDefault()"
1436
+ (click)="scrollToCategory(cat)"
1437
+ >{{ categoryIcon(cat) }}</button>
1438
+ }
1439
+ </div>
1440
+
1441
+ <div class="dm-emoji-picker-grid" #grid (scroll)="onGridScroll()">
1442
+ @if (searchQuery()) {
1443
+ @for (item of filteredEmojis(); track item.name) {
1444
+ <button
1445
+ type="button"
1446
+ class="dm-emoji-swatch"
1447
+ [title]="formatName(item.name)"
1448
+ (mousedown)="$event.preventDefault()"
1449
+ (click)="selectEmoji(item)"
1450
+ >{{ item.emoji }}</button>
1451
+ }
1452
+ @empty {
1453
+ <div class="dm-emoji-picker-empty">No emoji found</div>
1454
+ }
1455
+ } @else {
1456
+ @if (frequentlyUsed().length) {
1457
+ <div class="dm-emoji-picker-category-label">Frequently Used</div>
1458
+ @for (item of frequentlyUsed(); track item.name) {
1459
+ <button
1460
+ type="button"
1461
+ class="dm-emoji-swatch"
1462
+ [title]="formatName(item.name)"
1463
+ (mousedown)="$event.preventDefault()"
1464
+ (click)="selectEmoji(item)"
1465
+ >{{ item.emoji }}</button>
1466
+ }
1467
+ }
1468
+ @for (cat of categoryNames(); track cat) {
1469
+ <div class="dm-emoji-picker-category-label" [attr.data-category]="cat">{{ cat }}</div>
1470
+ @for (item of getCategory(cat); track item.name) {
1471
+ <button
1472
+ type="button"
1473
+ class="dm-emoji-swatch"
1474
+ [title]="formatName(item.name)"
1475
+ (mousedown)="$event.preventDefault()"
1476
+ (click)="selectEmoji(item)"
1477
+ >{{ item.emoji }}</button>
1478
+ }
1479
+ }
1480
+ }
1481
+ </div>
1482
+ </div>
1483
+ }
1484
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1485
+ }
1486
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalEmojiPickerComponent, decorators: [{
1487
+ type: Component,
1488
+ args: [{
1489
+ selector: 'domternal-emoji-picker',
1490
+ changeDetection: ChangeDetectionStrategy.OnPush,
1491
+ encapsulation: ViewEncapsulation.None,
1492
+ host: { 'class': 'dm-emoji-picker-host' },
1493
+ template: `
1494
+ @if (isOpen()) {
1495
+ <div class="dm-emoji-picker">
1496
+ <div class="dm-emoji-picker-search">
1497
+ <input
1498
+ #searchInput
1499
+ type="text"
1500
+ placeholder="Search emoji..."
1501
+ [value]="searchQuery()"
1502
+ (input)="onSearch($event)"
1503
+ (keydown.escape)="close()"
1504
+ />
1505
+ </div>
1506
+
1507
+ <div class="dm-emoji-picker-tabs" role="tablist">
1508
+ @for (cat of categoryNames(); track cat) {
1509
+ <button
1510
+ type="button"
1511
+ class="dm-emoji-picker-tab"
1512
+ [class.dm-emoji-picker-tab--active]="activeCategory() === cat"
1513
+ [title]="cat"
1514
+ (mousedown)="$event.preventDefault()"
1515
+ (click)="scrollToCategory(cat)"
1516
+ >{{ categoryIcon(cat) }}</button>
1517
+ }
1518
+ </div>
1519
+
1520
+ <div class="dm-emoji-picker-grid" #grid (scroll)="onGridScroll()">
1521
+ @if (searchQuery()) {
1522
+ @for (item of filteredEmojis(); track item.name) {
1523
+ <button
1524
+ type="button"
1525
+ class="dm-emoji-swatch"
1526
+ [title]="formatName(item.name)"
1527
+ (mousedown)="$event.preventDefault()"
1528
+ (click)="selectEmoji(item)"
1529
+ >{{ item.emoji }}</button>
1530
+ }
1531
+ @empty {
1532
+ <div class="dm-emoji-picker-empty">No emoji found</div>
1533
+ }
1534
+ } @else {
1535
+ @if (frequentlyUsed().length) {
1536
+ <div class="dm-emoji-picker-category-label">Frequently Used</div>
1537
+ @for (item of frequentlyUsed(); track item.name) {
1538
+ <button
1539
+ type="button"
1540
+ class="dm-emoji-swatch"
1541
+ [title]="formatName(item.name)"
1542
+ (mousedown)="$event.preventDefault()"
1543
+ (click)="selectEmoji(item)"
1544
+ >{{ item.emoji }}</button>
1545
+ }
1546
+ }
1547
+ @for (cat of categoryNames(); track cat) {
1548
+ <div class="dm-emoji-picker-category-label" [attr.data-category]="cat">{{ cat }}</div>
1549
+ @for (item of getCategory(cat); track item.name) {
1550
+ <button
1551
+ type="button"
1552
+ class="dm-emoji-swatch"
1553
+ [title]="formatName(item.name)"
1554
+ (mousedown)="$event.preventDefault()"
1555
+ (click)="selectEmoji(item)"
1556
+ >{{ item.emoji }}</button>
1557
+ }
1558
+ }
1559
+ }
1560
+ </div>
1561
+ </div>
1562
+ }
1563
+ `,
1564
+ }]
1565
+ }], ctorParameters: () => [], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "editor", required: true }] }], emojis: [{ type: i0.Input, args: [{ isSignal: true, alias: "emojis", required: true }] }] } });
1566
+
1567
+ /**
1568
+ * Generated bundle index. Do not edit.
1569
+ */
1570
+
1571
+ export { DEFAULT_EXTENSIONS, DomternalBubbleMenuComponent, DomternalEditorComponent, DomternalEmojiPickerComponent, DomternalFloatingMenuComponent, DomternalToolbarComponent };
1572
+ //# sourceMappingURL=domternal-angular.mjs.map