@domternal/react 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 (50) hide show
  1. package/README.md +12 -10
  2. package/dist/Domternal.d.ts +7 -7
  3. package/dist/Domternal.d.ts.map +1 -1
  4. package/dist/DomternalFloatingMenu.d.ts +29 -6
  5. package/dist/DomternalFloatingMenu.d.ts.map +1 -1
  6. package/dist/EditorContent.d.ts +2 -2
  7. package/dist/EditorContent.d.ts.map +1 -1
  8. package/dist/EditorContext.d.ts +2 -4
  9. package/dist/EditorContext.d.ts.map +1 -1
  10. package/dist/bubble-menu/DomternalBubbleMenu.d.ts +2 -1
  11. package/dist/bubble-menu/DomternalBubbleMenu.d.ts.map +1 -1
  12. package/dist/bubble-menu/useBubbleMenu.d.ts +36 -6
  13. package/dist/bubble-menu/useBubbleMenu.d.ts.map +1 -1
  14. package/dist/emoji-picker/DomternalEmojiPicker.d.ts +2 -1
  15. package/dist/emoji-picker/DomternalEmojiPicker.d.ts.map +1 -1
  16. package/dist/emoji-picker/useEmojiPicker.d.ts +5 -3
  17. package/dist/emoji-picker/useEmojiPicker.d.ts.map +1 -1
  18. package/dist/index.d.ts +121 -30
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +961 -77
  21. package/dist/index.js.map +1 -1
  22. package/dist/node-views/NodeViewContent.d.ts +2 -2
  23. package/dist/node-views/NodeViewContent.d.ts.map +1 -1
  24. package/dist/node-views/NodeViewWrapper.d.ts +2 -2
  25. package/dist/node-views/NodeViewWrapper.d.ts.map +1 -1
  26. package/dist/node-views/ReactNodeViewRenderer.d.ts +1 -1
  27. package/dist/node-views/ReactNodeViewRenderer.d.ts.map +1 -1
  28. package/dist/notion-color-picker/DomternalNotionColorPicker.d.ts +25 -0
  29. package/dist/notion-color-picker/DomternalNotionColorPicker.d.ts.map +1 -0
  30. package/dist/notion-color-picker/index.d.ts +5 -0
  31. package/dist/notion-color-picker/index.d.ts.map +1 -0
  32. package/dist/notion-color-picker/useNotionColorPicker.d.ts +47 -0
  33. package/dist/notion-color-picker/useNotionColorPicker.d.ts.map +1 -0
  34. package/dist/toolbar/DomternalToolbar.d.ts +2 -1
  35. package/dist/toolbar/DomternalToolbar.d.ts.map +1 -1
  36. package/dist/toolbar/ToolbarButton.d.ts +2 -1
  37. package/dist/toolbar/ToolbarButton.d.ts.map +1 -1
  38. package/dist/toolbar/ToolbarDropdown.d.ts +2 -1
  39. package/dist/toolbar/ToolbarDropdown.d.ts.map +1 -1
  40. package/dist/toolbar/ToolbarDropdownPanel.d.ts +2 -1
  41. package/dist/toolbar/ToolbarDropdownPanel.d.ts.map +1 -1
  42. package/dist/toolbar/useKeyboardNav.d.ts.map +1 -1
  43. package/dist/toolbar/useToolbarController.d.ts +6 -4
  44. package/dist/toolbar/useToolbarController.d.ts.map +1 -1
  45. package/dist/toolbar/useToolbarIcons.d.ts +4 -3
  46. package/dist/toolbar/useToolbarIcons.d.ts.map +1 -1
  47. package/dist/toolbar/useTooltip.d.ts.map +1 -1
  48. package/dist/useEditor.d.ts +4 -3
  49. package/dist/useEditor.d.ts.map +1 -1
  50. package/package.json +4 -2
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
- import { createContext, forwardRef, useImperativeHandle, useRef, useEffect, useState, useMemo, useCallback, useSyncExternalStore, useContext, Fragment, createElement } from 'react';
2
- import { Editor, Document, Paragraph, Text, BaseKeymap, History, PluginKey, createFloatingMenuPlugin, ToolbarController, positionFloatingOnce, defaultIcons, createBubbleMenuPlugin } from '@domternal/core';
1
+ import { createContext, forwardRef, useImperativeHandle, useRef, useEffect, useState, useMemo, useCallback, useSyncExternalStore, useContext, Fragment, useLayoutEffect, createElement } from 'react';
2
+ import { Editor, Document, Paragraph, Text, BaseKeymap, History, positionFloatingOnce, PluginKey, FloatingMenuController, defaultIcons, positionFloating, ToolbarController, defaultBubbleContexts, createBubbleMenuPlugin } from '@domternal/core';
3
3
  export { Editor, generateHTML, generateJSON, generateText } from '@domternal/core';
4
4
  import { jsxs, jsx, Fragment as Fragment$1 } from 'react/jsx-runtime';
5
+ import { createFloatingMenuPlugin } from '@domternal/extension-block-menu';
6
+ import { createPortal } from 'react-dom';
5
7
  import { createRoot } from 'react-dom/client';
6
8
 
7
9
  // src/useEditor.ts
@@ -95,7 +97,10 @@ function useEditor(options = {}, deps) {
95
97
  useEffect(() => {
96
98
  if (!deps || !instanceRef.current || instanceRef.current.isDestroyed) return;
97
99
  if (depsRef.current === deps) return;
98
- if (depsRef.current && deps.length === depsRef.current.length && deps.every((d, i) => d === depsRef.current[i])) return;
100
+ const prevDeps = depsRef.current;
101
+ if (prevDeps?.length === deps.length && deps.every((d, i) => d === prevDeps[i])) {
102
+ return;
103
+ }
99
104
  const element = instanceRef.current.view.dom.parentElement ?? document.createElement("div");
100
105
  destroyCurrentEditor();
101
106
  const initialContent = pendingContentRef.current ?? "";
@@ -119,7 +124,13 @@ function useEditor(options = {}, deps) {
119
124
  return { editor, editorRef };
120
125
  }
121
126
  function useEditorState(editor, selector) {
122
- if (selector) {
127
+ const isSelectorMode = typeof selector === "function";
128
+ const modeRef = useRef(null);
129
+ modeRef.current ??= isSelectorMode;
130
+ if (modeRef.current !== isSelectorMode) {
131
+ throw new Error("useEditorState selector mode must remain stable for a component instance.");
132
+ }
133
+ if (typeof selector === "function") {
123
134
  return useEditorStateSelector(editor, selector);
124
135
  }
125
136
  return useEditorStateFull(editor);
@@ -464,7 +475,7 @@ function useKeyboardNav(controllerRef, toolbarRef, closeDropdown) {
464
475
  const idx = controllerRef.current?.focusedIndex ?? 0;
465
476
  const buttons = toolbarRef.current?.querySelectorAll(".dm-toolbar-button");
466
477
  buttons?.[idx]?.focus();
467
- }, []);
478
+ }, [controllerRef, toolbarRef]);
468
479
  const focusDropdownItem = useCallback((direction, first) => {
469
480
  const panel = toolbarRef.current?.querySelector(".dm-toolbar-dropdown-panel");
470
481
  if (!panel) return;
@@ -478,7 +489,7 @@ function useKeyboardNav(controllerRef, toolbarRef, closeDropdown) {
478
489
  const idx = items.indexOf(current);
479
490
  const next = idx === -1 ? direction > 0 ? 0 : items.length - 1 : (idx + direction + items.length) % items.length;
480
491
  items[next]?.focus();
481
- }, []);
492
+ }, [toolbarRef]);
482
493
  const onKeyDown = useCallback((event) => {
483
494
  const controller = controllerRef.current;
484
495
  if (!controller) return;
@@ -501,7 +512,9 @@ function useKeyboardNav(controllerRef, toolbarRef, closeDropdown) {
501
512
  const btn = document.activeElement;
502
513
  if (btn?.getAttribute("aria-haspopup") && btn.closest(".dm-toolbar")) {
503
514
  btn.click();
504
- requestAnimationFrame(() => focusDropdownItem(0, true));
515
+ requestAnimationFrame(() => {
516
+ focusDropdownItem(0, true);
517
+ });
505
518
  }
506
519
  }
507
520
  break;
@@ -531,7 +544,7 @@ function useKeyboardNav(controllerRef, toolbarRef, closeDropdown) {
531
544
  }
532
545
  break;
533
546
  }
534
- }, [closeDropdown, focusCurrentButton, focusDropdownItem]);
547
+ }, [closeDropdown, focusCurrentButton, focusDropdownItem, controllerRef]);
535
548
  return { onKeyDown, focusCurrentButton };
