@domternal/angular 0.14.0 → 1.0.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.
@@ -1,7 +1,7 @@
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, refocusEditorAfterCommand, positionFloatingOnce, defaultIcons, ToolbarController, defaultBubbleContexts, PluginKey, createBubbleMenuPlugin, createFloatingMenuPlugin, FloatingMenuController, positionFloating } from '@domternal/core';
4
+ import { Document, Paragraph, Text, BaseKeymap, History, Editor, refocusEditorAfterCommand, positionFloatingOnce, defaultIcons, ToolbarController, defaultBubbleContexts, PluginKey, createBubbleShouldShow, createBubbleMenuPlugin, buildBubbleItemMaps, resolveBubbleNames, resolveBubbleMenuItems, createFloatingMenuPlugin, FloatingMenuController, positionFloating } from '@domternal/core';
5
5
  export { Editor } from '@domternal/core';
6
6
  import { DomSanitizer } from '@angular/platform-browser';
7
7
 
@@ -18,6 +18,12 @@ class DomternalEditorComponent {
18
18
  history = input(true, ...(ngDevMode ? [{ debugName: "history" }] : /* istanbul ignore next */ []));
19
19
  content = input('', ...(ngDevMode ? [{ debugName: "content" }] : /* istanbul ignore next */ []));
20
20
  editable = input(true, ...(ngDevMode ? [{ debugName: "editable" }] : /* istanbul ignore next */ []));
21
+ /**
22
+ * Editing experience preset. `'notion'` paints `dm-notion-mode` on this
23
+ * component's host and switches preset-aware extensions to their Notion
24
+ * behavior, replacing the hand-written class. Create-time only.
25
+ */
26
+ preset = input(undefined, ...(ngDevMode ? [{ debugName: "preset" }] : /* istanbul ignore next */ []));
21
27
  autofocus = input(false, ...(ngDevMode ? [{ debugName: "autofocus" }] : /* istanbul ignore next */ []));
22
28
  outputFormat = input('html', ...(ngDevMode ? [{ debugName: "outputFormat" }] : /* istanbul ignore next */ []));
23
29
  // === Outputs ===
@@ -146,12 +152,14 @@ class DomternalEditorComponent {
146
152
  const defaults = this.history()
147
153
  ? DEFAULT_EXTENSIONS
148
154
  : DEFAULT_EXTENSIONS.filter((extension) => extension.name !== 'history');
155
+ const preset = this.preset();
149
156
  this._editor = new Editor({
150
157
  element: this.editorRef().nativeElement,
151
158
  extensions: [...defaults, ...this.extensions()],
152
159
  content: initialContent,
153
160
  editable: this.editable(),
154
161
  autofocus: this.autofocus(),
162
+ ...(preset ? { preset } : {}),
155
163
  });
156
164
  this._isEditable.set(this.editable());
157
165
  // Set initial signal values
@@ -198,8 +206,8 @@ class DomternalEditorComponent {
198
206
  this.editorCreated.emit(editor);
199
207
  });
200
208
  }
201
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
202
- 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 }, history: { classPropertyName: "history", publicName: "history", 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: { attributes: { "data-dm-editor-ui": "" }, classAttribute: "dm-editor" }, providers: [
209
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DomternalEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
210
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.20", type: DomternalEditorComponent, isStandalone: true, selector: "domternal-editor", inputs: { extensions: { classPropertyName: "extensions", publicName: "extensions", isSignal: true, isRequired: false, transformFunction: null }, history: { classPropertyName: "history", publicName: "history", 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 }, preset: { classPropertyName: "preset", publicName: "preset", 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: { attributes: { "data-dm-editor-ui": "" }, classAttribute: "dm-editor" }, providers: [
203
211
  {
204
212
  provide: NG_VALUE_ACCESSOR,
205
213
  useExisting: forwardRef(() => DomternalEditorComponent),
@@ -207,7 +215,7 @@ class DomternalEditorComponent {
207
215
  },
208
216
  ], 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 });
209
217
  }
