@domternal/vue 0.6.2 → 0.7.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.
Files changed (62) hide show
  1. package/README.md +12 -10
  2. package/dist/Domternal.d.ts +34 -0
  3. package/dist/Domternal.d.ts.map +1 -0
  4. package/dist/DomternalEditor.d.ts +224 -0
  5. package/dist/DomternalEditor.d.ts.map +1 -0
  6. package/dist/DomternalFloatingMenu.d.ts +94 -0
  7. package/dist/DomternalFloatingMenu.d.ts.map +1 -0
  8. package/dist/EditorContent.d.ts +44 -0
  9. package/dist/EditorContent.d.ts.map +1 -0
  10. package/dist/EditorContext.d.ts +38 -0
  11. package/dist/EditorContext.d.ts.map +1 -0
  12. package/dist/bubble-menu/DomternalBubbleMenu.d.ts +87 -0
  13. package/dist/bubble-menu/DomternalBubbleMenu.d.ts.map +1 -0
  14. package/dist/bubble-menu/useBubbleMenu.d.ts +56 -0
  15. package/dist/bubble-menu/useBubbleMenu.d.ts.map +1 -0
  16. package/dist/emoji-picker/DomternalEmojiPicker.d.ts +31 -0
  17. package/dist/emoji-picker/DomternalEmojiPicker.d.ts.map +1 -0
  18. package/dist/emoji-picker/useEmojiPicker.d.ts +24 -0
  19. package/dist/emoji-picker/useEmojiPicker.d.ts.map +1 -0
  20. package/dist/index.d.ts +145 -33
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +962 -103
  23. package/dist/index.js.map +1 -1
  24. package/dist/node-views/NodeViewContent.d.ts +30 -0
  25. package/dist/node-views/NodeViewContent.d.ts.map +1 -0
  26. package/dist/node-views/NodeViewWrapper.d.ts +29 -0
  27. package/dist/node-views/NodeViewWrapper.d.ts.map +1 -0
  28. package/dist/node-views/VueNodeViewContext.d.ts +27 -0
  29. package/dist/node-views/VueNodeViewContext.d.ts.map +1 -0
  30. package/dist/node-views/VueNodeViewRenderer.d.ts +88 -0
  31. package/dist/node-views/VueNodeViewRenderer.d.ts.map +1 -0
  32. package/dist/notion-color-picker/DomternalNotionColorPicker.d.ts +22 -0
  33. package/dist/notion-color-picker/DomternalNotionColorPicker.d.ts.map +1 -0
  34. package/dist/notion-color-picker/index.d.ts +5 -0
  35. package/dist/notion-color-picker/index.d.ts.map +1 -0
  36. package/dist/notion-color-picker/useNotionColorPicker.d.ts +35 -0
  37. package/dist/notion-color-picker/useNotionColorPicker.d.ts.map +1 -0
  38. package/dist/toolbar/DomternalToolbar.d.ts +41 -0
  39. package/dist/toolbar/DomternalToolbar.d.ts.map +1 -0
  40. package/dist/toolbar/ToolbarButton.d.ts +72 -0
  41. package/dist/toolbar/ToolbarButton.d.ts.map +1 -0
  42. package/dist/toolbar/ToolbarDropdown.d.ts +76 -0
  43. package/dist/toolbar/ToolbarDropdown.d.ts.map +1 -0
  44. package/dist/toolbar/ToolbarDropdownPanel.d.ts +34 -0
  45. package/dist/toolbar/ToolbarDropdownPanel.d.ts.map +1 -0
  46. package/dist/toolbar/useComputedStyle.d.ts +12 -0
  47. package/dist/toolbar/useComputedStyle.d.ts.map +1 -0
  48. package/dist/toolbar/useKeyboardNav.d.ts +9 -0
  49. package/dist/toolbar/useKeyboardNav.d.ts.map +1 -0
  50. package/dist/toolbar/useToolbarController.d.ts +24 -0
  51. package/dist/toolbar/useToolbarController.d.ts.map +1 -0
  52. package/dist/toolbar/useToolbarIcons.d.ts +12 -0
  53. package/dist/toolbar/useToolbarIcons.d.ts.map +1 -0
  54. package/dist/toolbar/useTooltip.d.ts +5 -0
  55. package/dist/toolbar/useTooltip.d.ts.map +1 -0
  56. package/dist/useEditor.d.ts +63 -0
  57. package/dist/useEditor.d.ts.map +1 -0
  58. package/dist/useEditorState.d.ts +28 -0
  59. package/dist/useEditorState.d.ts.map +1 -0
  60. package/dist/utils.d.ts +39 -0
  61. package/dist/utils.d.ts.map +1 -0
  62. package/package.json +4 -2
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
- import { defineComponent, h, computed, Fragment, ref, onMounted, watch, onScopeDispose, watchEffect, inject, shallowRef, provide, getCurrentInstance, customRef, markRaw, shallowReactive, render } from 'vue';
2
- import { PluginKey, ToolbarController, createFloatingMenuPlugin, positionFloatingOnce, defaultIcons, createBubbleMenuPlugin, Editor, Document, Paragraph, Text, BaseKeymap, History } from '@domternal/core';
1
+ import { defineComponent, h, computed, Fragment, ref, watch, shallowRef, onMounted, nextTick, onScopeDispose, watchEffect, Teleport, inject, provide, getCurrentInstance, customRef, markRaw, shallowReactive, render } from 'vue';
2
+ import { positionFloatingOnce, PluginKey, positionFloating, ToolbarController, FloatingMenuController, defaultIcons, defaultBubbleContexts, createBubbleMenuPlugin, Editor, Document, Paragraph, Text, BaseKeymap, History } from '@domternal/core';
3
3
  export { Editor, generateHTML, generateJSON, generateText } from '@domternal/core';
4
+ import { createFloatingMenuPlugin } from '@domternal/extension-block-menu';
4
5
 
5
6
  // src/useEditor.ts
6
7
  var DEFAULT_EXTENSIONS = [Document, Paragraph, Text, BaseKeymap, History];
