@domternal/react 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,7 +19,7 @@ and React 19.
19
19
  pnpm add @domternal/react @domternal/core react react-dom
20
20
  ```
21
21
 
22
- `react` (>=18), `react-dom` (>=18), and `@domternal/core` (>=0.14.0) are peer dependencies. Import the theme
22
+ `react` (>=18), `react-dom` (>=18), and `@domternal/core` (>=0.15.0) are peer dependencies. Import the theme
23
23
  ([`@domternal/theme`](https://www.npmjs.com/package/@domternal/theme)) in your entry CSS for default styling:
24
24
 
25
25
  ```css
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { DependencyList, ReactNode, HTMLAttributes, Ref, RefObject, KeyboardEvent, ElementType, RefCallback } from 'react';
3
- import { AnyExtension, Content, FocusPosition, Editor, JSONContent, IconSet, ToolbarLayoutEntry, BubbleMenuOptions, FloatingMenuOptions, FloatingMenuItemsOverride, FloatingMenuKeymap } from '@domternal/core';
3
+ import { AnyExtension, Content, EditorPreset, FocusPosition, Editor, JSONContent, IconSet, ToolbarLayoutEntry, BubbleMenuOptions, FloatingMenuOptions, FloatingMenuItemsOverride, FloatingMenuKeymap } from '@domternal/core';
4
4
  export { AnyExtension, Content, Editor, FocusPosition, GenerateHTMLOptions, GenerateJSONOptions, GenerateTextOptions, JSONContent, generateHTML, generateJSON, generateText } from '@domternal/core';
5
5
 
6
6
  declare const DEFAULT_EXTENSIONS: AnyExtension[];
@@ -17,6 +17,12 @@ interface UseEditorOptions {
17
17
  content?: Content;
18
18
  /** Whether the editor is editable. @default true */
19
19
  editable?: boolean;
20
+ /**
21
+ * Editing experience preset. `'notion'` paints `dm-notion-mode` on the
22
+ * `.dm-editor` wrapper and switches preset-aware extensions to their
23
+ * Notion behavior, replacing the hand-written class. Create-time only.
24
+ */
25
+ preset?: EditorPreset;
20
26
  /** Where to autofocus on mount. @default false */
21
27
  autofocus?: FocusPosition;
22
28
  /** Output format for content comparison. @default 'html' */
package/dist/index.js CHANGED
@@ -53,7 +53,8 @@ function useEditor(options = {}, deps) {
53
53
  extensions: [...defaults, ...extensions],
54
54
  content: initialContent,
55
55
  editable,
56
- autofocus: focus
56
+ autofocus: focus,
57
+ ...options.preset ? { preset: options.preset } : {}
57
58
  });
58
59
  wireEvents(ed);
59
60
  instanceRef.current = ed;