536
549
  }
537
550
 
@@ -581,9 +594,15 @@ function ToolbarButton({
581
594
  tabIndex,
582
595
  disabled: isDisabled,
583
596
  dangerouslySetInnerHTML: { __html: iconHtml },
584
- onMouseDown: (e) => e.preventDefault(),
585
- onClick: (e) => onClick(item, e),
586
- onFocus: () => onFocus(item.name)
597
+ onMouseDown: (e) => {
598
+ e.preventDefault();
599
+ },
600
+ onClick: (e) => {
601
+ onClick(item, e);
602
+ },
603
+ onFocus: () => {
604
+ onFocus(item.name);
605
+ }
587
606
  }
588
607
  );
589
608
  }
@@ -611,8 +630,12 @@ function ToolbarDropdownPanel({
611
630
  "aria-label": sub.label,
612
631
  title: sub.label,
613
632
  style: { backgroundColor: sub.color },
614
- onMouseDown: (e) => e.preventDefault(),
615
- onClick: (e) => onItemClick(sub, e)
633
+ onMouseDown: (e) => {
634
+ e.preventDefault();
635
+ },
636
+ onClick: (e) => {
637
+ onItemClick(sub, e);
638
+ }
616
639
  },
617
640
  sub.name
618
641
  ) : /* @__PURE__ */ jsx(
@@ -624,8 +647,12 @@ function ToolbarDropdownPanel({
624
647
  tabIndex: -1,
625
648
  "aria-label": sub.label,
626
649
  dangerouslySetInnerHTML: { __html: getCachedItemContent(sub.icon, sub.label) },
627
- onMouseDown: (e) => e.preventDefault(),
628
- onClick: (e) => onItemClick(sub, e)
650
+ onMouseDown: (e) => {
651
+ e.preventDefault();
652
+ },
653
+ onClick: (e) => {
654
+ onItemClick(sub, e);
655
+ }
629
656
  },
630
657
  sub.name
631
658
  )
@@ -651,8 +678,12 @@ function ToolbarDropdownPanel({
651
678
  if (el && sub.style) el.setAttribute("style", sub.style);
652
679
  },
653
680
  dangerouslySetInnerHTML: { __html: getCachedItemContent(sub.icon, sub.label, dropdown.displayMode) },
654
- onMouseDown: (e) => e.preventDefault(),
655
- onClick: (e) => onItemClick(sub, e)
681
+ onMouseDown: (e) => {
682
+ e.preventDefault();
683
+ },
684
+ onClick: (e) => {
685
+ onItemClick(sub, e);
686
+ }
656
687
  },
657
688
  sub.name
658
689
  ))
@@ -686,9 +717,15 @@ function ToolbarDropdown({
686
717
  disabled: isDisabled,
687
718
  "data-dropdown": dropdown.name,
688
719
  dangerouslySetInnerHTML: { __html: triggerHtml },
689
- onMouseDown: (e) => e.preventDefault(),
690
- onClick: () => onToggle(dropdown),
691
- onFocus: () => onFocus(dropdown.name)
720
+ onMouseDown: (e) => {
721
+ e.preventDefault();
722
+ },
723
+ onClick: () => {
724
+ onToggle(dropdown);
725
+ },
726
+ onFocus: () => {
727
+ onFocus(dropdown.name);
728
+ }
692
729
  }
693
730
  ),
694
731
  isOpen && /* @__PURE__ */ jsx(
@@ -737,14 +774,16 @@ function DomternalToolbar({ editor: editorProp, icons, layout }) {
737
774
  return;
738
775
  }
739
776
  controllerRef.current?.executeCommand(item);
740
- requestAnimationFrame(() => editor.view.focus());
741
- }, [editor, closeDropdown]);
777
+ requestAnimationFrame(() => {
778
+ editor.view.focus();
779
+ });
780
+ }, [editor, closeDropdown, controllerRef]);
742
781
  const onDropdownItemClick = useCallback((item, event) => {
743
782
  if (!editor) return;
744
783
  let anchor;
745
784
  if (item.emitEvent) {
746
785
  const wrapper = event.currentTarget.closest(".dm-toolbar-dropdown-wrapper");
747
- anchor = wrapper?.querySelector(".dm-toolbar-dropdown-trigger");
786
+ anchor = wrapper?.querySelector(".dm-toolbar-dropdown-trigger") ?? void 0;
748
787
  }
749
788
  closeDropdown();
750
789
  if (item.emitEvent) {
@@ -752,14 +791,16 @@ function DomternalToolbar({ editor: editorProp, icons, layout }) {
752
791
  } else {
753
792
  controllerRef.current?.executeCommand(item);
754
793
  }
755
- requestAnimationFrame(() => editor.view.focus());
756
- }, [editor, closeDropdown]);
794
+ requestAnimationFrame(() => {
795
+ editor.view.focus();
796
+ });
797
+ }, [editor, closeDropdown, controllerRef]);
757
798
  const onButtonFocus = useCallback((name) => {
758
799
  const index = controllerRef.current?.getFlatIndex(name) ?? -1;
759
800
  if (index >= 0) {
760
801
  controllerRef.current?.setFocusedIndex(index);
761
802
  }
762
- }, []);
803
+ }, [controllerRef]);
763
804
  if (!editor) return null;