210
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalEditorComponent, decorators: [{
218
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DomternalEditorComponent, decorators: [{
211
219
  type: Component,
212
220
  args: [{ selector: 'domternal-editor', template: '<div #editorRef></div>', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: { class: 'dm-editor', 'data-dm-editor-ui': '' }, providers: [
213
221
  {
@@ -216,7 +224,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
216
224
  multi: true,
217
225
  },
218
226
  ], styles: [":host{display:block}\n"] }]
219
- }], ctorParameters: () => [], propDecorators: { editorRef: [{ type: i0.ViewChild, args: ['editorRef', { isSignal: true }] }], extensions: [{ type: i0.Input, args: [{ isSignal: true, alias: "extensions", required: false }] }], history: [{ type: i0.Input, args: [{ isSignal: true, alias: "history", 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"] }] } });
227
+ }], ctorParameters: () => [], propDecorators: { editorRef: [{ type: i0.ViewChild, args: ['editorRef', { isSignal: true }] }], extensions: [{ type: i0.Input, args: [{ isSignal: true, alias: "extensions", required: false }] }], history: [{ type: i0.Input, args: [{ isSignal: true, alias: "history", required: false }] }], content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }], preset: [{ type: i0.Input, args: [{ isSignal: true, alias: "preset", 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"] }] } });
220
228
 
221
229
  const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);
222
230
  class DomternalToolbarComponent {
@@ -231,6 +239,7 @@ class DomternalToolbarComponent {
231
239
  activeVersion = signal(0, ...(ngDevMode ? [{ debugName: "activeVersion" }] : /* istanbul ignore next */ []));
232
240
  controller = null;
233
241
  clickOutsideHandler = null;
242
+ escapeKeydownHandler = null;
234
243
  dismissOverlayHandler = null;
235
244
  editorEl = null;
236
245
  cleanupFloating = null;
@@ -569,6 +578,26 @@ class DomternalToolbarComponent {
569
578
  }
570
579
  };
571
580
  document.addEventListener('mousedown', this.clickOutsideHandler);
581
+ // Escape from anywhere: opening a dropdown with the mouse leaves focus
582
+ // outside the toolbar, so the host keydown handler never sees the key.
583
+ // Focus stays put, since the caret is usually still in the editor.
584
+ this.escapeKeydownHandler = (e) => {
585
+ if (!this.openDropdown())
586
+ return;
587
+ if (e.key !== 'Escape')
588
+ return;
589
+ // Focus inside the toolbar belongs to the host handler, which also
590
+ // restores focus. Not defaultPrevented: ProseMirror prevents Escape
591
+ // first whenever the caret is in the editor.
592
+ if (this.elRef.nativeElement.contains(document.activeElement))
593
+ return;
594
+ this.cleanupFloating?.();
595
+ this.cleanupFloating = null;
596
+ this.controller?.closeDropdown();
597
+ this.ngZone.run(() => { this.syncState(); });
598
+ e.preventDefault();
599
+ };
600
+ document.addEventListener('keydown', this.escapeKeydownHandler);
572
601
  // Listen for dismiss-overlays (e.g. table handle clicks that stopPropagation on mousedown)
573
602
  this.editorEl = editor.view.dom.closest('.dm-editor');
574
603
  if (this.editorEl) {
@@ -590,6 +619,10 @@ class DomternalToolbarComponent {
590
619
  document.removeEventListener('mousedown', this.clickOutsideHandler);
591
620
  this.clickOutsideHandler = null;
592
621
  }
622
+ if (this.escapeKeydownHandler) {
623
+ document.removeEventListener('keydown', this.escapeKeydownHandler);
624
+ this.escapeKeydownHandler = null;
625
+ }
593
626
  if (this.dismissOverlayHandler && this.editorEl) {
594
627
  this.editorEl.removeEventListener('dm:dismiss-overlays', this.dismissOverlayHandler);
595
628
  this.dismissOverlayHandler = null;
@@ -652,8 +685,8 @@ class DomternalToolbarComponent {
652
685
  const buttons = this.elRef.nativeElement.querySelectorAll('.dm-toolbar-button');
653
686
  buttons[idx]?.focus();
654
687
  }
655
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
656
- 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", "data-dm-editor-ui": "" }, listeners: { "keydown": "onKeydown($event)" }, properties: { "attr.aria-label": "\"Editor formatting\"" }, classAttribute: "dm-toolbar" }, ngImport: i0, template: `
688
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DomternalToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
689
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", 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", "data-dm-editor-ui": "" }, listeners: { "keydown": "onKeydown($event)" }, properties: { "attr.aria-label": "\"Editor formatting\"" }, classAttribute: "dm-toolbar" }, ngImport: i0, template: `
657
690
  @for (group of groups(); track group.name; let gi = $index) {
658
691
  @if (gi > 0) {
659
692
  <div class="dm-toolbar-separator" role="separator"></div>
@@ -755,7 +788,7 @@ class DomternalToolbarComponent {
755
788
  }
756
789
  `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
757
790
  }
758
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalToolbarComponent, decorators: [{
791
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DomternalToolbarComponent, decorators: [{
759
792
  type: Component,
760
793
  args: [{
761
794
  selector: 'domternal-toolbar',
@@ -872,15 +905,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
872
905
  }]
873
906
  }], 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 }] }] } });
874
907
 
875
- /** Check if a resolved position is inside a table cell or header. */
876
- function isInsideTableCell($pos) {
877
- for (let d = $pos.depth; d > 0; d--) {
878
- const name = $pos.node(d).type.name;
879
- if (name === 'tableCell' || name === 'tableHeader')
880
- return true;
881
- }
882
- return false;
883
- }
908
+ /* The item pipeline (which list this selection gets, what the schema allows,
909
+ which separators survive) is core's. This component renders the answer and
910
+ owns nothing about how it is reached. */
884
911
  class DomternalBubbleMenuComponent {
885
912
  editor = input.required(...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
886
913
  shouldShow = input(...(ngDevMode ? [undefined, { debugName: "shouldShow" }] : /* istanbul ignore next */ []));
@@ -889,7 +916,17 @@ class DomternalBubbleMenuComponent {
889
916
  updateDelay = input(0, ...(ngDevMode ? [{ debugName: "updateDelay" }] : /* istanbul ignore next */ []));
890
917
  /** Fixed item names (e.g. ['bold', 'italic', 'code']). Omit for auto mode (all format items). */
891
918
  items = input(undefined, ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
892
- /** Context-aware: map context names to item arrays, `true` for all valid items, or `null` to disable */
919
+ /**
920
+ * Context-aware: map context names to item arrays, `true` for all valid
921
+ * items, or `null` to disable.
922
+ *
923
+ * Typed through core's `BubbleContexts` rather than the same shape written
924
+ * out here. Angular emits an input's declared type into the package types,
925
+ * and an inline union came out of that emit as `Record<string, true |
926
+ * string[]>`: the `null` that disables a context was missing from the
927
+ * published type, so the documented way to switch one off did not
928
+ * type-check. A named type survives the trip intact.
929
+ */
893
930
  contexts = input(undefined, ...(ngDevMode ? [{ debugName: "contexts" }] : /* istanbul ignore next */ []));
894
931
  /**
895
932
  * Custom icon overrides. Falls back to default Phosphor icons for unmapped keys.
@@ -913,6 +950,15 @@ class DomternalBubbleMenuComponent {
913
950
  }
914
951
  /** Internal - updated on transactions. Not meant to be set from outside. */
915
952
  resolvedItems = signal([], ...(ngDevMode ? [{ debugName: "resolvedItems" }] : /* istanbul ignore next */ []));
953
+ /**
954
+ * The list core hands back, stored once. A default or fallback list comes
955
+ * back by reference and a signal set to its current value notifies nobody,
956
+ * so those passes cost nothing; a list resolved from names is rebuilt each
957
+ * time, as it always was.
958
+ */
959
+ setResolvedItems(items) {
960
+ this.resolvedItems.set(items);
961
+ }
916
962
  /** Internal - true when the BlockContextMenu extension is loaded; toggles the "..." trailing button. */
917
963
  showBlockMenuButton = signal(false, ...(ngDevMode ? [{ debugName: "showBlockMenuButton" }] : /* istanbul ignore next */ []));
918
964
  /** 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. */
@@ -921,6 +967,14 @@ class DomternalBubbleMenuComponent {
921
967
  isNodeSelection = signal(false, ...(ngDevMode ? [{ debugName: "isNodeSelection" }] : /* istanbul ignore next */ []));
922
968
  /** Internal - true when the NotionColorPicker extension is loaded; toggles the "A" color trigger. */
923
969
  showColorPickerButton = signal(false, ...(ngDevMode ? [{ debugName: "showColorPickerButton" }] : /* istanbul ignore next */ []));
970
+ /**
971
+ * The trailing triggers, resolved once for the template: each is read twice,
972
+ * for the button and for the separator that may lead it.
973
+ */
974
+ showColorTrigger = computed(() => this.showColorPickerButton() && !this.isNodeSelection(), ...(ngDevMode ? [{ debugName: "showColorTrigger" }] : /* istanbul ignore next */ []));
975
+ showBlockTrigger = computed(() => this.showBlockMenuButton() && !this.isNodeSelection(), ...(ngDevMode ? [{ debugName: "showBlockTrigger" }] : /* istanbul ignore next */ []));
976
+ /** notionColorPicker extension loaded (immutable per editor). */
977
+ hasNotionColorPicker = false;
924
978
  /** Current text color CSS variable expression for the trigger glyph (null = default). */
925
979
  currentTextColorVar = signal(null, ...(ngDevMode ? [{ debugName: "currentTextColorVar" }] : /* istanbul ignore next */ []));
926
980
  /** Current background color CSS variable expression for the trigger underline (null = transparent). */
@@ -932,12 +986,15 @@ class DomternalBubbleMenuComponent {
932
986
  sanitizer = inject(DomSanitizer);
933
987
  ngZone = inject(NgZone);
934
988
  activeVersion = signal(0, ...(ngDevMode ? [{ debugName: "activeVersion" }] : /* istanbul ignore next */ []));
935
- itemMap = new Map();
936
- dropdownMap = new Map();
989
+ /** Rebuilt per editor by `setupItemTracking`; empty until then. */
990
+ maps = {
991
+ itemMap: new Map(),
992
+ dropdownMap: new Map(),
993
+ bubbleDefaults: new Map(),
994
+ };
937
995
  activeMap = new Map();
938
996
  disabledMap = new Map();
939
997
  htmlCache = new Map();
940
- bubbleDefaults = new Map();
941
998
  transactionHandler = null;
942
999
  // Dropdown state for bubble-menu-embedded dropdowns (e.g. text-align).
943
1000
  openDropdown = signal(null, ...(ngDevMode ? [{ debugName: "openDropdown" }] : /* istanbul ignore next */ []));
@@ -948,11 +1005,8 @@ class DomternalBubbleMenuComponent {
948
1005
  dropdownCaret = '<svg class="dm-dropdown-caret" width="10" height="10" viewBox="0 0 10 10">' +
949
1006
  '<path d="M2 4l3 3 3-3" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
950
1007
  constructor() {
951
- // Each component instance needs a unique plugin key so that multiple
952
- // bubble-menus mounted in the same editor do not collide. Prefer
953
- // crypto.randomUUID over Math.random() for collision-free uniqueness
954
- // (Math.random across two simultaneous mounts has a small but real
955
- // collision probability, and SSR may share the random seed).
1008
+ // Each component instance needs a unique plugin key so that multiple bubble-
1009
+ // menus mounted in the same editor do not collide.
956
1010
  const crypto = globalThis.crypto;
957
1011
  const suffix = crypto?.randomUUID?.().slice(0, 8) ?? Math.random().toString(36).slice(2, 8);
958
1012
  this.pluginKey = new PluginKey('angularBubbleMenu-' + suffix);
@@ -963,37 +1017,15 @@ class DomternalBubbleMenuComponent {
963
1017
  });
964
1018
  afterNextRender(() => {
965
1019
  const editor = this.editor();
1020
+ /* Item tracking first, and the order is load-bearing: it is what builds
1021
+ `this.maps`, and the visibility rule below reads that object once.
1022
+ With the plugin registered first the rule closed over an empty map,
1023
+ and `bubbleDefaults.has(...)` was false for every node selection, so
1024
+ an image never got a menu at all. */
1025
+ this.setupItemTracking(editor);
966
1026
  const ctxs = this.resolveContexts(editor);
967
- let shouldShowFn = this.shouldShow();
968
- if (!shouldShowFn) {
969
- if (ctxs) {
970
- shouldShowFn = ({ state }) => {
971
- const context = this.detectContext(state.selection, ctxs);
972
- if (!context)
973
- return false;
974
- if (context in ctxs) {
975
- const val = ctxs[context];
976
- if (val === null)
977
- return false;
978
- return val === true || (Array.isArray(val) && val.length > 0);
979
- }
980
- return this.bubbleDefaults.has(context);
981
- };
982
- }
983
- else {
984
- // Auto/items mode: show when any endpoint's parent allows marks
985
- shouldShowFn = ({ state }) => {
986
- if (state.selection.empty)
987
- return false;
988
- if (state.selection.node)
989
- return this.bubbleDefaults.has(state.selection.node.type.name);
990
- if (isInsideTableCell(state.selection.$from))
991
- return false;
992
- return state.selection.$from.parent.type.spec.marks !== ''
993
- || state.selection.$to.parent.type.spec.marks !== '';
994
- };
995
- }
996
- }
1027
+ const shouldShowFn = this.shouldShow() ??
1028
+ createBubbleShouldShow(this.maps, ctxs);
997
1029
  const plugin = createBubbleMenuPlugin({
998
1030
  pluginKey: this.pluginKey,
999
1031
  editor,
@@ -1004,7 +1036,6 @@ class DomternalBubbleMenuComponent {
1004
1036
  updateDelay: this.updateDelay(),
1005
1037
  });
1006
1038
  editor.registerPlugin(plugin);
1007
- this.setupItemTracking(editor);
1008
1039
  });
1009
1040
  }
1010
1041
  ngOnDestroy() {
@@ -1079,168 +1110,29 @@ class DomternalBubbleMenuComponent {
1079
1110
  }));
1080
1111
  }
1081
1112
  // === Internal ===
1082
- buildItemMap(editor) {
1083
- this.itemMap.clear();
1084
- this.dropdownMap.clear();
1085
- for (const item of editor.toolbarItems) {
1086
- if (item.type === 'button') {
1087
- this.itemMap.set(item.name, item);
1088
- }
1089
- else if (item.type === 'dropdown') {
1090
- this.dropdownMap.set(item.name, item);
1091
- for (const sub of item.items) {
1092
- this.itemMap.set(sub.name, sub);
1093
- }
1094
- }
1095
- }
1096
- }
1097
- resolveNames(names) {
1098
- const result = [];
1099
- let sepIdx = 0;
1100
- for (const name of names) {
1101
- if (name === '|') {
1102
- result.push({ type: 'separator', name: `sep-${String(sepIdx++)}` });
1103
- }
1104
- else {
1105
- const dropdown = this.dropdownMap.get(name);
1106
- if (dropdown) {
1107
- result.push(dropdown);
1108
- continue;
1109
- }
1110
- const item = this.itemMap.get(name);
1111
- if (item)
1112
- result.push(item);
1113
- }
1114
- }
1115
- return result;
1116
- }
1117
- getFormatItems() {
1118
- return Array.from(this.itemMap.values())
1119
- .filter(item => item.group === 'format')
1120
- .sort((a, b) => (b.priority ?? 100) - (a.priority ?? 100));
1121
- }
1122
- detectContext(selection, ctxs) {
1123
- // CellSelection (duck-type: has $anchorCell) - never show bubble menu
1124
- if ('$anchorCell' in selection)
1125
- return null;
1126
- if (selection.node)
1127
- return selection.node.type.name;
1128
- if (selection.empty)
1129
- return null;
1130
- // TextSelection inside a table cell → 'table' context.
1131
- // Cross-cell TextSelection (drag across cells) → hide bubble menu.
1132
- const fromCell = this.findCellNode(selection.$from);
1133
- if (fromCell) {
1134
- const toCell = this.findCellNode(selection.$to);
1135
- if (toCell && fromCell !== toCell)
1136
- return null;
1137
- return 'table';
1138
- }
1139
- const fromName = selection.$from.parent.type.name;
1140
- if (fromName in ctxs)
1141
- return fromName;
1142
- if ('text' in ctxs && selection.$from.parent.type.spec.marks !== '')
1143
- return 'text';
1144
- // Cross-block: also check $to so bold/italic is offered when any endpoint allows marks
1145
- const toName = selection.$to.parent.type.name;
1146
- if (toName in ctxs)
1147
- return toName;
1148
- if ('text' in ctxs && selection.$to.parent.type.spec.marks !== '')
1149
- return 'text';
1150
- return null;
1151
- }
1152
- findCellNode(pos) {
1153
- for (let d = pos.depth; d > 0; d--) {
1154
- const node = pos.node(d);
1155
- if (node.type.name === 'tableCell' || node.type.name === 'tableHeader') {
1156
- return node;
1157
- }
1158
- }
1159
- return null;
1160
- }
1161
- /** Filters out mark items that the context node type doesn't allow (e.g. bold on codeBlock) */
1162
- filterBySchema(editor, contextName, items) {
1163
- if (contextName === 'text' || contextName === 'table')
1164
- return items;
1165
- const schema = editor.state.schema;
1166
- if (!schema)
1167
- return items;
1168
- const nodeType = schema.nodes[contextName];
1169
- if (!nodeType)
1170
- return items;
1171
- return items.filter(item => {
1172
- const markName = typeof item.isActive === 'string' ? item.isActive : null;
1173
- if (!markName)
1174
- return true;
1175
- const markType = schema.marks[markName];
1176
- if (!markType)
1177
- return true;
1178
- return nodeType.allowsMarkType(markType);
1179
- });
1180
- }
1181
- buildBubbleDefaults(editor) {
1182
- this.bubbleDefaults.clear();
1183
- const byCtx = new Map();
1184
- const addItem = (btn) => {
1185
- const ctx = btn['bubbleMenu'];
1186
- if (!ctx)
1187
- return;
1188
- let arr = byCtx.get(ctx);
1189
- if (!arr) {
1190
- arr = [];
1191
- byCtx.set(ctx, arr);
1192
- }
1193
- arr.push(btn);
1194
- };
1195
- for (const item of editor.toolbarItems) {
1196
- if (item.type === 'button')
1197
- addItem(item);
1198
- else if (item.type === 'dropdown') {
1199
- for (const sub of item.items)
1200
- addItem(sub);
1201
- }
1202
- }
1203
- for (const [ctx, items] of byCtx) {
1204
- items.sort((a, b) => (b.priority ?? 100) - (a.priority ?? 100));
1205
- const result = [];
1206
- let lastGroup;
1207
- let sepIdx = 0;
1208
- for (const item of items) {
1209
- if (lastGroup !== undefined && item.group !== lastGroup) {
1210
- result.push({ type: 'separator', name: `bsep-${String(sepIdx++)}` });
1211
- }
1212
- result.push(item);
1213
- lastGroup = item.group;
1214
- }
1215
- this.bubbleDefaults.set(ctx, result);
1216
- }
1217
- }
1218
1113
  setupItemTracking(editor) {
1219
- this.buildItemMap(editor);
1220
- this.buildBubbleDefaults(editor);
1114
+ /* One pipeline, built once per editor. Every renderer asks core the same
1115
+ question, so none of them answers it. */
1116
+ this.maps = buildBubbleItemMaps(editor);
1221
1117
  this.showBlockMenuButton.set(editor.extensionManager.extensions.some((e) => e.name === 'blockContextMenu'));
1222
- this.showColorPickerButton.set(editor.extensionManager.extensions.some((e) => e.name === 'notionColorPicker'));
1223
- const items = this.items();
1224
- const defaultItems = this.resolveNames(items ?? ['bold', 'italic', 'underline']);
1225
- if (this.resolveContexts(editor)) {
1226
- this.updateContextItems(editor);
1227
- }
1228
- else {
1229
- this.resolvedItems.set(defaultItems);
1230
- }
1118
+ // Presence cached; the show decision needs a live listener (per sync below).
1119
+ this.hasNotionColorPicker =
1120
+ editor.extensionManager.extensions.some((e) => e.name === 'notionColorPicker');
1121
+ const fallbackItems = resolveBubbleNames(this.items() ?? ['bold', 'italic', 'underline'], this.maps.itemMap, this.maps.dropdownMap);
1122
+ const refreshItems = () => {
1123
+ this.setResolvedItems(resolveBubbleMenuItems({
1124
+ editor,
1125
+ maps: this.maps,
1126
+ contexts: this.resolveContexts(editor),
1127
+ fallbackItems,
1128
+ }));
1129
+ };
1130
+ refreshItems();
1231
1131
  this.transactionHandler = () => {
1232
1132
  this.ngZone.run(() => {
1233
1133
  const sel = editor.state.selection;
1234
1134
  this.isNodeSelection.set(!!sel.node);
1235
- if (this.resolveContexts(editor)) {
1236
- this.updateContextItems(editor);
1237
- }
1238
- else if (sel.node && this.bubbleDefaults.has(sel.node.type.name)) {
1239
- this.resolvedItems.set(this.bubbleDefaults.get(sel.node.type.name) ?? []);
1240
- }
1241
- else {
1242
- this.resolvedItems.set(defaultItems);
1243
- }
1135
+ refreshItems();
1244
1136
  this.updateStates(editor);
1245
1137
  this.syncTrailingButtonsState(editor);
1246
1138
  this.activeVersion.update((v) => v + 1);
@@ -1258,6 +1150,8 @@ class DomternalBubbleMenuComponent {
1258
1150
  */
1259
1151
  syncTrailingButtonsState(editor) {
1260
1152
  this.isNodeSelection.set(!!editor.state.selection.node);
1153
+ // Hidden without a live notionColorOpen listener: the trigger would be dead.
1154
+ this.showColorPickerButton.set(this.hasNotionColorPicker && editor.listenerCount('notionColorOpen') > 0);
1261
1155
  if (this.showColorPickerButton())
1262
1156
  this.syncColorTriggerState(editor);
1263
1157
  if (this.showBlockMenuButton())
@@ -1293,35 +1187,6 @@ class DomternalBubbleMenuComponent {
1293
1187
  this.currentBgColorVar.set(bgToken ? `var(--dm-block-bg-${bgToken})` : null);
1294
1188
  this.hasAnyColor.set(textToken !== null || bgToken !== null);
1295
1189
  }
1296
- updateContextItems(editor) {
1297
- const ctxs = this.resolveContexts(editor);
1298
- if (!ctxs)
1299
- return;
1300
- const ctx = this.detectContext(editor.state.selection, ctxs);
1301
- if (!ctx) {
1302
- this.resolvedItems.set([]);
1303
- return;
1304
- }
1305
- if (ctx in ctxs) {
1306
- const val = ctxs[ctx];
1307
- if (val === null || (Array.isArray(val) && val.length === 0)) {
1308
- this.resolvedItems.set([]);
1309
- return;
1310
- }
1311
- if (val === true) {
1312
- this.resolvedItems.set(this.filterBySchema(editor, ctx, this.getFormatItems()));
1313
- }
1314
- else if (Array.isArray(val)) {
1315
- const resolved = this.resolveNames(val);
1316
- const buttons = resolved.filter((i) => i.type !== 'separator');
1317
- const filtered = new Set(this.filterBySchema(editor, ctx, buttons).map(b => b.name));
1318
- this.resolvedItems.set(resolved.filter(i => i.type === 'separator' || filtered.has(i.name)));
1319
- }
1320
- }
1321
- else {
1322
- this.resolvedItems.set(this.bubbleDefaults.get(ctx) ?? []);
1323
- }
1324
- }
1325
1190
  updateStates(editor) {
1326
1191
  let canProxy = null;
1327
1192
  try {
@@ -1401,21 +1266,13 @@ class DomternalBubbleMenuComponent {
1401
1266
  if (!isOpening)
1402
1267
  return;
1403
1268
  this.openDropdown.set(dropdown.name);
1404
- // NOTE: we deliberately do NOT broadcast `dm:dismiss-overlays` here.
1405
- // The bubble menu plugin in core listens for that event and would
1406
- // hide itself - taking our just-opened dropdown's trigger with it.
1407
- // Other overlays (color picker, link popover) close themselves via
1408
- // their own click-outside handlers when the user clicks our trigger,
1409
- // so cooperative dismissal still works.
1269
+ // NOTE: we deliberately do NOT broadcast `dm:dismiss-overlays` here. The
1270
+ // bubble menu plugin in core listens for that event and would hide itself -
1271
+ // taking our just-opened dropdown's trigger with it.
1410
1272
  const editorElBroadcast = this.menuEl().nativeElement.closest('.dm-editor');
1411
- // Position the panel against the trigger after Angular renders it.
1412
- // We deliberately keep the panel INSIDE `.dm-bubble-menu` so it
1413
- // inherits the bubble-menu-scoped button tokens (--dm-button-active-bg
1414
- // / --dm-button-active-color). Reparenting into `.dm-editor` was
1415
- // tempting for consistency with the color picker, but that dropped
1416
- // the active-state colors because those tokens aren't defined at
1417
- // editor scope. floating-ui handles cross-element positioning fine
1418
- // without reparenting.
1273
+ // Position the panel against the trigger after Angular renders it. We
1274
+ // deliberately keep the panel INSIDE `.dm-bubble-menu` so it inherits the
1275
+ // bubble-menu-scoped button tokens (--dm-button-active-bg / --dm-button-
1419
1276
  requestAnimationFrame(() => {
1420
1277
  const trigger = event?.currentTarget
1421
1278
  ?? this.menuEl().nativeElement.querySelector(`[data-dropdown="${dropdown.name}"]`);
@@ -1484,8 +1341,8 @@ class DomternalBubbleMenuComponent {
1484
1341
  this.dropdownCleanupFloating = null;
1485
1342
  this.openDropdown.set(null);
1486
1343
  }
1487
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1488
- 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: `
1344
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DomternalBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1345
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", 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: `
1489
1346
  <div #menuEl class="dm-bubble-menu" role="toolbar" aria-label="Text formatting">
1490
1347
  @for (item of resolvedItems(); track item.name) {
1491
1348
  @if (item.type === 'separator') {
@@ -1532,8 +1389,10 @@ class DomternalBubbleMenuComponent {
1532
1389
  (click)="executeCommand(asButton(item), $event)"></button>
1533
1390
  }
1534
1391
  }
1535
- @if (showColorPickerButton() && !isNodeSelection()) {
1536
- <span class="dm-toolbar-separator" role="separator"></span>
1392
+ @if (showColorTrigger()) {
1393
+ @if (resolvedItems().length > 0) {
1394
+ <span class="dm-toolbar-separator" role="separator"></span>
1395
+ }
1537
1396
  <button #colorBtn type="button" class="dm-toolbar-button dm-ncp-trigger"
1538
1397
  [class.dm-toolbar-button--active]="hasAnyColor()"
1539
1398
  title="Text and background color"
@@ -1547,8 +1406,10 @@ class DomternalBubbleMenuComponent {
1547
1406
  [style.background-color]="currentBgColorVar()"></span>
1548
1407
  </button>
1549
1408
  }
1550
- @if (showBlockMenuButton() && !isNodeSelection()) {
1551
- <span class="dm-toolbar-separator" role="separator"></span>
1409
+ @if (showBlockTrigger()) {
1410
+ @if (resolvedItems().length > 0 || showColorTrigger()) {
1411
+ <span class="dm-toolbar-separator" role="separator"></span>
1412
+ }
1552
1413
  <button #blockMenuBtn type="button" class="dm-toolbar-button"
1553
1414
  [disabled]="blockMenuButtonDisabled()"
1554
1415
  [title]="blockMenuButtonDisabled() ? 'Block actions (select within a single block)' : 'More options'"
@@ -1562,7 +1423,7 @@ class DomternalBubbleMenuComponent {
1562
1423
  </div>
1563
1424
  `, isInline: true, styles: [":host{display:contents}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1564
1425
  }
1565
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalBubbleMenuComponent, decorators: [{
1426
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DomternalBubbleMenuComponent, decorators: [{
1566
1427
  type: Component,
1567
1428
  args: [{ selector: 'domternal-bubble-menu', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
1568
1429
  <div #menuEl class="dm-bubble-menu" role="toolbar" aria-label="Text formatting">
@@ -1611,8 +1472,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
1611
1472
  (click)="executeCommand(asButton(item), $event)"></button>
1612
1473
  }
1613
1474
  }
1614
- @if (showColorPickerButton() && !isNodeSelection()) {
1615
- <span class="dm-toolbar-separator" role="separator"></span>
1475
+ @if (showColorTrigger()) {
1476
+ @if (resolvedItems().length > 0) {
1477
+ <span class="dm-toolbar-separator" role="separator"></span>
1478
+ }
1616
1479
  <button #colorBtn type="button" class="dm-toolbar-button dm-ncp-trigger"
1617
1480
  [class.dm-toolbar-button--active]="hasAnyColor()"
1618
1481
  title="Text and background color"
@@ -1626,8 +1489,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
1626
1489
  [style.background-color]="currentBgColorVar()"></span>
1627
1490
  </button>
1628
1491
  }
1629
- @if (showBlockMenuButton() && !isNodeSelection()) {
1630
- <span class="dm-toolbar-separator" role="separator"></span>
1492
+ @if (showBlockTrigger()) {
1493
+ @if (resolvedItems().length > 0 || showColorTrigger()) {
1494
+ <span class="dm-toolbar-separator" role="separator"></span>
1495
+ }
1631
1496
  <button #blockMenuBtn type="button" class="dm-toolbar-button"
1632
1497
  [disabled]="blockMenuButtonDisabled()"
1633
1498
  [title]="blockMenuButtonDisabled() ? 'Block actions (select within a single block)' : 'More options'"
@@ -1818,8 +1683,8 @@ class DomternalFloatingMenuComponent {
1818
1683
  return;
1819
1684
  }
1820
1685
  }
1821
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalFloatingMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1822
- 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: `
1686
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DomternalFloatingMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1687
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", 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: `
1823
1688
  <div
1824
1689
  #menuEl
1825
1690
  class="dm-floating-menu"
@@ -1865,7 +1730,7 @@ class DomternalFloatingMenuComponent {
1865
1730
  </div>
1866
1731
  `, isInline: true, styles: [":host{display:contents}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1867
1732
  }
1868
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalFloatingMenuComponent, decorators: [{
1733
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DomternalFloatingMenuComponent, decorators: [{
1869
1734
  type: Component,
1870
1735
  args: [{ selector: 'domternal-floating-menu', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
1871
1736
  <div
@@ -1920,10 +1785,10 @@ const CATEGORY_ICONS = {
1920
1785
  'Animals & Nature': '\u{1F431}',
1921
1786
  'Food & Drink': '\u{1F355}',
1922
1787
  'Travel & Places': '\u{1F697}',
1923
- 'Activities': '\u{26BD}',
1924
- 'Objects': '\u{1F4A1}',
1925
- 'Symbols': '\u{1F523}',
1926
- 'Flags': '\u{1F3C1}',
1788
+ Activities: '\u{26BD}',
1789
+ Objects: '\u{1F4A1}',
1790
+ Symbols: '\u{1F523}',
1791
+ Flags: '\u{1F3C1}',
1927
1792
  };
1928
1793
  class DomternalEmojiPickerComponent {
1929
1794
  editor = input.required(...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
@@ -1960,8 +1825,7 @@ class DomternalEmojiPickerComponent {
1960
1825
  if (searchFn) {
1961
1826
  return searchFn(query);
1962
1827
  }
1963
- return this.emojis().filter((item) => item.name.includes(query) ||
1964
- item.group.toLowerCase().includes(query));
1828
+ return this.emojis().filter((item) => item.name.includes(query) || item.group.toLowerCase().includes(query));
1965
1829
  }, ...(ngDevMode ? [{ debugName: "filteredEmojis" }] : /* istanbul ignore next */ []));
1966
1830
  frequentlyUsed = computed(() => {
1967
1831
  // Re-evaluate when panel opens (isOpen changes)
@@ -1978,12 +1842,17 @@ class DomternalEmojiPickerComponent {
1978
1842
  const nameMap = storage['_nameMap'];
1979
1843
  if (!nameMap)
1980
1844
  return [];
1981
- return names.slice(0, 16).map((n) => nameMap.get(n)).filter(Boolean);
1845
+ return names
1846
+ .slice(0, 16)
1847
+ .map((n) => nameMap.get(n))
1848
+ .filter(Boolean);
1982
1849
  }, ...(ngDevMode ? [{ debugName: "frequentlyUsed" }] : /* istanbul ignore next */ []));
1983
1850
  constructor() {
1984
1851
  effect(() => {
1985
1852
  const editor = this.editor();
1986
- untracked(() => { this.setupEventListener(editor); });
1853
+ untracked(() => {
1854
+ this.setupEventListener(editor);
1855
+ });
1987
1856
  });
1988
1857
  }
1989
1858
  ngOnDestroy() {
@@ -2025,7 +1894,8 @@ class DomternalEmojiPickerComponent {
2025
1894
  // Focus first emoji swatch after scroll completes
2026
1895
  setTimeout(() => {
2027
1896
  const firstSwatch = label.nextElementSibling;
2028
- if (firstSwatch instanceof HTMLElement && firstSwatch.classList.contains('dm-emoji-swatch')) {
1897
+ if (firstSwatch instanceof HTMLElement &&
1898
+ firstSwatch.classList.contains('dm-emoji-swatch')) {
2029
1899
  firstSwatch.focus();
2030
1900
  }
2031
1901
  }, 50);
@@ -2050,7 +1920,7 @@ class DomternalEmojiPickerComponent {
2050
1920
  return;
2051
1921
  }
2052
1922
  const cols = 8;
2053
- let next = idx;
1923
+ let next;
2054
1924
  switch (event.key) {
2055
1925
  case 'ArrowRight':
2056
1926
  event.preventDefault();
@@ -2073,7 +1943,8 @@ class DomternalEmojiPickerComponent {
2073
1943
  event.preventDefault();
2074
1944
  swatches[idx]?.click();
2075
1945
  return;
2076
- default: return;
1946
+ default:
1947
+ return;
2077
1948
  }
2078
1949
  swatches[next]?.focus();
2079
1950
  }
@@ -2145,14 +2016,18 @@ class DomternalEmojiPickerComponent {
2145
2016
  !this.elRef.nativeElement.contains(target) &&
2146
2017
  target !== this.anchorEl &&
2147
2018
  !this.anchorEl?.contains(target)) {
2148
- this.ngZone.run(() => { this.close(); });
2019
+ this.ngZone.run(() => {
2020
+ this.close();
2021
+ });
2149
2022
  }
2150
2023
  };
2151
2024
  document.addEventListener('mousedown', this.clickOutsideHandler);
2152
2025
  this.keydownHandler = (e) => {
2153
2026
  if (e.key === 'Escape' && this.isOpen()) {
2154
2027
  e.preventDefault();
2155
- this.ngZone.run(() => { this.close(); });
2028
+ this.ngZone.run(() => {
2029
+ this.close();
2030
+ });
2156
2031
  }
2157
2032
  };
2158
2033
  document.addEventListener('keydown', this.keydownHandler);
@@ -2188,8 +2063,8 @@ class DomternalEmojiPickerComponent {
2188
2063
  this.eventHandler = null;
2189
2064
  }
2190
2065
  }
2191
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalEmojiPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2192
- 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: `
2066
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DomternalEmojiPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2067
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", 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: `
2193
2068
  @if (isOpen()) {
2194
2069
  <div class="dm-emoji-picker">
2195
2070
  <div class="dm-emoji-picker-search">
@@ -2216,11 +2091,18 @@ class DomternalEmojiPickerComponent {
2216
2091
  [attr.aria-label]="cat"
2217
2092
  (mousedown)="$event.preventDefault()"
2218
2093
  (click)="scrollToCategory(cat)"
2219
- >{{ categoryIcon(cat) }}</button>
2094
+ >
2095
+ {{ categoryIcon(cat) }}
2096
+ </button>
2220
2097
  }
2221
2098
  </div>
2222
2099
 
2223
- <div class="dm-emoji-picker-grid" #grid (scroll)="onGridScroll()" (keydown)="onGridKeydown($event)">
2100
+ <div
2101
+ class="dm-emoji-picker-grid"
2102
+ #grid
2103
+ (scroll)="onGridScroll()"
2104
+ (keydown)="onGridKeydown($event)"
2105
+ >
2224
2106
  @if (searchQuery()) {
2225
2107
  @for (item of filteredEmojis(); track item.name) {
2226
2108
  <button
@@ -2231,9 +2113,10 @@ class DomternalEmojiPickerComponent {
2231
2113
  [attr.aria-label]="formatName(item.name)"
2232
2114
  (mousedown)="$event.preventDefault()"
2233
2115
  (click)="selectEmoji(item)"
2234
- >{{ item.emoji }}</button>
2235
- }
2236
- @empty {
2116
+ >
2117
+ {{ item.emoji }}
2118
+ </button>
2119
+ } @empty {
2237
2120
  <div class="dm-emoji-picker-empty">No emoji found</div>
2238
2121
  }
2239
2122
  } @else {
@@ -2248,7 +2131,9 @@ class DomternalEmojiPickerComponent {
2248
2131
  [attr.aria-label]="formatName(item.name)"
2249
2132
  (mousedown)="$event.preventDefault()"
2250
2133
  (click)="selectEmoji(item)"
2251
- >{{ item.emoji }}</button>
2134
+ >
2135
+ {{ item.emoji }}
2136
+ </button>
2252
2137
  }
2253
2138
  }
2254
2139
  @for (cat of categoryNames(); track cat) {
@@ -2262,7 +2147,9 @@ class DomternalEmojiPickerComponent {
2262
2147
  [attr.aria-label]="formatName(item.name)"
2263
2148
  (mousedown)="$event.preventDefault()"
2264
2149
  (click)="selectEmoji(item)"
2265
- >{{ item.emoji }}</button>
2150
+ >
2151
+ {{ item.emoji }}
2152
+ </button>
2266
2153
  }
2267
2154
  }
2268
2155
  }
@@ -2271,13 +2158,13 @@ class DomternalEmojiPickerComponent {
2271
2158
  }
2272
2159
  `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2273
2160
  }
2274
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalEmojiPickerComponent, decorators: [{
2161
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DomternalEmojiPickerComponent, decorators: [{
2275
2162
  type: Component,
2276
2163
  args: [{
2277
2164
  selector: 'domternal-emoji-picker',
2278
2165
  changeDetection: ChangeDetectionStrategy.OnPush,
2279
2166
  encapsulation: ViewEncapsulation.None,
2280
- host: { 'class': 'dm-emoji-picker-host' },
2167
+ host: { class: 'dm-emoji-picker-host' },
2281
2168
  template: `
2282
2169
  @if (isOpen()) {
2283
2170
  <div class="dm-emoji-picker">
@@ -2305,11 +2192,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
2305
2192
  [attr.aria-label]="cat"
2306
2193
  (mousedown)="$event.preventDefault()"
2307
2194
  (click)="scrollToCategory(cat)"
2308
- >{{ categoryIcon(cat) }}</button>
2195
+ >
2196
+ {{ categoryIcon(cat) }}
2197
+ </button>
2309
2198
  }
2310
2199
  </div>
2311
2200
 
2312
- <div class="dm-emoji-picker-grid" #grid (scroll)="onGridScroll()" (keydown)="onGridKeydown($event)">
2201
+ <div
2202
+ class="dm-emoji-picker-grid"
2203
+ #grid
2204
+ (scroll)="onGridScroll()"
2205
+ (keydown)="onGridKeydown($event)"
2206
+ >
2313
2207
  @if (searchQuery()) {
2314
2208
  @for (item of filteredEmojis(); track item.name) {
2315
2209
  <button
@@ -2320,9 +2214,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
2320
2214
  [attr.aria-label]="formatName(item.name)"
2321
2215
  (mousedown)="$event.preventDefault()"
2322
2216
  (click)="selectEmoji(item)"
2323
- >{{ item.emoji }}</button>
2324
- }
2325
- @empty {
2217
+ >
2218
+ {{ item.emoji }}
2219
+ </button>
2220
+ } @empty {
2326
2221
  <div class="dm-emoji-picker-empty">No emoji found</div>
2327
2222
  }
2328
2223
  } @else {
@@ -2337,7 +2232,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
2337
2232
  [attr.aria-label]="formatName(item.name)"
2338
2233
  (mousedown)="$event.preventDefault()"
2339
2234
  (click)="selectEmoji(item)"
2340
- >{{ item.emoji }}</button>
2235
+ >
2236
+ {{ item.emoji }}
2237
+ </button>
2341
2238
  }
2342
2239
  }
2343
2240
  @for (cat of categoryNames(); track cat) {
@@ -2351,7 +2248,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
2351
2248
  [attr.aria-label]="formatName(item.name)"
2352
2249
  (mousedown)="$event.preventDefault()"
2353
2250
  (click)="selectEmoji(item)"
2354
- >{{ item.emoji }}</button>
2251
+ >
2252
+ {{ item.emoji }}
2253
+ </button>
2355
2254
  }
2356
2255
  }
2357
2256
  }
@@ -2362,6 +2261,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
2362
2261
  }]
2363
2262
  }], ctorParameters: () => [], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "editor", required: true }] }], emojis: [{ type: i0.Input, args: [{ isSignal: true, alias: "emojis", required: true }] }] } });
2364
2263
 
2264
+ function paletteFromExtensionOptions(options) {
2265
+ if (typeof options !== 'object' || options === null || !('palette' in options))
2266
+ return [];
2267
+ const palette = options.palette;
2268
+ if (!Array.isArray(palette) || !palette.every((token) => typeof token === 'string')) {
2269
+ return [];
2270
+ }
2271
+ return [...palette];
2272
+ }
2365
2273
  /**
2366
2274
  * Display labels for the named-token palette. Used in tooltips / aria labels;
2367
2275
  * unknown tokens fall back to a title-cased version of the raw key.
@@ -2399,7 +2307,9 @@ class DomternalNotionColorPickerComponent {
2399
2307
  constructor() {
2400
2308
  effect(() => {
2401
2309
  const editor = this.editor();
2402
- untracked(() => { this.setupEventListener(editor); });
2310
+ untracked(() => {
2311
+ this.setupEventListener(editor);
2312
+ });
2403
2313
  });
2404
2314
  }
2405
2315
  ngOnDestroy() {
@@ -2439,7 +2349,7 @@ class DomternalNotionColorPickerComponent {
2439
2349
  const idx = active ? swatches.indexOf(active) : -1;
2440
2350
  if (idx === -1)
2441
2351
  return;
2442
- let next = idx;
2352
+ let next;
2443
2353
  switch (event.key) {
2444
2354
  case 'ArrowRight':
2445
2355
  event.preventDefault();
@@ -2561,13 +2471,17 @@ class DomternalNotionColorPickerComponent {
2561
2471
  return;
2562
2472
  if (this.anchorEl?.contains(target))
2563
2473
  return;
2564
- this.ngZone.run(() => { this.close({ refocus: false }); });
2474
+ this.ngZone.run(() => {
2475
+ this.close({ refocus: false });
2476
+ });
2565
2477
  };
2566
2478
  document.addEventListener('mousedown', this.clickOutsideHandler);
2567
2479
  this.keydownHandler = (e) => {
2568
2480
  if (e.key === 'Escape' && this.isOpen()) {
2569
2481
  e.preventDefault();
2570
- this.ngZone.run(() => { this.close({ refocus: true }); });
2482
+ this.ngZone.run(() => {
2483
+ this.close({ refocus: true });
2484
+ });
2571
2485
  }
2572
2486
  };
2573
2487
  document.addEventListener('keydown', this.keydownHandler);
@@ -2580,11 +2494,15 @@ class DomternalNotionColorPickerComponent {
2580
2494
  // (hideout, route change). Positioning against a detached anchor is
2581
2495
  // a no-op; close instead. Matches React/Vue behaviour.
2582
2496
  if (this.anchorEl && !this.anchorEl.isConnected) {
2583
- this.ngZone.run(() => { this.close({ refocus: false }); });
2497
+ this.ngZone.run(() => {
2498
+ this.close({ refocus: false });
2499
+ });
2584
2500
  return;
2585
2501
  }
2586
2502
  if (this.editor().state.selection.empty) {
2587
- this.ngZone.run(() => { this.close({ refocus: false }); });
2503
+ this.ngZone.run(() => {
2504
+ this.close({ refocus: false });
2505
+ });
2588
2506
  }
2589
2507
  else {
2590
2508
  // Selection changed but stays non-empty (e.g. shift+arrow): refresh
@@ -2638,8 +2556,7 @@ class DomternalNotionColorPickerComponent {
2638
2556
  }
2639
2557
  readPalette() {
2640
2558
  const ext = this.editor().extensionManager.extensions.find((e) => e.name === 'notionColorPicker');
2641
- const options = (ext?.options ?? null);
2642
- return options?.palette ? [...options.palette] : [];
2559
+ return paletteFromExtensionOptions(ext?.options);
2643
2560
  }
2644
2561
  getStorage() {
2645
2562
  const storage = this.editor().storage;
@@ -2663,8 +2580,8 @@ class DomternalNotionColorPickerComponent {
2663
2580
  this.eventHandler = null;
2664
2581
  }
2665
2582
  }
2666
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalNotionColorPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2667
- 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: `
2583
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DomternalNotionColorPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2584
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: DomternalNotionColorPickerComponent, isStandalone: true, selector: "domternal-notion-color-picker", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
2668
2585
  @if (isOpen()) {
2669
2586
  <div
2670
2587
  class="dm-notion-color-picker"
@@ -2738,7 +2655,7 @@ class DomternalNotionColorPickerComponent {
2738
2655
  }
2739
2656
  `, isInline: true, styles: [":host{display:contents}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2740
2657
  }
2741
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalNotionColorPickerComponent, decorators: [{
2658
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DomternalNotionColorPickerComponent, decorators: [{
2742
2659
  type: Component,
2743
2660
  args: [{ selector: 'domternal-notion-color-picker', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
2744
2661
  @if (isOpen()) {