@@ -96,6 +97,7 @@ function useEditor(options = {}, deps) {
96
97
  const mount = editorRef.current;
97
98
  if (mount && existing.view.dom.parentElement !== mount) {
98
99
  mount.appendChild(existing.view.dom);
100
+ existing.adoptPresetClass();
99
101
  }
100
102
  callbacksRef.current.onCreate?.(existing);
101
103
  return () => {
@@ -260,6 +262,7 @@ function useToolbarController(editor, layout) {
260
262
  const toolbarRef = useRef(null);
261
263
  const cleanupFloatingRef = useRef(null);
262
264
  const clickOutsideRef = useRef(null);
265
+ const escapeKeydownRef = useRef(null);
263
266
  const dismissOverlayRef = useRef(null);
264
267
  const editorElRef = useRef(null);
265
268
  const syncStateRafRef = useRef(0);
@@ -295,6 +298,18 @@ function useToolbarController(editor, layout) {
295
298
  };
296
299
  clickOutsideRef.current = clickOutside;
297
300
  document.addEventListener("mousedown", clickOutside);
301
+ const escapeKeydown = (e) => {
302
+ if (!controller.openDropdown) return;
303
+ if (e.key !== "Escape") return;
304
+ if (toolbarRef.current?.contains(document.activeElement)) return;
305
+ cleanupFloatingRef.current?.();
306
+ cleanupFloatingRef.current = null;
307
+ controller.closeDropdown();
308
+ syncState();
309
+ e.preventDefault();
310
+ };
311
+ escapeKeydownRef.current = escapeKeydown;
312
+ document.addEventListener("keydown", escapeKeydown);
298
313
  const editorEl = editor.view.dom.closest(".dm-editor");
299
314
  editorElRef.current = editorEl;
300
315
  if (editorEl) {
@@ -317,6 +332,10 @@ function useToolbarController(editor, layout) {
317
332
  document.removeEventListener("mousedown", clickOutsideRef.current);
318
333
  clickOutsideRef.current = null;
319
334
  }
335
+ if (escapeKeydownRef.current) {
336
+ document.removeEventListener("keydown", escapeKeydownRef.current);
337
+ escapeKeydownRef.current = null;
338
+ }
320
339
  if (dismissOverlayRef.current && editorElRef.current) {
321
340
  editorElRef.current.removeEventListener("dm:dismiss-overlays", dismissOverlayRef.current);
322
341
  dismissOverlayRef.current = null;
@@ -603,6 +622,19 @@ function getInlineStyleAtCursor(editor, prop) {
603
622
  return null;
604
623
  }
605
624
  }
625
+ function useInnerHtml() {
626
+ const cache = useRef(null);
627
+ cache.current ??= /* @__PURE__ */ new Map();
628
+ return (html) => {
629
+ const store = cache.current;
630
+ let prop = store.get(html);
631
+ if (prop === void 0) {
632
+ prop = { __html: html };
633
+ store.set(html, prop);
634
+ }
635
+ return prop;
636
+ };
637
+ }
606
638
  function ToolbarButton({
607
639
  item,
608
640
  isActive,
@@ -614,6 +646,7 @@ function ToolbarButton({
614
646
  onClick,
615
647
  onFocus
616
648
  }) {
649
+ const innerHtml = useInnerHtml();
617
650
  return /* @__PURE__ */ jsx(
618
651
  "button",
619
652
  {
@@ -625,7 +658,7 @@ function ToolbarButton({
625
658
  title: tooltip,
626
659
  tabIndex,
627
660
  disabled: isDisabled,
628
- dangerouslySetInnerHTML: { __html: iconHtml },
661
+ dangerouslySetInnerHTML: innerHtml(iconHtml),
629
662
  onMouseDown: (e) => {
630
663
  e.preventDefault();
631
664
  },
@@ -644,6 +677,7 @@ function ToolbarDropdownPanel({
644
677
  getCachedItemContent,
645
678
  onItemClick
646
679
  }) {
680
+ const innerHtml = useInnerHtml();
647
681
  if (dropdown.layout === "grid") {
648
682
  return /* @__PURE__ */ jsx(
649
683
  "div",
@@ -678,7 +712,7 @@ function ToolbarDropdownPanel({
678
712
  role: "menuitem",
679
713
  tabIndex: -1,
680
714
  "aria-label": sub.label,
681
- dangerouslySetInnerHTML: { __html: getCachedItemContent(sub.icon, sub.label) },
715
+ dangerouslySetInnerHTML: innerHtml(getCachedItemContent(sub.icon, sub.label)),
682
716
  onMouseDown: (e) => {
683
717
  e.preventDefault();
684
718
  },
@@ -709,7 +743,7 @@ function ToolbarDropdownPanel({
709
743
  ref: (el) => {
710
744
  if (el && sub.style) el.setAttribute("style", sub.style);
711
745
  },
712
- dangerouslySetInnerHTML: { __html: getCachedItemContent(sub.icon, sub.label, dropdown.displayMode) },
746
+ dangerouslySetInnerHTML: innerHtml(getCachedItemContent(sub.icon, sub.label, dropdown.displayMode)),
713
747
  onMouseDown: (e) => {
714
748
  e.preventDefault();
715
749
  },
@@ -735,6 +769,7 @@ function ToolbarDropdown({
735
769
  onItemClick,
736
770
  onFocus
737
771
  }) {
772
+ const innerHtml = useInnerHtml();
738
773
  return /* @__PURE__ */ jsxs("div", { className: "dm-toolbar-dropdown-wrapper", children: [
739
774
  /* @__PURE__ */ jsx(
740
775
  "button",
@@ -748,7 +783,7 @@ function ToolbarDropdown({
748
783
  tabIndex,
749
784
  disabled: isDisabled,
750
785
  "data-dropdown": dropdown.name,
751
- dangerouslySetInnerHTML: { __html: triggerHtml },
786
+ dangerouslySetInnerHTML: innerHtml(triggerHtml),
752
787
  onMouseDown: (e) => {
753
788
  e.preventDefault();
754
789
  },
@@ -1140,7 +1175,8 @@ function useBubbleMenu(options) {
1140
1175
  }
1141
1176
  setTrailing({
1142
1177
  isNodeSelection: isNode,
1143
- showColorPickerButton: hasNotionColorPicker,
1178
+ // Hidden without a live notionColorOpen listener: the trigger would be dead.
1179
+ showColorPickerButton: hasNotionColorPicker && ed.listenerCount("notionColorOpen") > 0,
1144
1180
  showBlockMenuButton: hasBlockContextMenu,
1145
1181
  blockMenuButtonDisabled: blockMenuDisabled,
1146
1182
  currentTextColorVar: textVar,
@@ -1297,6 +1333,7 @@ function DomternalBubbleMenu({
1297
1333
  }, []);
1298
1334
  const colorBtnRef = useRef(null);
1299
1335
  const blockMenuBtnRef = useRef(null);
1336
+ const innerHtml = useInnerHtml();
1300
1337
  return /* @__PURE__ */ jsxs("div", { ref: menuRef, className: "dm-bubble-menu", role: "toolbar", "aria-label": "Text formatting", children: [
1301
1338
  resolvedItems.map((item) => {
1302
1339
  if (item.type === "separator") {
@@ -1335,7 +1372,7 @@ function DomternalBubbleMenu({
1335
1372
  title: btn.label,
1336
1373
  "aria-label": btn.label,
1337
1374
  "aria-pressed": active,
1338
- dangerouslySetInnerHTML: { __html: getCachedHtml(btn.icon) },
1375
+ dangerouslySetInnerHTML: innerHtml(getCachedHtml(btn.icon)),
1339
1376
  onMouseDown: (e) => {
1340
1377
  e.preventDefault();
1341
1378
  },
@@ -1395,7 +1432,7 @@ function DomternalBubbleMenu({
1395
1432
  title: trailing.blockMenuButtonDisabled ? "Block actions (select within a single block)" : "More options",
1396
1433
  "aria-label": "More options",
1397
1434
  "aria-haspopup": "menu",
1398
- dangerouslySetInnerHTML: { __html: getCachedHtml("dotsThree") },
1435
+ dangerouslySetInnerHTML: innerHtml(getCachedHtml("dotsThree")),
1399
1436
  onMouseDown: (e) => {
1400
1437
  e.preventDefault();
1401
1438
  },
@@ -1420,6 +1457,7 @@ function BubbleDropdown({
1420
1457
  }) {
1421
1458
  const triggerRef = useRef(null);
1422
1459
  const panelRef = useRef(null);
1460
+ const innerHtml = useInnerHtml();
1423
1461
  const dropdownActive = dropdown.items.some((sub) => isItemActive(sub));
1424
1462
  const activeChild = dropdown.dynamicIcon ? dropdown.items.find((sub) => isItemActive(sub)) : void 0;
1425
1463
  const triggerIcon = activeChild?.icon ?? dropdown.icon;
@@ -1471,7 +1509,7 @@ function BubbleDropdown({
1471
1509
  "aria-label": dropdown.label,
1472
1510
  title: dropdown.label,
1473
1511
  "data-dropdown": dropdown.name,
1474
- dangerouslySetInnerHTML: { __html: triggerHtml },
1512
+ dangerouslySetInnerHTML: innerHtml(triggerHtml),
1475
1513
  onMouseDown: (e) => {
1476
1514
  e.preventDefault();
1477
1515
  },
@@ -1496,7 +1534,7 @@ function BubbleDropdown({
1496
1534
  className: `dm-toolbar-dropdown-item${subActive ? " dm-toolbar-dropdown-item--active" : ""}`,
1497
1535
  role: "menuitem",
1498
1536
  "aria-label": sub.label,
1499
- dangerouslySetInnerHTML: { __html: subHtml },
1537
+ dangerouslySetInnerHTML: innerHtml(subHtml),
1500
1538
  onMouseDown: (e) => {
1501
1539
  e.preventDefault();
1502
1540
  },
@@ -1682,6 +1720,7 @@ function FloatingMenuItemButton({
1682
1720
  iconHtml,
1683
1721
  onClick
1684
1722
  }) {
1723
+ const innerHtml = useInnerHtml();
1685
1724
  const handleClick = useCallback(() => {
1686
1725
  onClick(item);
1687
1726
  }, [item, onClick]);
@@ -1708,7 +1747,7 @@ function FloatingMenuItemButton({
1708
1747
  {
1709
1748
  className: "dm-floating-menu-item-icon",
1710
1749
  "aria-hidden": "true",
1711
- dangerouslySetInnerHTML: { __html: iconHtml }
1750
+ dangerouslySetInnerHTML: innerHtml(iconHtml)
1712
1751
  }
1713
1752
  ),
1714
1753
  /* @__PURE__ */ jsx("span", { className: "dm-floating-menu-item-label", children: item.label }),
@@ -2099,6 +2138,7 @@ function DomternalContent({ className }) {
2099
2138
  const editorDom = editor.view.dom;
2100
2139
  if (editorDom.parentElement !== container) {
2101
2140
  container.appendChild(editorDom);
2141
+ editor.adoptPresetClass();
2102
2142
  }
2103
2143
  }, [editor]);
2104
2144
  const classes = className ? `dm-editor ${className}` : "dm-editor";
@@ -2203,6 +2243,7 @@ function EditorContent({ editor, innerRef, ...htmlProps }) {
2203
2243
  const editorDom = editor.view.dom;
2204
2244
  if (editorDom.parentElement !== container) {
2205
2245
  container.appendChild(editorDom);
2246
+ editor.adoptPresetClass();
2206
2247
  }
2207
2248
  return () => {
2208
2249
  };