@monolith-forensics/monolith-ui 1.9.1-dev.8 → 1.9.1-dev.9

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.
@@ -3,12 +3,9 @@ import { DropDownItem, DropDownMenuProps } from "../../DropDownMenu";
3
3
  import { ReactElement } from "react";
4
4
  import { ButtonProps } from "../../Button";
5
5
  import { Editor } from "@tiptap/react";
6
- export interface BubbleMenuProps {
6
+ export interface BubbleMenuContentProps {
7
7
  className?: string;
8
8
  editor: Editor;
9
- rect: DOMRect;
10
- open: boolean;
11
- onOpen?: (element: HTMLElement) => void;
12
9
  customMenuItems?: BubbleItem[];
13
10
  }
14
11
  interface BubbleMenuDropDownItem extends DropDownItem {
@@ -39,5 +36,8 @@ export type BubbleItem = {
39
36
  buttonProps?: Partial<ButtonProps>;
40
37
  onClick?: (editor: Editor) => void;
41
38
  };
42
- declare const BubbleMenu: React.FC<BubbleMenuProps>;
39
+ export type BubbleMenuOptions = {
40
+ customMenuItems?: BubbleItem[];
41
+ };
42
+ declare const BubbleMenu: React.FC<BubbleMenuContentProps>;
43
43
  export default BubbleMenu;
@@ -3,8 +3,6 @@ import styled, { useTheme } from "styled-components";
3
3
  import { Extensions } from "../Enums";
4
4
  import { BoldIcon, ItalicIcon, UnderlineIcon, CaseSensitiveIcon, ListIcon, ListOrderedIcon, StrikethroughIcon, Heading1Icon, Heading2Icon, Heading3Icon, Heading4Icon, RemoveFormattingIcon, SquircleIcon, } from "lucide-react";
5
5
  import { DropDownMenu, } from "../../DropDownMenu";
6
- import { FloatingPortal, useFloating } from "@floating-ui/react";
7
- import { useEffect, useRef } from "react";
8
6
  import { Button } from "../../Button";
9
7
  import TextColors from "../Enums/TextColors";
10
8
  const getMenuItems = (editor, customMenuItems, theme) => {
@@ -224,7 +222,6 @@ const getMenuItems = (editor, customMenuItems, theme) => {
224
222
  ];
225
223
  };
226
224
  const BubbleMenuContent = styled.div `
227
- position: fixed;
228
225
  display: flex;
229
226
  justify-content: space-between;
230
227
  align-items: center;
@@ -272,51 +269,23 @@ const BubbleItemButton = styled(Button) `
272
269
  background-color: ${({ theme }) => theme.palette.action.hover};
273
270
  }
274
271
  `;
275
- const BubbleMenu = ({ editor, rect, open, onOpen, customMenuItems = [], }) => {
276
- var _a;
277
- const menuRef = useRef(null);
278
- const { refs, elements } = useFloating();
272
+ const BubbleMenu = ({ className, editor, customMenuItems = [], }) => {
279
273
  const theme = useTheme();
280
- useEffect(() => {
281
- if (open && onOpen) {
282
- onOpen(elements.floating);
283
- }
284
- }, [open, onOpen, elements.floating]);
285
- const elementWidth = ((_a = elements.floating) === null || _a === void 0 ? void 0 : _a.offsetWidth) || 0;
286
274
  const { from, to } = editor.state.selection;
287
275
  const selectedText = editor.state.doc.textBetween(from, to, "\n", "\n");
288
- let top = (rect === null || rect === void 0 ? void 0 : rect.top) ? rect.top - 50 : 0;
289
- if (top < 10) {
290
- top = 10; // add some padding
291
- }
292
- let left = (rect === null || rect === void 0 ? void 0 : rect.left) ? rect.left + rect.width / 2 - elementWidth / 2 : 0;
293
- if (left < 10) {
294
- left = 10; // add some padding
295
- }
296
- // Check if the menu is overflowing on the right
297
- const rightOverflow = left + elementWidth - window.innerWidth;
298
- if (rightOverflow > -10) {
299
- left -= rightOverflow + 10; // add some padding
300
- }
301
- return (_jsx(FloatingPortal, { preserveTabOrder: true, children: open && (_jsx(BubbleMenuContent, { ref: (ref) => {
302
- refs.setFloating(ref);
303
- menuRef.current = ref;
304
- }, style: {
305
- top,
306
- left,
307
- }, children: getMenuItems(editor, customMenuItems, theme).map((item) => {
308
- var _a;
309
- if (item.type === "button") {
310
- const isActive = (_a = item.isActive) === null || _a === void 0 ? void 0 : _a.call(item, editor);
311
- return (_jsx(BubbleItemButton, { variant: "subtle", onClick: () => { var _a; return (_a = item === null || item === void 0 ? void 0 : item.onClick) === null || _a === void 0 ? void 0 : _a.call(item, editor); }, color: isActive ? "primary" : undefined, selected: isActive, children: item.icon && _jsx(item.icon, { size: 14 }) }, item.name));
312
- }
313
- if (item.type === "menu") {
314
- return (_jsx(DropDownMenu, Object.assign({ data: item.items, size: "xs", arrow: item.arrow, buttonProps: item.buttonProps, variant: "subtle", buttonRender: item.buttonRender, onItemSelect: (item) => { var _a, _b; return (_b = (_a = item === null || item === void 0 ? void 0 : item.data) === null || _a === void 0 ? void 0 : _a.command) === null || _b === void 0 ? void 0 : _b.call(_a, editor, selectedText); }, dropDownProps: {
315
- style: { width: 135 },
316
- } }, item.dropDownProps, { children: item.icon
317
- ? (_jsx(item.icon, { size: 14 }))
318
- : (item.label || item.name) }), item.name));
319
- }
320
- }) })) }));
276
+ return (_jsx(BubbleMenuContent, { className: className, children: getMenuItems(editor, customMenuItems, theme).map((item) => {
277
+ var _a;
278
+ if (item.type === "button") {
279
+ const isActive = (_a = item.isActive) === null || _a === void 0 ? void 0 : _a.call(item, editor);
280
+ return (_jsx(BubbleItemButton, { variant: "subtle", onClick: () => { var _a; return (_a = item === null || item === void 0 ? void 0 : item.onClick) === null || _a === void 0 ? void 0 : _a.call(item, editor); }, color: isActive ? "primary" : undefined, selected: isActive, children: item.icon && _jsx(item.icon, { size: 14 }) }, item.name));
281
+ }
282
+ if (item.type === "menu") {
283
+ return (_jsx(DropDownMenu, Object.assign({ data: item.items, size: "xs", arrow: item.arrow, buttonProps: item.buttonProps, variant: "subtle", buttonRender: item.buttonRender, onItemSelect: (item) => { var _a, _b; return (_b = (_a = item === null || item === void 0 ? void 0 : item.data) === null || _a === void 0 ? void 0 : _a.command) === null || _b === void 0 ? void 0 : _b.call(_a, editor, selectedText); }, dropDownProps: {
284
+ style: { width: 135 },
285
+ } }, item.dropDownProps, { children: item.icon
286
+ ? (_jsx(item.icon, { size: 14 }))
287
+ : (item.label || item.name) }), item.name));
288
+ }
289
+ }) }));
321
290
  };
322
291
  export default BubbleMenu;
@@ -1,12 +1,10 @@
1
1
  import { Extension } from "@tiptap/core";
2
2
  import { HandleImageUpload } from "../Plugins";
3
3
  import { Extensions } from "../Enums";
4
- import { BubbleMenuOptions } from "./BubbleMenuExtension";
5
4
  export type ExtensionType = (typeof Extensions)[keyof typeof Extensions];
6
5
  interface GetTipTapExtensionsProps {
7
6
  extensions?: ExtensionType[];
8
7
  slashCommands?: any[];
9
- bubbleMenuOptions?: BubbleMenuOptions;
10
8
  handleImageUpload?: HandleImageUpload;
11
9
  }
12
10
  declare const getTipTapExtensions: (props: GetTipTapExtensionsProps) => Extension[];
@@ -10,7 +10,6 @@ import { InputRule, Extension } from "@tiptap/core";
10
10
  import { ImageActionsPlugin, UploadImagesPlugin, } from "../Plugins";
11
11
  import getSlashCommand from "./getSlashCommand";
12
12
  import { Extensions } from "../Enums";
13
- import BubbleMenu from "./BubbleMenuExtension";
14
13
  const CustomImage = TiptapImage.extend({
15
14
  // Add data-uuid attribute to image
16
15
  addAttributes() {
@@ -42,7 +41,7 @@ const CustomStorage = Extension.create({
42
41
  };
43
42
  },
44
43
  });
45
- const getTipTapExtensions = ({ extensions = [], slashCommands = [], bubbleMenuOptions, handleImageUpload, }) => {
44
+ const getTipTapExtensions = ({ extensions = [], slashCommands = [], handleImageUpload, }) => {
46
45
  return [
47
46
  {
48
47
  name: "starterKit",
@@ -162,11 +161,6 @@ const getTipTapExtensions = ({ extensions = [], slashCommands = [], bubbleMenuOp
162
161
  category: "default",
163
162
  extension: CustomStorage,
164
163
  },
165
- {
166
- name: Extensions.BubbleMenu,
167
- category: "default",
168
- extension: BubbleMenu.configure(bubbleMenuOptions),
169
- },
170
164
  {
171
165
  name: Extensions.SlashCommand,
172
166
  extension: getSlashCommand({
@@ -1,7 +1,7 @@
1
1
  import { Editor } from "@tiptap/react";
2
2
  import { ExtensionType } from "./Extensions/getTiptapExtensions";
3
3
  import { HandleImageUrlUpload, HandleImageUpload } from "./Plugins/UploadImagesPlugin";
4
- import { BubbleMenuOptions } from "./Extensions/BubbleMenuExtension";
4
+ import { BubbleMenuOptions } from "./Components/BubbleMenu";
5
5
  import { ToolbarOptions } from "./Toolbar/Toolbar";
6
6
  type RichTextEditorProps = {
7
7
  className?: string;
@@ -8,15 +8,18 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
- import { useEffect, useRef, useState } from "react";
11
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
12
12
  import styled from "styled-components";
13
13
  import { EditorContent, useEditor } from "@tiptap/react";
14
+ import { BubbleMenu as TiptapBubbleMenu, } from "@tiptap/react/menus";
15
+ import { isTextSelection } from "@tiptap/core";
14
16
  import { DOMParser as ProseMirrorDOMParser } from "@tiptap/pm/model";
15
17
  import { Toolbar } from "./Toolbar";
16
18
  import getTipTapExtensions from "./Extensions/getTiptapExtensions";
17
19
  import { Extensions, SlashCommands } from "./Enums";
18
20
  import { addImagePlaceholder, removeImagePlaceholder, startImageUpload, } from "./Plugins/UploadImagesPlugin";
19
21
  import SaveBadge from "./Components/SaveBadge";
22
+ import BubbleMenuContent from "./Components/BubbleMenu";
20
23
  import Fonts from "./Enums/Fonts";
21
24
  import RichTextEditorContext from "./Contexts/RichTextEditorContext";
22
25
  const getImageFilesFromClipboard = (clipboardData) => {
@@ -547,12 +550,52 @@ export const RichTextEditor = ({ className, editorInstanceRef, defaultValue = ""
547
550
  const isControlled = value !== undefined;
548
551
  const hasImageExtension = extensions.includes(Extensions.Image);
549
552
  const hasSlashCommandExtension = extensions.includes(Extensions.SlashCommand);
553
+ const hasBubbleMenuExtension = extensions.includes(Extensions.BubbleMenu);
550
554
  const hasImageSlashCommand = hasSlashCommandExtension && slashCommands.includes(SlashCommands.Image);
551
555
  const onChangeRef = useRef(onChange);
556
+ const bubbleMenuPortalRef = useRef(null);
552
557
  const [fontState, setFontState] = useState(font || Fonts.DEFAULT);
553
558
  useEffect(() => {
554
559
  onChangeRef.current = onChange;
555
560
  }, [onChange]);
561
+ const getBubbleMenuPortalRoot = useCallback(() => {
562
+ if (bubbleMenuPortalRef.current) {
563
+ return bubbleMenuPortalRef.current;
564
+ }
565
+ const portal = document.createElement("div");
566
+ portal.setAttribute("data-monolith-bubble-menu-portal", "");
567
+ document.body.appendChild(portal);
568
+ bubbleMenuPortalRef.current = portal;
569
+ return portal;
570
+ }, []);
571
+ useEffect(() => {
572
+ return () => {
573
+ var _a;
574
+ (_a = bubbleMenuPortalRef.current) === null || _a === void 0 ? void 0 : _a.remove();
575
+ bubbleMenuPortalRef.current = null;
576
+ };
577
+ }, []);
578
+ const shouldShowBubbleMenu = useCallback(({ editor, element, view, state, from, to }) => {
579
+ const { selection } = state;
580
+ const isChildOfMenu = element.contains(document.activeElement);
581
+ const hasEditorFocus = view.hasFocus() || isChildOfMenu;
582
+ const selectedText = state.doc.textBetween(from, to).trim();
583
+ const isEmptyTextBlock = !selectedText && isTextSelection(state.selection);
584
+ if (!hasEditorFocus ||
585
+ selection.empty ||
586
+ isEmptyTextBlock ||
587
+ !editor.isEditable) {
588
+ return false;
589
+ }
590
+ return true;
591
+ }, []);
592
+ const bubbleMenuPositionOptions = useMemo(() => ({
593
+ strategy: "fixed",
594
+ placement: "top",
595
+ offset: 8,
596
+ flip: false,
597
+ shift: { padding: 10 },
598
+ }), []);
556
599
  if (hasImageSlashCommand && !hasImageExtension) {
557
600
  throw new Error("Extensions.Image is required when using the Image slash command.");
558
601
  }
@@ -568,7 +611,6 @@ export const RichTextEditor = ({ className, editorInstanceRef, defaultValue = ""
568
611
  extensions: getTipTapExtensions({
569
612
  extensions,
570
613
  slashCommands,
571
- bubbleMenuOptions,
572
614
  handleImageUpload,
573
615
  }),
574
616
  editorProps: {
@@ -632,5 +674,5 @@ export const RichTextEditor = ({ className, editorInstanceRef, defaultValue = ""
632
674
  return (_jsx(StyledContent, { className: className, children: _jsxs(RichTextEditorContext.Provider, { value: {
633
675
  font: fontState,
634
676
  setFont: setFontState,
635
- }, children: [showToolbar && (_jsx(Toolbar, { editor: editor, toolbarOptions: toolbarOptions })), saving && _jsx(SaveBadge, {}), _jsx(EditorContent, { className: "editor-content", editor: editor, "data-font": fontState || null, style: style })] }) }));
677
+ }, children: [showToolbar && (_jsx(Toolbar, { editor: editor, toolbarOptions: toolbarOptions })), saving && _jsx(SaveBadge, {}), editor && hasBubbleMenuExtension && (_jsx(TiptapBubbleMenu, { editor: editor, pluginKey: "bubbleMenu", updateDelay: 150, appendTo: getBubbleMenuPortalRoot, shouldShow: shouldShowBubbleMenu, options: bubbleMenuPositionOptions, children: _jsx(BubbleMenuContent, { editor: editor, customMenuItems: bubbleMenuOptions === null || bubbleMenuOptions === void 0 ? void 0 : bubbleMenuOptions.customMenuItems }) })), _jsx(EditorContent, { className: "editor-content", editor: editor, "data-font": fontState || null, style: style })] }) }));
636
678
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monolith-forensics/monolith-ui",
3
- "version": "1.9.1-dev.8",
3
+ "version": "1.9.1-dev.9",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "author": "Matt Danner (Monolith Forensics LLC)",
@@ -1,7 +0,0 @@
1
- import { Extension } from "@tiptap/core";
2
- import { BubbleItem } from "../Components/BubbleMenu";
3
- export type BubbleMenuOptions = {
4
- customMenuItems?: BubbleItem[];
5
- };
6
- declare const BubbleMenu: Extension<any, any>;
7
- export default BubbleMenu;
@@ -1,157 +0,0 @@
1
- import { Extension, isNodeSelection, isTextSelection, posToDOMRect, } from "@tiptap/core";
2
- import { Plugin, PluginKey } from "@tiptap/pm/state";
3
- import { ReactRenderer } from "@tiptap/react";
4
- import BubbleMenuComponent from "../Components/BubbleMenu";
5
- class Menu {
6
- constructor({ view, editor, customMenuItems, }) {
7
- this.mousedownHandler = (event) => {
8
- this.preventShow = true;
9
- };
10
- this.mouseUpHandler = (event) => {
11
- this.preventShow = false;
12
- this.update(this.editor.view);
13
- };
14
- this.focusHandler = () => {
15
- // this.editor.commands.setTextSelection({ from: 0, to: 0 });
16
- // we use `setTimeout` to make sure `selection` is already updated
17
- setTimeout(() => this.update(this.editor.view));
18
- };
19
- this.blurHandler = ({ event }) => {
20
- var _a;
21
- if (this.preventShow) {
22
- this.preventShow = false;
23
- return;
24
- }
25
- if ((event === null || event === void 0 ? void 0 : event.relatedTarget) &&
26
- ((_a = this.floating) === null || _a === void 0 ? void 0 : _a.contains(event === null || event === void 0 ? void 0 : event.relatedTarget))) {
27
- return;
28
- }
29
- // clear text selection
30
- // this.editor.commands.setTextSelection({ from: 0, to: 0 });
31
- this.hide();
32
- };
33
- this.editor = editor;
34
- this.view = view;
35
- this.rect = null;
36
- this.preventShow = false;
37
- this.floating = null;
38
- this.isOpen = false;
39
- // create and mount react component
40
- if (!this.component) {
41
- this.component = new ReactRenderer(BubbleMenuComponent, {
42
- props: {
43
- editor: this.editor,
44
- open: false,
45
- onOpen: (ref) => {
46
- this.floating = ref;
47
- },
48
- customMenuItems,
49
- },
50
- editor: this.editor,
51
- });
52
- document.body.appendChild(this.component.element);
53
- }
54
- // don't show the bubble during selection of text
55
- this.view.dom.addEventListener("mousedown", this.mousedownHandler, {
56
- capture: true,
57
- });
58
- this.view.dom.addEventListener("mouseup", this.mouseUpHandler);
59
- this.editor.on("blur", this.blurHandler);
60
- this.editor.on("focus", this.focusHandler);
61
- }
62
- update(view, oldState) {
63
- var _a;
64
- const { state, composing } = view;
65
- const { doc, selection } = state;
66
- const { empty, ranges } = selection;
67
- const from = Math.min(...ranges.map((range) => range.$from.pos));
68
- const to = Math.max(...ranges.map((range) => range.$to.pos));
69
- const selectionChanged = !(oldState === null || oldState === void 0 ? void 0 : oldState.selection.eq(view.state.selection));
70
- const docChanged = !(oldState === null || oldState === void 0 ? void 0 : oldState.doc.eq(view.state.doc));
71
- const isSame = !selectionChanged && !docChanged;
72
- if (composing || isSame) {
73
- return;
74
- }
75
- // Sometime check for `empty` is not enough.
76
- // Doubleclick an empty paragraph returns a node size of 2.
77
- // So we check also for an empty text size.
78
- const isEmptyTextBlock = !doc.textBetween(from, to).length && isTextSelection(state.selection);
79
- // When clicking on a element inside the bubble menu the editor "blur" event
80
- // is called and the bubble menu item is focussed. In this case we should
81
- // consider the menu as part of the editor and keep showing the menu
82
- const isChildOfMenu = (_a = this === null || this === void 0 ? void 0 : this.floating) === null || _a === void 0 ? void 0 : _a.contains(document.activeElement);
83
- const hasEditorFocus = view.hasFocus() || isChildOfMenu;
84
- if (!hasEditorFocus ||
85
- empty ||
86
- isEmptyTextBlock ||
87
- !this.editor.isEditable ||
88
- this.preventShow) {
89
- this.hide();
90
- }
91
- else {
92
- // only set position when it is not already open
93
- // otherwise the menu will jump around when the selection changes or text formatting is applied
94
- if (!this.isOpen) {
95
- if (isNodeSelection(state.selection)) {
96
- let node = view.nodeDOM(from);
97
- const nodeViewWrapper = node.dataset.nodeViewWrapper
98
- ? node
99
- : node.querySelector("[data-node-view-wrapper]");
100
- if (nodeViewWrapper) {
101
- node = nodeViewWrapper.firstChild;
102
- }
103
- if (node) {
104
- this.rect = node.getBoundingClientRect();
105
- }
106
- }
107
- else {
108
- this.rect = posToDOMRect(view, from, to);
109
- }
110
- }
111
- this.show();
112
- }
113
- }
114
- show() {
115
- if (this.component) {
116
- this.component.updateProps({ open: true, rect: this.rect });
117
- this.isOpen = true;
118
- }
119
- }
120
- hide() {
121
- if (this.component) {
122
- this.component.updateProps({ open: false, rect: null });
123
- this.isOpen = false;
124
- }
125
- }
126
- destroy() {
127
- if (this.component) {
128
- this.view.dom.removeEventListener("mousedown", this.mousedownHandler);
129
- this.view.dom.removeEventListener("mouseup", this.mouseUpHandler);
130
- this.component.destroy();
131
- }
132
- }
133
- }
134
- const BubbleMenu = Extension.create({
135
- name: "bubbleMenu",
136
- addOptions() {
137
- return {
138
- bubbleMenuOptions: {},
139
- };
140
- },
141
- addProseMirrorPlugins() {
142
- return [
143
- new Plugin({
144
- key: new PluginKey("bubbleMenu"),
145
- view: (view) => {
146
- var _a;
147
- return new Menu({
148
- view,
149
- editor: this.editor,
150
- customMenuItems: ((_a = this === null || this === void 0 ? void 0 : this.options) === null || _a === void 0 ? void 0 : _a.customMenuItems) || [],
151
- });
152
- },
153
- }),
154
- ];
155
- },
156
- });
157
- export default BubbleMenu;