@@ -46,7 +47,7 @@ function useEditor(options = {}) {
46
47
  pendingContent = current.getJSON();
47
48
  options.onDestroy?.();
48
49
  const dom = current.view.dom;
49
- const parent = dom?.parentNode;
50
+ const parent = dom.parentNode;
50
51
  if (parent) {
51
52
  const clone = dom.cloneNode(true);
52
53
  clone.style.pointerEvents = "none";
@@ -242,7 +243,8 @@ function provideEditor(editor) {
242
243
  if (instance) {
243
244
  const buildCtx = () => {
244
245
  const ctx = Object.create(instance.appContext);
245
- ctx.provides = instance.provides;
246
+ const instanceWithProvides = instance;
247
+ ctx.provides = instanceWithProvides.provides;
246
248
  return ctx;
247
249
  };
248
250
  pendingAppContextStore.value = buildCtx();
@@ -344,8 +346,9 @@ function useToolbarController(editor, layout) {
344
346
  function isDropdownActive(dropdown) {
345
347
  if (dropdown.layout === "grid") return false;
346
348
  if (dropdown.dynamicLabel) return false;
347
- if (!controller) return false;
348
- return dropdown.items.some((item) => controller.activeMap.get(item.name) ?? false);
349
+ const ctl = controller;
350
+ if (!ctl) return false;
351
+ return dropdown.items.some((item) => ctl.activeMap.get(item.name) ?? false);
349
352
  }
350
353
  function getAriaExpanded(item) {
351
354
  if (!item.emitEvent) return null;
@@ -559,7 +562,9 @@ function useKeyboardNav(controllerRef, toolbarRef, closeDropdown) {
559
562
  const btn = document.activeElement;
560
563
  if (btn?.getAttribute("aria-haspopup") && btn.closest(".dm-toolbar")) {
561
564
  btn.click();
562
- requestAnimationFrame(() => focusDropdownItem(0, true));
565
+ requestAnimationFrame(() => {
566
+ focusDropdownItem(0, true);
567
+ });
563
568
  }
564
569
  }
565
570
  break;
@@ -594,31 +599,25 @@ function useKeyboardNav(controllerRef, toolbarRef, closeDropdown) {
594
599
  }
595
600
 
596
601
  // src/toolbar/useComputedStyle.ts
602
+ function resolveElementAtCursor(editor) {
603
+ const { from } = editor.state.selection;
604
+ const { node } = editor.view.domAtPos(from);
605
+ return node instanceof HTMLElement ? node : node.parentElement;
606
+ }
597
607
  function getComputedStyleAtCursor(editor, prop) {
598
608
  try {
599
- const { from } = editor.state.selection;
600
- const domAtPos = editor.view.domAtPos(from);
601
- let node = domAtPos.node;
602
- if (!(node instanceof HTMLElement)) {
603
- node = node.parentElement;
604
- }
609
+ const node = resolveElementAtCursor(editor);
605
610
  if (!node) return null;
606
- const el = node;
607
- const inline = el.style.getPropertyValue(prop);
611
+ const inline = node.style.getPropertyValue(prop);
608
612
  if (inline) return inline;
609
- return window.getComputedStyle(el).getPropertyValue(prop) || null;
613
+ return window.getComputedStyle(node).getPropertyValue(prop) || null;
610
614
  } catch {
611
615
  return null;
612
616
  }
613
617
  }
614
618
  function getInlineStyleAtCursor(editor, prop) {
615
619
  try {
616
- const { from } = editor.state.selection;
617
- const domAtPos = editor.view.domAtPos(from);
618
- let node = domAtPos.node;
619
- if (!(node instanceof HTMLElement)) {
620
- node = node.parentElement;
621
- }
620
+ const node = resolveElementAtCursor(editor);
622
621
  if (!node) return null;
623
622
  return node.style.getPropertyValue(prop) || null;
624
623
  } catch {
@@ -651,9 +650,15 @@ var ToolbarButton = defineComponent({
651
650
  "aria-expanded": props.ariaExpanded === "true" ? true : void 0,
652
651
  "aria-label": props.item.label,
653
652
  title: props.tooltip,
654
- onMousedown: (e) => e.preventDefault(),
655
- onClick: (e) => emit("click", props.item, e),
656
- onFocus: () => emit("focus", props.item.name)
653
+ onMousedown: (e) => {
654
+ e.preventDefault();
655
+ },
656
+ onClick: (e) => {
657
+ emit("click", props.item, e);
658
+ },
659
+ onFocus: () => {
660
+ emit("focus", props.item.name);
661
+ }
657
662
  });
658
663
  }
659
664
  });
@@ -689,8 +694,12 @@ var ToolbarDropdownPanel = defineComponent({
689
694
  "aria-label": sub.label,
690
695
  title: sub.label,
691
696
  style: { backgroundColor: sub.color },
692
- onMousedown: (e) => e.preventDefault(),
693
- onClick: (e) => emit("itemClick", sub, e)
697
+ onMousedown: (e) => {
698
+ e.preventDefault();
699
+ },
700
+ onClick: (e) => {
701
+ emit("itemClick", sub, e);
702
+ }
694
703
  }) : h("button", {
695
704
  key: sub.name,
696
705
  type: "button",
@@ -699,8 +708,12 @@ var ToolbarDropdownPanel = defineComponent({
699
708
  tabindex: -1,
700
709
  "aria-label": sub.label,
701
710
  innerHTML: getCachedItemContent(sub.icon, sub.label),
702
- onMousedown: (e) => e.preventDefault(),
703
- onClick: (e) => emit("itemClick", sub, e)
711
+ onMousedown: (e) => {
712
+ e.preventDefault();
713
+ },
714
+ onClick: (e) => {
715
+ emit("itemClick", sub, e);
716
+ }
704
717
  })
705
718
  )
706
719
  );
@@ -725,8 +738,12 @@ var ToolbarDropdownPanel = defineComponent({
725
738
  onVnodeMounted: (vnode) => {
726
739
  if (sub.style && vnode.el) vnode.el.setAttribute("style", sub.style);
727
740
  },
728
- onMousedown: (e) => e.preventDefault(),
729
- onClick: (e) => emit("itemClick", sub, e)
741
+ onMousedown: (e) => {
742
+ e.preventDefault();
743
+ },
744
+ onClick: (e) => {
745
+ emit("itemClick", sub, e);
746
+ }
730
747
  })
731
748
  )
732
749
  );
@@ -765,9 +782,15 @@ var ToolbarDropdown = defineComponent({
765
782
  disabled: props.isDisabled,
766
783
  "data-dropdown": props.dropdown.name,
767
784
  innerHTML: props.triggerHtml,
768
- onMousedown: (e) => e.preventDefault(),
769
- onClick: () => emit("toggle", props.dropdown),
770
- onFocus: () => emit("focus", props.dropdown.name)
785
+ onMousedown: (e) => {
786
+ e.preventDefault();
787
+ },
788
+ onClick: () => {
789
+ emit("toggle", props.dropdown);
790
+ },
791
+ onFocus: () => {
792
+ emit("focus", props.dropdown.name);
793
+ }
771
794
  })
772
795
  ];
773
796
  if (props.isOpen) {
@@ -776,7 +799,9 @@ var ToolbarDropdown = defineComponent({
776
799
  dropdown: props.dropdown,
777
800
  isActive: props.isActive,
778
801
  getCachedItemContent: props.getCachedItemContent,
779
- onItemClick: (item, event) => emit("itemClick", item, event)
802
+ onItemClick: (item, event) => {
803
+ emit("itemClick", item, event);
804
+ }
780
805
  })
781
806
  );
782
807
  }
@@ -828,20 +853,23 @@ var DomternalToolbar = defineComponent({
828
853
  closeDropdown();
829
854
  }
830
855
  if (item.emitEvent) {
831
- const anchor = event?.target?.closest?.(".dm-toolbar-button") ?? event?.target;
856
+ const target = event?.target;
857
+ const anchor = target?.closest(".dm-toolbar-button") ?? target;
832
858
  editor.emit(item.emitEvent, { anchorElement: anchor });
833
859
  return;
834
860
  }
835
861
  executeCommand(item);
836
- requestAnimationFrame(() => editor.view.focus());
862
+ requestAnimationFrame(() => {
863
+ editor.view.focus();
864
+ });
837
865
  }
838
866
  function onDropdownItemClick(item, event) {
839
867
  const editor = props.editor ?? contextEditor.value;
840
868
  if (!editor) return;
841
869
  let anchor;
842
870
  if (item.emitEvent) {
843
- const wrapper = event.target?.closest?.(".dm-toolbar-dropdown-wrapper");
844
- anchor = wrapper?.querySelector(".dm-toolbar-dropdown-trigger");
871
+ const wrapper = event.target.closest(".dm-toolbar-dropdown-wrapper");
872
+ anchor = wrapper?.querySelector(".dm-toolbar-dropdown-trigger") ?? void 0;
845
873
  }
846
874
  closeDropdown();
847
875
  if (item.emitEvent) {
@@ -849,7 +877,9 @@ var DomternalToolbar = defineComponent({
849
877
  } else {
850
878
  executeCommand(item);
851
879
  }
852
- requestAnimationFrame(() => editor.view.focus());
880
+ requestAnimationFrame(() => {
881
+ editor.view.focus();
882
+ });
853
883
  }
854
884
  function onButtonFocus(name) {
855
885
  const index = controllerRef.current?.getFlatIndex(name) ?? -1;
@@ -889,7 +919,9 @@ var DomternalToolbar = defineComponent({
889
919
  tooltip: getTooltip(btn),
890
920
  iconHtml: getCachedIcon(btn.icon),
891
921
  ariaExpanded: getAriaExpanded(btn),
892
- onClick: (clickedItem, event) => onButtonClick(clickedItem, event),
922
+ onClick: (clickedItem, event) => {
923
+ onButtonClick(clickedItem, event);
924
+ },
893
925
  onFocus: onButtonFocus
894
926
  });
895
927
  }
@@ -898,18 +930,18 @@ var DomternalToolbar = defineComponent({
898
930
  const activeItem = dd.items.find((sub) => controllerRef.current?.activeMap.get(sub.name));
899
931
  let triggerHtml = getDropdownTriggerHtml(dd, activeItem);
900
932
  if (dd.dynamicLabel && !activeItem && dd.computedStyleProperty) {
901
- let computed6;
933
+ let computed7;
902
934
  if (dd.computedStyleProperty === "font-family") {
903
- computed6 = getInlineStyleAtCursor(editor, dd.computedStyleProperty);
904
- if (computed6) {
905
- const first = computed6.split(",")[0]?.replace(/['"]+/g, "").trim();
906
- computed6 = first || null;
935
+ computed7 = getInlineStyleAtCursor(editor, dd.computedStyleProperty);
936
+ if (computed7) {
937
+ const first = computed7.split(",")[0]?.replace(/['"]+/g, "").trim();
938
+ computed7 = first ?? null;
907
939
  }
908
940
  } else {
909
- computed6 = getComputedStyleAtCursor(editor, dd.computedStyleProperty);
941
+ computed7 = getComputedStyleAtCursor(editor, dd.computedStyleProperty);
910
942
  }
911
- if (computed6) {
912
- triggerHtml = `<span class="dm-toolbar-trigger-label">${computed6}</span>${DROPDOWN_CARET}`;
943
+ if (computed7) {
944
+ triggerHtml = `<span class="dm-toolbar-trigger-label">${computed7}</span>${DROPDOWN_CARET}`;
913
945
  }
914
946
  }
915
947
  return h(ToolbarDropdown, {
@@ -936,6 +968,15 @@ var DomternalToolbar = defineComponent({
936
968
  };
937
969
  }
938
970
  });
971
+ var INITIAL_TRAILING_STATE = {
972
+ isNodeSelection: false,
973
+ showColorPickerButton: false,
974
+ showBlockMenuButton: false,
975
+ blockMenuButtonDisabled: false,
976
+ currentTextColorVar: null,
977
+ currentBgColorVar: null,
978
+ hasAnyColor: false
979
+ };
939
980
  function isInsideTableCell($pos) {
940
981
  for (let d = $pos.depth; d > 0; d--) {
941
982
  const name = $pos.node(d).type.name;
@@ -951,26 +992,37 @@ function findCellNode(pos) {
951
992
  return null;
952
993
  }
953
994
  function useBubbleMenu(options) {
954
- const { editor, shouldShow, placement = "top", offset = 8, updateDelay = 0, items, contexts } = options;
995
+ const { editor, shouldShow, placement = "top", offset = 8, updateDelay = 0, items, contexts: explicitContexts, icons: iconsRef } = options;
955
996
  const menuRef = ref();
956
- const pluginKey = new PluginKey("vueBubbleMenu-" + Math.random().toString(36).slice(2, 8));
997
+ const cryptoRef = globalThis.crypto;
998
+ const pluginKey = new PluginKey(
999
+ "vueBubbleMenu-" + (cryptoRef?.randomUUID?.().slice(0, 8) ?? Math.random().toString(36).slice(2, 8))
1000
+ );
957
1001
  const resolvedItems = shallowRef([]);
958
1002
  const activeVersion = useDebouncedRef(0);
1003
+ const trailing = shallowRef(INITIAL_TRAILING_STATE);
959
1004
  const activeMapRef = /* @__PURE__ */ new Map();
960
1005
  const disabledMapRef = /* @__PURE__ */ new Map();
961
1006
  let itemMap;
1007
+ let dropdownMap;
962
1008
  let bubbleDefaults;
963
1009
  let currentResolvedItems = [];
964
1010
  let initialized = false;
965
1011
  let stopEditorWatch = null;
966
1012
  const doInit = (ed) => {
967
- if (initialized || !ed || ed.isDestroyed || !menuRef.value) return;
1013
+ if (initialized || ed.isDestroyed || !menuRef.value) return;
968
1014
  initialized = true;
1015
+ const contexts = explicitContexts ?? (items ? void 0 : defaultBubbleContexts(ed));
1016
+ const exts = ed.extensionManager.extensions;
1017
+ const hasNotionColorPicker = exts.some((e) => e.name === "notionColorPicker");
1018
+ const hasBlockContextMenu = exts.some((e) => e.name === "blockContextMenu");
969
1019
  itemMap = /* @__PURE__ */ new Map();
1020
+ dropdownMap = /* @__PURE__ */ new Map();
970
1021
  for (const item of ed.toolbarItems) {
971
1022
  if (item.type === "button") {
972
1023
  itemMap.set(item.name, item);
973
1024
  } else if (item.type === "dropdown") {
1025
+ dropdownMap.set(item.name, item);
974
1026
  for (const sub of item.items) {
975
1027
  itemMap.set(sub.name, sub);
976
1028
  }
@@ -1001,7 +1053,7 @@ function useBubbleMenu(options) {
1001
1053
  let sepIdx = 0;
1002
1054
  for (const item of ctxItems) {
1003
1055
  if (lastGroup !== void 0 && item.group !== lastGroup) {
1004
- result.push({ type: "separator", name: `bsep-${sepIdx++}` });
1056
+ result.push({ type: "separator", name: `bsep-${String(sepIdx++)}` });
1005
1057
  }
1006
1058
  result.push(item);
1007
1059
  lastGroup = item.group;
@@ -1013,8 +1065,13 @@ function useBubbleMenu(options) {
1013
1065
  let sepIdx = 0;
1014
1066
  for (const name of names) {
1015
1067
  if (name === "|") {
1016
- result.push({ type: "separator", name: `sep-${sepIdx++}` });
1068
+ result.push({ type: "separator", name: `sep-${String(sepIdx++)}` });
1017
1069
  } else {
1070
+ const dropdown = dropdownMap.get(name);
1071
+ if (dropdown) {
1072
+ result.push(dropdown);
1073
+ continue;
1074
+ }
1018
1075
  const item = itemMap.get(name);
1019
1076
  if (item) result.push(item);
1020
1077
  }
@@ -1051,7 +1108,7 @@ function useBubbleMenu(options) {
1051
1108
  return schemaItems.filter((item) => {
1052
1109
  const markName = typeof item.isActive === "string" ? item.isActive : null;
1053
1110
  if (!markName) return true;
1054
- const markType = schema.marks?.[markName];
1111
+ const markType = schema.marks[markName];
1055
1112
  if (!markType) return true;
1056
1113
  return nodeType.allowsMarkType(markType);
1057
1114
  });
@@ -1071,7 +1128,8 @@ function useBubbleMenu(options) {
1071
1128
  };
1072
1129
  } else {
1073
1130
  shouldShowFn = ({ state }) => {
1074
- if (state.selection.empty || state.selection.node) return false;
1131
+ if (state.selection.empty) return false;
1132
+ if (state.selection.node) return bubbleDefaults.has(state.selection.node.type.name);
1075
1133
  if (isInsideTableCell(state.selection.$from)) return false;
1076
1134
  return state.selection.$from.parent.type.spec.marks !== "" || state.selection.$to.parent.type.spec.marks !== "";
1077
1135
  };
@@ -1104,26 +1162,77 @@ function useBubbleMenu(options) {
1104
1162
  canProxy = currentEd.can();
1105
1163
  } catch {
1106
1164
  }
1107
- for (const item of currentResolvedItems) {
1108
- if (item.type === "separator") continue;
1109
- activeMapRef.set(item.name, ToolbarController.resolveActive(currentEd, item));
1165
+ const trackButton = (btn) => {
1166
+ activeMapRef.set(btn.name, ToolbarController.resolveActive(currentEd, btn));
1110
1167
  try {
1111
- const canCmd = canProxy?.[item.command];
1112
- disabledMapRef.set(item.name, canCmd ? !(item.commandArgs?.length ? canCmd(...item.commandArgs) : canCmd()) : false);
1168
+ const canCmd = typeof btn.command === "string" ? canProxy?.[btn.command] : void 0;
1169
+ disabledMapRef.set(btn.name, canCmd ? !(btn.commandArgs?.length ? canCmd(...btn.commandArgs) : canCmd()) : false);
1113
1170
  } catch {
1114
- disabledMapRef.set(item.name, false);
1171
+ disabledMapRef.set(btn.name, false);
1115
1172
  }
1173
+ };
1174
+ for (const item of currentResolvedItems) {
1175
+ if (item.type === "separator") continue;
1176
+ if (item.type === "dropdown") {
1177
+ for (const sub of item.items) trackButton(sub);
1178
+ continue;
1179
+ }
1180
+ trackButton(item);
1181
+ }
1182
+ };
1183
+ const defaultItems = items ? resolveNames(items) : resolveNames(["bold", "italic", "underline"]);
1184
+ const syncTrailingState = (currentEd) => {
1185
+ const sel = currentEd.state.selection;
1186
+ const isNode = !!sel.node;
1187
+ let blockMenuDisabled = false;
1188
+ if (hasBlockContextMenu) {
1189
+ const { $from, $to } = currentEd.state.selection;
1190
+ if ($from.depth < 1 || $to.depth < 1) {
1191
+ blockMenuDisabled = true;
1192
+ } else {
1193
+ blockMenuDisabled = $from.before(1) !== $to.before(1);
1194
+ }
1195
+ }
1196
+ let textVar = null;
1197
+ let bgVar = null;
1198
+ let hasAny = false;
1199
+ if (hasNotionColorPicker) {
1200
+ const mark = currentEd.state.selection.$from.marks().find((m) => m.type.name === "textStyle");
1201
+ const attrs = mark?.attrs ?? {};
1202
+ const tToken = attrs.colorToken ?? null;
1203
+ const bToken = attrs.backgroundColorToken ?? null;
1204
+ textVar = tToken ? `var(--dm-block-text-${tToken})` : null;
1205
+ bgVar = bToken ? `var(--dm-block-bg-${bToken})` : null;
1206
+ hasAny = tToken !== null || bToken !== null;
1116
1207
  }
1208
+ trailing.value = {
1209
+ isNodeSelection: isNode,
1210
+ showColorPickerButton: hasNotionColorPicker,
1211
+ showBlockMenuButton: hasBlockContextMenu,
1212
+ blockMenuButtonDisabled: blockMenuDisabled,
1213
+ currentTextColorVar: textVar,
1214
+ currentBgColorVar: bgVar,
1215
+ hasAnyColor: hasAny
1216
+ };
1117
1217
  };
1118
1218
  const transactionHandler = () => {
1119
1219
  if (contexts) {
1120
1220
  updateContextItems(ed, contexts, detectContext, resolveNames, getFormatItems, filterBySchema, bubbleDefaults, setItems);
1221
+ } else {
1222
+ const sel = ed.state.selection;
1223
+ if (sel.node && bubbleDefaults.has(sel.node.type.name)) {
1224
+ setItems(bubbleDefaults.get(sel.node.type.name) ?? []);
1225
+ } else {
1226
+ setItems(defaultItems);
1227
+ }
1121
1228
  }
1122
1229
  updateStates(ed);
1230
+ syncTrailingState(ed);
1123
1231
  activeVersion.value++;
1124
1232
  };
1125
1233
  ed.on("transaction", transactionHandler);
1126
1234
  updateStates(ed);
1235
+ syncTrailingState(ed);
1127
1236
  initializedEditor = ed;
1128
1237
  initializedHandler = transactionHandler;
1129
1238
  };
@@ -1181,17 +1290,39 @@ function useBubbleMenu(options) {
1181
1290
  const isItemDisabled = (item) => {
1182
1291
  return disabledMapRef.get(item.name) ?? false;
1183
1292
  };
1184
- const executeCommand = (item) => {
1293
+ const executeCommand = (item, event) => {
1185
1294
  const ed = editor.value;
1186
1295
  if (!ed) return;
1187
1296
  if (item.emitEvent) {
1188
- ed.emit(item.emitEvent, {});
1297
+ const anchor = event?.currentTarget ?? event?.target ?? null;
1298
+ ed.emit(item.emitEvent, { anchorElement: anchor });
1189
1299
  return;
1190
1300
  }
1191
1301
  ToolbarController.executeItem(ed, item);
1192
1302
  };
1193
1303
  const getCachedIcon = (name) => {
1194
- return defaultIcons[name] ?? "";
1304
+ return iconsRef?.value?.[name] ?? defaultIcons[name] ?? "";
1305
+ };
1306
+ const openColorPicker = (anchor) => {
1307
+ const ed = editor.value;
1308
+ if (!ed) return;
1309
+ ed.emit(
1310
+ "notionColorOpen",
1311
+ { anchorElement: anchor }
1312
+ );
1313
+ };
1314
+ const openBlockContextMenu = (anchor) => {
1315
+ const ed = editor.value;
1316
+ if (!ed) return;
1317
+ const $from = ed.state.selection.$from;
1318
+ if ($from.depth < 1) return;
1319
+ const depth = $from.depth > 1 && $from.node($from.depth - 1).type.name !== "doc" ? $from.depth - 1 : $from.depth;
1320
+ const blockPos = $from.before(depth);
1321
+ const editorEl = ed.view.dom.closest(".dm-editor");
1322
+ editorEl?.dispatchEvent(new CustomEvent("dm:block-context-menu-open", {
1323
+ bubbles: false,
1324
+ detail: { blockPos, anchorElement: anchor }
1325
+ }));
1195
1326
  };
1196
1327
  return {
1197
1328
  menuRef,
@@ -1200,11 +1331,15 @@ function useBubbleMenu(options) {
1200
1331
  isItemDisabled,
1201
1332
  executeCommand,
1202
1333
  activeVersion,
1203
- getCachedIcon
1334
+ getCachedIcon,
1335
+ trailing,
1336
+ openColorPicker,
1337
+ openBlockContextMenu
1204
1338
  };
1205
1339
  }
1206
1340
 
1207
1341
  // src/bubble-menu/DomternalBubbleMenu.ts
1342
+ var DROPDOWN_CARET2 = '<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>';
1208
1343
  var DomternalBubbleMenu = defineComponent({
1209
1344
  name: "DomternalBubbleMenu",
1210
1345
  props: {
@@ -1214,10 +1349,13 @@ var DomternalBubbleMenu = defineComponent({
1214
1349
  offset: { type: Number, default: 8 },
1215
1350
  updateDelay: { type: Number, default: 0 },
1216
1351
  items: { type: Array, default: void 0 },
1217
- contexts: { type: Object, default: void 0 }
1352
+ contexts: { type: Object, default: void 0 },
1353
+ icons: { type: Object, default: void 0 }
1218
1354
  },
1219
1355
  setup(props, { slots }) {
1220
1356
  const { editor: contextEditor } = useCurrentEditor();
1357
+ const editorRef = computed(() => props.editor ?? contextEditor.value);
1358
+ const iconsRef = computed(() => props.icons);
1221
1359
  const {
1222
1360
  menuRef,
1223
1361
  resolvedItems,
@@ -1225,39 +1363,244 @@ var DomternalBubbleMenu = defineComponent({
1225
1363
  isItemDisabled,
1226
1364
  executeCommand,
1227
1365
  activeVersion,
1228
- getCachedIcon
1366
+ getCachedIcon,
1367
+ trailing,
1368
+ openColorPicker,
1369
+ openBlockContextMenu
1229
1370
  } = useBubbleMenu({
1230
- editor: computed(() => props.editor ?? contextEditor.value),
1371
+ editor: editorRef,
1231
1372
  shouldShow: props.shouldShow,
1232
1373
  placement: props.placement,
1233
1374
  offset: props.offset,
1234
1375
  updateDelay: props.updateDelay,
1235
1376
  items: props.items,
1236
- contexts: props.contexts
1377
+ contexts: props.contexts,
1378
+ icons: iconsRef
1237
1379
  });
1380
+ const openDropdown = ref(null);
1381
+ const closeDropdown = () => {
1382
+ openDropdown.value = null;
1383
+ };
1384
+ const executeSubItem = (sub) => {
1385
+ closeDropdown();
1386
+ const ed = editorRef.value;
1387
+ if (!ed) return;
1388
+ ToolbarController.executeItem(ed, sub);
1389
+ requestAnimationFrame(() => {
1390
+ ed.view.focus();
1391
+ });
1392
+ };
1393
+ const colorBtnRef = ref();
1394
+ const blockMenuBtnRef = ref();
1238
1395
  return () => {
1239
1396
  void activeVersion.value;
1240
- return h("div", { ref: menuRef, class: "dm-bubble-menu", role: "toolbar", "aria-label": "Text formatting" }, [
1241
- ...resolvedItems.value.map((item) => {
1242
- if (item.type === "separator") {
1243
- return h("span", { key: item.name, class: "dm-toolbar-separator", role: "separator" });
1397
+ const t = trailing.value;
1398
+ const children = [];
1399
+ for (const item of resolvedItems.value) {
1400
+ if (item.type === "separator") {
1401
+ children.push(h("span", { key: item.name, class: "dm-toolbar-separator", role: "separator" }));
1402
+ continue;
1403
+ }
1404
+ if (item.type === "dropdown") {
1405
+ children.push(
1406
+ h(BubbleDropdown, {
1407
+ key: item.name,
1408
+ dropdown: item,
1409
+ isOpen: openDropdown.value === item.name,
1410
+ isItemActive,
1411
+ getCachedIcon,
1412
+ activeVersion: activeVersion.value,
1413
+ executeSubItem,
1414
+ onToggle: () => {
1415
+ openDropdown.value = openDropdown.value === item.name ? null : item.name;
1416
+ },
1417
+ onClose: closeDropdown
1418
+ })
1419
+ );
1420
+ continue;
1421
+ }
1422
+ const btn = item;
1423
+ const active = isItemActive(btn);
1424
+ children.push(h("button", {
1425
+ key: btn.name,
1426
+ type: "button",
1427
+ class: ["dm-toolbar-button", active && "dm-toolbar-button--active"],
1428
+ disabled: isItemDisabled(btn),
1429
+ "aria-label": btn.label,
1430
+ "aria-pressed": active,
1431
+ title: btn.label,
1432
+ innerHTML: getCachedIcon(btn.icon),
1433
+ onMousedown: (e) => {
1434
+ e.preventDefault();
1435
+ },
1436
+ onClick: (e) => {
1437
+ executeCommand(btn, e);
1438
+ }
1439
+ }));
1440
+ }
1441
+ if (t.showColorPickerButton && !t.isNodeSelection) {
1442
+ children.push(
1443
+ h("span", { class: "dm-toolbar-separator", role: "separator" }),
1444
+ h("button", {
1445
+ ref: colorBtnRef,
1446
+ type: "button",
1447
+ class: ["dm-toolbar-button", "dm-ncp-trigger", t.hasAnyColor && "dm-toolbar-button--active"],
1448
+ title: "Text and background color",
1449
+ "aria-label": "Text and background color",
1450
+ "aria-haspopup": "dialog",
1451
+ onMousedown: (e) => {
1452
+ e.preventDefault();
1453
+ },
1454
+ onClick: () => {
1455
+ if (colorBtnRef.value) openColorPicker(colorBtnRef.value);
1456
+ }
1457
+ }, [
1458
+ h("span", {
1459
+ class: "dm-ncp-trigger-glyph",
1460
+ style: t.currentTextColorVar ? { color: t.currentTextColorVar } : void 0
1461
+ }, "A"),
1462
+ h("span", {
1463
+ class: "dm-ncp-trigger-underline",
1464
+ style: t.currentBgColorVar ? { backgroundColor: t.currentBgColorVar } : void 0
1465
+ })
1466
+ ])
1467
+ );
1468
+ }
1469
+ if (t.showBlockMenuButton && !t.isNodeSelection) {
1470
+ children.push(
1471
+ h("span", { class: "dm-toolbar-separator", role: "separator" }),
1472
+ h("button", {
1473
+ ref: blockMenuBtnRef,
1474
+ type: "button",
1475
+ class: "dm-toolbar-button",
1476
+ disabled: t.blockMenuButtonDisabled,
1477
+ title: t.blockMenuButtonDisabled ? "Block actions (select within a single block)" : "More options",
1478
+ "aria-label": "More options",
1479
+ "aria-haspopup": "menu",
1480
+ innerHTML: getCachedIcon("dotsThree"),
1481
+ onMousedown: (e) => {
1482
+ e.preventDefault();
1483
+ },
1484
+ onClick: () => {
1485
+ if (blockMenuBtnRef.value) openBlockContextMenu(blockMenuBtnRef.value);
1486
+ }
1487
+ })
1488
+ );
1489
+ }
1490
+ const slotContent = slots["default"]?.();
1491
+ if (slotContent) children.push(...slotContent);
1492
+ return h("div", {
1493
+ ref: menuRef,
1494
+ class: "dm-bubble-menu",
1495
+ role: "toolbar",
1496
+ "aria-label": "Text formatting"
1497
+ }, children);
1498
+ };
1499
+ }
1500
+ });
1501
+ var BubbleDropdown = defineComponent({
1502
+ name: "BubbleDropdown",
1503
+ props: {
1504
+ dropdown: { type: Object, required: true },
1505
+ isOpen: { type: Boolean, required: true },
1506
+ isItemActive: { type: Function, required: true },
1507
+ getCachedIcon: { type: Function, required: true },
1508
+ activeVersion: { type: Number, required: true },
1509
+ executeSubItem: { type: Function, required: true }
1510
+ },
1511
+ emits: ["toggle", "close"],
1512
+ setup(props, { emit }) {
1513
+ const triggerRef = ref();
1514
+ const panelRef = ref();
1515
+ watch(
1516
+ () => props.isOpen,
1517
+ (open, _old, onCleanup) => {
1518
+ if (!open) return;
1519
+ const trigger = triggerRef.value;
1520
+ const panel = panelRef.value;
1521
+ if (!trigger || !panel) return;
1522
+ const cleanupFloating = positionFloatingOnce(trigger, panel, {
1523
+ placement: "bottom-start",
1524
+ offsetValue: 4
1525
+ });
1526
+ const controller = new AbortController();
1527
+ const { signal } = controller;
1528
+ document.addEventListener("mousedown", (e) => {
1529
+ const target = e.target;
1530
+ if (!target) return;
1531
+ if (panel.contains(target)) return;
1532
+ if (trigger.contains(target)) return;
1533
+ emit("close");
1534
+ }, { signal });
1535
+ document.addEventListener("keydown", (e) => {
1536
+ if (e.key === "Escape") {
1537
+ e.preventDefault();
1538
+ emit("close");
1244
1539
  }
1245
- const btn = item;
1246
- const active = isItemActive(btn);
1540
+ }, { signal });
1541
+ const editorEl = trigger.closest(".dm-editor");
1542
+ editorEl?.addEventListener("dm:dismiss-overlays", () => {
1543
+ emit("close");
1544
+ }, { signal });
1545
+ onCleanup(() => {
1546
+ controller.abort();
1547
+ cleanupFloating();
1548
+ });
1549
+ },
1550
+ { flush: "post" }
1551
+ );
1552
+ return () => {
1553
+ void props.activeVersion;
1554
+ const dropdown = props.dropdown;
1555
+ const dropdownActive = dropdown.items.some((sub) => props.isItemActive(sub));
1556
+ const activeChild = dropdown.dynamicIcon ? dropdown.items.find((sub) => props.isItemActive(sub)) : void 0;
1557
+ const triggerIcon = activeChild?.icon ?? dropdown.icon;
1558
+ const triggerHtml = props.getCachedIcon(triggerIcon) + DROPDOWN_CARET2;
1559
+ return h("div", {
1560
+ class: "dm-toolbar-dropdown-wrapper",
1561
+ "data-dropdown-wrapper": dropdown.name
1562
+ }, [
1563
+ h("button", {
1564
+ ref: triggerRef,
1565
+ type: "button",
1566
+ class: ["dm-toolbar-button", "dm-toolbar-dropdown-trigger", dropdownActive && "dm-toolbar-button--active"],
1567
+ "aria-expanded": props.isOpen,
1568
+ "aria-haspopup": "true",
1569
+ "aria-label": dropdown.label,
1570
+ title: dropdown.label,
1571
+ "data-dropdown": dropdown.name,
1572
+ innerHTML: triggerHtml,
1573
+ onMousedown: (e) => {
1574
+ e.preventDefault();
1575
+ },
1576
+ onClick: () => {
1577
+ emit("toggle");
1578
+ }
1579
+ }),
1580
+ props.isOpen ? h("div", {
1581
+ ref: panelRef,
1582
+ class: "dm-toolbar-dropdown-panel",
1583
+ role: "menu",
1584
+ "data-dm-editor-ui": "",
1585
+ "data-dropdown-panel": dropdown.name
1586
+ }, dropdown.items.map((sub) => {
1587
+ const subActive = props.isItemActive(sub);
1588
+ const subHtml = `${props.getCachedIcon(sub.icon)} ${sub.label}`;
1247
1589
  return h("button", {
1248
- key: btn.name,
1590
+ key: sub.name,
1249
1591
  type: "button",
1250
- class: ["dm-toolbar-button", active && "dm-toolbar-button--active"],
1251
- disabled: isItemDisabled(btn),
1252
- "aria-label": btn.label,
1253
- "aria-pressed": active,
1254
- title: btn.label,
1255
- innerHTML: getCachedIcon(btn.icon),
1256
- onMousedown: (e) => e.preventDefault(),
1257
- onClick: () => executeCommand(btn)
1592
+ class: ["dm-toolbar-dropdown-item", subActive && "dm-toolbar-dropdown-item--active"],
1593
+ role: "menuitem",
1594
+ "aria-label": sub.label,
1595
+ innerHTML: subHtml,
1596
+ onMousedown: (e) => {
1597
+ e.preventDefault();
1598
+ },
1599
+ onClick: () => {
1600
+ props.executeSubItem(sub);
1601
+ }
1258
1602
  });
1259
- }),
1260
- slots["default"]?.()
1603
+ })) : null
1261
1604
  ]);
1262
1605
  };
1263
1606
  }
@@ -1267,25 +1610,53 @@ var DomternalFloatingMenu = defineComponent({
1267
1610
  props: {
1268
1611
  editor: { type: Object, default: void 0 },
1269
1612
  shouldShow: { type: Function, default: void 0 },
1270
- offset: { type: Number, default: 0 }
1613
+ offset: { type: Number, default: 0 },
1614
+ items: {
1615
+ type: [Array, Function],
1616
+ default: void 0
1617
+ },
1618
+ keymap: { type: Object, default: void 0 },
1619
+ icons: { type: Object, default: void 0 },
1620
+ requireExplicitTrigger: { type: Boolean, default: false }
1271
1621
  },
1272
1622
  setup(props, { slots }) {
1273
1623
  const { editor: contextEditor } = useCurrentEditor();
1274
1624
  const menuRef = ref();
1275
- const pluginKey = new PluginKey("vueFloatingMenu-" + Math.random().toString(36).slice(2, 8));
1625
+ const cryptoRef = globalThis.crypto;
1626
+ const pluginKey = new PluginKey(
1627
+ "vueFloatingMenu-" + (cryptoRef?.randomUUID?.().slice(0, 8) ?? Math.random().toString(36).slice(2, 8))
1628
+ );
1629
+ const controller = shallowRef(null);
1630
+ const version = ref(0);
1276
1631
  let registered = false;
1277
1632
  let stopWatch = null;
1633
+ let currentEditor = null;
1634
+ const resolveIcon = (name) => {
1635
+ if (!name) return "";
1636
+ return props.icons?.[name] ?? defaultIcons[name] ?? "";
1637
+ };
1278
1638
  const doRegister = (editor) => {
1279
1639
  if (registered || editor.isDestroyed || !menuRef.value) return;
1280
1640
  registered = true;
1641
+ currentEditor = editor;
1281
1642
  const plugin = createFloatingMenuPlugin({
1282
1643
  pluginKey,
1283
1644
  editor,
1284
1645
  element: menuRef.value,
1285
1646
  ...props.shouldShow && { shouldShow: props.shouldShow },
1286
- offset: props.offset
1647
+ offset: props.offset,
1648
+ ...props.keymap && { keymap: props.keymap },
1649
+ requireExplicitTrigger: props.requireExplicitTrigger
1287
1650
  });
1288
1651
  editor.registerPlugin(plugin);
1652
+ const hasCustomSlot = Boolean(slots["default"]);
1653
+ if (!hasCustomSlot) {
1654
+ const ctl = new FloatingMenuController(editor, () => {
1655
+ version.value++;
1656
+ }, props.items);
1657
+ ctl.subscribe();
1658
+ controller.value = ctl;
1659
+ }
1289
1660
  };
1290
1661
  onMounted(() => {
1291
1662
  const ed = props.editor ?? contextEditor.value;
@@ -1304,14 +1675,149 @@ var DomternalFloatingMenu = defineComponent({
1304
1675
  );
1305
1676
  }
1306
1677
  });
1678
+ watch(
1679
+ () => [controller.value?.focusedIndex ?? -1, version.value],
1680
+ ([focusedIndex]) => {
1681
+ if (focusedIndex < 0 || !menuRef.value) return;
1682
+ void nextTick(() => {
1683
+ const target = menuRef.value?.querySelector(
1684
+ `[data-floating-menu-index="${String(focusedIndex)}"]`
1685
+ );
1686
+ target?.focus();
1687
+ });
1688
+ }
1689
+ );
1307
1690
  onScopeDispose(() => {
1308
1691
  stopWatch?.();
1309
- const editor = props.editor ?? contextEditor.value;
1692
+ controller.value?.destroy();
1693
+ controller.value = null;
1694
+ const editor = currentEditor ?? props.editor ?? contextEditor.value;
1310
1695
  if (editor && !editor.isDestroyed) {
1311
1696
  editor.unregisterPlugin(pluginKey);
1312
1697
  }
1313
1698
  });
1314
- return () => h("div", { ref: menuRef, class: "dm-floating-menu" }, slots["default"]?.());
1699
+ const onItemClick = (item) => {
1700
+ const ctl = controller.value;
1701
+ const ed = currentEditor;
1702
+ if (!ctl || !ed) return;
1703
+ ctl.execute(item);
1704
+ requestAnimationFrame(() => {
1705
+ ed.view.focus();
1706
+ });
1707
+ };
1708
+ const onMenuKeyDown = (e) => {
1709
+ const ctl = controller.value;
1710
+ if (!ctl) return;
1711
+ const focused = ctl.focusedItem();
1712
+ switch (e.key) {
1713
+ case "ArrowDown":
1714
+ e.preventDefault();
1715
+ ctl.next();
1716
+ return;
1717
+ case "ArrowUp":
1718
+ e.preventDefault();
1719
+ ctl.prev();
1720
+ return;
1721
+ case "Home":
1722
+ e.preventDefault();
1723
+ ctl.first();
1724
+ return;
1725
+ case "End":
1726
+ e.preventDefault();
1727
+ ctl.last();
1728
+ return;
1729
+ case "Escape":
1730
+ e.preventDefault();
1731
+ e.stopPropagation();
1732
+ ctl.leaveMenu();
1733
+ currentEditor?.view.focus();
1734
+ return;
1735
+ case "Enter":
1736
+ case " ":
1737
+ if (focused) {
1738
+ e.preventDefault();
1739
+ onItemClick(focused);
1740
+ }
1741
+ return;
1742
+ default:
1743
+ return;
1744
+ }
1745
+ };
1746
+ return () => {
1747
+ if (slots["default"]) {
1748
+ return h(
1749
+ "div",
1750
+ { ref: menuRef, class: "dm-floating-menu", "data-dm-editor-ui": "" },
1751
+ slots["default"]()
1752
+ );
1753
+ }
1754
+ const ctl = controller.value;
1755
+ void version.value;
1756
+ const groups = ctl?.groups ?? [];
1757
+ const focusedIndex = ctl?.focusedIndex ?? -1;
1758
+ const flatNames = groups.flatMap((g) => g.items.map((i) => i.name));
1759
+ return h(
1760
+ "div",
1761
+ {
1762
+ ref: menuRef,
1763
+ class: "dm-floating-menu",
1764
+ role: "menu",
1765
+ "aria-label": "Insert block",
1766
+ "data-dm-editor-ui": "",
1767
+ onKeydown: onMenuKeyDown
1768
+ },
1769
+ groups.map((group, gi) => {
1770
+ const groupId = `dm-fm-g${String(gi)}`;
1771
+ return h("div", { key: group.name || `__group-${String(gi)}`, class: "dm-floating-menu-group-wrapper" }, [
1772
+ group.name ? h("div", { class: "dm-floating-menu-group-label", id: groupId }, group.name) : null,
1773
+ h(
1774
+ "div",
1775
+ {
1776
+ class: "dm-floating-menu-group",
1777
+ role: "group",
1778
+ ...group.name ? { "aria-labelledby": groupId } : {}
1779
+ },
1780
+ group.items.map((item) => {
1781
+ const flatIndex = flatNames.indexOf(item.name);
1782
+ const isFocused = flatIndex === focusedIndex;
1783
+ const disabled = ctl?.isDisabled(item) ?? false;
1784
+ const iconHtml = resolveIcon(item.icon);
1785
+ return h(
1786
+ "button",
1787
+ {
1788
+ key: item.name,
1789
+ type: "button",
1790
+ role: "menuitem",
1791
+ class: "dm-floating-menu-item",
1792
+ "data-floating-menu-item": item.name,
1793
+ "data-floating-menu-index": String(flatIndex),
1794
+ tabindex: isFocused || focusedIndex < 0 && flatIndex === 0 ? 0 : -1,
1795
+ "aria-disabled": disabled ? "true" : void 0,
1796
+ "aria-keyshortcuts": item.shortcut,
1797
+ disabled,
1798
+ onMousedown: (e) => {
1799
+ e.preventDefault();
1800
+ },
1801
+ onClick: () => {
1802
+ onItemClick(item);
1803
+ }
1804
+ },
1805
+ [
1806
+ iconHtml ? h("span", {
1807
+ class: "dm-floating-menu-item-icon",
1808
+ "aria-hidden": "true",
1809
+ innerHTML: iconHtml
1810
+ }) : null,
1811
+ h("span", { class: "dm-floating-menu-item-label" }, item.label),
1812
+ item.shortcut ? h("span", { class: "dm-floating-menu-item-shortcut", "aria-hidden": "true" }, item.shortcut) : null
1813
+ ]
1814
+ );
1815
+ })
1816
+ )
1817
+ ]);
1818
+ })
1819
+ );
1820
+ };
1315
1821
  }
1316
1822
  });
1317
1823
  var SCROLL_SETTLE_MS = 50;
@@ -1350,7 +1856,8 @@ function useEmojiPicker(editor, emojis) {
1350
1856
  const frequentlyUsed = computed(() => {
1351
1857
  if (!isOpen.value) return [];
1352
1858
  const storage = getEmojiStorage(editor.value);
1353
- const getFreq = storage?.["getFrequentlyUsed"];
1859
+ if (!storage) return [];
1860
+ const getFreq = storage["getFrequentlyUsed"];
1354
1861
  if (!getFreq) return [];
1355
1862
  const names = getFreq();
1356
1863
  if (!names.length) return [];
@@ -1382,7 +1889,9 @@ function useEmojiPicker(editor, emojis) {
1382
1889
  clickOutsideHandler = (e) => {
1383
1890
  const target = e.target;
1384
1891
  if (pickerRef.value && !pickerRef.value.contains(target) && target !== anchorEl && !anchorEl?.contains(target)) {
1385
- requestAnimationFrame(() => close());
1892
+ requestAnimationFrame(() => {
1893
+ close();
1894
+ });
1386
1895
  }
1387
1896
  };
1388
1897
  document.addEventListener("mousedown", clickOutsideHandler);
@@ -1560,7 +2069,7 @@ var DomternalEmojiPicker = defineComponent({
1560
2069
  const swatches = Array.from(grid.querySelectorAll(".dm-emoji-swatch"));
1561
2070
  if (!swatches.length) return;
1562
2071
  const current = document.activeElement;
1563
- let idx = swatches.indexOf(current);
2072
+ const idx = swatches.indexOf(current);
1564
2073
  if (idx === -1) {
1565
2074
  if (["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp"].includes(event.key)) {
1566
2075
  event.preventDefault();
@@ -1605,8 +2114,12 @@ var DomternalEmojiPicker = defineComponent({
1605
2114
  tabindex: -1,
1606
2115
  title: formatName(item.name),
1607
2116
  "aria-label": formatName(item.name),
1608
- onMousedown: (e) => e.preventDefault(),
1609
- onClick: () => selectEmoji(item)
2117
+ onMousedown: (e) => {
2118
+ e.preventDefault();
2119
+ },
2120
+ onClick: () => {
2121
+ selectEmoji(item);
2122
+ }
1610
2123
  }, item.emoji);
1611
2124
  }
1612
2125
  return () => {
@@ -1641,8 +2154,12 @@ var DomternalEmojiPicker = defineComponent({
1641
2154
  "aria-selected": activeCategory.value === cat,
1642
2155
  title: cat,
1643
2156
  "aria-label": cat,
1644
- onMousedown: (e) => e.preventDefault(),
1645
- onClick: () => scrollToCategory(cat)
2157
+ onMousedown: (e) => {
2158
+ e.preventDefault();
2159
+ },
2160
+ onClick: () => {
2161
+ scrollToCategory(cat);
2162
+ }
1646
2163
  }, categoryIcon(cat))
1647
2164
  )
1648
2165
  ),
@@ -1876,6 +2393,348 @@ var EditorContent = defineComponent({
1876
2393
  });
1877
2394
  }
1878
2395
  });
2396
+ var TOKEN_LABELS = {
2397
+ gray: "Gray",
2398
+ brown: "Brown",
2399
+ orange: "Orange",
2400
+ yellow: "Yellow",
2401
+ green: "Green",
2402
+ blue: "Blue",
2403
+ purple: "Purple",
2404
+ pink: "Pink",
2405
+ red: "Red"
2406
+ };
2407
+ function useNotionColorPicker(options) {
2408
+ const { editor } = options;
2409
+ const isOpen = ref(false);
2410
+ const anchorEl = shallowRef(null);
2411
+ const hostEl = shallowRef(null);
2412
+ const panelRef = ref();
2413
+ const currentTextToken = ref(null);
2414
+ const currentBgToken = ref(null);
2415
+ const palette = shallowRef([]);
2416
+ const setStorageOpen2 = (open) => {
2417
+ const ed = editor.value;
2418
+ if (!ed) return;
2419
+ const slot = ed.storage["notionColorPicker"];
2420
+ if (slot && typeof slot === "object") {
2421
+ slot.isOpen = open;
2422
+ }
2423
+ };
2424
+ const syncFromSelection = () => {
2425
+ const ed = editor.value;
2426
+ if (!ed) return;
2427
+ const { selection } = ed.state;
2428
+ let mark = null;
2429
+ if (selection.empty) {
2430
+ mark = selection.$from.marks().find((m) => m.type.name === "textStyle") ?? null;
2431
+ } else {
2432
+ ed.state.doc.nodesBetween(selection.from, selection.to, (node) => {
2433
+ if (mark) return false;
2434
+ if (node.isText) {
2435
+ const found = node.marks.find((m) => m.type.name === "textStyle");
2436
+ if (found) mark = found;
2437
+ }
2438
+ return true;
2439
+ });
2440
+ }
2441
+ const attrs = mark?.attrs ?? {};
2442
+ currentTextToken.value = attrs.colorToken ?? null;
2443
+ currentBgToken.value = attrs.backgroundColorToken ?? null;
2444
+ };
2445
+ const close = (opts = {}) => {
2446
+ if (!isOpen.value) return;
2447
+ isOpen.value = false;
2448
+ setStorageOpen2(false);
2449
+ if (opts.refocus) {
2450
+ editor.value?.view.focus();
2451
+ }
2452
+ anchorEl.value = null;
2453
+ };
2454
+ watch(
2455
+ editor,
2456
+ (ed, _oldEd, onCleanup) => {
2457
+ if (!ed || ed.isDestroyed) return;
2458
+ hostEl.value = ed.view.dom.closest(".dm-editor");
2459
+ const ext = ed.extensionManager.extensions.find((e) => e.name === "notionColorPicker");
2460
+ const extOptions = ext?.options ?? null;
2461
+ palette.value = extOptions?.palette ? [...extOptions.palette] : [];
2462
+ const onOpen = (...args) => {
2463
+ const detail = args[0];
2464
+ const incomingAnchor = detail?.anchorElement;
2465
+ if (!incomingAnchor) return;
2466
+ if (isOpen.value && anchorEl.value === incomingAnchor) {
2467
+ close({ refocus: true });
2468
+ return;
2469
+ }
2470
+ anchorEl.value = incomingAnchor;
2471
+ syncFromSelection();
2472
+ isOpen.value = true;
2473
+ setStorageOpen2(true);
2474
+ };
2475
+ const onSelectionUpdate = () => {
2476
+ if (!isOpen.value) return;
2477
+ if (!anchorEl.value?.isConnected) {
2478
+ close();
2479
+ return;
2480
+ }
2481
+ if (ed.state.selection.empty) {
2482
+ close();
2483
+ } else {
2484
+ syncFromSelection();
2485
+ }
2486
+ };
2487
+ ed.on("notionColorOpen", onOpen);
2488
+ ed.on("selectionUpdate", onSelectionUpdate);
2489
+ onCleanup(() => {
2490
+ ed.off("notionColorOpen", onOpen);
2491
+ ed.off("selectionUpdate", onSelectionUpdate);
2492
+ if (isOpen.value) setStorageOpen2(false);
2493
+ });
2494
+ },
2495
+ { immediate: true }
2496
+ );
2497
+ watch(isOpen, (open, _old, onCleanup) => {
2498
+ if (!open) return;
2499
+ const controller = new AbortController();
2500
+ const { signal } = controller;
2501
+ document.addEventListener("mousedown", (e) => {
2502
+ const target = e.target;
2503
+ if (!target) return;
2504
+ if (panelRef.value?.contains(target)) return;
2505
+ if (anchorEl.value?.contains(target)) return;
2506
+ close({ refocus: false });
2507
+ }, { signal });
2508
+ document.addEventListener("keydown", (e) => {
2509
+ if (e.key === "Escape" && isOpen.value) {
2510
+ e.preventDefault();
2511
+ close({ refocus: true });
2512
+ }
2513
+ }, { signal });
2514
+ onCleanup(() => {
2515
+ controller.abort();
2516
+ });
2517
+ });
2518
+ const applyText = (token) => {
2519
+ const ed = editor.value;
2520
+ if (!ed) return;
2521
+ ed.commands.setTextColorToken(token);
2522
+ syncFromSelection();
2523
+ };
2524
+ const applyBg = (token) => {
2525
+ const ed = editor.value;
2526
+ if (!ed) return;
2527
+ ed.commands.setBackgroundColorToken(token);
2528
+ syncFromSelection();
2529
+ };
2530
+ const tokenLabel = (token) => {
2531
+ return TOKEN_LABELS[token] ?? token.charAt(0).toUpperCase() + token.slice(1);
2532
+ };
2533
+ const onPanelKeydown = (event) => {
2534
+ const cols = 5;
2535
+ const root = panelRef.value;
2536
+ if (!root) return;
2537
+ const swatches = Array.from(
2538
+ root.querySelectorAll(".dm-ncp-swatch")
2539
+ );
2540
+ if (!swatches.length) return;
2541
+ const active = document.activeElement;
2542
+ const idx = active ? swatches.indexOf(active) : -1;
2543
+ if (idx === -1) return;
2544
+ let next = idx;
2545
+ switch (event.key) {
2546
+ case "ArrowRight":
2547
+ event.preventDefault();
2548
+ next = Math.min(idx + 1, swatches.length - 1);
2549
+ break;
2550
+ case "ArrowLeft":
2551
+ event.preventDefault();
2552
+ next = Math.max(idx - 1, 0);
2553
+ break;
2554
+ case "ArrowDown":
2555
+ event.preventDefault();
2556
+ next = Math.min(idx + cols, swatches.length - 1);
2557
+ break;
2558
+ case "ArrowUp":
2559
+ event.preventDefault();
2560
+ next = Math.max(idx - cols, 0);
2561
+ break;
2562
+ case "Home":
2563
+ event.preventDefault();
2564
+ next = 0;
2565
+ break;
2566
+ case "End":
2567
+ event.preventDefault();
2568
+ next = swatches.length - 1;
2569
+ break;
2570
+ default:
2571
+ return;
2572
+ }
2573
+ swatches[next]?.focus();
2574
+ };
2575
+ return {
2576
+ isOpen,
2577
+ hostEl,
2578
+ anchorEl,
2579
+ panelRef,
2580
+ currentTextToken,
2581
+ currentBgToken,
2582
+ palette,
2583
+ applyText,
2584
+ applyBg,
2585
+ close,
2586
+ tokenLabel,
2587
+ onPanelKeydown
2588
+ };
2589
+ }
2590
+
2591
+ // src/notion-color-picker/DomternalNotionColorPicker.ts
2592
+ var DomternalNotionColorPicker = defineComponent({
2593
+ name: "DomternalNotionColorPicker",
2594
+ props: {
2595
+ editor: { type: Object, default: void 0 }
2596
+ },
2597
+ setup(props, { slots }) {
2598
+ const { editor: contextEditor } = useCurrentEditor();
2599
+ const editorRef = computed(() => props.editor ?? contextEditor.value);
2600
+ const api = useNotionColorPicker({ editor: editorRef });
2601
+ const {
2602
+ isOpen,
2603
+ hostEl,
2604
+ anchorEl,
2605
+ panelRef,
2606
+ currentTextToken,
2607
+ currentBgToken,
2608
+ palette,
2609
+ applyText,
2610
+ applyBg,
2611
+ tokenLabel,
2612
+ onPanelKeydown
2613
+ } = api;
2614
+ watch(
2615
+ [isOpen, anchorEl],
2616
+ ([open, anchor], _old, onCleanup) => {
2617
+ if (!open || !anchor || !panelRef.value) return;
2618
+ const panel = panelRef.value;
2619
+ const cleanupFloating = positionFloating(anchor, panel, {
2620
+ placement: "bottom-start",
2621
+ offsetValue: 4
2622
+ });
2623
+ let id2 = 0;
2624
+ const id1 = requestAnimationFrame(() => {
2625
+ id2 = requestAnimationFrame(() => {
2626
+ if (!panel.isConnected) return;
2627
+ const active = panel.querySelector(".dm-ncp-swatch.dm-ncp-active");
2628
+ const fallback = panel.querySelector('.dm-ncp-swatch--text[data-color="null"]');
2629
+ (active ?? fallback)?.focus({ preventScroll: true });
2630
+ });
2631
+ });
2632
+ onCleanup(() => {
2633
+ cancelAnimationFrame(id1);
2634
+ if (id2) cancelAnimationFrame(id2);
2635
+ cleanupFloating();
2636
+ });
2637
+ },
2638
+ { flush: "post" }
2639
+ );
2640
+ const renderDefaultPanel = () => {
2641
+ const tToken = currentTextToken.value;
2642
+ const bToken = currentBgToken.value;
2643
+ const pal = palette.value;
2644
+ return [
2645
+ h("div", { class: "dm-ncp-section" }, [
2646
+ h("div", { class: "dm-ncp-label" }, "Text color"),
2647
+ h("div", { class: "dm-ncp-grid" }, [
2648
+ h("button", {
2649
+ type: "button",
2650
+ class: ["dm-ncp-swatch", "dm-ncp-swatch--text", tToken === null && "dm-ncp-active"],
2651
+ "aria-pressed": tToken === null,
2652
+ "data-color": "null",
2653
+ title: "Default text color",
2654
+ "aria-label": "Default text color",
2655
+ onMousedown: (e) => {
2656
+ e.preventDefault();
2657
+ },
2658
+ onClick: () => {
2659
+ applyText(null);
2660
+ }
2661
+ }),
2662
+ ...pal.map((t) => h("button", {
2663
+ key: t,
2664
+ type: "button",
2665
+ class: ["dm-ncp-swatch", "dm-ncp-swatch--text", tToken === t && "dm-ncp-active"],
2666
+ "aria-pressed": tToken === t,
2667
+ "data-color": t,
2668
+ title: tokenLabel(t),
2669
+ "aria-label": `${tokenLabel(t)} text`,
2670
+ onMousedown: (e) => {
2671
+ e.preventDefault();
2672
+ },
2673
+ onClick: () => {
2674
+ applyText(t);
2675
+ }
2676
+ }))
2677
+ ])
2678
+ ]),
2679
+ h("div", { class: "dm-ncp-section" }, [
2680
+ h("div", { class: "dm-ncp-label" }, "Background color"),
2681
+ h("div", { class: "dm-ncp-grid" }, [
2682
+ h("button", {
2683
+ type: "button",
2684
+ class: ["dm-ncp-swatch", "dm-ncp-swatch--bg", bToken === null && "dm-ncp-active"],
2685
+ "aria-pressed": bToken === null,
2686
+ "data-color": "null",
2687
+ title: "Default background",
2688
+ "aria-label": "Default background",
2689
+ onMousedown: (e) => {
2690
+ e.preventDefault();
2691
+ },
2692
+ onClick: () => {
2693
+ applyBg(null);
2694
+ }
2695
+ }),
2696
+ ...pal.map((t) => h("button", {
2697
+ key: t,
2698
+ type: "button",
2699
+ class: ["dm-ncp-swatch", "dm-ncp-swatch--bg", bToken === t && "dm-ncp-active"],
2700
+ "aria-pressed": bToken === t,
2701
+ "data-color": t,
2702
+ title: `${tokenLabel(t)} background`,
2703
+ "aria-label": `${tokenLabel(t)} background`,
2704
+ onMousedown: (e) => {
2705
+ e.preventDefault();
2706
+ },
2707
+ onClick: () => {
2708
+ applyBg(t);
2709
+ }
2710
+ }))
2711
+ ])
2712
+ ])
2713
+ ];
2714
+ };
2715
+ return () => {
2716
+ if (!isOpen.value || !hostEl.value) return null;
2717
+ const slot = slots["default"];
2718
+ const slotChildren = slot ? slot({ api }) : void 0;
2719
+ const panelChildren = slotChildren ?? renderDefaultPanel();
2720
+ return h(Teleport, { to: hostEl.value }, [
2721
+ h("div", {
2722
+ ref: panelRef,
2723
+ class: "dm-notion-color-picker",
2724
+ "data-show": "",
2725
+ "data-dm-editor-ui": "",
2726
+ role: "dialog",
2727
+ "aria-label": "Text and background color",
2728
+ // Intentional non-modal: the picker doesn't trap focus (outside-click
2729
+ // closes, editor stays interactive). `role="dialog"` defaults to
2730
+ // modal=true for screen readers, so we explicitly opt out.
2731
+ "aria-modal": "false",
2732
+ onKeydown: onPanelKeydown
2733
+ }, panelChildren)
2734
+ ]);
2735
+ };
2736
+ }
2737
+ });
1879
2738
  var NODE_VIEW_ON_DRAG_START = /* @__PURE__ */ Symbol("domternal-node-view-drag");
1880
2739
  var NODE_VIEW_CONTENT_REF = /* @__PURE__ */ Symbol("domternal-node-view-content");
1881
2740
  function useVueNodeView() {
@@ -1886,7 +2745,7 @@ function useVueNodeView() {
1886
2745
 
1887
2746
  // src/node-views/VueNodeViewRenderer.ts
1888
2747
  function VueNodeViewRenderer(component, options = {}) {
1889
- const normalizedComponent = typeof component === "function" && "__vccOpts" in component ? component["__vccOpts"] ?? component : component;
2748
+ const normalizedComponent = typeof component === "function" ? component["__vccOpts"] ?? component : component;
1890
2749
  markRaw(normalizedComponent);
1891
2750
  const constructor = (node, _view, getPos, decorations) => {
1892
2751
  const ctx = constructor.__domternalContext;
@@ -1899,8 +2758,8 @@ function VueNodeViewRenderer(component, options = {}) {
1899
2758
  appContextStore.set(editor, appContext);
1900
2759
  }
1901
2760
  }
1902
- if (!appContext) {
1903
- if (typeof globalThis !== "undefined" && globalThis.__DEV__ !== false) {
2761
+ if (!appContext || !editor) {
2762
+ if (globalThis.__DEV__ !== false) {
1904
2763
  console.warn(
1905
2764
  "[VueNodeViewRenderer] appContext not found for editor. Custom Vue node views require provideEditor(editor) to be called, either manually after useEditor() or automatically via <Domternal> root."
1906
2765
  );
@@ -2058,6 +2917,6 @@ var NodeViewContent = defineComponent({
2058
2917
  }
2059
2918
  });
2060
2919
 
2061
- export { DEFAULT_EXTENSIONS, Domternal, DomternalBubbleMenu, DomternalEditor, DomternalEmojiPicker, DomternalFloatingMenu, DomternalToolbar, EDITOR_KEY, EditorContent, NodeViewContent, NodeViewWrapper, VueNodeViewRenderer, provideEditor, useCurrentEditor, useEditor, useEditorState, useVueNodeView };
2920
+ export { DEFAULT_EXTENSIONS, Domternal, DomternalBubbleMenu, DomternalEditor, DomternalEmojiPicker, DomternalFloatingMenu, DomternalNotionColorPicker, DomternalToolbar, EDITOR_KEY, EditorContent, NodeViewContent, NodeViewWrapper, VueNodeViewRenderer, provideEditor, useCurrentEditor, useEditor, useEditorState, useNotionColorPicker, useVueNodeView };
2062
2921
  //# sourceMappingURL=index.js.map
2063
2922
  //# sourceMappingURL=index.js.map