@domternal/angular 0.6.2 → 0.7.1
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/README.md +12 -10
- package/dist/README.md +12 -10
- package/dist/fesm2022/domternal-angular.mjs +1190 -84
- package/dist/fesm2022/domternal-angular.mjs.map +1 -1
- package/dist/types/domternal-angular.d.ts +164 -10
- package/package.json +5 -3
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { viewChild, input, output, signal, inject, NgZone, afterNextRender, effect, untracked, forwardRef, ViewEncapsulation, ChangeDetectionStrategy, Component, ElementRef, computed } from '@angular/core';
|
|
3
3
|
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
4
|
-
import { Document, Paragraph, Text, BaseKeymap, History, Editor, positionFloatingOnce, defaultIcons, ToolbarController, PluginKey, createBubbleMenuPlugin,
|
|
4
|
+
import { Document, Paragraph, Text, BaseKeymap, History, Editor, positionFloatingOnce, defaultIcons, ToolbarController, defaultBubbleContexts, PluginKey, createBubbleMenuPlugin, FloatingMenuController, positionFloating } from '@domternal/core';
|
|
5
5
|
export { Editor } from '@domternal/core';
|
|
6
6
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
7
|
+
import { createFloatingMenuPlugin } from '@domternal/extension-block-menu';
|
|
7
8
|
|
|
8
9
|
const DEFAULT_EXTENSIONS = [Document, Paragraph, Text, BaseKeymap, History];
|
|
9
10
|
class DomternalEditorComponent {
|
|
@@ -53,8 +54,9 @@ class DomternalEditorComponent {
|
|
|
53
54
|
const editable = this.editable();
|
|
54
55
|
if (!this._editor || this._editor.isDestroyed)
|
|
55
56
|
return;
|
|
57
|
+
const ed = this._editor;
|
|
56
58
|
untracked(() => {
|
|
57
|
-
|
|
59
|
+
ed.setEditable(editable);
|
|
58
60
|
this._isEditable.set(editable);
|
|
59
61
|
});
|
|
60
62
|
});
|
|
@@ -64,15 +66,16 @@ class DomternalEditorComponent {
|
|
|
64
66
|
const format = this.outputFormat();
|
|
65
67
|
if (!this._editor || this._editor.isDestroyed)
|
|
66
68
|
return;
|
|
69
|
+
const ed = this._editor;
|
|
67
70
|
untracked(() => {
|
|
68
71
|
const current = format === 'html'
|
|
69
|
-
?
|
|
70
|
-
: JSON.stringify(
|
|
72
|
+
? ed.getHTML()
|
|
73
|
+
: JSON.stringify(ed.getJSON());
|
|
71
74
|
const incoming = format === 'html'
|
|
72
75
|
? content
|
|
73
76
|
: JSON.stringify(content);
|
|
74
77
|
if (incoming !== current) {
|
|
75
|
-
|
|
78
|
+
ed.setContent(content, false);
|
|
76
79
|
}
|
|
77
80
|
});
|
|
78
81
|
});
|
|
@@ -148,9 +151,10 @@ class DomternalEditorComponent {
|
|
|
148
151
|
this._htmlContent.set(this._editor.getHTML());
|
|
149
152
|
this._jsonContent.set(this._editor.getJSON());
|
|
150
153
|
this._isEmpty.set(this._editor.isEmpty);
|
|
151
|
-
this._editor
|
|
154
|
+
const editor = this._editor;
|
|
155
|
+
editor.on('transaction', ({ transaction }) => {
|
|
152
156
|
this.ngZone.run(() => {
|
|
153
|
-
const ed =
|
|
157
|
+
const ed = editor;
|
|
154
158
|
if (transaction.docChanged) {
|
|
155
159
|
const html = ed.getHTML();
|
|
156
160
|
this._htmlContent.set(html);
|
|
@@ -165,22 +169,22 @@ class DomternalEditorComponent {
|
|
|
165
169
|
}
|
|
166
170
|
});
|
|
167
171
|
});
|
|
168
|
-
|
|
172
|
+
editor.on('focus', ({ event }) => {
|
|
169
173
|
this.ngZone.run(() => {
|
|
170
174
|
this._isFocused.set(true);
|
|
171
|
-
this.focusChanged.emit({ editor
|
|
175
|
+
this.focusChanged.emit({ editor, event });
|
|
172
176
|
});
|
|
173
177
|
});
|
|
174
|
-
|
|
178
|
+
editor.on('blur', ({ event }) => {
|
|
175
179
|
this.ngZone.run(() => {
|
|
176
180
|
this._isFocused.set(false);
|
|
177
|
-
this.blurChanged.emit({ editor
|
|
181
|
+
this.blurChanged.emit({ editor, event });
|
|
178
182
|
this.onTouched();
|
|
179
183
|
});
|
|
180
184
|
});
|
|
181
185
|
// Emit editor created
|
|
182
186
|
this.ngZone.run(() => {
|
|
183
|
-
this.editorCreated.emit(
|
|
187
|
+
this.editorCreated.emit(editor);
|
|
184
188
|
});
|
|
185
189
|
}
|
|
186
190
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
@@ -222,13 +226,13 @@ class DomternalToolbarComponent {
|
|
|
222
226
|
ngZone = inject(NgZone);
|
|
223
227
|
elRef = inject(ElementRef);
|
|
224
228
|
sanitizer = inject(DomSanitizer);
|
|
225
|
-
/** SafeHtml cache
|
|
229
|
+
/** SafeHtml cache - same reference returned for same key, prevents DOM churn */
|
|
226
230
|
htmlCache = new Map();
|
|
227
231
|
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
232
|
constructor() {
|
|
229
233
|
effect(() => {
|
|
230
234
|
const editor = this.editor();
|
|
231
|
-
untracked(() => this.setupController(editor));
|
|
235
|
+
untracked(() => { this.setupController(editor); });
|
|
232
236
|
});
|
|
233
237
|
}
|
|
234
238
|
ngOnDestroy() {
|
|
@@ -269,7 +273,7 @@ class DomternalToolbarComponent {
|
|
|
269
273
|
}
|
|
270
274
|
return cached;
|
|
271
275
|
}
|
|
272
|
-
// Non-grid dropdown
|
|
276
|
+
// Non-grid dropdown - show active sub-item's label as text
|
|
273
277
|
if (dropdown.dynamicLabel) {
|
|
274
278
|
if (activeItem)
|
|
275
279
|
return this.getCachedTriggerLabel(activeItem.label);
|
|
@@ -281,7 +285,7 @@ class DomternalToolbarComponent {
|
|
|
281
285
|
computed = this.getInlineStyleAtCursor(dropdown.computedStyleProperty);
|
|
282
286
|
if (computed) {
|
|
283
287
|
const first = computed.split(',')[0]?.replace(/['"]+/g, '').trim();
|
|
284
|
-
computed = first
|
|
288
|
+
computed = first ?? null;
|
|
285
289
|
}
|
|
286
290
|
}
|
|
287
291
|
else {
|
|
@@ -300,7 +304,7 @@ class DomternalToolbarComponent {
|
|
|
300
304
|
}
|
|
301
305
|
/**
|
|
302
306
|
* Returns 'true' when an emitEvent button's panel is open, null otherwise.
|
|
303
|
-
* Maps to [attr.aria-expanded]
|
|
307
|
+
* Maps to [attr.aria-expanded] - null removes the attribute entirely.
|
|
304
308
|
*/
|
|
305
309
|
getAriaExpanded(item) {
|
|
306
310
|
if (!item.emitEvent)
|
|
@@ -408,7 +412,7 @@ class DomternalToolbarComponent {
|
|
|
408
412
|
// Always refocus editor after executing a command via toolbar button.
|
|
409
413
|
// Mouse clicks already keep focus via mousedown.preventDefault();
|
|
410
414
|
// keyboard activations (Enter/Space) need explicit refocus.
|
|
411
|
-
requestAnimationFrame(() => this.editor().view.focus());
|
|
415
|
+
requestAnimationFrame(() => { this.editor().view.focus(); });
|
|
412
416
|
}
|
|
413
417
|
onDropdownToggle(dropdown) {
|
|
414
418
|
this.cleanupFloating?.();
|
|
@@ -448,7 +452,7 @@ class DomternalToolbarComponent {
|
|
|
448
452
|
this.controller?.executeCommand(item);
|
|
449
453
|
}
|
|
450
454
|
// Refocus editor so ::selection highlight stays visible
|
|
451
|
-
requestAnimationFrame(() => this.editor().view.focus());
|
|
455
|
+
requestAnimationFrame(() => { this.editor().view.focus(); });
|
|
452
456
|
}
|
|
453
457
|
onButtonFocus(name) {
|
|
454
458
|
const index = this.controller?.getFlatIndex(name) ?? -1;
|
|
@@ -489,7 +493,7 @@ class DomternalToolbarComponent {
|
|
|
489
493
|
const btn = document.activeElement;
|
|
490
494
|
if (btn?.getAttribute('aria-haspopup') && btn.closest('.dm-toolbar')) {
|
|
491
495
|
btn.click();
|
|
492
|
-
requestAnimationFrame(() => this.focusDropdownItem(0, true));
|
|
496
|
+
requestAnimationFrame(() => { this.focusDropdownItem(0, true); });
|
|
493
497
|
}
|
|
494
498
|
}
|
|
495
499
|
break;
|
|
@@ -541,7 +545,7 @@ class DomternalToolbarComponent {
|
|
|
541
545
|
}
|
|
542
546
|
setupController(editor) {
|
|
543
547
|
this.destroyController();
|
|
544
|
-
this.controller = new ToolbarController(editor, () => this.ngZone.run(() => this.syncState()), this.layout());
|
|
548
|
+
this.controller = new ToolbarController(editor, () => { this.ngZone.run(() => { this.syncState(); }); }, this.layout());
|
|
545
549
|
this.controller.subscribe();
|
|
546
550
|
this.syncState();
|
|
547
551
|
// Click outside to close dropdown
|
|
@@ -550,7 +554,7 @@ class DomternalToolbarComponent {
|
|
|
550
554
|
this.cleanupFloating?.();
|
|
551
555
|
this.cleanupFloating = null;
|
|
552
556
|
this.controller?.closeDropdown();
|
|
553
|
-
this.ngZone.run(() => this.syncState());
|
|
557
|
+
this.ngZone.run(() => { this.syncState(); });
|
|
554
558
|
}
|
|
555
559
|
};
|
|
556
560
|
document.addEventListener('mousedown', this.clickOutsideHandler);
|
|
@@ -562,7 +566,7 @@ class DomternalToolbarComponent {
|
|
|
562
566
|
this.cleanupFloating?.();
|
|
563
567
|
this.cleanupFloating = null;
|
|
564
568
|
this.controller?.closeDropdown();
|
|
565
|
-
this.ngZone.run(() => this.syncState());
|
|
569
|
+
this.ngZone.run(() => { this.syncState(); });
|
|
566
570
|
}
|
|
567
571
|
};
|
|
568
572
|
this.editorEl.addEventListener('dm:dismiss-overlays', this.dismissOverlayHandler);
|
|
@@ -616,7 +620,7 @@ class DomternalToolbarComponent {
|
|
|
616
620
|
return null;
|
|
617
621
|
}
|
|
618
622
|
}
|
|
619
|
-
/** Read only inline style
|
|
623
|
+
/** Read only inline style - no computed fallback (used for font-family). */
|
|
620
624
|
getInlineStyleAtCursor(prop) {
|
|
621
625
|
try {
|
|
622
626
|
const { from } = this.editor().state.selection;
|
|
@@ -873,27 +877,82 @@ class DomternalBubbleMenuComponent {
|
|
|
873
877
|
offset = input(8, ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
|
|
874
878
|
updateDelay = input(0, ...(ngDevMode ? [{ debugName: "updateDelay" }] : /* istanbul ignore next */ []));
|
|
875
879
|
/** Fixed item names (e.g. ['bold', 'italic', 'code']). Omit for auto mode (all format items). */
|
|
876
|
-
items = input(...(ngDevMode ? [
|
|
880
|
+
items = input(undefined, ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
877
881
|
/** Context-aware: map context names to item arrays, `true` for all valid items, or `null` to disable */
|
|
878
|
-
contexts = input(...(ngDevMode ? [
|
|
879
|
-
/**
|
|
882
|
+
contexts = input(undefined, ...(ngDevMode ? [{ debugName: "contexts" }] : /* istanbul ignore next */ []));
|
|
883
|
+
/**
|
|
884
|
+
* Custom icon overrides. Falls back to default Phosphor icons for unmapped keys.
|
|
885
|
+
* For nullable bindings, use `iconsSignal() ?? {}` to satisfy strict template checks.
|
|
886
|
+
*/
|
|
887
|
+
icons = input(undefined, ...(ngDevMode ? [{ debugName: "icons" }] : /* istanbul ignore next */ []));
|
|
888
|
+
/**
|
|
889
|
+
* Returns the effective contexts map: the explicit `contexts` input
|
|
890
|
+
* when provided, the standard default when neither `contexts` nor
|
|
891
|
+
* `items` is set, or `undefined` (items-mode) when only `items` is set.
|
|
892
|
+
* The standard default is richer when the editor sits inside
|
|
893
|
+
* `.dm-notion-mode`.
|
|
894
|
+
*/
|
|
895
|
+
resolveContexts(editor) {
|
|
896
|
+
const explicit = this.contexts();
|
|
897
|
+
if (explicit !== undefined)
|
|
898
|
+
return explicit;
|
|
899
|
+
if (this.items() !== undefined)
|
|
900
|
+
return undefined;
|
|
901
|
+
return defaultBubbleContexts(editor);
|
|
902
|
+
}
|
|
903
|
+
/** Internal - updated on transactions. Not meant to be set from outside. */
|
|
880
904
|
resolvedItems = signal([], ...(ngDevMode ? [{ debugName: "resolvedItems" }] : /* istanbul ignore next */ []));
|
|
905
|
+
/** Internal - true when the BlockContextMenu extension is loaded; toggles the "..." trailing button. */
|
|
906
|
+
showBlockMenuButton = signal(false, ...(ngDevMode ? [{ debugName: "showBlockMenuButton" }] : /* istanbul ignore next */ []));
|
|
907
|
+
/** Internal - true when the selection spans more than one top-level block; the "..." trigger is disabled in that case because block-level commands have no unambiguous target. */
|
|
908
|
+
blockMenuButtonDisabled = signal(false, ...(ngDevMode ? [{ debugName: "blockMenuButtonDisabled" }] : /* istanbul ignore next */ []));
|
|
909
|
+
/** Internal - true when the current selection is a NodeSelection (image, HR). The text-color and block-context triggers are hidden in that case: a node has no inline text to color and its bubble menu already exposes node-specific actions. */
|
|
910
|
+
isNodeSelection = signal(false, ...(ngDevMode ? [{ debugName: "isNodeSelection" }] : /* istanbul ignore next */ []));
|
|
911
|
+
/** Internal - true when the NotionColorPicker extension is loaded; toggles the "A" color trigger. */
|
|
912
|
+
showColorPickerButton = signal(false, ...(ngDevMode ? [{ debugName: "showColorPickerButton" }] : /* istanbul ignore next */ []));
|
|
913
|
+
/** Current text color CSS variable expression for the trigger glyph (null = default). */
|
|
914
|
+
currentTextColorVar = signal(null, ...(ngDevMode ? [{ debugName: "currentTextColorVar" }] : /* istanbul ignore next */ []));
|
|
915
|
+
/** Current background color CSS variable expression for the trigger underline (null = transparent). */
|
|
916
|
+
currentBgColorVar = signal(null, ...(ngDevMode ? [{ debugName: "currentBgColorVar" }] : /* istanbul ignore next */ []));
|
|
917
|
+
/** Internal - true when the selection has any token-based text or background color applied. */
|
|
918
|
+
hasAnyColor = signal(false, ...(ngDevMode ? [{ debugName: "hasAnyColor" }] : /* istanbul ignore next */ []));
|
|
881
919
|
menuEl = viewChild.required('menuEl');
|
|
882
920
|
pluginKey;
|
|
883
921
|
sanitizer = inject(DomSanitizer);
|
|
884
922
|
ngZone = inject(NgZone);
|
|
885
923
|
activeVersion = signal(0, ...(ngDevMode ? [{ debugName: "activeVersion" }] : /* istanbul ignore next */ []));
|
|
886
924
|
itemMap = new Map();
|
|
925
|
+
dropdownMap = new Map();
|
|
887
926
|
activeMap = new Map();
|
|
888
927
|
disabledMap = new Map();
|
|
889
928
|
htmlCache = new Map();
|
|
890
929
|
bubbleDefaults = new Map();
|
|
891
930
|
transactionHandler = null;
|
|
931
|
+
// Dropdown state for bubble-menu-embedded dropdowns (e.g. text-align).
|
|
932
|
+
openDropdown = signal(null, ...(ngDevMode ? [{ debugName: "openDropdown" }] : /* istanbul ignore next */ []));
|
|
933
|
+
dropdownCleanupFloating = null;
|
|
934
|
+
dropdownOutsideHandler = null;
|
|
935
|
+
dropdownKeydownHandler = null;
|
|
936
|
+
dropdownDismissHandler = null;
|
|
937
|
+
dropdownCaret = '<svg class="dm-dropdown-caret" width="10" height="10" viewBox="0 0 10 10">' +
|
|
938
|
+
'<path d="M2 4l3 3 3-3" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
892
939
|
constructor() {
|
|
893
|
-
|
|
940
|
+
// Each component instance needs a unique plugin key so that multiple
|
|
941
|
+
// bubble-menus mounted in the same editor do not collide. Prefer
|
|
942
|
+
// crypto.randomUUID over Math.random() for collision-free uniqueness
|
|
943
|
+
// (Math.random across two simultaneous mounts has a small but real
|
|
944
|
+
// collision probability, and SSR may share the random seed).
|
|
945
|
+
const crypto = globalThis.crypto;
|
|
946
|
+
const suffix = crypto?.randomUUID?.().slice(0, 8) ?? Math.random().toString(36).slice(2, 8);
|
|
947
|
+
this.pluginKey = new PluginKey('angularBubbleMenu-' + suffix);
|
|
948
|
+
// Cache keys do not include icon-set identity, so flush on swap.
|
|
949
|
+
effect(() => {
|
|
950
|
+
this.icons();
|
|
951
|
+
this.htmlCache.clear();
|
|
952
|
+
});
|
|
894
953
|
afterNextRender(() => {
|
|
895
954
|
const editor = this.editor();
|
|
896
|
-
const ctxs = this.
|
|
955
|
+
const ctxs = this.resolveContexts(editor);
|
|
897
956
|
let shouldShowFn = this.shouldShow();
|
|
898
957
|
if (!shouldShowFn) {
|
|
899
958
|
if (ctxs) {
|
|
@@ -913,8 +972,10 @@ class DomternalBubbleMenuComponent {
|
|
|
913
972
|
else {
|
|
914
973
|
// Auto/items mode: show when any endpoint's parent allows marks
|
|
915
974
|
shouldShowFn = ({ state }) => {
|
|
916
|
-
if (state.selection.empty
|
|
975
|
+
if (state.selection.empty)
|
|
917
976
|
return false;
|
|
977
|
+
if (state.selection.node)
|
|
978
|
+
return this.bubbleDefaults.has(state.selection.node.type.name);
|
|
918
979
|
if (isInsideTableCell(state.selection.$from))
|
|
919
980
|
return false;
|
|
920
981
|
return state.selection.$from.parent.type.spec.marks !== ''
|
|
@@ -956,27 +1017,66 @@ class DomternalBubbleMenuComponent {
|
|
|
956
1017
|
getCachedIcon(name) {
|
|
957
1018
|
let cached = this.htmlCache.get(name);
|
|
958
1019
|
if (!cached) {
|
|
959
|
-
|
|
1020
|
+
const custom = this.icons();
|
|
1021
|
+
cached = this.sanitizer.bypassSecurityTrustHtml(custom?.[name] ?? defaultIcons[name] ?? '');
|
|
960
1022
|
this.htmlCache.set(name, cached);
|
|
961
1023
|
}
|
|
962
1024
|
return cached;
|
|
963
1025
|
}
|
|
964
|
-
executeCommand(item) {
|
|
1026
|
+
executeCommand(item, event) {
|
|
965
1027
|
if (item.emitEvent) {
|
|
1028
|
+
// Forward the clicked button as the anchor so listeners (LinkPopover,
|
|
1029
|
+
// image popover, emoji picker) can position their panels against it
|
|
1030
|
+
// - matches `openColorPicker(anchor)` and the toolbar's executeItem.
|
|
1031
|
+
const anchor = event?.currentTarget ??
|
|
1032
|
+
event?.target ?? null;
|
|
966
1033
|
// emitEvent is a dynamic string; cast needed to bypass strict EventEmitter<EditorEvents> typing
|
|
967
|
-
this.editor().emit(item.emitEvent, {});
|
|
1034
|
+
this.editor().emit(item.emitEvent, { anchorElement: anchor });
|
|
968
1035
|
return;
|
|
969
1036
|
}
|
|
970
1037
|
ToolbarController.executeItem(this.editor(), item);
|
|
971
1038
|
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Emit the `notionColorOpen` event with the trigger button as the anchor.
|
|
1041
|
+
* The framework popover component (DomternalNotionColorPickerComponent)
|
|
1042
|
+
* listens for this event and positions itself against the anchor.
|
|
1043
|
+
*/
|
|
1044
|
+
openColorPicker(anchor) {
|
|
1045
|
+
this.editor().emit('notionColorOpen', { anchorElement: anchor });
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Open the BlockContextMenu against the cursor's containing block. Skips
|
|
1049
|
+
* the textblock for nested cases (cursor in `listItem > paragraph` targets
|
|
1050
|
+
* the listItem, not the paragraph) so Delete / Turn into operate on the
|
|
1051
|
+
* "visual block" the user is editing.
|
|
1052
|
+
*/
|
|
1053
|
+
openBlockContextMenu(anchor) {
|
|
1054
|
+
const editor = this.editor();
|
|
1055
|
+
const $from = editor.state.selection.$from;
|
|
1056
|
+
if ($from.depth < 1)
|
|
1057
|
+
return;
|
|
1058
|
+
// Walk one level up to use the container when the cursor's textblock
|
|
1059
|
+
// isn't a direct doc child; otherwise use the textblock itself.
|
|
1060
|
+
const depth = $from.depth > 1 && $from.node($from.depth - 1).type.name !== 'doc'
|
|
1061
|
+
? $from.depth - 1
|
|
1062
|
+
: $from.depth;
|
|
1063
|
+
const blockPos = $from.before(depth);
|
|
1064
|
+
const editorEl = editor.view.dom.closest('.dm-editor');
|
|
1065
|
+
editorEl?.dispatchEvent(new CustomEvent('dm:block-context-menu-open', {
|
|
1066
|
+
bubbles: false,
|
|
1067
|
+
detail: { blockPos, anchorElement: anchor },
|
|
1068
|
+
}));
|
|
1069
|
+
}
|
|
972
1070
|
// === Internal ===
|
|
973
1071
|
buildItemMap(editor) {
|
|
974
1072
|
this.itemMap.clear();
|
|
1073
|
+
this.dropdownMap.clear();
|
|
975
1074
|
for (const item of editor.toolbarItems) {
|
|
976
1075
|
if (item.type === 'button') {
|
|
977
1076
|
this.itemMap.set(item.name, item);
|
|
978
1077
|
}
|
|
979
1078
|
else if (item.type === 'dropdown') {
|
|
1079
|
+
this.dropdownMap.set(item.name, item);
|
|
980
1080
|
for (const sub of item.items) {
|
|
981
1081
|
this.itemMap.set(sub.name, sub);
|
|
982
1082
|
}
|
|
@@ -988,9 +1088,14 @@ class DomternalBubbleMenuComponent {
|
|
|
988
1088
|
let sepIdx = 0;
|
|
989
1089
|
for (const name of names) {
|
|
990
1090
|
if (name === '|') {
|
|
991
|
-
result.push({ type: 'separator', name: `sep-${sepIdx++}` });
|
|
1091
|
+
result.push({ type: 'separator', name: `sep-${String(sepIdx++)}` });
|
|
992
1092
|
}
|
|
993
1093
|
else {
|
|
1094
|
+
const dropdown = this.dropdownMap.get(name);
|
|
1095
|
+
if (dropdown) {
|
|
1096
|
+
result.push(dropdown);
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
994
1099
|
const item = this.itemMap.get(name);
|
|
995
1100
|
if (item)
|
|
996
1101
|
result.push(item);
|
|
@@ -1004,7 +1109,7 @@ class DomternalBubbleMenuComponent {
|
|
|
1004
1109
|
.sort((a, b) => (b.priority ?? 100) - (a.priority ?? 100));
|
|
1005
1110
|
}
|
|
1006
1111
|
detectContext(selection, ctxs) {
|
|
1007
|
-
// CellSelection (duck-type: has $anchorCell)
|
|
1112
|
+
// CellSelection (duck-type: has $anchorCell) - never show bubble menu
|
|
1008
1113
|
if ('$anchorCell' in selection)
|
|
1009
1114
|
return null;
|
|
1010
1115
|
if (selection.node)
|
|
@@ -1056,7 +1161,7 @@ class DomternalBubbleMenuComponent {
|
|
|
1056
1161
|
const markName = typeof item.isActive === 'string' ? item.isActive : null;
|
|
1057
1162
|
if (!markName)
|
|
1058
1163
|
return true;
|
|
1059
|
-
const markType = schema.marks
|
|
1164
|
+
const markType = schema.marks[markName];
|
|
1060
1165
|
if (!markType)
|
|
1061
1166
|
return true;
|
|
1062
1167
|
return nodeType.allowsMarkType(markType);
|
|
@@ -1091,7 +1196,7 @@ class DomternalBubbleMenuComponent {
|
|
|
1091
1196
|
let sepIdx = 0;
|
|
1092
1197
|
for (const item of items) {
|
|
1093
1198
|
if (lastGroup !== undefined && item.group !== lastGroup) {
|
|
1094
|
-
result.push({ type: 'separator', name: `bsep-${sepIdx++}` });
|
|
1199
|
+
result.push({ type: 'separator', name: `bsep-${String(sepIdx++)}` });
|
|
1095
1200
|
}
|
|
1096
1201
|
result.push(item);
|
|
1097
1202
|
lastGroup = item.group;
|
|
@@ -1102,29 +1207,85 @@ class DomternalBubbleMenuComponent {
|
|
|
1102
1207
|
setupItemTracking(editor) {
|
|
1103
1208
|
this.buildItemMap(editor);
|
|
1104
1209
|
this.buildBubbleDefaults(editor);
|
|
1105
|
-
|
|
1210
|
+
this.showBlockMenuButton.set(editor.extensionManager.extensions.some((e) => e.name === 'blockContextMenu'));
|
|
1211
|
+
this.showColorPickerButton.set(editor.extensionManager.extensions.some((e) => e.name === 'notionColorPicker'));
|
|
1212
|
+
const items = this.items();
|
|
1213
|
+
const defaultItems = this.resolveNames(items ?? ['bold', 'italic', 'underline']);
|
|
1214
|
+
if (this.resolveContexts(editor)) {
|
|
1106
1215
|
this.updateContextItems(editor);
|
|
1107
1216
|
}
|
|
1108
|
-
else if (this.items()) {
|
|
1109
|
-
this.resolvedItems.set(this.resolveNames(this.items()));
|
|
1110
|
-
}
|
|
1111
1217
|
else {
|
|
1112
|
-
this.resolvedItems.set(
|
|
1218
|
+
this.resolvedItems.set(defaultItems);
|
|
1113
1219
|
}
|
|
1114
1220
|
this.transactionHandler = () => {
|
|
1115
1221
|
this.ngZone.run(() => {
|
|
1116
|
-
|
|
1222
|
+
const sel = editor.state.selection;
|
|
1223
|
+
this.isNodeSelection.set(!!sel.node);
|
|
1224
|
+
if (this.resolveContexts(editor)) {
|
|
1117
1225
|
this.updateContextItems(editor);
|
|
1118
1226
|
}
|
|
1227
|
+
else if (sel.node && this.bubbleDefaults.has(sel.node.type.name)) {
|
|
1228
|
+
this.resolvedItems.set(this.bubbleDefaults.get(sel.node.type.name) ?? []);
|
|
1229
|
+
}
|
|
1230
|
+
else {
|
|
1231
|
+
this.resolvedItems.set(defaultItems);
|
|
1232
|
+
}
|
|
1119
1233
|
this.updateStates(editor);
|
|
1120
|
-
this.
|
|
1234
|
+
this.syncTrailingButtonsState(editor);
|
|
1235
|
+
this.activeVersion.update((v) => v + 1);
|
|
1121
1236
|
});
|
|
1122
1237
|
};
|
|
1123
1238
|
editor.on('transaction', this.transactionHandler);
|
|
1124
1239
|
this.updateStates(editor);
|
|
1240
|
+
this.syncTrailingButtonsState(editor);
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Refresh state for the optional trailing buttons (A color trigger,
|
|
1244
|
+
* "..." block menu) and the isNodeSelection flag. Called from both
|
|
1245
|
+
* setupItemTracking init and the per-transaction handler so the two
|
|
1246
|
+
* paths stay in sync.
|
|
1247
|
+
*/
|
|
1248
|
+
syncTrailingButtonsState(editor) {
|
|
1249
|
+
this.isNodeSelection.set(!!editor.state.selection.node);
|
|
1250
|
+
if (this.showColorPickerButton())
|
|
1251
|
+
this.syncColorTriggerState(editor);
|
|
1252
|
+
if (this.showBlockMenuButton())
|
|
1253
|
+
this.syncBlockMenuButtonState(editor);
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Disable the "..." trigger when the selection spans more than one
|
|
1257
|
+
* top-level block. The block context menu targets the block at $from, so
|
|
1258
|
+
* a multi-block selection has no unambiguous target.
|
|
1259
|
+
*/
|
|
1260
|
+
syncBlockMenuButtonState(editor) {
|
|
1261
|
+
const { $from, $to } = editor.state.selection;
|
|
1262
|
+
// depth(1) addresses the top-level child of doc; if either end is
|
|
1263
|
+
// shallower we can't compare blocks meaningfully.
|
|
1264
|
+
if ($from.depth < 1 || $to.depth < 1) {
|
|
1265
|
+
this.blockMenuButtonDisabled.set(true);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
this.blockMenuButtonDisabled.set($from.before(1) !== $to.before(1));
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Update the trigger's indicator colors to mirror the current selection.
|
|
1272
|
+
* Used by the "A" button so the underline matches whatever the user has
|
|
1273
|
+
* applied at the cursor (Notion-style live preview).
|
|
1274
|
+
*/
|
|
1275
|
+
syncColorTriggerState(editor) {
|
|
1276
|
+
const $from = editor.state.selection.$from;
|
|
1277
|
+
const mark = $from.marks().find((m) => m.type.name === 'textStyle');
|
|
1278
|
+
const attrs = (mark?.attrs ?? {});
|
|
1279
|
+
const textToken = attrs.colorToken ?? null;
|
|
1280
|
+
const bgToken = attrs.backgroundColorToken ?? null;
|
|
1281
|
+
this.currentTextColorVar.set(textToken ? `var(--dm-block-text-${textToken})` : null);
|
|
1282
|
+
this.currentBgColorVar.set(bgToken ? `var(--dm-block-bg-${bgToken})` : null);
|
|
1283
|
+
this.hasAnyColor.set(textToken !== null || bgToken !== null);
|
|
1125
1284
|
}
|
|
1126
1285
|
updateContextItems(editor) {
|
|
1127
|
-
const ctxs = this.
|
|
1286
|
+
const ctxs = this.resolveContexts(editor);
|
|
1287
|
+
if (!ctxs)
|
|
1288
|
+
return;
|
|
1128
1289
|
const ctx = this.detectContext(editor.state.selection, ctxs);
|
|
1129
1290
|
if (!ctx) {
|
|
1130
1291
|
this.resolvedItems.set([]);
|
|
@@ -1155,40 +1316,237 @@ class DomternalBubbleMenuComponent {
|
|
|
1155
1316
|
try {
|
|
1156
1317
|
canProxy = editor.can();
|
|
1157
1318
|
}
|
|
1158
|
-
catch { }
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
continue;
|
|
1162
|
-
this.activeMap.set(item.name, ToolbarController.resolveActive(editor, item));
|
|
1319
|
+
catch { /* ignore */ }
|
|
1320
|
+
const trackButton = (btn) => {
|
|
1321
|
+
this.activeMap.set(btn.name, ToolbarController.resolveActive(editor, btn));
|
|
1163
1322
|
try {
|
|
1164
|
-
const canCmd = canProxy?.[
|
|
1165
|
-
this.disabledMap.set(
|
|
1166
|
-
? !(
|
|
1323
|
+
const canCmd = typeof btn.command === 'string' ? canProxy?.[btn.command] : undefined;
|
|
1324
|
+
this.disabledMap.set(btn.name, canCmd
|
|
1325
|
+
? !(btn.commandArgs?.length ? canCmd(...btn.commandArgs) : canCmd())
|
|
1167
1326
|
: false);
|
|
1168
1327
|
}
|
|
1169
1328
|
catch {
|
|
1170
|
-
this.disabledMap.set(
|
|
1329
|
+
this.disabledMap.set(btn.name, false);
|
|
1171
1330
|
}
|
|
1331
|
+
};
|
|
1332
|
+
for (const item of this.resolvedItems()) {
|
|
1333
|
+
if (item.type === 'separator')
|
|
1334
|
+
continue;
|
|
1335
|
+
if (item.type === 'dropdown') {
|
|
1336
|
+
// Track each child so dynamicIcon + isActive lookups work.
|
|
1337
|
+
for (const sub of item.items)
|
|
1338
|
+
trackButton(sub);
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
trackButton(item);
|
|
1172
1342
|
}
|
|
1173
1343
|
}
|
|
1344
|
+
// === Dropdown helpers (shared shape with toolbar.component.ts) ===
|
|
1345
|
+
asButton(item) {
|
|
1346
|
+
return item;
|
|
1347
|
+
}
|
|
1348
|
+
asDropdown(item) {
|
|
1349
|
+
return item;
|
|
1350
|
+
}
|
|
1351
|
+
isDropdownActive(dropdown) {
|
|
1352
|
+
this.activeVersion();
|
|
1353
|
+
return dropdown.items.some((sub) => this.activeMap.get(sub.name) ?? false);
|
|
1354
|
+
}
|
|
1355
|
+
/** Trigger HTML: swap icon to the active child when `dynamicIcon` is set. */
|
|
1356
|
+
getDropdownTriggerHtml(dropdown) {
|
|
1357
|
+
this.activeVersion();
|
|
1358
|
+
const activeItem = dropdown.dynamicIcon
|
|
1359
|
+
? dropdown.items.find((sub) => this.activeMap.get(sub.name))
|
|
1360
|
+
: undefined;
|
|
1361
|
+
const iconName = activeItem?.icon ?? dropdown.icon;
|
|
1362
|
+
const key = `t:${iconName}`;
|
|
1363
|
+
let cached = this.htmlCache.get(key);
|
|
1364
|
+
if (!cached) {
|
|
1365
|
+
const custom = this.icons();
|
|
1366
|
+
const svg = custom?.[iconName] ?? defaultIcons[iconName] ?? '';
|
|
1367
|
+
cached = this.sanitizer.bypassSecurityTrustHtml(svg + this.dropdownCaret);
|
|
1368
|
+
this.htmlCache.set(key, cached);
|
|
1369
|
+
}
|
|
1370
|
+
return cached;
|
|
1371
|
+
}
|
|
1372
|
+
getCachedItemContent(iconName, label) {
|
|
1373
|
+
const key = `dc:${iconName}:${label}`;
|
|
1374
|
+
let cached = this.htmlCache.get(key);
|
|
1375
|
+
if (!cached) {
|
|
1376
|
+
const custom = this.icons();
|
|
1377
|
+
const svg = custom?.[iconName] ?? defaultIcons[iconName] ?? '';
|
|
1378
|
+
cached = this.sanitizer.bypassSecurityTrustHtml(`${svg} ${label}`);
|
|
1379
|
+
this.htmlCache.set(key, cached);
|
|
1380
|
+
}
|
|
1381
|
+
return cached;
|
|
1382
|
+
}
|
|
1383
|
+
isSubItemActive(name) {
|
|
1384
|
+
this.activeVersion();
|
|
1385
|
+
return this.activeMap.get(name) ?? false;
|
|
1386
|
+
}
|
|
1387
|
+
onDropdownToggle(dropdown, event) {
|
|
1388
|
+
const isOpening = this.openDropdown() !== dropdown.name;
|
|
1389
|
+
this.cleanupDropdown();
|
|
1390
|
+
if (!isOpening)
|
|
1391
|
+
return;
|
|
1392
|
+
this.openDropdown.set(dropdown.name);
|
|
1393
|
+
// NOTE: we deliberately do NOT broadcast `dm:dismiss-overlays` here.
|
|
1394
|
+
// The bubble menu plugin in core listens for that event and would
|
|
1395
|
+
// hide itself - taking our just-opened dropdown's trigger with it.
|
|
1396
|
+
// Other overlays (color picker, link popover) close themselves via
|
|
1397
|
+
// their own click-outside handlers when the user clicks our trigger,
|
|
1398
|
+
// so cooperative dismissal still works.
|
|
1399
|
+
const editorElBroadcast = this.menuEl().nativeElement.closest('.dm-editor');
|
|
1400
|
+
// Position the panel against the trigger after Angular renders it.
|
|
1401
|
+
// We deliberately keep the panel INSIDE `.dm-bubble-menu` so it
|
|
1402
|
+
// inherits the bubble-menu-scoped button tokens (--dm-button-active-bg
|
|
1403
|
+
// / --dm-button-active-color). Reparenting into `.dm-editor` was
|
|
1404
|
+
// tempting for consistency with the color picker, but that dropped
|
|
1405
|
+
// the active-state colors because those tokens aren't defined at
|
|
1406
|
+
// editor scope. floating-ui handles cross-element positioning fine
|
|
1407
|
+
// without reparenting.
|
|
1408
|
+
requestAnimationFrame(() => {
|
|
1409
|
+
const trigger = event?.currentTarget
|
|
1410
|
+
?? this.menuEl().nativeElement.querySelector(`[data-dropdown="${dropdown.name}"]`);
|
|
1411
|
+
const panel = this.menuEl().nativeElement.querySelector(`[data-dropdown-panel="${dropdown.name}"]`);
|
|
1412
|
+
if (!trigger || !panel)
|
|
1413
|
+
return;
|
|
1414
|
+
this.dropdownCleanupFloating?.();
|
|
1415
|
+
this.dropdownCleanupFloating = positionFloatingOnce(trigger, panel, {
|
|
1416
|
+
placement: 'bottom-start',
|
|
1417
|
+
offsetValue: 4,
|
|
1418
|
+
});
|
|
1419
|
+
});
|
|
1420
|
+
// Click-outside dismissal. mousedown is used so the dropdown closes
|
|
1421
|
+
// BEFORE the bubble menu's blur handler runs.
|
|
1422
|
+
const onOutside = (e) => {
|
|
1423
|
+
const target = e.target;
|
|
1424
|
+
if (!target)
|
|
1425
|
+
return;
|
|
1426
|
+
const panel = document.querySelector(`[data-dropdown-panel="${dropdown.name}"]`);
|
|
1427
|
+
const trigger = this.menuEl().nativeElement.querySelector(`[data-dropdown="${dropdown.name}"]`);
|
|
1428
|
+
if (panel?.contains(target))
|
|
1429
|
+
return;
|
|
1430
|
+
if (trigger?.contains(target))
|
|
1431
|
+
return;
|
|
1432
|
+
this.cleanupDropdown();
|
|
1433
|
+
};
|
|
1434
|
+
document.addEventListener('mousedown', onOutside);
|
|
1435
|
+
this.dropdownOutsideHandler = onOutside;
|
|
1436
|
+
// Escape closes the dropdown.
|
|
1437
|
+
const onKeydown = (e) => {
|
|
1438
|
+
if (e.key === 'Escape') {
|
|
1439
|
+
e.preventDefault();
|
|
1440
|
+
this.cleanupDropdown();
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1443
|
+
document.addEventListener('keydown', onKeydown);
|
|
1444
|
+
this.dropdownKeydownHandler = onKeydown;
|
|
1445
|
+
// Listen for cooperative dismissals from other overlays opening.
|
|
1446
|
+
const onDismiss = () => { this.cleanupDropdown(); };
|
|
1447
|
+
if (editorElBroadcast) {
|
|
1448
|
+
editorElBroadcast.addEventListener('dm:dismiss-overlays', onDismiss);
|
|
1449
|
+
this.dropdownDismissHandler = () => {
|
|
1450
|
+
editorElBroadcast.removeEventListener('dm:dismiss-overlays', onDismiss);
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
onDropdownItemClick(item) {
|
|
1455
|
+
this.cleanupDropdown();
|
|
1456
|
+
ToolbarController.executeItem(this.editor(), item);
|
|
1457
|
+
requestAnimationFrame(() => { this.editor().view.focus(); });
|
|
1458
|
+
}
|
|
1459
|
+
cleanupDropdown() {
|
|
1460
|
+
if (this.dropdownOutsideHandler) {
|
|
1461
|
+
document.removeEventListener('mousedown', this.dropdownOutsideHandler);
|
|
1462
|
+
this.dropdownOutsideHandler = null;
|
|
1463
|
+
}
|
|
1464
|
+
if (this.dropdownKeydownHandler) {
|
|
1465
|
+
document.removeEventListener('keydown', this.dropdownKeydownHandler);
|
|
1466
|
+
this.dropdownKeydownHandler = null;
|
|
1467
|
+
}
|
|
1468
|
+
if (this.dropdownDismissHandler) {
|
|
1469
|
+
this.dropdownDismissHandler();
|
|
1470
|
+
this.dropdownDismissHandler = null;
|
|
1471
|
+
}
|
|
1472
|
+
this.dropdownCleanupFloating?.();
|
|
1473
|
+
this.dropdownCleanupFloating = null;
|
|
1474
|
+
this.openDropdown.set(null);
|
|
1475
|
+
}
|
|
1174
1476
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1175
|
-
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: `
|
|
1477
|
+
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 }, icons: { classPropertyName: "icons", publicName: "icons", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "menuEl", first: true, predicate: ["menuEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
1176
1478
|
<div #menuEl class="dm-bubble-menu" role="toolbar" aria-label="Text formatting">
|
|
1177
1479
|
@for (item of resolvedItems(); track item.name) {
|
|
1178
1480
|
@if (item.type === 'separator') {
|
|
1179
1481
|
<span class="dm-toolbar-separator" role="separator"></span>
|
|
1482
|
+
} @else if (item.type === 'dropdown') {
|
|
1483
|
+
<div class="dm-toolbar-dropdown-wrapper" [attr.data-dropdown-wrapper]="asDropdown(item).name">
|
|
1484
|
+
<button type="button"
|
|
1485
|
+
class="dm-toolbar-button dm-toolbar-dropdown-trigger"
|
|
1486
|
+
[class.dm-toolbar-button--active]="isDropdownActive(asDropdown(item))"
|
|
1487
|
+
[attr.aria-expanded]="openDropdown() === asDropdown(item).name"
|
|
1488
|
+
[attr.aria-haspopup]="'true'"
|
|
1489
|
+
[attr.aria-label]="asDropdown(item).label"
|
|
1490
|
+
[title]="asDropdown(item).label"
|
|
1491
|
+
[attr.data-dropdown]="asDropdown(item).name"
|
|
1492
|
+
[innerHTML]="getDropdownTriggerHtml(asDropdown(item))"
|
|
1493
|
+
(mousedown)="$event.preventDefault()"
|
|
1494
|
+
(click)="onDropdownToggle(asDropdown(item), $event)"></button>
|
|
1495
|
+
@if (openDropdown() === asDropdown(item).name) {
|
|
1496
|
+
<div class="dm-toolbar-dropdown-panel" role="menu"
|
|
1497
|
+
data-dm-editor-ui
|
|
1498
|
+
[attr.data-dropdown-panel]="asDropdown(item).name">
|
|
1499
|
+
@for (sub of asDropdown(item).items; track sub.name) {
|
|
1500
|
+
<button type="button"
|
|
1501
|
+
class="dm-toolbar-dropdown-item"
|
|
1502
|
+
[class.dm-toolbar-dropdown-item--active]="isSubItemActive(sub.name)"
|
|
1503
|
+
role="menuitem"
|
|
1504
|
+
[attr.aria-label]="sub.label"
|
|
1505
|
+
[innerHTML]="getCachedItemContent(sub.icon, sub.label)"
|
|
1506
|
+
(mousedown)="$event.preventDefault()"
|
|
1507
|
+
(click)="onDropdownItemClick(sub)"></button>
|
|
1508
|
+
}
|
|
1509
|
+
</div>
|
|
1510
|
+
}
|
|
1511
|
+
</div>
|
|
1180
1512
|
} @else {
|
|
1181
1513
|
<button type="button" class="dm-toolbar-button"
|
|
1182
|
-
[class.dm-toolbar-button--active]="isItemActive(item)"
|
|
1183
|
-
[attr.aria-pressed]="isItemActive(item)"
|
|
1184
|
-
[disabled]="isItemDisabled(item)"
|
|
1185
|
-
[title]="item.label"
|
|
1186
|
-
[attr.aria-label]="item.label"
|
|
1187
|
-
[innerHTML]="getCachedIcon(item.icon)"
|
|
1514
|
+
[class.dm-toolbar-button--active]="isItemActive(asButton(item))"
|
|
1515
|
+
[attr.aria-pressed]="isItemActive(asButton(item))"
|
|
1516
|
+
[disabled]="isItemDisabled(asButton(item))"
|
|
1517
|
+
[title]="asButton(item).label"
|
|
1518
|
+
[attr.aria-label]="asButton(item).label"
|
|
1519
|
+
[innerHTML]="getCachedIcon(asButton(item).icon)"
|
|
1188
1520
|
(mousedown)="$event.preventDefault()"
|
|
1189
|
-
(click)="executeCommand(item)"></button>
|
|
1521
|
+
(click)="executeCommand(asButton(item), $event)"></button>
|
|
1190
1522
|
}
|
|
1191
1523
|
}
|
|
1524
|
+
@if (showColorPickerButton() && !isNodeSelection()) {
|
|
1525
|
+
<span class="dm-toolbar-separator" role="separator"></span>
|
|
1526
|
+
<button #colorBtn type="button" class="dm-toolbar-button dm-ncp-trigger"
|
|
1527
|
+
[class.dm-toolbar-button--active]="hasAnyColor()"
|
|
1528
|
+
title="Text and background color"
|
|
1529
|
+
aria-label="Text and background color"
|
|
1530
|
+
aria-haspopup="dialog"
|
|
1531
|
+
(mousedown)="$event.preventDefault()"
|
|
1532
|
+
(click)="openColorPicker(colorBtn)">
|
|
1533
|
+
<span class="dm-ncp-trigger-glyph"
|
|
1534
|
+
[style.color]="currentTextColorVar()">A</span>
|
|
1535
|
+
<span class="dm-ncp-trigger-underline"
|
|
1536
|
+
[style.background-color]="currentBgColorVar()"></span>
|
|
1537
|
+
</button>
|
|
1538
|
+
}
|
|
1539
|
+
@if (showBlockMenuButton() && !isNodeSelection()) {
|
|
1540
|
+
<span class="dm-toolbar-separator" role="separator"></span>
|
|
1541
|
+
<button #blockMenuBtn type="button" class="dm-toolbar-button"
|
|
1542
|
+
[disabled]="blockMenuButtonDisabled()"
|
|
1543
|
+
[title]="blockMenuButtonDisabled() ? 'Block actions (select within a single block)' : 'More options'"
|
|
1544
|
+
aria-label="More options"
|
|
1545
|
+
aria-haspopup="menu"
|
|
1546
|
+
[innerHTML]="getCachedIcon('dotsThree')"
|
|
1547
|
+
(mousedown)="$event.preventDefault()"
|
|
1548
|
+
(click)="openBlockContextMenu(blockMenuBtn)"></button>
|
|
1549
|
+
}
|
|
1192
1550
|
<ng-content />
|
|
1193
1551
|
</div>
|
|
1194
1552
|
`, isInline: true, styles: [":host{display:contents}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
@@ -1200,57 +1558,350 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
|
|
|
1200
1558
|
@for (item of resolvedItems(); track item.name) {
|
|
1201
1559
|
@if (item.type === 'separator') {
|
|
1202
1560
|
<span class="dm-toolbar-separator" role="separator"></span>
|
|
1561
|
+
} @else if (item.type === 'dropdown') {
|
|
1562
|
+
<div class="dm-toolbar-dropdown-wrapper" [attr.data-dropdown-wrapper]="asDropdown(item).name">
|
|
1563
|
+
<button type="button"
|
|
1564
|
+
class="dm-toolbar-button dm-toolbar-dropdown-trigger"
|
|
1565
|
+
[class.dm-toolbar-button--active]="isDropdownActive(asDropdown(item))"
|
|
1566
|
+
[attr.aria-expanded]="openDropdown() === asDropdown(item).name"
|
|
1567
|
+
[attr.aria-haspopup]="'true'"
|
|
1568
|
+
[attr.aria-label]="asDropdown(item).label"
|
|
1569
|
+
[title]="asDropdown(item).label"
|
|
1570
|
+
[attr.data-dropdown]="asDropdown(item).name"
|
|
1571
|
+
[innerHTML]="getDropdownTriggerHtml(asDropdown(item))"
|
|
1572
|
+
(mousedown)="$event.preventDefault()"
|
|
1573
|
+
(click)="onDropdownToggle(asDropdown(item), $event)"></button>
|
|
1574
|
+
@if (openDropdown() === asDropdown(item).name) {
|
|
1575
|
+
<div class="dm-toolbar-dropdown-panel" role="menu"
|
|
1576
|
+
data-dm-editor-ui
|
|
1577
|
+
[attr.data-dropdown-panel]="asDropdown(item).name">
|
|
1578
|
+
@for (sub of asDropdown(item).items; track sub.name) {
|
|
1579
|
+
<button type="button"
|
|
1580
|
+
class="dm-toolbar-dropdown-item"
|
|
1581
|
+
[class.dm-toolbar-dropdown-item--active]="isSubItemActive(sub.name)"
|
|
1582
|
+
role="menuitem"
|
|
1583
|
+
[attr.aria-label]="sub.label"
|
|
1584
|
+
[innerHTML]="getCachedItemContent(sub.icon, sub.label)"
|
|
1585
|
+
(mousedown)="$event.preventDefault()"
|
|
1586
|
+
(click)="onDropdownItemClick(sub)"></button>
|
|
1587
|
+
}
|
|
1588
|
+
</div>
|
|
1589
|
+
}
|
|
1590
|
+
</div>
|
|
1203
1591
|
} @else {
|
|
1204
1592
|
<button type="button" class="dm-toolbar-button"
|
|
1205
|
-
[class.dm-toolbar-button--active]="isItemActive(item)"
|
|
1206
|
-
[attr.aria-pressed]="isItemActive(item)"
|
|
1207
|
-
[disabled]="isItemDisabled(item)"
|
|
1208
|
-
[title]="item.label"
|
|
1209
|
-
[attr.aria-label]="item.label"
|
|
1210
|
-
[innerHTML]="getCachedIcon(item.icon)"
|
|
1593
|
+
[class.dm-toolbar-button--active]="isItemActive(asButton(item))"
|
|
1594
|
+
[attr.aria-pressed]="isItemActive(asButton(item))"
|
|
1595
|
+
[disabled]="isItemDisabled(asButton(item))"
|
|
1596
|
+
[title]="asButton(item).label"
|
|
1597
|
+
[attr.aria-label]="asButton(item).label"
|
|
1598
|
+
[innerHTML]="getCachedIcon(asButton(item).icon)"
|
|
1211
1599
|
(mousedown)="$event.preventDefault()"
|
|
1212
|
-
(click)="executeCommand(item)"></button>
|
|
1600
|
+
(click)="executeCommand(asButton(item), $event)"></button>
|
|
1213
1601
|
}
|
|
1214
1602
|
}
|
|
1603
|
+
@if (showColorPickerButton() && !isNodeSelection()) {
|
|
1604
|
+
<span class="dm-toolbar-separator" role="separator"></span>
|
|
1605
|
+
<button #colorBtn type="button" class="dm-toolbar-button dm-ncp-trigger"
|
|
1606
|
+
[class.dm-toolbar-button--active]="hasAnyColor()"
|
|
1607
|
+
title="Text and background color"
|
|
1608
|
+
aria-label="Text and background color"
|
|
1609
|
+
aria-haspopup="dialog"
|
|
1610
|
+
(mousedown)="$event.preventDefault()"
|
|
1611
|
+
(click)="openColorPicker(colorBtn)">
|
|
1612
|
+
<span class="dm-ncp-trigger-glyph"
|
|
1613
|
+
[style.color]="currentTextColorVar()">A</span>
|
|
1614
|
+
<span class="dm-ncp-trigger-underline"
|
|
1615
|
+
[style.background-color]="currentBgColorVar()"></span>
|
|
1616
|
+
</button>
|
|
1617
|
+
}
|
|
1618
|
+
@if (showBlockMenuButton() && !isNodeSelection()) {
|
|
1619
|
+
<span class="dm-toolbar-separator" role="separator"></span>
|
|
1620
|
+
<button #blockMenuBtn type="button" class="dm-toolbar-button"
|
|
1621
|
+
[disabled]="blockMenuButtonDisabled()"
|
|
1622
|
+
[title]="blockMenuButtonDisabled() ? 'Block actions (select within a single block)' : 'More options'"
|
|
1623
|
+
aria-label="More options"
|
|
1624
|
+
aria-haspopup="menu"
|
|
1625
|
+
[innerHTML]="getCachedIcon('dotsThree')"
|
|
1626
|
+
(mousedown)="$event.preventDefault()"
|
|
1627
|
+
(click)="openBlockContextMenu(blockMenuBtn)"></button>
|
|
1628
|
+
}
|
|
1215
1629
|
<ng-content />
|
|
1216
1630
|
</div>
|
|
1217
1631
|
`, styles: [":host{display:contents}\n"] }]
|
|
1218
|
-
}], 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 }] }] } });
|
|
1632
|
+
}], 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 }] }], icons: [{ type: i0.Input, args: [{ isSignal: true, alias: "icons", required: false }] }], menuEl: [{ type: i0.ViewChild, args: ['menuEl', { isSignal: true }] }] } });
|
|
1219
1633
|
|
|
1634
|
+
/**
|
|
1635
|
+
* Block-insert floating menu for Angular.
|
|
1636
|
+
*
|
|
1637
|
+
* Renders defaults collected from the editor's extensions via
|
|
1638
|
+
* `addFloatingMenuItems()`; use `items` to override the list and `icons`
|
|
1639
|
+
* to swap the icon set.
|
|
1640
|
+
*/
|
|
1220
1641
|
class DomternalFloatingMenuComponent {
|
|
1221
1642
|
editor = input.required(...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
|
|
1222
1643
|
shouldShow = input(...(ngDevMode ? [undefined, { debugName: "shouldShow" }] : /* istanbul ignore next */ []));
|
|
1223
1644
|
offset = input(0, ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
|
|
1645
|
+
items = input(undefined, ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
1646
|
+
keymap = input(undefined, ...(ngDevMode ? [{ debugName: "keymap" }] : /* istanbul ignore next */ []));
|
|
1647
|
+
icons = input(undefined, ...(ngDevMode ? [{ debugName: "icons" }] : /* istanbul ignore next */ []));
|
|
1648
|
+
/**
|
|
1649
|
+
* When true, the menu does NOT auto-show on every empty paragraph;
|
|
1650
|
+
* it only opens when the BlockHandle `+` button (or any caller of
|
|
1651
|
+
* `showFloatingMenu`) explicitly triggers it. Notion-style behaviour
|
|
1652
|
+
* - empty rows show a placeholder, the slash menu is the keyboard
|
|
1653
|
+
* trigger, the `+` button is the gutter trigger.
|
|
1654
|
+
* @default false
|
|
1655
|
+
*/
|
|
1656
|
+
requireExplicitTrigger = input(false, ...(ngDevMode ? [{ debugName: "requireExplicitTrigger" }] : /* istanbul ignore next */ []));
|
|
1224
1657
|
menuEl = viewChild.required('menuEl');
|
|
1658
|
+
ngZone = inject(NgZone);
|
|
1659
|
+
sanitizer = inject(DomSanitizer);
|
|
1225
1660
|
pluginKey;
|
|
1661
|
+
controller = null;
|
|
1662
|
+
// Signal that bumps on every controller change - templates read it to
|
|
1663
|
+
// re-run @for tracking. OnPush components need explicit change triggers.
|
|
1664
|
+
version = signal(0, ...(ngDevMode ? [{ debugName: "version" }] : /* istanbul ignore next */ []));
|
|
1665
|
+
iconCache = new Map();
|
|
1666
|
+
groups = computed(() => {
|
|
1667
|
+
// Read version to subscribe to controller updates.
|
|
1668
|
+
void this.version();
|
|
1669
|
+
return this.controller?.groups ?? [];
|
|
1670
|
+
}, ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
|
|
1671
|
+
flatNames = computed(() => {
|
|
1672
|
+
void this.version();
|
|
1673
|
+
return this.groups().flatMap((g) => g.items.map((i) => i.name));
|
|
1674
|
+
}, ...(ngDevMode ? [{ debugName: "flatNames" }] : /* istanbul ignore next */ []));
|
|
1675
|
+
focusedIndex = computed(() => {
|
|
1676
|
+
void this.version();
|
|
1677
|
+
return this.controller?.focusedIndex ?? -1;
|
|
1678
|
+
}, ...(ngDevMode ? [{ debugName: "focusedIndex" }] : /* istanbul ignore next */ []));
|
|
1226
1679
|
constructor() {
|
|
1227
|
-
//
|
|
1228
|
-
|
|
1680
|
+
// Prefer crypto.randomUUID for collision-free uniqueness (Math.random
|
|
1681
|
+
// across simultaneous mounts may collide; SSR can share the seed).
|
|
1682
|
+
// Matches the bubble-menu component's pattern.
|
|
1683
|
+
const cryptoRef = globalThis.crypto;
|
|
1684
|
+
const suffix = cryptoRef?.randomUUID?.().slice(0, 8) ?? Math.random().toString(36).slice(2, 8);
|
|
1685
|
+
this.pluginKey = new PluginKey('angularFloatingMenu-' + suffix);
|
|
1229
1686
|
afterNextRender(() => {
|
|
1687
|
+
const editor = this.editor();
|
|
1230
1688
|
const shouldShow = this.shouldShow();
|
|
1689
|
+
const keymap = this.keymap();
|
|
1231
1690
|
const plugin = createFloatingMenuPlugin({
|
|
1232
1691
|
pluginKey: this.pluginKey,
|
|
1233
|
-
editor
|
|
1692
|
+
editor,
|
|
1234
1693
|
element: this.menuEl().nativeElement,
|
|
1235
1694
|
...(shouldShow && { shouldShow }),
|
|
1236
1695
|
offset: this.offset(),
|
|
1696
|
+
...(keymap && { keymap }),
|
|
1697
|
+
requireExplicitTrigger: this.requireExplicitTrigger(),
|
|
1698
|
+
});
|
|
1699
|
+
editor.registerPlugin(plugin);
|
|
1700
|
+
// Create controller for default rendering. ProseMirror transactions
|
|
1701
|
+
// fire outside Angular zone, so signal updates inside the onChange
|
|
1702
|
+
// callback must be wrapped to trigger OnPush change detection.
|
|
1703
|
+
const controller = new FloatingMenuController(editor, () => {
|
|
1704
|
+
this.ngZone.run(() => { this.version.update((v) => v + 1); });
|
|
1705
|
+
}, this.items());
|
|
1706
|
+
controller.subscribe();
|
|
1707
|
+
this.controller = controller;
|
|
1708
|
+
// Controller is a plain field, not a signal - bumping `version` is
|
|
1709
|
+
// what tells the `groups`/`focusedIndex` computeds to re-evaluate
|
|
1710
|
+
// and pick up the freshly assigned controller instance.
|
|
1711
|
+
this.version.update((v) => v + 1);
|
|
1712
|
+
});
|
|
1713
|
+
// Imperatively focus the active menuitem on focusedIndex change.
|
|
1714
|
+
effect(() => {
|
|
1715
|
+
const idx = this.focusedIndex();
|
|
1716
|
+
if (idx < 0)
|
|
1717
|
+
return;
|
|
1718
|
+
queueMicrotask(() => {
|
|
1719
|
+
const target = this.menuEl().nativeElement.querySelector(`[data-floating-menu-index="${String(idx)}"]`);
|
|
1720
|
+
target?.focus();
|
|
1237
1721
|
});
|
|
1238
|
-
this.editor().registerPlugin(plugin);
|
|
1239
1722
|
});
|
|
1240
1723
|
}
|
|
1241
1724
|
ngOnDestroy() {
|
|
1725
|
+
this.controller?.destroy();
|
|
1726
|
+
this.controller = null;
|
|
1242
1727
|
const editor = this.editor();
|
|
1243
1728
|
if (!editor.isDestroyed) {
|
|
1244
1729
|
editor.unregisterPlugin(this.pluginKey);
|
|
1245
1730
|
}
|
|
1246
1731
|
}
|
|
1732
|
+
// === Template helpers ===
|
|
1733
|
+
flatIndexOf(name) {
|
|
1734
|
+
return this.flatNames().indexOf(name);
|
|
1735
|
+
}
|
|
1736
|
+
tabIndexFor(name) {
|
|
1737
|
+
const flat = this.flatIndexOf(name);
|
|
1738
|
+
const focused = this.focusedIndex();
|
|
1739
|
+
if (flat === focused)
|
|
1740
|
+
return 0;
|
|
1741
|
+
// When menu not yet entered, first item is tabbable so external focus
|
|
1742
|
+
// (e.g. Tab traversal) lands naturally.
|
|
1743
|
+
if (focused < 0 && flat === 0)
|
|
1744
|
+
return 0;
|
|
1745
|
+
return -1;
|
|
1746
|
+
}
|
|
1747
|
+
isItemDisabled(item) {
|
|
1748
|
+
return this.controller?.isDisabled(item) ?? false;
|
|
1749
|
+
}
|
|
1750
|
+
iconHtml(name) {
|
|
1751
|
+
if (!name)
|
|
1752
|
+
return null;
|
|
1753
|
+
const existing = this.iconCache.get(name);
|
|
1754
|
+
if (existing)
|
|
1755
|
+
return existing;
|
|
1756
|
+
const raw = this.icons()?.[name] ?? defaultIcons[name] ?? '';
|
|
1757
|
+
if (!raw)
|
|
1758
|
+
return null;
|
|
1759
|
+
const sanitized = this.sanitizer.bypassSecurityTrustHtml(raw);
|
|
1760
|
+
this.iconCache.set(name, sanitized);
|
|
1761
|
+
return sanitized;
|
|
1762
|
+
}
|
|
1763
|
+
onItemClick(item) {
|
|
1764
|
+
const editor = this.editor();
|
|
1765
|
+
const ctl = this.controller;
|
|
1766
|
+
if (!ctl)
|
|
1767
|
+
return;
|
|
1768
|
+
ctl.execute(item);
|
|
1769
|
+
requestAnimationFrame(() => { editor.view.focus(); });
|
|
1770
|
+
}
|
|
1771
|
+
onKeyDown(e) {
|
|
1772
|
+
const ctl = this.controller;
|
|
1773
|
+
if (!ctl)
|
|
1774
|
+
return;
|
|
1775
|
+
const focused = ctl.focusedItem();
|
|
1776
|
+
switch (e.key) {
|
|
1777
|
+
case 'ArrowDown':
|
|
1778
|
+
e.preventDefault();
|
|
1779
|
+
ctl.next();
|
|
1780
|
+
return;
|
|
1781
|
+
case 'ArrowUp':
|
|
1782
|
+
e.preventDefault();
|
|
1783
|
+
ctl.prev();
|
|
1784
|
+
return;
|
|
1785
|
+
case 'Home':
|
|
1786
|
+
e.preventDefault();
|
|
1787
|
+
ctl.first();
|
|
1788
|
+
return;
|
|
1789
|
+
case 'End':
|
|
1790
|
+
e.preventDefault();
|
|
1791
|
+
ctl.last();
|
|
1792
|
+
return;
|
|
1793
|
+
case 'Escape':
|
|
1794
|
+
e.preventDefault();
|
|
1795
|
+
e.stopPropagation();
|
|
1796
|
+
ctl.leaveMenu();
|
|
1797
|
+
this.editor().view.focus();
|
|
1798
|
+
return;
|
|
1799
|
+
case 'Enter':
|
|
1800
|
+
case ' ':
|
|
1801
|
+
if (focused) {
|
|
1802
|
+
e.preventDefault();
|
|
1803
|
+
this.onItemClick(focused);
|
|
1804
|
+
}
|
|
1805
|
+
return;
|
|
1806
|
+
default:
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1247
1810
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalFloatingMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1248
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.
|
|
1811
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.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 }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, keymap: { classPropertyName: "keymap", publicName: "keymap", isSignal: true, isRequired: false, transformFunction: null }, icons: { classPropertyName: "icons", publicName: "icons", isSignal: true, isRequired: false, transformFunction: null }, requireExplicitTrigger: { classPropertyName: "requireExplicitTrigger", publicName: "requireExplicitTrigger", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "menuEl", first: true, predicate: ["menuEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
1812
|
+
<div
|
|
1813
|
+
#menuEl
|
|
1814
|
+
class="dm-floating-menu"
|
|
1815
|
+
role="menu"
|
|
1816
|
+
aria-label="Insert block"
|
|
1817
|
+
data-dm-editor-ui=""
|
|
1818
|
+
(keydown)="onKeyDown($event)"
|
|
1819
|
+
>
|
|
1820
|
+
@for (group of groups(); track group.name || $index; let gi = $index) {
|
|
1821
|
+
@if (group.name) {
|
|
1822
|
+
<div class="dm-floating-menu-group-label" [id]="'dm-fm-g' + gi">{{ group.name }}</div>
|
|
1823
|
+
}
|
|
1824
|
+
<div
|
|
1825
|
+
class="dm-floating-menu-group"
|
|
1826
|
+
role="group"
|
|
1827
|
+
[attr.aria-labelledby]="group.name ? 'dm-fm-g' + gi : null"
|
|
1828
|
+
>
|
|
1829
|
+
@for (item of group.items; track item.name) {
|
|
1830
|
+
<button
|
|
1831
|
+
type="button"
|
|
1832
|
+
role="menuitem"
|
|
1833
|
+
class="dm-floating-menu-item"
|
|
1834
|
+
[attr.data-floating-menu-item]="item.name"
|
|
1835
|
+
[attr.data-floating-menu-index]="flatIndexOf(item.name)"
|
|
1836
|
+
[attr.tabindex]="tabIndexFor(item.name)"
|
|
1837
|
+
[attr.aria-disabled]="isItemDisabled(item) ? 'true' : null"
|
|
1838
|
+
[attr.aria-keyshortcuts]="item.shortcut"
|
|
1839
|
+
[disabled]="isItemDisabled(item)"
|
|
1840
|
+
(mousedown)="$event.preventDefault()"
|
|
1841
|
+
(click)="onItemClick(item)"
|
|
1842
|
+
>
|
|
1843
|
+
@if (iconHtml(item.icon); as html) {
|
|
1844
|
+
<span class="dm-floating-menu-item-icon" aria-hidden="true" [innerHTML]="html"></span>
|
|
1845
|
+
}
|
|
1846
|
+
<span class="dm-floating-menu-item-label">{{ item.label }}</span>
|
|
1847
|
+
@if (item.shortcut) {
|
|
1848
|
+
<span class="dm-floating-menu-item-shortcut" aria-hidden="true">{{ item.shortcut }}</span>
|
|
1849
|
+
}
|
|
1850
|
+
</button>
|
|
1851
|
+
}
|
|
1852
|
+
</div>
|
|
1853
|
+
}
|
|
1854
|
+
</div>
|
|
1855
|
+
`, isInline: true, styles: [":host{display:contents}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1249
1856
|
}
|
|
1250
1857
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalFloatingMenuComponent, decorators: [{
|
|
1251
1858
|
type: Component,
|
|
1252
|
-
args: [{ selector: 'domternal-floating-menu', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template:
|
|
1253
|
-
|
|
1859
|
+
args: [{ selector: 'domternal-floating-menu', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
|
|
1860
|
+
<div
|
|
1861
|
+
#menuEl
|
|
1862
|
+
class="dm-floating-menu"
|
|
1863
|
+
role="menu"
|
|
1864
|
+
aria-label="Insert block"
|
|
1865
|
+
data-dm-editor-ui=""
|
|
1866
|
+
(keydown)="onKeyDown($event)"
|
|
1867
|
+
>
|
|
1868
|
+
@for (group of groups(); track group.name || $index; let gi = $index) {
|
|
1869
|
+
@if (group.name) {
|
|
1870
|
+
<div class="dm-floating-menu-group-label" [id]="'dm-fm-g' + gi">{{ group.name }}</div>
|
|
1871
|
+
}
|
|
1872
|
+
<div
|
|
1873
|
+
class="dm-floating-menu-group"
|
|
1874
|
+
role="group"
|
|
1875
|
+
[attr.aria-labelledby]="group.name ? 'dm-fm-g' + gi : null"
|
|
1876
|
+
>
|
|
1877
|
+
@for (item of group.items; track item.name) {
|
|
1878
|
+
<button
|
|
1879
|
+
type="button"
|
|
1880
|
+
role="menuitem"
|
|
1881
|
+
class="dm-floating-menu-item"
|
|
1882
|
+
[attr.data-floating-menu-item]="item.name"
|
|
1883
|
+
[attr.data-floating-menu-index]="flatIndexOf(item.name)"
|
|
1884
|
+
[attr.tabindex]="tabIndexFor(item.name)"
|
|
1885
|
+
[attr.aria-disabled]="isItemDisabled(item) ? 'true' : null"
|
|
1886
|
+
[attr.aria-keyshortcuts]="item.shortcut"
|
|
1887
|
+
[disabled]="isItemDisabled(item)"
|
|
1888
|
+
(mousedown)="$event.preventDefault()"
|
|
1889
|
+
(click)="onItemClick(item)"
|
|
1890
|
+
>
|
|
1891
|
+
@if (iconHtml(item.icon); as html) {
|
|
1892
|
+
<span class="dm-floating-menu-item-icon" aria-hidden="true" [innerHTML]="html"></span>
|
|
1893
|
+
}
|
|
1894
|
+
<span class="dm-floating-menu-item-label">{{ item.label }}</span>
|
|
1895
|
+
@if (item.shortcut) {
|
|
1896
|
+
<span class="dm-floating-menu-item-shortcut" aria-hidden="true">{{ item.shortcut }}</span>
|
|
1897
|
+
}
|
|
1898
|
+
</button>
|
|
1899
|
+
}
|
|
1900
|
+
</div>
|
|
1901
|
+
}
|
|
1902
|
+
</div>
|
|
1903
|
+
`, styles: [":host{display:contents}\n"] }]
|
|
1904
|
+
}], 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 }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], keymap: [{ type: i0.Input, args: [{ isSignal: true, alias: "keymap", required: false }] }], icons: [{ type: i0.Input, args: [{ isSignal: true, alias: "icons", required: false }] }], requireExplicitTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "requireExplicitTrigger", required: false }] }], menuEl: [{ type: i0.ViewChild, args: ['menuEl', { isSignal: true }] }] } });
|
|
1254
1905
|
|
|
1255
1906
|
const CATEGORY_ICONS = {
|
|
1256
1907
|
'Smileys & Emotion': '\u{1F600}',
|
|
@@ -1305,7 +1956,9 @@ class DomternalEmojiPickerComponent {
|
|
|
1305
1956
|
// Re-evaluate when panel opens (isOpen changes)
|
|
1306
1957
|
this.isOpen();
|
|
1307
1958
|
const storage = this.getEmojiStorage();
|
|
1308
|
-
|
|
1959
|
+
if (!storage)
|
|
1960
|
+
return [];
|
|
1961
|
+
const getFreq = storage['getFrequentlyUsed'];
|
|
1309
1962
|
if (!getFreq)
|
|
1310
1963
|
return [];
|
|
1311
1964
|
const names = getFreq();
|
|
@@ -1319,7 +1972,7 @@ class DomternalEmojiPickerComponent {
|
|
|
1319
1972
|
constructor() {
|
|
1320
1973
|
effect(() => {
|
|
1321
1974
|
const editor = this.editor();
|
|
1322
|
-
untracked(() => this.setupEventListener(editor));
|
|
1975
|
+
untracked(() => { this.setupEventListener(editor); });
|
|
1323
1976
|
});
|
|
1324
1977
|
}
|
|
1325
1978
|
ngOnDestroy() {
|
|
@@ -1376,9 +2029,9 @@ class DomternalEmojiPickerComponent {
|
|
|
1376
2029
|
if (!swatches.length)
|
|
1377
2030
|
return;
|
|
1378
2031
|
const current = document.activeElement;
|
|
1379
|
-
|
|
2032
|
+
const idx = swatches.indexOf(current);
|
|
1380
2033
|
if (idx === -1) {
|
|
1381
|
-
// Focus is on grid container, not a swatch
|
|
2034
|
+
// Focus is on grid container, not a swatch - enter the grid
|
|
1382
2035
|
if (['ArrowRight', 'ArrowDown', 'ArrowLeft', 'ArrowUp'].includes(event.key)) {
|
|
1383
2036
|
event.preventDefault();
|
|
1384
2037
|
swatches[0]?.focus();
|
|
@@ -1481,14 +2134,14 @@ class DomternalEmojiPickerComponent {
|
|
|
1481
2134
|
!this.elRef.nativeElement.contains(target) &&
|
|
1482
2135
|
target !== this.anchorEl &&
|
|
1483
2136
|
!this.anchorEl?.contains(target)) {
|
|
1484
|
-
this.ngZone.run(() => this.close());
|
|
2137
|
+
this.ngZone.run(() => { this.close(); });
|
|
1485
2138
|
}
|
|
1486
2139
|
};
|
|
1487
2140
|
document.addEventListener('mousedown', this.clickOutsideHandler);
|
|
1488
2141
|
this.keydownHandler = (e) => {
|
|
1489
2142
|
if (e.key === 'Escape' && this.isOpen()) {
|
|
1490
2143
|
e.preventDefault();
|
|
1491
|
-
this.ngZone.run(() => this.close());
|
|
2144
|
+
this.ngZone.run(() => { this.close(); });
|
|
1492
2145
|
}
|
|
1493
2146
|
};
|
|
1494
2147
|
document.addEventListener('keydown', this.keydownHandler);
|
|
@@ -1698,9 +2351,462 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
|
|
|
1698
2351
|
}]
|
|
1699
2352
|
}], ctorParameters: () => [], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "editor", required: true }] }], emojis: [{ type: i0.Input, args: [{ isSignal: true, alias: "emojis", required: true }] }] } });
|
|
1700
2353
|
|
|
2354
|
+
/**
|
|
2355
|
+
* Display labels for the named-token palette. Used in tooltips / aria labels;
|
|
2356
|
+
* unknown tokens fall back to a title-cased version of the raw key.
|
|
2357
|
+
*/
|
|
2358
|
+
const TOKEN_LABELS = {
|
|
2359
|
+
gray: 'Gray',
|
|
2360
|
+
brown: 'Brown',
|
|
2361
|
+
orange: 'Orange',
|
|
2362
|
+
yellow: 'Yellow',
|
|
2363
|
+
green: 'Green',
|
|
2364
|
+
blue: 'Blue',
|
|
2365
|
+
purple: 'Purple',
|
|
2366
|
+
pink: 'Pink',
|
|
2367
|
+
red: 'Red',
|
|
2368
|
+
};
|
|
2369
|
+
class DomternalNotionColorPickerComponent {
|
|
2370
|
+
editor = input.required(...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
|
|
2371
|
+
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
|
|
2372
|
+
currentTextToken = signal(null, ...(ngDevMode ? [{ debugName: "currentTextToken" }] : /* istanbul ignore next */ []));
|
|
2373
|
+
currentBgToken = signal(null, ...(ngDevMode ? [{ debugName: "currentBgToken" }] : /* istanbul ignore next */ []));
|
|
2374
|
+
palette = signal([], ...(ngDevMode ? [{ debugName: "palette" }] : /* istanbul ignore next */ []));
|
|
2375
|
+
anchorEl = null;
|
|
2376
|
+
// Reference to the picker panel after it has been reparented into
|
|
2377
|
+
// `.dm-editor`. After reparenting `elRef.nativeElement.contains(panel)`
|
|
2378
|
+
// returns false, so any inside-click detection must consult `panelEl`
|
|
2379
|
+
// directly rather than walking up from the component host.
|
|
2380
|
+
panelEl = null;
|
|
2381
|
+
ngZone = inject(NgZone);
|
|
2382
|
+
elRef = inject(ElementRef);
|
|
2383
|
+
eventHandler = null;
|
|
2384
|
+
clickOutsideHandler = null;
|
|
2385
|
+
keydownHandler = null;
|
|
2386
|
+
selectionHandler = null;
|
|
2387
|
+
cleanupFloating = null;
|
|
2388
|
+
constructor() {
|
|
2389
|
+
effect(() => {
|
|
2390
|
+
const editor = this.editor();
|
|
2391
|
+
untracked(() => { this.setupEventListener(editor); });
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
ngOnDestroy() {
|
|
2395
|
+
this.cleanup();
|
|
2396
|
+
}
|
|
2397
|
+
tokenLabel(token) {
|
|
2398
|
+
return TOKEN_LABELS[token] ?? token.charAt(0).toUpperCase() + token.slice(1);
|
|
2399
|
+
}
|
|
2400
|
+
applyText(token) {
|
|
2401
|
+
const editor = this.editor();
|
|
2402
|
+
editor.commands.setTextColorToken(token);
|
|
2403
|
+
// Picker stays open so the user can chain picks; close mirrors the bubble
|
|
2404
|
+
// menu (outside-click / Escape / empty selection).
|
|
2405
|
+
this.syncFromSelection();
|
|
2406
|
+
}
|
|
2407
|
+
applyBg(token) {
|
|
2408
|
+
const editor = this.editor();
|
|
2409
|
+
editor.commands.setBackgroundColorToken(token);
|
|
2410
|
+
this.syncFromSelection();
|
|
2411
|
+
}
|
|
2412
|
+
/**
|
|
2413
|
+
* Arrow-key navigation across the 5-column swatch grids. Arrows move
|
|
2414
|
+
* within and across rows, Home/End jump to grid boundaries, Tab leaves
|
|
2415
|
+
* the picker (browser default), Enter/Space activates the focused swatch.
|
|
2416
|
+
* Focus moves sequentially through Text color → Background color so
|
|
2417
|
+
* keyboard users can reach any swatch without re-grabbing Tab.
|
|
2418
|
+
*/
|
|
2419
|
+
onPanelKeydown(event) {
|
|
2420
|
+
const cols = 5;
|
|
2421
|
+
// Query off the tracked panel; after reparenting into `.dm-editor` the
|
|
2422
|
+
// swatches are no longer descendants of the component host element.
|
|
2423
|
+
const root = this.panelEl ?? this.elRef.nativeElement;
|
|
2424
|
+
const swatches = Array.from(root.querySelectorAll('.dm-ncp-swatch'));
|
|
2425
|
+
if (!swatches.length)
|
|
2426
|
+
return;
|
|
2427
|
+
const active = document.activeElement;
|
|
2428
|
+
const idx = active ? swatches.indexOf(active) : -1;
|
|
2429
|
+
if (idx === -1)
|
|
2430
|
+
return;
|
|
2431
|
+
let next = idx;
|
|
2432
|
+
switch (event.key) {
|
|
2433
|
+
case 'ArrowRight':
|
|
2434
|
+
event.preventDefault();
|
|
2435
|
+
next = Math.min(idx + 1, swatches.length - 1);
|
|
2436
|
+
break;
|
|
2437
|
+
case 'ArrowLeft':
|
|
2438
|
+
event.preventDefault();
|
|
2439
|
+
next = Math.max(idx - 1, 0);
|
|
2440
|
+
break;
|
|
2441
|
+
case 'ArrowDown':
|
|
2442
|
+
event.preventDefault();
|
|
2443
|
+
next = Math.min(idx + cols, swatches.length - 1);
|
|
2444
|
+
break;
|
|
2445
|
+
case 'ArrowUp':
|
|
2446
|
+
event.preventDefault();
|
|
2447
|
+
next = Math.max(idx - cols, 0);
|
|
2448
|
+
break;
|
|
2449
|
+
case 'Home':
|
|
2450
|
+
event.preventDefault();
|
|
2451
|
+
next = 0;
|
|
2452
|
+
break;
|
|
2453
|
+
case 'End':
|
|
2454
|
+
event.preventDefault();
|
|
2455
|
+
next = swatches.length - 1;
|
|
2456
|
+
break;
|
|
2457
|
+
default:
|
|
2458
|
+
return;
|
|
2459
|
+
}
|
|
2460
|
+
swatches[next]?.focus();
|
|
2461
|
+
}
|
|
2462
|
+
close(opts = {}) {
|
|
2463
|
+
if (!this.isOpen())
|
|
2464
|
+
return;
|
|
2465
|
+
this.cleanupFloating?.();
|
|
2466
|
+
this.cleanupFloating = null;
|
|
2467
|
+
this.isOpen.set(false);
|
|
2468
|
+
this.setStorageOpen(false);
|
|
2469
|
+
this.anchorEl = null;
|
|
2470
|
+
this.panelEl = null;
|
|
2471
|
+
this.removeGlobalListeners();
|
|
2472
|
+
if (opts.refocus) {
|
|
2473
|
+
this.editor().view.focus();
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
setupEventListener(editor) {
|
|
2477
|
+
this.cleanup();
|
|
2478
|
+
this.eventHandler = (...args) => {
|
|
2479
|
+
const data = args[0];
|
|
2480
|
+
this.ngZone.run(() => {
|
|
2481
|
+
const anchor = data?.anchorElement;
|
|
2482
|
+
if (!anchor)
|
|
2483
|
+
return;
|
|
2484
|
+
// Toggle: clicking the same anchor while open closes the picker.
|
|
2485
|
+
if (this.isOpen() && this.anchorEl === anchor) {
|
|
2486
|
+
this.close({ refocus: true });
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
this.anchorEl = anchor;
|
|
2490
|
+
this.palette.set(this.readPalette());
|
|
2491
|
+
this.syncFromSelection();
|
|
2492
|
+
this.isOpen.set(true);
|
|
2493
|
+
this.setStorageOpen(true);
|
|
2494
|
+
this.addGlobalListeners();
|
|
2495
|
+
// Position panel against the trigger after Angular renders the DOM.
|
|
2496
|
+
// Focus management (move into the first/active swatch) runs on a
|
|
2497
|
+
// second rAF so positioning + paint has flushed; focusing during the
|
|
2498
|
+
// same frame as mount can race with the bubble-menu's blur handler
|
|
2499
|
+
// and momentarily hide the floating layer Playwright observes.
|
|
2500
|
+
requestAnimationFrame(() => {
|
|
2501
|
+
const panel = this.elRef.nativeElement.querySelector('.dm-notion-color-picker');
|
|
2502
|
+
if (panel && this.anchorEl) {
|
|
2503
|
+
// Reparent into `.dm-editor` so the panel inherits the editor's
|
|
2504
|
+
// CSS custom properties (`--dm-block-bg-*`, `--dm-block-text-*`,
|
|
2505
|
+
// surface/border tokens). All variables in @domternal/theme are
|
|
2506
|
+
// scoped to `.dm-editor`; without reparenting the picker would
|
|
2507
|
+
// sit as a sibling and render with bare fallbacks.
|
|
2508
|
+
const editorEl = this.anchorEl.closest('.dm-editor');
|
|
2509
|
+
if (editorEl && panel.parentElement !== editorEl) {
|
|
2510
|
+
editorEl.appendChild(panel);
|
|
2511
|
+
}
|
|
2512
|
+
this.panelEl = panel;
|
|
2513
|
+
this.cleanupFloating?.();
|
|
2514
|
+
this.cleanupFloating = positionFloating(this.anchorEl, panel, {
|
|
2515
|
+
placement: 'bottom-start',
|
|
2516
|
+
offsetValue: 4,
|
|
2517
|
+
});
|
|
2518
|
+
}
|
|
2519
|
+
requestAnimationFrame(() => {
|
|
2520
|
+
if (!this.isOpen())
|
|
2521
|
+
return; // bailed out between frames
|
|
2522
|
+
// Use the tracked panel reference: after reparenting into
|
|
2523
|
+
// `.dm-editor` the panel is no longer a descendant of the
|
|
2524
|
+
// component host, so `elRef.nativeElement.querySelector` returns
|
|
2525
|
+
// null and focus never lands.
|
|
2526
|
+
const p = this.panelEl;
|
|
2527
|
+
if (!p)
|
|
2528
|
+
return;
|
|
2529
|
+
const active = p.querySelector('.dm-ncp-swatch.dm-ncp-active');
|
|
2530
|
+
const fallback = p.querySelector('.dm-ncp-swatch--text[data-color="null"]');
|
|
2531
|
+
(active ?? fallback)?.focus({ preventScroll: true });
|
|
2532
|
+
});
|
|
2533
|
+
});
|
|
2534
|
+
});
|
|
2535
|
+
};
|
|
2536
|
+
editor.on('notionColorOpen', this.eventHandler);
|
|
2537
|
+
}
|
|
2538
|
+
addGlobalListeners() {
|
|
2539
|
+
this.clickOutsideHandler = (e) => {
|
|
2540
|
+
if (!this.isOpen())
|
|
2541
|
+
return;
|
|
2542
|
+
const target = e.target;
|
|
2543
|
+
// Clicks inside the reparented panel or on the anchor (which toggles
|
|
2544
|
+
// via the event handler) must not trigger outside-close. The panel
|
|
2545
|
+
// has been moved out of `elRef.nativeElement` to inherit editor CSS
|
|
2546
|
+
// vars, so we cannot rely on the host element for containment.
|
|
2547
|
+
if (this.panelEl?.contains(target))
|
|
2548
|
+
return;
|
|
2549
|
+
if (this.elRef.nativeElement.contains(target))
|
|
2550
|
+
return;
|
|
2551
|
+
if (this.anchorEl?.contains(target))
|
|
2552
|
+
return;
|
|
2553
|
+
this.ngZone.run(() => { this.close({ refocus: false }); });
|
|
2554
|
+
};
|
|
2555
|
+
document.addEventListener('mousedown', this.clickOutsideHandler);
|
|
2556
|
+
this.keydownHandler = (e) => {
|
|
2557
|
+
if (e.key === 'Escape' && this.isOpen()) {
|
|
2558
|
+
e.preventDefault();
|
|
2559
|
+
this.ngZone.run(() => { this.close({ refocus: true }); });
|
|
2560
|
+
}
|
|
2561
|
+
};
|
|
2562
|
+
document.addEventListener('keydown', this.keydownHandler);
|
|
2563
|
+
// Empty selection (user clicked elsewhere) closes the picker since the
|
|
2564
|
+
// bubble-menu anchor it sits on is about to vanish anyway.
|
|
2565
|
+
this.selectionHandler = () => {
|
|
2566
|
+
if (!this.isOpen())
|
|
2567
|
+
return;
|
|
2568
|
+
// Defensive: bubble menu may have vanished between transactions
|
|
2569
|
+
// (hideout, route change). Positioning against a detached anchor is
|
|
2570
|
+
// a no-op; close instead. Matches React/Vue behaviour.
|
|
2571
|
+
if (this.anchorEl && !this.anchorEl.isConnected) {
|
|
2572
|
+
this.ngZone.run(() => { this.close({ refocus: false }); });
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
if (this.editor().state.selection.empty) {
|
|
2576
|
+
this.ngZone.run(() => { this.close({ refocus: false }); });
|
|
2577
|
+
}
|
|
2578
|
+
else {
|
|
2579
|
+
// Selection changed but stays non-empty (e.g. shift+arrow): refresh
|
|
2580
|
+
// the active-state indicators against the new mark set.
|
|
2581
|
+
this.syncFromSelection();
|
|
2582
|
+
}
|
|
2583
|
+
};
|
|
2584
|
+
this.editor().on('selectionUpdate', this.selectionHandler);
|
|
2585
|
+
}
|
|
2586
|
+
removeGlobalListeners() {
|
|
2587
|
+
if (this.clickOutsideHandler) {
|
|
2588
|
+
document.removeEventListener('mousedown', this.clickOutsideHandler);
|
|
2589
|
+
this.clickOutsideHandler = null;
|
|
2590
|
+
}
|
|
2591
|
+
if (this.keydownHandler) {
|
|
2592
|
+
document.removeEventListener('keydown', this.keydownHandler);
|
|
2593
|
+
this.keydownHandler = null;
|
|
2594
|
+
}
|
|
2595
|
+
if (this.selectionHandler) {
|
|
2596
|
+
this.editor().off('selectionUpdate', this.selectionHandler);
|
|
2597
|
+
this.selectionHandler = null;
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
syncFromSelection() {
|
|
2601
|
+
const editor = this.editor();
|
|
2602
|
+
const { selection } = editor.state;
|
|
2603
|
+
// Empty cursor: stored-marks semantics (what newly-typed text inherits).
|
|
2604
|
+
// Non-empty: scan the range for the first text node carrying a textStyle
|
|
2605
|
+
// mark. `$from.nodeAfter` can land on a block (paragraph) when the
|
|
2606
|
+
// selection starts at a block boundary (e.g. AllSelection / selectAll),
|
|
2607
|
+
// so walking text nodes is necessary for correct active-state detection.
|
|
2608
|
+
let mark = null;
|
|
2609
|
+
if (selection.empty) {
|
|
2610
|
+
mark = selection.$from.marks().find((m) => m.type.name === 'textStyle') ?? null;
|
|
2611
|
+
}
|
|
2612
|
+
else {
|
|
2613
|
+
editor.state.doc.nodesBetween(selection.from, selection.to, (node) => {
|
|
2614
|
+
if (mark)
|
|
2615
|
+
return false;
|
|
2616
|
+
if (node.isText) {
|
|
2617
|
+
const found = node.marks.find((m) => m.type.name === 'textStyle');
|
|
2618
|
+
if (found)
|
|
2619
|
+
mark = found;
|
|
2620
|
+
}
|
|
2621
|
+
return true;
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2624
|
+
const attrs = (mark?.attrs ?? {});
|
|
2625
|
+
this.currentTextToken.set(attrs.colorToken ?? null);
|
|
2626
|
+
this.currentBgToken.set(attrs.backgroundColorToken ?? null);
|
|
2627
|
+
}
|
|
2628
|
+
readPalette() {
|
|
2629
|
+
const ext = this.editor().extensionManager.extensions.find((e) => e.name === 'notionColorPicker');
|
|
2630
|
+
const options = (ext?.options ?? null);
|
|
2631
|
+
return options?.palette ? [...options.palette] : [];
|
|
2632
|
+
}
|
|
2633
|
+
getStorage() {
|
|
2634
|
+
const storage = this.editor().storage;
|
|
2635
|
+
const slot = storage['notionColorPicker'];
|
|
2636
|
+
if (!slot || typeof slot !== 'object')
|
|
2637
|
+
return null;
|
|
2638
|
+
return slot;
|
|
2639
|
+
}
|
|
2640
|
+
setStorageOpen(open) {
|
|
2641
|
+
const storage = this.getStorage();
|
|
2642
|
+
if (storage)
|
|
2643
|
+
storage.isOpen = open;
|
|
2644
|
+
}
|
|
2645
|
+
cleanup() {
|
|
2646
|
+
this.removeGlobalListeners();
|
|
2647
|
+
this.cleanupFloating?.();
|
|
2648
|
+
this.cleanupFloating = null;
|
|
2649
|
+
if (this.eventHandler) {
|
|
2650
|
+
const editor = this.editor();
|
|
2651
|
+
editor.off('notionColorOpen', this.eventHandler);
|
|
2652
|
+
this.eventHandler = null;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalNotionColorPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2656
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: DomternalNotionColorPickerComponent, isStandalone: true, selector: "domternal-notion-color-picker", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
|
|
2657
|
+
@if (isOpen()) {
|
|
2658
|
+
<div
|
|
2659
|
+
class="dm-notion-color-picker"
|
|
2660
|
+
data-show
|
|
2661
|
+
data-dm-editor-ui
|
|
2662
|
+
role="dialog"
|
|
2663
|
+
aria-label="Text and background color"
|
|
2664
|
+
aria-modal="false"
|
|
2665
|
+
(keydown)="onPanelKeydown($event)"
|
|
2666
|
+
>
|
|
2667
|
+
<div class="dm-ncp-section">
|
|
2668
|
+
<div class="dm-ncp-label">Text color</div>
|
|
2669
|
+
<div class="dm-ncp-grid">
|
|
2670
|
+
<button
|
|
2671
|
+
type="button"
|
|
2672
|
+
class="dm-ncp-swatch dm-ncp-swatch--text"
|
|
2673
|
+
[class.dm-ncp-active]="currentTextToken() === null"
|
|
2674
|
+
[attr.aria-pressed]="currentTextToken() === null"
|
|
2675
|
+
data-color="null"
|
|
2676
|
+
title="Default text color"
|
|
2677
|
+
aria-label="Default text color"
|
|
2678
|
+
(mousedown)="$event.preventDefault()"
|
|
2679
|
+
(click)="applyText(null)"
|
|
2680
|
+
></button>
|
|
2681
|
+
@for (t of palette(); track t) {
|
|
2682
|
+
<button
|
|
2683
|
+
type="button"
|
|
2684
|
+
class="dm-ncp-swatch dm-ncp-swatch--text"
|
|
2685
|
+
[class.dm-ncp-active]="currentTextToken() === t"
|
|
2686
|
+
[attr.aria-pressed]="currentTextToken() === t"
|
|
2687
|
+
[attr.data-color]="t"
|
|
2688
|
+
[title]="tokenLabel(t)"
|
|
2689
|
+
[attr.aria-label]="tokenLabel(t) + ' text'"
|
|
2690
|
+
(mousedown)="$event.preventDefault()"
|
|
2691
|
+
(click)="applyText(t)"
|
|
2692
|
+
></button>
|
|
2693
|
+
}
|
|
2694
|
+
</div>
|
|
2695
|
+
</div>
|
|
2696
|
+
|
|
2697
|
+
<div class="dm-ncp-section">
|
|
2698
|
+
<div class="dm-ncp-label">Background color</div>
|
|
2699
|
+
<div class="dm-ncp-grid">
|
|
2700
|
+
<button
|
|
2701
|
+
type="button"
|
|
2702
|
+
class="dm-ncp-swatch dm-ncp-swatch--bg"
|
|
2703
|
+
[class.dm-ncp-active]="currentBgToken() === null"
|
|
2704
|
+
[attr.aria-pressed]="currentBgToken() === null"
|
|
2705
|
+
data-color="null"
|
|
2706
|
+
title="Default background"
|
|
2707
|
+
aria-label="Default background"
|
|
2708
|
+
(mousedown)="$event.preventDefault()"
|
|
2709
|
+
(click)="applyBg(null)"
|
|
2710
|
+
></button>
|
|
2711
|
+
@for (t of palette(); track t) {
|
|
2712
|
+
<button
|
|
2713
|
+
type="button"
|
|
2714
|
+
class="dm-ncp-swatch dm-ncp-swatch--bg"
|
|
2715
|
+
[class.dm-ncp-active]="currentBgToken() === t"
|
|
2716
|
+
[attr.aria-pressed]="currentBgToken() === t"
|
|
2717
|
+
[attr.data-color]="t"
|
|
2718
|
+
[title]="tokenLabel(t) + ' background'"
|
|
2719
|
+
[attr.aria-label]="tokenLabel(t) + ' background'"
|
|
2720
|
+
(mousedown)="$event.preventDefault()"
|
|
2721
|
+
(click)="applyBg(t)"
|
|
2722
|
+
></button>
|
|
2723
|
+
}
|
|
2724
|
+
</div>
|
|
2725
|
+
</div>
|
|
2726
|
+
</div>
|
|
2727
|
+
}
|
|
2728
|
+
`, isInline: true, styles: [":host{display:contents}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
2729
|
+
}
|
|
2730
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalNotionColorPickerComponent, decorators: [{
|
|
2731
|
+
type: Component,
|
|
2732
|
+
args: [{ selector: 'domternal-notion-color-picker', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
|
|
2733
|
+
@if (isOpen()) {
|
|
2734
|
+
<div
|
|
2735
|
+
class="dm-notion-color-picker"
|
|
2736
|
+
data-show
|
|
2737
|
+
data-dm-editor-ui
|
|
2738
|
+
role="dialog"
|
|
2739
|
+
aria-label="Text and background color"
|
|
2740
|
+
aria-modal="false"
|
|
2741
|
+
(keydown)="onPanelKeydown($event)"
|
|
2742
|
+
>
|
|
2743
|
+
<div class="dm-ncp-section">
|
|
2744
|
+
<div class="dm-ncp-label">Text color</div>
|
|
2745
|
+
<div class="dm-ncp-grid">
|
|
2746
|
+
<button
|
|
2747
|
+
type="button"
|
|
2748
|
+
class="dm-ncp-swatch dm-ncp-swatch--text"
|
|
2749
|
+
[class.dm-ncp-active]="currentTextToken() === null"
|
|
2750
|
+
[attr.aria-pressed]="currentTextToken() === null"
|
|
2751
|
+
data-color="null"
|
|
2752
|
+
title="Default text color"
|
|
2753
|
+
aria-label="Default text color"
|
|
2754
|
+
(mousedown)="$event.preventDefault()"
|
|
2755
|
+
(click)="applyText(null)"
|
|
2756
|
+
></button>
|
|
2757
|
+
@for (t of palette(); track t) {
|
|
2758
|
+
<button
|
|
2759
|
+
type="button"
|
|
2760
|
+
class="dm-ncp-swatch dm-ncp-swatch--text"
|
|
2761
|
+
[class.dm-ncp-active]="currentTextToken() === t"
|
|
2762
|
+
[attr.aria-pressed]="currentTextToken() === t"
|
|
2763
|
+
[attr.data-color]="t"
|
|
2764
|
+
[title]="tokenLabel(t)"
|
|
2765
|
+
[attr.aria-label]="tokenLabel(t) + ' text'"
|
|
2766
|
+
(mousedown)="$event.preventDefault()"
|
|
2767
|
+
(click)="applyText(t)"
|
|
2768
|
+
></button>
|
|
2769
|
+
}
|
|
2770
|
+
</div>
|
|
2771
|
+
</div>
|
|
2772
|
+
|
|
2773
|
+
<div class="dm-ncp-section">
|
|
2774
|
+
<div class="dm-ncp-label">Background color</div>
|
|
2775
|
+
<div class="dm-ncp-grid">
|
|
2776
|
+
<button
|
|
2777
|
+
type="button"
|
|
2778
|
+
class="dm-ncp-swatch dm-ncp-swatch--bg"
|
|
2779
|
+
[class.dm-ncp-active]="currentBgToken() === null"
|
|
2780
|
+
[attr.aria-pressed]="currentBgToken() === null"
|
|
2781
|
+
data-color="null"
|
|
2782
|
+
title="Default background"
|
|
2783
|
+
aria-label="Default background"
|
|
2784
|
+
(mousedown)="$event.preventDefault()"
|
|
2785
|
+
(click)="applyBg(null)"
|
|
2786
|
+
></button>
|
|
2787
|
+
@for (t of palette(); track t) {
|
|
2788
|
+
<button
|
|
2789
|
+
type="button"
|
|
2790
|
+
class="dm-ncp-swatch dm-ncp-swatch--bg"
|
|
2791
|
+
[class.dm-ncp-active]="currentBgToken() === t"
|
|
2792
|
+
[attr.aria-pressed]="currentBgToken() === t"
|
|
2793
|
+
[attr.data-color]="t"
|
|
2794
|
+
[title]="tokenLabel(t) + ' background'"
|
|
2795
|
+
[attr.aria-label]="tokenLabel(t) + ' background'"
|
|
2796
|
+
(mousedown)="$event.preventDefault()"
|
|
2797
|
+
(click)="applyBg(t)"
|
|
2798
|
+
></button>
|
|
2799
|
+
}
|
|
2800
|
+
</div>
|
|
2801
|
+
</div>
|
|
2802
|
+
</div>
|
|
2803
|
+
}
|
|
2804
|
+
`, styles: [":host{display:contents}\n"] }]
|
|
2805
|
+
}], ctorParameters: () => [], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "editor", required: true }] }] } });
|
|
2806
|
+
|
|
1701
2807
|
/**
|
|
1702
2808
|
* Generated bundle index. Do not edit.
|
|
1703
2809
|
*/
|
|
1704
2810
|
|
|
1705
|
-
export { DEFAULT_EXTENSIONS, DomternalBubbleMenuComponent, DomternalEditorComponent, DomternalEmojiPickerComponent, DomternalFloatingMenuComponent, DomternalToolbarComponent };
|
|
2811
|
+
export { DEFAULT_EXTENSIONS, DomternalBubbleMenuComponent, DomternalEditorComponent, DomternalEmojiPickerComponent, DomternalFloatingMenuComponent, DomternalNotionColorPickerComponent, DomternalToolbarComponent };
|
|
1706
2812
|
//# sourceMappingURL=domternal-angular.mjs.map
|