@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.
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +71 -0
- package/dist/fesm2022/domternal-angular.mjs +1572 -0
- package/dist/fesm2022/domternal-angular.mjs.map +1 -0
- package/dist/types/domternal-angular.d.ts +235 -0
- package/package.json +57 -0
|
@@ -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
|