764
805
  return /* @__PURE__ */ jsx(
765
806
  "div",
@@ -801,7 +842,7 @@ function DomternalToolbar({ editor: editorProp, icons, layout }) {
801
842
  computed = getInlineStyleAtCursor(editor, dd.computedStyleProperty);
802
843
  if (computed) {
803
844
  const first = computed.split(",")[0]?.replace(/['"]+/g, "").trim();
804
- computed = first || null;
845
+ computed = first ?? null;
805
846
  }
806
847
  } else {
807
848
  computed = getComputedStyleAtCursor(editor, dd.computedStyleProperty);
@@ -834,6 +875,15 @@ function DomternalToolbar({ editor: editorProp, icons, layout }) {
834
875
  }
835
876
  );
836
877
  }
878
+ var INITIAL_TRAILING_STATE = {
879
+ isNodeSelection: false,
880
+ showColorPickerButton: false,
881
+ showBlockMenuButton: false,
882
+ blockMenuButtonDisabled: false,
883
+ currentTextColorVar: null,
884
+ currentBgColorVar: null,
885
+ hasAnyColor: false
886
+ };
837
887
  function isInsideTableCell($pos) {
838
888
  for (let d = $pos.depth; d > 0; d--) {
839
889
  const name = $pos.node(d).type.name;
@@ -849,23 +899,32 @@ function findCellNode(pos) {
849
899
  return null;
850
900
  }
851
901
  function useBubbleMenu(options) {
852
- const { editor, shouldShow, placement = "top", offset = 8, updateDelay = 0, items, contexts } = options;
902
+ const { editor, shouldShow, placement = "top", offset = 8, updateDelay = 0, items, contexts: explicitContexts, icons } = options;
903
+ const contexts = explicitContexts ?? (items ? void 0 : editor ? defaultBubbleContexts(editor) : void 0);
853
904
  const menuRef = useRef(null);
854
- const pluginKeyRef = useRef(new PluginKey("reactBubbleMenu-" + Math.random().toString(36).slice(2, 8)));
905
+ const pluginKeyRef = useRef(new PluginKey("reactBubbleMenu-" + (globalThis.crypto?.randomUUID?.().slice(0, 8) ?? Math.random().toString(36).slice(2, 8))));
855
906
  const [resolvedItems, setResolvedItems] = useState([]);
856
907
  const [activeVersion, setActiveVersion] = useState(0);
908
+ const [trailing, setTrailing] = useState(INITIAL_TRAILING_STATE);
857
909
  const activeMapRef = useRef(/* @__PURE__ */ new Map());
858
910
  const disabledMapRef = useRef(/* @__PURE__ */ new Map());
859
911
  const itemMapRef = useRef(/* @__PURE__ */ new Map());
860
912
  const bubbleDefaultsRef = useRef(/* @__PURE__ */ new Map());
861
913
  const resolvedItemsRef = useRef([]);
914
+ const editorRef = useRef(editor);
915
+ editorRef.current = editor;
862
916
  useEffect(() => {
863
917
  if (!editor || editor.isDestroyed || !menuRef.current) return;
918
+ const exts = editor.extensionManager.extensions;
919
+ const hasNotionColorPicker = exts.some((e) => e.name === "notionColorPicker");
920
+ const hasBlockContextMenu = exts.some((e) => e.name === "blockContextMenu");
864
921
  const itemMap = /* @__PURE__ */ new Map();
922
+ const dropdownMap = /* @__PURE__ */ new Map();
865
923
  for (const item of editor.toolbarItems) {
866
924
  if (item.type === "button") {
867
925
  itemMap.set(item.name, item);
868
926
  } else if (item.type === "dropdown") {
927
+ dropdownMap.set(item.name, item);
869
928
  for (const sub of item.items) {
870
929
  itemMap.set(sub.name, sub);
871
930
  }
@@ -897,7 +956,7 @@ function useBubbleMenu(options) {
897
956
  let sepIdx = 0;
898
957
  for (const item of ctxItems) {
899
958
  if (lastGroup !== void 0 && item.group !== lastGroup) {
900
- result.push({ type: "separator", name: `bsep-${sepIdx++}` });
959
+ result.push({ type: "separator", name: `bsep-${String(sepIdx++)}` });
901
960
  }
902
961
  result.push(item);
903
962
  lastGroup = item.group;
@@ -910,8 +969,13 @@ function useBubbleMenu(options) {
910
969
  let sepIdx = 0;
911
970
  for (const name of names) {
912
971
  if (name === "|") {
913
- result.push({ type: "separator", name: `sep-${sepIdx++}` });
972
+ result.push({ type: "separator", name: `sep-${String(sepIdx++)}` });
914
973
  } else {
974
+ const dropdown = dropdownMap.get(name);
975
+ if (dropdown) {
976
+ result.push(dropdown);
977
+ continue;
978
+ }
915
979
  const item = itemMap.get(name);
916
980
  if (item) result.push(item);
917
981
  }
@@ -948,7 +1012,7 @@ function useBubbleMenu(options) {
948
1012
  return schemaItems.filter((item) => {
949
1013
  const markName = typeof item.isActive === "string" ? item.isActive : null;
950
1014
  if (!markName) return true;
951
- const markType = schema.marks?.[markName];
1015
+ const markType = schema.marks[markName];
952
1016
  if (!markType) return true;
953
1017
  return nodeType.allowsMarkType(markType);
954
1018
  });
@@ -968,14 +1032,16 @@ function useBubbleMenu(options) {
968
1032
  };
969
1033
  } else {
970
1034
  shouldShowFn = ({ state }) => {
971
- if (state.selection.empty || state.selection.node) return false;
1035
+ if (state.selection.empty) return false;
1036
+ if (state.selection.node) return bubbleDefaults.has(state.selection.node.type.name);
972
1037
  if (isInsideTableCell(state.selection.$from)) return false;
973
1038
  return state.selection.$from.parent.type.spec.marks !== "" || state.selection.$to.parent.type.spec.marks !== "";
974
1039
  };
975
1040
  }
976
1041
  }
1042
+ const pluginKey = pluginKeyRef.current;
977
1043
  const plugin = createBubbleMenuPlugin({
978
- pluginKey: pluginKeyRef.current,
1044
+ pluginKey,
979
1045
  editor,
980
1046
  element: menuRef.current,
981
1047
  shouldShow: shouldShowFn,
@@ -1001,30 +1067,81 @@ function useBubbleMenu(options) {
1001
1067
  canProxy = ed.can();
1002
1068
  } catch {
1003
1069
  }
1004
- for (const item of resolvedItemsRef.current) {
1005
- if (item.type === "separator") continue;
1006
- activeMapRef.current.set(item.name, ToolbarController.resolveActive(ed, item));
1070
+ const trackButton = (btn) => {
1071
+ activeMapRef.current.set(btn.name, ToolbarController.resolveActive(ed, btn));
1007
1072
  try {
1008
- const canCmd = canProxy?.[item.command];
1009
- disabledMapRef.current.set(item.name, canCmd ? !(item.commandArgs?.length ? canCmd(...item.commandArgs) : canCmd()) : false);
1073
+ const canCmd = typeof btn.command === "string" ? canProxy?.[btn.command] : void 0;
1074
+ disabledMapRef.current.set(btn.name, canCmd ? !(btn.commandArgs?.length ? canCmd(...btn.commandArgs) : canCmd()) : false);
1010
1075
  } catch {
1011
- disabledMapRef.current.set(item.name, false);
1076
+ disabledMapRef.current.set(btn.name, false);
1012
1077
  }
1078
+ };
1079
+ for (const item of resolvedItemsRef.current) {
1080
+ if (item.type === "separator") continue;
1081
+ if (item.type === "dropdown") {
1082
+ for (const sub of item.items) trackButton(sub);
1083
+ continue;
1084
+ }
1085
+ trackButton(item);
1013
1086
  }
1014
1087
  };
1088
+ const defaultItems = items ? resolveNames(items) : resolveNames(["bold", "italic", "underline"]);
1089
+ const syncTrailingState = (ed) => {
1090
+ const sel = ed.state.selection;
1091
+ const isNode = !!sel.node;
1092
+ let blockMenuDisabled = false;
1093
+ if (hasBlockContextMenu) {
1094
+ const { $from, $to } = ed.state.selection;
1095
+ if ($from.depth < 1 || $to.depth < 1) {
1096
+ blockMenuDisabled = true;
1097
+ } else {
1098
+ blockMenuDisabled = $from.before(1) !== $to.before(1);
1099
+ }
1100
+ }
1101
+ let textVar = null;
1102
+ let bgVar = null;
1103
+ let hasAny = false;
1104
+ if (hasNotionColorPicker) {
1105
+ const mark = ed.state.selection.$from.marks().find((m) => m.type.name === "textStyle");
1106
+ const attrs = mark?.attrs ?? {};
1107
+ const tToken = attrs.colorToken ?? null;
1108
+ const bToken = attrs.backgroundColorToken ?? null;
1109
+ textVar = tToken ? `var(--dm-block-text-${tToken})` : null;
1110
+ bgVar = bToken ? `var(--dm-block-bg-${bToken})` : null;
1111
+ hasAny = tToken !== null || bToken !== null;
1112
+ }
1113
+ setTrailing({
1114
+ isNodeSelection: isNode,
1115
+ showColorPickerButton: hasNotionColorPicker,
1116
+ showBlockMenuButton: hasBlockContextMenu,
1117
+ blockMenuButtonDisabled: blockMenuDisabled,
1118
+ currentTextColorVar: textVar,
1119
+ currentBgColorVar: bgVar,
1120
+ hasAnyColor: hasAny
1121
+ });
1122
+ };
1015
1123
  const transactionHandler = () => {
1016
1124
  if (contexts) {
1017
1125
  updateContextItems(editor, contexts, detectContext, resolveNames, getFormatItems, filterBySchema, bubbleDefaults, setItems);
1126
+ } else {
1127
+ const sel = editor.state.selection;
1128
+ if (sel.node && bubbleDefaults.has(sel.node.type.name)) {
1129
+ setItems(bubbleDefaults.get(sel.node.type.name) ?? []);
1130
+ } else {
1131
+ setItems(defaultItems);
1132
+ }
1018
1133
  }
1019
1134
  updateStates(editor);
1135
+ syncTrailingState(editor);
1020
1136
  setActiveVersion((v) => v + 1);
1021
1137
  };
1022
1138
  editor.on("transaction", transactionHandler);
1023
1139
  updateStates(editor);
1140
+ syncTrailingState(editor);
1024
1141
  return () => {
1025
1142
  editor.off("transaction", transactionHandler);
1026
1143
  if (!editor.isDestroyed) {
1027
- editor.unregisterPlugin(pluginKeyRef.current);
1144
+ editor.unregisterPlugin(pluginKey);
1028
1145
  }
1029
1146
  };
1030
1147
  }, [editor]);
@@ -1058,14 +1175,36 @@ function useBubbleMenu(options) {
1058
1175
  const isItemDisabled = (item) => {
1059
1176
  return disabledMapRef.current.get(item.name) ?? false;
1060
1177
  };
1061
- const executeCommand = (item) => {
1178
+ const executeCommand = (item, event) => {
1062
1179
  if (!editor) return;
1063
1180
  if (item.emitEvent) {
1064
- editor.emit(item.emitEvent, {});
1181
+ const anchor = event?.currentTarget ?? event?.target ?? null;
1182
+ editor.emit(item.emitEvent, { anchorElement: anchor });
1065
1183
  return;
1066
1184
  }
1067
1185
  ToolbarController.executeItem(editor, item);
1068
1186
  };
1187
+ const openColorPicker = (anchor) => {
1188
+ const ed = editorRef.current;
1189
+ if (!ed) return;
1190
+ ed.emit(
1191
+ "notionColorOpen",
1192
+ { anchorElement: anchor }
1193
+ );
1194
+ };
1195
+ const openBlockContextMenu = (anchor) => {
1196
+ const ed = editorRef.current;
1197
+ if (!ed) return;
1198
+ const $from = ed.state.selection.$from;
1199
+ if ($from.depth < 1) return;
1200
+ const depth = $from.depth > 1 && $from.node($from.depth - 1).type.name !== "doc" ? $from.depth - 1 : $from.depth;
1201
+ const blockPos = $from.before(depth);
1202
+ const editorEl = ed.view.dom.closest(".dm-editor");
1203
+ editorEl?.dispatchEvent(new CustomEvent("dm:block-context-menu-open", {
1204
+ bubbles: false,
1205
+ detail: { blockPos, anchorElement: anchor }
1206
+ }));
1207
+ };
1069
1208
  return {
1070
1209
  menuRef,
1071
1210
  resolvedItems,
@@ -1073,9 +1212,13 @@ function useBubbleMenu(options) {
1073
1212
  isItemDisabled,
1074
1213
  executeCommand,
1075
1214
  activeVersion,
1076
- getCachedIcon: (name) => defaultIcons[name] ?? ""
1215
+ getCachedIcon: (name) => icons?.[name] ?? defaultIcons[name] ?? "",
1216
+ trailing,
1217
+ openColorPicker,
1218
+ openBlockContextMenu
1077
1219
  };
1078
1220
  }
1221
+ 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>';
1079
1222
  function DomternalBubbleMenu({
1080
1223
  editor: editorProp,
1081
1224
  shouldShow,
@@ -1084,18 +1227,22 @@ function DomternalBubbleMenu({
1084
1227
  updateDelay,
1085
1228
  items,
1086
1229
  contexts,
1230
+ icons,
1087
1231
  children
1088
1232
  }) {
1089
1233
  const { editor: contextEditor } = useCurrentEditor();
1090
1234
  const editor = editorProp ?? contextEditor;
1091
- const htmlCacheRef = useRef(/* @__PURE__ */ new Map());
1092
1235
  const {
1093
1236
  menuRef,
1094
1237
  resolvedItems,
1095
1238
  isItemActive,
1096
1239
  isItemDisabled,
1097
1240
  executeCommand,
1098
- getCachedIcon
1241
+ activeVersion,
1242
+ getCachedIcon,
1243
+ trailing,
1244
+ openColorPicker,
1245
+ openBlockContextMenu
1099
1246
  } = useBubbleMenu({
1100
1247
  editor,
1101
1248
  shouldShow,
@@ -1103,21 +1250,54 @@ function DomternalBubbleMenu({
1103
1250
  offset,
1104
1251
  updateDelay,
1105
1252
  items,
1106
- contexts
1253
+ contexts,
1254
+ icons
1107
1255
  });
1108
- const getCachedHtml = (name) => {
1109
- const cache = htmlCacheRef.current;
1110
- const cached = cache.get(name);
1111
- if (cached) return cached;
1112
- const html = getCachedIcon(name);
1113
- cache.set(name, html);
1114
- return html;
1115
- };
1256
+ const getCachedHtml = useMemo(() => {
1257
+ const cache = /* @__PURE__ */ new Map();
1258
+ return (name) => {
1259
+ const cached = cache.get(name);
1260
+ if (cached !== void 0) return cached;
1261
+ const html = getCachedIcon(name);
1262
+ cache.set(name, html);
1263
+ return html;
1264
+ };
1265
+ }, [getCachedIcon]);
1266
+ const [openDropdown, setOpenDropdown] = useState(null);
1267
+ const closeDropdown = useCallback(() => {
1268
+ setOpenDropdown(null);
1269
+ }, []);
1270
+ const colorBtnRef = useRef(null);
1271
+ const blockMenuBtnRef = useRef(null);
1116
1272
  return /* @__PURE__ */ jsxs("div", { ref: menuRef, className: "dm-bubble-menu", role: "toolbar", "aria-label": "Text formatting", children: [
1117
1273
  resolvedItems.map((item) => {
1118
1274
  if (item.type === "separator") {
1119
1275
  return /* @__PURE__ */ jsx("span", { className: "dm-toolbar-separator", role: "separator" }, item.name);
1120
1276
  }
1277
+ if (item.type === "dropdown") {
1278
+ return /* @__PURE__ */ jsx(
1279
+ BubbleDropdown,
1280
+ {
1281
+ dropdown: item,
1282
+ isOpen: openDropdown === item.name,
1283
+ onToggle: () => {
1284
+ setOpenDropdown(openDropdown === item.name ? null : item.name);
1285
+ },
1286
+ onClose: closeDropdown,
1287
+ isItemActive,
1288
+ getCachedHtml,
1289
+ activeVersion,
1290
+ executeSubItem: (sub) => {
1291
+ closeDropdown();
1292
+ executeCommand(sub);
1293
+ requestAnimationFrame(() => {
1294
+ editor?.view.focus();
1295
+ });
1296
+ }
1297
+ },
1298
+ item.name
1299
+ );
1300
+ }
1121
1301
  const btn = item;
1122
1302
  const active = isItemActive(btn);
1123
1303
  return /* @__PURE__ */ jsx(
@@ -1130,48 +1310,388 @@ function DomternalBubbleMenu({
1130
1310
  "aria-label": btn.label,
1131
1311
  "aria-pressed": active,
1132
1312
  dangerouslySetInnerHTML: { __html: getCachedHtml(btn.icon) },
1133
- onMouseDown: (e) => e.preventDefault(),
1134
- onClick: () => executeCommand(btn)
1313
+ onMouseDown: (e) => {
1314
+ e.preventDefault();
1315
+ },
1316
+ onClick: (e) => {
1317
+ executeCommand(btn, e);
1318
+ }
1135
1319
  },
1136
1320
  btn.name
1137
1321
  );
1138
1322
  }),
1323
+ trailing.showColorPickerButton && !trailing.isNodeSelection && /* @__PURE__ */ jsxs(Fragment$1, { children: [
1324
+ /* @__PURE__ */ jsx("span", { className: "dm-toolbar-separator", role: "separator" }),
1325
+ /* @__PURE__ */ jsxs(
1326
+ "button",
1327
+ {
1328
+ ref: colorBtnRef,
1329
+ type: "button",
1330
+ className: `dm-toolbar-button dm-ncp-trigger${trailing.hasAnyColor ? " dm-toolbar-button--active" : ""}`,
1331
+ title: "Text and background color",
1332
+ "aria-label": "Text and background color",
1333
+ "aria-haspopup": "dialog",
1334
+ onMouseDown: (e) => {
1335
+ e.preventDefault();
1336
+ },
1337
+ onClick: () => {
1338
+ if (colorBtnRef.current) openColorPicker(colorBtnRef.current);
1339
+ },
1340
+ children: [
1341
+ /* @__PURE__ */ jsx(
1342
+ "span",
1343
+ {
1344
+ className: "dm-ncp-trigger-glyph",
1345
+ style: trailing.currentTextColorVar ? { color: trailing.currentTextColorVar } : void 0,
1346
+ children: "A"
1347
+ }
1348
+ ),
1349
+ /* @__PURE__ */ jsx(
1350
+ "span",
1351
+ {
1352
+ className: "dm-ncp-trigger-underline",
1353
+ style: trailing.currentBgColorVar ? { backgroundColor: trailing.currentBgColorVar } : void 0
1354
+ }
1355
+ )
1356
+ ]
1357
+ }
1358
+ )
1359
+ ] }),
1360
+ trailing.showBlockMenuButton && !trailing.isNodeSelection && /* @__PURE__ */ jsxs(Fragment$1, { children: [
1361
+ /* @__PURE__ */ jsx("span", { className: "dm-toolbar-separator", role: "separator" }),
1362
+ /* @__PURE__ */ jsx(
1363
+ "button",
1364
+ {
1365
+ ref: blockMenuBtnRef,
1366
+ type: "button",
1367
+ className: "dm-toolbar-button",
1368
+ disabled: trailing.blockMenuButtonDisabled,
1369
+ title: trailing.blockMenuButtonDisabled ? "Block actions (select within a single block)" : "More options",
1370
+ "aria-label": "More options",
1371
+ "aria-haspopup": "menu",
1372
+ dangerouslySetInnerHTML: { __html: getCachedHtml("dotsThree") },
1373
+ onMouseDown: (e) => {
1374
+ e.preventDefault();
1375
+ },
1376
+ onClick: () => {
1377
+ if (blockMenuBtnRef.current) openBlockContextMenu(blockMenuBtnRef.current);
1378
+ }
1379
+ }
1380
+ )
1381
+ ] }),
1139
1382
  children
1140
1383
  ] });
1141
1384
  }
1385
+ function BubbleDropdown({
1386
+ dropdown,
1387
+ isOpen,
1388
+ onToggle,
1389
+ onClose,
1390
+ isItemActive,
1391
+ getCachedHtml,
1392
+ activeVersion,
1393
+ executeSubItem
1394
+ }) {
1395
+ const triggerRef = useRef(null);
1396
+ const panelRef = useRef(null);
1397
+ const dropdownActive = dropdown.items.some((sub) => isItemActive(sub));
1398
+ const activeChild = dropdown.dynamicIcon ? dropdown.items.find((sub) => isItemActive(sub)) : void 0;
1399
+ const triggerIcon = activeChild?.icon ?? dropdown.icon;
1400
+ const triggerHtml = getCachedHtml(triggerIcon) + DROPDOWN_CARET2;
1401
+ useLayoutEffect(() => {
1402
+ if (!isOpen) return;
1403
+ const trigger = triggerRef.current;
1404
+ const panel = panelRef.current;
1405
+ if (!trigger || !panel) return;
1406
+ const cleanupFloating = positionFloatingOnce(trigger, panel, {
1407
+ placement: "bottom-start",
1408
+ offsetValue: 4
1409
+ });
1410
+ const controller = new AbortController();
1411
+ const { signal } = controller;
1412
+ document.addEventListener("mousedown", (e) => {
1413
+ const target = e.target;
1414
+ if (!target) return;
1415
+ if (panel.contains(target)) return;
1416
+ if (trigger.contains(target)) return;
1417
+ onClose();
1418
+ }, { signal });
1419
+ document.addEventListener("keydown", (e) => {
1420
+ if (e.key === "Escape") {
1421
+ e.preventDefault();
1422
+ onClose();
1423
+ }
1424
+ }, { signal });
1425
+ const editorEl = trigger.closest(".dm-editor");
1426
+ if (editorEl) {
1427
+ editorEl.addEventListener("dm:dismiss-overlays", () => {
1428
+ onClose();
1429
+ }, { signal });
1430
+ }
1431
+ return () => {
1432
+ controller.abort();
1433
+ cleanupFloating();
1434
+ };
1435
+ }, [isOpen, onClose]);
1436
+ return /* @__PURE__ */ jsxs("div", { className: "dm-toolbar-dropdown-wrapper", "data-dropdown-wrapper": dropdown.name, children: [
1437
+ /* @__PURE__ */ jsx(
1438
+ "button",
1439
+ {
1440
+ ref: triggerRef,
1441
+ type: "button",
1442
+ className: `dm-toolbar-button dm-toolbar-dropdown-trigger${dropdownActive ? " dm-toolbar-button--active" : ""}`,
1443
+ "aria-expanded": isOpen,
1444
+ "aria-haspopup": "true",
1445
+ "aria-label": dropdown.label,
1446
+ title: dropdown.label,
1447
+ "data-dropdown": dropdown.name,
1448
+ dangerouslySetInnerHTML: { __html: triggerHtml },
1449
+ onMouseDown: (e) => {
1450
+ e.preventDefault();
1451
+ },
1452
+ onClick: onToggle
1453
+ }
1454
+ ),
1455
+ isOpen && /* @__PURE__ */ jsx(
1456
+ "div",
1457
+ {
1458
+ ref: panelRef,
1459
+ className: "dm-toolbar-dropdown-panel",
1460
+ role: "menu",
1461
+ "data-dm-editor-ui": true,
1462
+ "data-dropdown-panel": dropdown.name,
1463
+ children: dropdown.items.map((sub) => {
1464
+ const subActive = isItemActive(sub);
1465
+ const subHtml = `${getCachedHtml(sub.icon)} ${sub.label}`;
1466
+ return /* @__PURE__ */ jsx(
1467
+ "button",
1468
+ {
1469
+ type: "button",
1470
+ className: `dm-toolbar-dropdown-item${subActive ? " dm-toolbar-dropdown-item--active" : ""}`,
1471
+ role: "menuitem",
1472
+ "aria-label": sub.label,
1473
+ dangerouslySetInnerHTML: { __html: subHtml },
1474
+ onMouseDown: (e) => {
1475
+ e.preventDefault();
1476
+ },
1477
+ onClick: () => {
1478
+ executeSubItem(sub);
1479
+ }
1480
+ },
1481
+ sub.name
1482
+ );
1483
+ })
1484
+ }
1485
+ )
1486
+ ] });
1487
+ }
1142
1488
  function DomternalFloatingMenu({
1143
1489
  editor: editorProp,
1144
1490
  shouldShow,
1145
1491
  offset = 0,
1492
+ items,
1493
+ keymap,
1494
+ icons,
1495
+ requireExplicitTrigger = false,
1146
1496
  children
1147
1497
  }) {
1148
1498
  const { editor: contextEditor } = useCurrentEditor();
1149
1499
  const editor = editorProp ?? contextEditor;
1150
1500
  const menuRef = useRef(null);
1151
1501
  const pluginKeyRef = useRef(
1152
- new PluginKey("reactFloatingMenu-" + Math.random().toString(36).slice(2, 8))
1502
+ new PluginKey("reactFloatingMenu-" + (globalThis.crypto?.randomUUID?.().slice(0, 8) ?? Math.random().toString(36).slice(2, 8)))
1153
1503
  );
1154
1504
  const shouldShowRef = useRef(shouldShow);
1155
1505
  shouldShowRef.current = shouldShow;
1156
1506
  const offsetRef = useRef(offset);
1157
1507
  offsetRef.current = offset;
1508
+ const keymapRef = useRef(keymap);
1509
+ keymapRef.current = keymap;
1510
+ const requireExplicitTriggerRef = useRef(requireExplicitTrigger);
1511
+ requireExplicitTriggerRef.current = requireExplicitTrigger;
1158
1512
  useEffect(() => {
1159
1513
  if (!editor || editor.isDestroyed || !menuRef.current) return;
1514
+ const pluginKey = pluginKeyRef.current;
1160
1515
  const plugin = createFloatingMenuPlugin({
1161
- pluginKey: pluginKeyRef.current,
1516
+ pluginKey,
1162
1517
  editor,
1163
1518
  element: menuRef.current,
1164
1519
  ...shouldShowRef.current && { shouldShow: shouldShowRef.current },
1165
- offset: offsetRef.current
1520
+ offset: offsetRef.current,
1521
+ ...keymapRef.current && { keymap: keymapRef.current },
1522
+ requireExplicitTrigger: requireExplicitTriggerRef.current
1166
1523
  });
1167
1524
  editor.registerPlugin(plugin);
1168
1525
  return () => {
1169
- if (!editor.isDestroyed) {
1170
- editor.unregisterPlugin(pluginKeyRef.current);
1171
- }
1526
+ if (!editor.isDestroyed) editor.unregisterPlugin(pluginKey);
1527
+ };
1528
+ }, [editor]);
1529
+ const useDefaultRender = !children;
1530
+ const [, bump] = useState(0);
1531
+ const forceRender = useCallback(() => {
1532
+ bump((v) => v + 1);
1533
+ }, []);
1534
+ const controllerRef = useRef(null);
1535
+ useEffect(() => {
1536
+ if (!useDefaultRender || !editor || editor.isDestroyed) {
1537
+ controllerRef.current = null;
1538
+ return;
1539
+ }
1540
+ const controller2 = new FloatingMenuController(editor, forceRender, items);
1541
+ controller2.subscribe();
1542
+ controllerRef.current = controller2;
1543
+ forceRender();
1544
+ return () => {
1545
+ controller2.destroy();
1546
+ controllerRef.current = null;
1172
1547
  };
1548
+ }, [editor, useDefaultRender, items, forceRender]);
1549
+ const controller = controllerRef.current;
1550
+ const groups = useMemo(() => controller?.groups ?? [], [controller]);
1551
+ const focusedIndex = controller?.focusedIndex ?? -1;
1552
+ useLayoutEffect(() => {
1553
+ if (focusedIndex < 0 || !menuRef.current) return;
1554
+ const target = menuRef.current.querySelector(
1555
+ `[data-floating-menu-index="${String(focusedIndex)}"]`
1556
+ );
1557
+ target?.focus();
1558
+ }, [focusedIndex]);
1559
+ const resolveIcon = useCallback((name) => {
1560
+ if (!name) return "";
1561
+ return icons?.[name] ?? defaultIcons[name] ?? "";
1562
+ }, [icons]);
1563
+ const onItemClick = useCallback((item) => {
1564
+ if (!editor || !controllerRef.current) return;
1565
+ controllerRef.current.execute(item);
1566
+ requestAnimationFrame(() => {
1567
+ editor.view.focus();
1568
+ });
1173
1569
  }, [editor]);
1174
- return /* @__PURE__ */ jsx("div", { ref: menuRef, className: "dm-floating-menu", children });
1570
+ const onMenuKeyDown = useCallback((e) => {
1571
+ const ctl = controllerRef.current;
1572
+ if (!ctl) return;
1573
+ const focused = ctl.focusedItem();
1574
+ switch (e.key) {
1575
+ case "ArrowDown":
1576
+ e.preventDefault();
1577
+ ctl.next();
1578
+ return;
1579
+ case "ArrowUp":
1580
+ e.preventDefault();
1581
+ ctl.prev();
1582
+ return;
1583
+ case "Home":
1584
+ e.preventDefault();
1585
+ ctl.first();
1586
+ return;
1587
+ case "End":
1588
+ e.preventDefault();
1589
+ ctl.last();
1590
+ return;
1591
+ case "Escape":
1592
+ e.preventDefault();
1593
+ e.stopPropagation();
1594
+ ctl.leaveMenu();
1595
+ if (editor) editor.view.focus();
1596
+ return;
1597
+ case "Enter":
1598
+ case " ":
1599
+ if (focused) {
1600
+ e.preventDefault();
1601
+ onItemClick(focused);
1602
+ }
1603
+ return;
1604
+ default:
1605
+ return;
1606
+ }
1607
+ }, [editor, onItemClick]);
1608
+ const flatNames = useMemo(
1609
+ () => groups.flatMap((g) => g.items.map((i) => i.name)),
1610
+ [groups]
1611
+ );
1612
+ if (!editor && !useDefaultRender) return null;
1613
+ return /* @__PURE__ */ jsx(
1614
+ "div",
1615
+ {
1616
+ ref: menuRef,
1617
+ className: "dm-floating-menu",
1618
+ role: "menu",
1619
+ "aria-label": "Insert block",
1620
+ "data-dm-editor-ui": "",
1621
+ onKeyDown: useDefaultRender ? onMenuKeyDown : void 0,
1622
+ children: useDefaultRender ? groups.map((group, gi) => /* @__PURE__ */ jsxs(Fragment, { children: [
1623
+ group.name && /* @__PURE__ */ jsx("div", { className: "dm-floating-menu-group-label", id: `dm-fm-g${String(gi)}`, children: group.name }),
1624
+ /* @__PURE__ */ jsx(
1625
+ "div",
1626
+ {
1627
+ className: "dm-floating-menu-group",
1628
+ role: "group",
1629
+ ...group.name && { "aria-labelledby": `dm-fm-g${String(gi)}` },
1630
+ children: group.items.map((item) => {
1631
+ const flatIndex = flatNames.indexOf(item.name);
1632
+ const isFocused = flatIndex === focusedIndex;
1633
+ const disabled = controller?.isDisabled(item) ?? false;
1634
+ return /* @__PURE__ */ jsx(
1635
+ FloatingMenuItemButton,
1636
+ {
1637
+ item,
1638
+ flatIndex,
1639
+ tabIndex: isFocused || focusedIndex < 0 && flatIndex === 0 ? 0 : -1,
1640
+ disabled,
1641
+ iconHtml: resolveIcon(item.icon),
1642
+ onClick: onItemClick
1643
+ },
1644
+ item.name
1645
+ );
1646
+ })
1647
+ }
1648
+ )
1649
+ ] }, group.name || `__group-${String(gi)}`)) : children
1650
+ }
1651
+ );
1652
+ }
1653
+ function FloatingMenuItemButton({
1654
+ item,
1655
+ flatIndex,
1656
+ tabIndex,
1657
+ disabled,
1658
+ iconHtml,
1659
+ onClick
1660
+ }) {
1661
+ const handleClick = useCallback(() => {
1662
+ onClick(item);
1663
+ }, [item, onClick]);
1664
+ const onMouseDown = useCallback((e) => {
1665
+ e.preventDefault();
1666
+ }, []);
1667
+ return /* @__PURE__ */ jsxs(
1668
+ "button",
1669
+ {
1670
+ type: "button",
1671
+ role: "menuitem",
1672
+ className: "dm-floating-menu-item",
1673
+ "data-floating-menu-item": item.name,
1674
+ "data-floating-menu-index": String(flatIndex),
1675
+ tabIndex,
1676
+ "aria-disabled": disabled || void 0,
1677
+ "aria-keyshortcuts": item.shortcut,
1678
+ disabled,
1679
+ onClick: handleClick,
1680
+ onMouseDown,
1681
+ children: [
1682
+ iconHtml && /* @__PURE__ */ jsx(
1683
+ "span",
1684
+ {
1685
+ className: "dm-floating-menu-item-icon",
1686
+ "aria-hidden": "true",
1687
+ dangerouslySetInnerHTML: { __html: iconHtml }
1688
+ }
1689
+ ),
1690
+ /* @__PURE__ */ jsx("span", { className: "dm-floating-menu-item-label", children: item.label }),
1691
+ item.shortcut && /* @__PURE__ */ jsx("span", { className: "dm-floating-menu-item-shortcut", "aria-hidden": "true", children: item.shortcut })
1692
+ ]
1693
+ }
1694
+ );
1175
1695
  }
1176
1696
  function useEmojiPicker(editor, emojis) {
1177
1697
  const [isOpen, setIsOpen] = useState(false);
@@ -1209,7 +1729,8 @@ function useEmojiPicker(editor, emojis) {
1209
1729
  const frequentlyUsed = useMemo(() => {
1210
1730
  if (!isOpen) return [];
1211
1731
  const storage = getEmojiStorage(editor);
1212
- const getFreq = storage?.["getFrequentlyUsed"];
1732
+ if (!storage) return [];
1733
+ const getFreq = storage["getFrequentlyUsed"];
1213
1734
  if (!getFreq) return [];
1214
1735
  const names = getFreq();
1215
1736
  if (!names.length) return [];
@@ -1242,7 +1763,9 @@ function useEmojiPicker(editor, emojis) {
1242
1763
  clickOutsideRef.current = (e) => {
1243
1764
  const target = e.target;
1244
1765
  if (pickerRef.current && !pickerRef.current.contains(target) && target !== anchorRef.current && !anchorRef.current?.contains(target)) {
1245
- requestAnimationFrame(() => close());
1766
+ requestAnimationFrame(() => {
1767
+ close();
1768
+ });
1246
1769
  }
1247
1770
  };
1248
1771
  document.addEventListener("mousedown", clickOutsideRef.current);
@@ -1405,7 +1928,7 @@ function DomternalEmojiPicker({ editor: editorProp, emojis }) {
1405
1928
  const swatches = Array.from(grid.querySelectorAll(".dm-emoji-swatch"));
1406
1929
  if (!swatches.length) return;
1407
1930
  const current = document.activeElement;
1408
- let idx = swatches.indexOf(current);
1931
+ const idx = swatches.indexOf(current);
1409
1932
  if (idx === -1) {
1410
1933
  if (["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp"].includes(event.key)) {
1411
1934
  event.preventDefault();
@@ -1466,8 +1989,12 @@ function DomternalEmojiPicker({ editor: editorProp, emojis }) {
1466
1989
  "aria-selected": activeCategory === cat,
1467
1990
  title: cat,
1468
1991
  "aria-label": cat,
1469
- onMouseDown: (e) => e.preventDefault(),
1470
- onClick: () => scrollToCategory(cat),
1992
+ onMouseDown: (e) => {
1993
+ e.preventDefault();
1994
+ },
1995
+ onClick: () => {
1996
+ scrollToCategory(cat);
1997
+ },
1471
1998
  children: categoryIcon(cat)
1472
1999
  },
1473
2000
  cat
@@ -1480,8 +2007,12 @@ function DomternalEmojiPicker({ editor: editorProp, emojis }) {
1480
2007
  tabIndex: -1,
1481
2008
  title: formatName(item.name),
1482
2009
  "aria-label": formatName(item.name),
1483
- onMouseDown: (e) => e.preventDefault(),
1484
- onClick: () => selectEmoji(item),
2010
+ onMouseDown: (e) => {
2011
+ e.preventDefault();
2012
+ },
2013
+ onClick: () => {
2014
+ selectEmoji(item);
2015
+ },
1485
2016
  children: item.emoji
1486
2017
  },
1487
2018
  item.name
@@ -1496,8 +2027,12 @@ function DomternalEmojiPicker({ editor: editorProp, emojis }) {
1496
2027
  tabIndex: -1,
1497
2028
  title: formatName(item.name),
1498
2029
  "aria-label": formatName(item.name),
1499
- onMouseDown: (e) => e.preventDefault(),
1500
- onClick: () => selectEmoji(item),
2030
+ onMouseDown: (e) => {
2031
+ e.preventDefault();
2032
+ },
2033
+ onClick: () => {
2034
+ selectEmoji(item);
2035
+ },
1501
2036
  children: item.emoji
1502
2037
  },
1503
2038
  item.name
@@ -1513,8 +2048,12 @@ function DomternalEmojiPicker({ editor: editorProp, emojis }) {
1513
2048
  tabIndex: -1,
1514
2049
  title: formatName(item.name),
1515
2050
  "aria-label": formatName(item.name),
1516
- onMouseDown: (e) => e.preventDefault(),
1517
- onClick: () => selectEmoji(item),
2051
+ onMouseDown: (e) => {
2052
+ e.preventDefault();
2053
+ },
2054
+ onClick: () => {
2055
+ selectEmoji(item);
2056
+ },
1518
2057
  children: item.emoji
1519
2058
  },
1520
2059
  item.name
@@ -1657,6 +2196,349 @@ function EditorContent({ editor, innerRef, ...htmlProps }) {
1657
2196
  }
1658
2197
  );
1659
2198
  }
2199
+ var TOKEN_LABELS = {
2200
+ gray: "Gray",
2201
+ brown: "Brown",
2202
+ orange: "Orange",
2203
+ yellow: "Yellow",
2204
+ green: "Green",
2205
+ blue: "Blue",
2206
+ purple: "Purple",
2207
+ pink: "Pink",
2208
+ red: "Red"
2209
+ };
2210
+ function useNotionColorPicker(options) {
2211
+ const { editor } = options;
2212
+ const [isOpen, setIsOpen] = useState(false);
2213
+ const [anchorEl, setAnchorEl] = useState(null);
2214
+ const [hostEl, setHostEl] = useState(null);
2215
+ const [currentTextToken, setCurrentTextToken] = useState(null);
2216
+ const [currentBgToken, setCurrentBgToken] = useState(null);
2217
+ const [palette, setPalette] = useState([]);
2218
+ const panelRef = useRef(null);
2219
+ const isOpenRef = useRef(false);
2220
+ isOpenRef.current = isOpen;
2221
+ const editorRef = useRef(editor);
2222
+ useEffect(() => {
2223
+ editorRef.current = editor;
2224
+ }, [editor]);
2225
+ const anchorRef = useRef(null);
2226
+ anchorRef.current = anchorEl;
2227
+ const setStorageOpen2 = useCallback((open) => {
2228
+ const ed = editorRef.current;
2229
+ if (!ed) return;
2230
+ const slot = ed.storage["notionColorPicker"];
2231
+ if (slot && typeof slot === "object") {
2232
+ slot.isOpen = open;
2233
+ }
2234
+ }, []);
2235
+ const syncFromSelection = useCallback(() => {
2236
+ const ed = editorRef.current;
2237
+ if (!ed) return;
2238
+ const { selection } = ed.state;
2239
+ let mark = null;
2240
+ if (selection.empty) {
2241
+ mark = selection.$from.marks().find((m) => m.type.name === "textStyle") ?? null;
2242
+ } else {
2243
+ ed.state.doc.nodesBetween(selection.from, selection.to, (node) => {
2244
+ if (mark) return false;
2245
+ if (node.isText) {
2246
+ const found = node.marks.find((m) => m.type.name === "textStyle");
2247
+ if (found) mark = found;
2248
+ }
2249
+ return true;
2250
+ });
2251
+ }
2252
+ const attrs = mark?.attrs ?? {};
2253
+ setCurrentTextToken(attrs.colorToken ?? null);
2254
+ setCurrentBgToken(attrs.backgroundColorToken ?? null);
2255
+ }, []);
2256
+ const close = useCallback((opts = {}) => {
2257
+ if (!isOpenRef.current) return;
2258
+ setIsOpen(false);
2259
+ setStorageOpen2(false);
2260
+ if (opts.refocus) {
2261
+ editorRef.current?.view.focus();
2262
+ }
2263
+ setAnchorEl(null);
2264
+ }, [setStorageOpen2]);
2265
+ useEffect(() => {
2266
+ if (!editor || editor.isDestroyed) return;
2267
+ const host = editor.view.dom.closest(".dm-editor") ?? null;
2268
+ setHostEl(host);
2269
+ const ext = editor.extensionManager.extensions.find((e) => e.name === "notionColorPicker");
2270
+ const extOptions = ext?.options ?? null;
2271
+ setPalette(extOptions?.palette ? [...extOptions.palette] : []);
2272
+ const onOpen = (...args) => {
2273
+ const detail = args[0];
2274
+ const incomingAnchor = detail?.anchorElement;
2275
+ if (!incomingAnchor) return;
2276
+ if (isOpenRef.current && anchorRef.current === incomingAnchor) {
2277
+ close({ refocus: true });
2278
+ return;
2279
+ }
2280
+ setAnchorEl(incomingAnchor);
2281
+ syncFromSelection();
2282
+ setIsOpen(true);
2283
+ setStorageOpen2(true);
2284
+ };
2285
+ const onSelectionUpdate = () => {
2286
+ if (!isOpenRef.current) return;
2287
+ if (!anchorRef.current?.isConnected) {
2288
+ close();
2289
+ return;
2290
+ }
2291
+ if (editor.state.selection.empty) {
2292
+ close();
2293
+ } else {
2294
+ syncFromSelection();
2295
+ }
2296
+ };
2297
+ editor.on("notionColorOpen", onOpen);
2298
+ editor.on("selectionUpdate", onSelectionUpdate);
2299
+ return () => {
2300
+ editor.off("notionColorOpen", onOpen);
2301
+ editor.off("selectionUpdate", onSelectionUpdate);
2302
+ if (isOpenRef.current) setStorageOpen2(false);
2303
+ };
2304
+ }, [editor, close, syncFromSelection, setStorageOpen2]);
2305
+ useEffect(() => {
2306
+ if (!isOpen) return;
2307
+ const controller = new AbortController();
2308
+ const { signal } = controller;
2309
+ document.addEventListener("mousedown", (e) => {
2310
+ const target = e.target;
2311
+ if (!target) return;
2312
+ if (panelRef.current?.contains(target)) return;
2313
+ if (anchorRef.current?.contains(target)) return;
2314
+ close({ refocus: false });
2315
+ }, { signal });
2316
+ document.addEventListener("keydown", (e) => {
2317
+ if (e.key === "Escape" && isOpenRef.current) {
2318
+ e.preventDefault();
2319
+ close({ refocus: true });
2320
+ }
2321
+ }, { signal });
2322
+ return () => {
2323
+ controller.abort();
2324
+ };
2325
+ }, [isOpen, close]);
2326
+ const applyText = useCallback((token) => {
2327
+ const ed = editorRef.current;
2328
+ if (!ed) return;
2329
+ ed.commands.setTextColorToken(token);
2330
+ syncFromSelection();
2331
+ }, [syncFromSelection]);
2332
+ const applyBg = useCallback((token) => {
2333
+ const ed = editorRef.current;
2334
+ if (!ed) return;
2335
+ ed.commands.setBackgroundColorToken(token);
2336
+ syncFromSelection();
2337
+ }, [syncFromSelection]);
2338
+ const tokenLabel = useCallback((token) => {
2339
+ return TOKEN_LABELS[token] ?? token.charAt(0).toUpperCase() + token.slice(1);
2340
+ }, []);
2341
+ const onPanelKeydown = useCallback((event) => {
2342
+ const cols = 5;
2343
+ const root = panelRef.current;
2344
+ if (!root) return;
2345
+ const swatches = Array.from(
2346
+ root.querySelectorAll(".dm-ncp-swatch")
2347
+ );
2348
+ if (!swatches.length) return;
2349
+ const active = document.activeElement;
2350
+ const idx = active ? swatches.indexOf(active) : -1;
2351
+ if (idx === -1) return;
2352
+ let next = idx;
2353
+ switch (event.key) {
2354
+ case "ArrowRight":
2355
+ event.preventDefault();
2356
+ next = Math.min(idx + 1, swatches.length - 1);
2357
+ break;
2358
+ case "ArrowLeft":
2359
+ event.preventDefault();
2360
+ next = Math.max(idx - 1, 0);
2361
+ break;
2362
+ case "ArrowDown":
2363
+ event.preventDefault();
2364
+ next = Math.min(idx + cols, swatches.length - 1);
2365
+ break;
2366
+ case "ArrowUp":
2367
+ event.preventDefault();
2368
+ next = Math.max(idx - cols, 0);
2369
+ break;
2370
+ case "Home":
2371
+ event.preventDefault();
2372
+ next = 0;
2373
+ break;
2374
+ case "End":
2375
+ event.preventDefault();
2376
+ next = swatches.length - 1;
2377
+ break;
2378
+ default:
2379
+ return;
2380
+ }
2381
+ swatches[next]?.focus();
2382
+ }, []);
2383
+ return {
2384
+ isOpen,
2385
+ hostEl,
2386
+ anchorEl,
2387
+ panelRef,
2388
+ currentTextToken,
2389
+ currentBgToken,
2390
+ palette,
2391
+ applyText,
2392
+ applyBg,
2393
+ close,
2394
+ tokenLabel,
2395
+ onPanelKeydown
2396
+ };
2397
+ }
2398
+ function DomternalNotionColorPicker({
2399
+ editor: editorProp,
2400
+ children
2401
+ }) {
2402
+ const { editor: contextEditor } = useCurrentEditor();
2403
+ const editor = editorProp ?? contextEditor;
2404
+ const api = useNotionColorPicker({ editor });
2405
+ const {
2406
+ isOpen,
2407
+ hostEl,
2408
+ anchorEl,
2409
+ panelRef,
2410
+ currentTextToken,
2411
+ currentBgToken,
2412
+ palette,
2413
+ applyText,
2414
+ applyBg,
2415
+ tokenLabel,
2416
+ onPanelKeydown
2417
+ } = api;
2418
+ useLayoutEffect(() => {
2419
+ if (!isOpen || !anchorEl || !panelRef.current) return;
2420
+ const panel = panelRef.current;
2421
+ const cleanupFloating = positionFloating(anchorEl, panel, {
2422
+ placement: "bottom-start",
2423
+ offsetValue: 4
2424
+ });
2425
+ let id2 = 0;
2426
+ const id1 = requestAnimationFrame(() => {
2427
+ id2 = requestAnimationFrame(() => {
2428
+ if (!panel.isConnected) return;
2429
+ const active = panel.querySelector(".dm-ncp-swatch.dm-ncp-active");
2430
+ const fallback = panel.querySelector('.dm-ncp-swatch--text[data-color="null"]');
2431
+ (active ?? fallback)?.focus({ preventScroll: true });
2432
+ });
2433
+ });
2434
+ return () => {
2435
+ cancelAnimationFrame(id1);
2436
+ if (id2) cancelAnimationFrame(id2);
2437
+ cleanupFloating();
2438
+ };
2439
+ }, [isOpen, anchorEl, panelRef]);
2440
+ if (!isOpen || !hostEl) return null;
2441
+ const defaultContent = /* @__PURE__ */ jsxs(Fragment$1, { children: [
2442
+ /* @__PURE__ */ jsxs("div", { className: "dm-ncp-section", children: [
2443
+ /* @__PURE__ */ jsx("div", { className: "dm-ncp-label", children: "Text color" }),
2444
+ /* @__PURE__ */ jsxs("div", { className: "dm-ncp-grid", children: [
2445
+ /* @__PURE__ */ jsx(
2446
+ "button",
2447
+ {
2448
+ type: "button",
2449
+ className: `dm-ncp-swatch dm-ncp-swatch--text${currentTextToken === null ? " dm-ncp-active" : ""}`,
2450
+ "aria-pressed": currentTextToken === null,
2451
+ "data-color": "null",
2452
+ title: "Default text color",
2453
+ "aria-label": "Default text color",
2454
+ onMouseDown: (e) => {
2455
+ e.preventDefault();
2456
+ },
2457
+ onClick: () => {
2458
+ applyText(null);
2459
+ }
2460
+ }
2461
+ ),
2462
+ palette.map((t) => /* @__PURE__ */ jsx(
2463
+ "button",
2464
+ {
2465
+ type: "button",
2466
+ className: `dm-ncp-swatch dm-ncp-swatch--text${currentTextToken === t ? " dm-ncp-active" : ""}`,
2467
+ "aria-pressed": currentTextToken === t,
2468
+ "data-color": t,
2469
+ title: tokenLabel(t),
2470
+ "aria-label": `${tokenLabel(t)} text`,
2471
+ onMouseDown: (e) => {
2472
+ e.preventDefault();
2473
+ },
2474
+ onClick: () => {
2475
+ applyText(t);
2476
+ }
2477
+ },
2478
+ t
2479
+ ))
2480
+ ] })
2481
+ ] }),
2482
+ /* @__PURE__ */ jsxs("div", { className: "dm-ncp-section", children: [
2483
+ /* @__PURE__ */ jsx("div", { className: "dm-ncp-label", children: "Background color" }),
2484
+ /* @__PURE__ */ jsxs("div", { className: "dm-ncp-grid", children: [
2485
+ /* @__PURE__ */ jsx(
2486
+ "button",
2487
+ {
2488
+ type: "button",
2489
+ className: `dm-ncp-swatch dm-ncp-swatch--bg${currentBgToken === null ? " dm-ncp-active" : ""}`,
2490
+ "aria-pressed": currentBgToken === null,
2491
+ "data-color": "null",
2492
+ title: "Default background",
2493
+ "aria-label": "Default background",
2494
+ onMouseDown: (e) => {
2495
+ e.preventDefault();
2496
+ },
2497
+ onClick: () => {
2498
+ applyBg(null);
2499
+ }
2500
+ }
2501
+ ),
2502
+ palette.map((t) => /* @__PURE__ */ jsx(
2503
+ "button",
2504
+ {
2505
+ type: "button",
2506
+ className: `dm-ncp-swatch dm-ncp-swatch--bg${currentBgToken === t ? " dm-ncp-active" : ""}`,
2507
+ "aria-pressed": currentBgToken === t,
2508
+ "data-color": t,
2509
+ title: `${tokenLabel(t)} background`,
2510
+ "aria-label": `${tokenLabel(t)} background`,
2511
+ onMouseDown: (e) => {
2512
+ e.preventDefault();
2513
+ },
2514
+ onClick: () => {
2515
+ applyBg(t);
2516
+ }
2517
+ },
2518
+ t
2519
+ ))
2520
+ ] })
2521
+ ] })
2522
+ ] });
2523
+ const content = typeof children === "function" ? children(api) : children ?? defaultContent;
2524
+ return createPortal(
2525
+ /* @__PURE__ */ jsx(
2526
+ "div",
2527
+ {
2528
+ ref: panelRef,
2529
+ className: "dm-notion-color-picker",
2530
+ "data-show": true,
2531
+ "data-dm-editor-ui": true,
2532
+ role: "dialog",
2533
+ "aria-label": "Text and background color",
2534
+ "aria-modal": "false",
2535
+ onKeyDown: onPanelKeydown,
2536
+ children: content
2537
+ }
2538
+ ),
2539
+ hostEl
2540
+ );
2541
+ }
1660
2542
  var ReactNodeViewContext = createContext(null);
1661
2543
  var ReactNodeViewProvider = ReactNodeViewContext.Provider;
1662
2544
  function useReactNodeView() {
@@ -1775,7 +2657,9 @@ var ReactNodeView = class {
1775
2657
  }
1776
2658
  destroy() {
1777
2659
  const root = this.root;
1778
- setTimeout(() => root.unmount(), 0);
2660
+ setTimeout(() => {
2661
+ root.unmount();
2662
+ }, 0);
1779
2663
  }
1780
2664
  ignoreMutation(mutation) {
1781
2665
  if (!this.contentDOM) return true;
@@ -1810,6 +2694,6 @@ function NodeViewContent({ as: Tag = "div", style, ...props }) {
1810
2694
  );
1811
2695
  }
1812
2696
 
1813
- export { DEFAULT_EXTENSIONS, Domternal, DomternalBubbleMenu, DomternalEditor, DomternalEmojiPicker, DomternalFloatingMenu, DomternalToolbar, EditorContent, EditorProvider, NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer, useCurrentEditor, useEditor, useEditorState, useReactNodeView };
2697
+ export { DEFAULT_EXTENSIONS, Domternal, DomternalBubbleMenu, DomternalEditor, DomternalEmojiPicker, DomternalFloatingMenu, DomternalNotionColorPicker, DomternalToolbar, EditorContent, EditorProvider, NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer, useCurrentEditor, useEditor, useEditorState, useNotionColorPicker, useReactNodeView };
1814
2698
  //# sourceMappingURL=index.js.map
1815
2699
  //# sourceMappingURL=index.js.map