-
Notifications
You must be signed in to change notification settings - Fork 51
/
Copy pathindex.tsx
175 lines (160 loc) · 6.01 KB
/
index.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
/// <reference types="@tiptap/extension-link" />
import { makeStyles } from "tss-react/mui";
import type { Except } from "type-fest";
import ControlledBubbleMenu, {
type ControlledBubbleMenuProps,
} from "../ControlledBubbleMenu";
import { useRichTextEditorContext } from "../context";
import {
LinkMenuState,
type LinkBubbleMenuHandlerStorage,
} from "../extensions/LinkBubbleMenuHandler";
import EditLinkMenuContent, {
type EditLinkMenuContentProps,
} from "./EditLinkMenuContent";
import ViewLinkMenuContent, {
type ViewLinkMenuContentProps,
} from "./ViewLinkMenuContent";
export interface LinkBubbleMenuProps
extends Partial<
Except<ControlledBubbleMenuProps, "open" | "editor" | "children">
> {
/**
* Override the default text content/labels in this interface. For any value
* that is omitted in this object, it falls back to the default content.
*/
labels?: ViewLinkMenuContentProps["labels"] &
EditLinkMenuContentProps["labels"];
}
const useStyles = makeStyles({ name: { LinkBubbleMenu } })((theme) => ({
content: {
padding: theme.spacing(1.5, 2, 0.5),
},
}));
/**
* A component that renders a bubble menu when viewing, creating, or editing a
* link. Requires the mui-tiptap LinkBubbleMenuHandler extension and Tiptap's
* Link extension (@tiptap/extension-link, https://tiptap.dev/api/marks/link) to
* both be included in your editor `extensions` array.
*
* Pairs well with the `<MenuButtonEditLink />` component.
*
* If you're using `RichTextEditor`, include this component via
* `RichTextEditor`’s `children` render-prop. Otherwise, include the
* `LinkBubbleMenu` as a child of the component where you call `useEditor` and
* render your `RichTextField` or `RichTextContent`. (The bubble menu itself
* will be positioned appropriately no matter where you put it in your React
* tree, as long as it is re-rendered whenever the Tiptap `editor` forces an
* update, which will happen if it's a child of the component using
* `useEditor`).
*/
export default function LinkBubbleMenu({
labels,
...controlledBubbleMenuProps
}: LinkBubbleMenuProps) {
const { classes } = useStyles();
const editor = useRichTextEditorContext();
if (!editor?.isEditable) {
return null;
}
if (!("linkBubbleMenuHandler" in editor.storage)) {
throw new Error(
"You must add the LinkBubbleMenuHandler extension to the useEditor `extensions` array in order to use this component!"
);
}
const handlerStorage = editor.storage
.linkBubbleMenuHandler as LinkBubbleMenuHandlerStorage;
// Update the menu step if the bubble menu state has changed
const menuState = handlerStorage.state;
let linkMenuContent = null;
if (menuState === LinkMenuState.VIEW_LINK_DETAILS) {
linkMenuContent = (
<ViewLinkMenuContent
editor={editor}
onCancel={editor.commands.closeLinkBubbleMenu}
onEdit={editor.commands.editLinkInBubbleMenu}
onRemove={() => {
// Remove the link and place the cursor at the end of the link (which
// requires "focus" to take effect)
editor
.chain()
.unsetLink()
.setTextSelection(editor.state.selection.to)
.focus()
.run();
}}
labels={labels}
/>
);
} else if (menuState === LinkMenuState.EDIT_LINK) {
linkMenuContent = (
<EditLinkMenuContent
editor={editor}
onCancel={editor.commands.closeLinkBubbleMenu}
onSave={({ text, link }) => {
editor
.chain()
// Make sure if we're updating a link, we update the link for the
// full link "mark"
.extendMarkRange("link")
// Update the link href and its text content
.command(({ tr, state }) => {
const existingHref = editor.isActive("link")
? (editor.getAttributes("link").href as string)
: "";
const { selection, schema } = state;
if (existingHref) {
// Get the resolved position from the selection
const resolvedPos = state.doc.resolve(selection.from);
const nodeAfter = resolvedPos.nodeAfter;
if (nodeAfter?.isText) {
// Insert new text without changing the link mark
tr.insertText(text, selection.from, selection.to);
// Set the link separately to ensure the link mark is applied
tr.addMark(
selection.from,
selection.from + text.length,
schema.marks.link.create({ href: link })
);
}
} else {
tr.insertText(text, selection.from, selection.to);
tr.addMark(
selection.from,
selection.from + text.length,
schema.marks.link.create({ href: link })
);
}
return true;
})
// Note that as of "@tiptap/extension-link" 2.0.0-beta.37 when
// `autolink` is on (which we want), adding the link mark directly
// via `insertContent` above wasn't sufficient for the link mark to
// be applied (though specifying it above is still necessary), so we
// insert the content there and call `setLink` separately here.
// Unclear why this separate command is necessary, but it does the
// trick.
.setLink({
href: link,
})
// Place the cursor at the end of the link (which requires "focus"
// to take effect)
.focus()
.run();
editor.commands.closeLinkBubbleMenu();
}}
labels={labels}
/>
);
}
return (
<ControlledBubbleMenu
editor={editor}
open={menuState !== LinkMenuState.HIDDEN}
{...handlerStorage.bubbleMenuOptions}
{...controlledBubbleMenuProps}
>
<div className={classes.content}>{linkMenuContent}</div>
</ControlledBubbleMenu>
);
}