@react-email/editor 0.0.0-experimental.2 → 0.0.0-experimental.21
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/dist/index.cjs +4205 -0
- package/dist/index.d.cts +1164 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +958 -68
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3187 -490
- package/dist/index.mjs.map +1 -1
- package/dist/ui/bubble-menu/bubble-menu.css +146 -0
- package/dist/ui/button-bubble-menu/button-bubble-menu.css +29 -0
- package/dist/ui/image-bubble-menu/image-bubble-menu.css +29 -0
- package/dist/ui/link-bubble-menu/link-bubble-menu.css +66 -0
- package/dist/ui/slash-command/slash-command.css +44 -0
- package/dist/ui/themes/default.css +750 -0
- package/package.json +51 -17
- package/dist/index.d.ts +0 -274
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -1399
package/dist/index.mjs
CHANGED
|
@@ -1,176 +1,100 @@
|
|
|
1
|
-
import { Extension, Mark, Node, findChildren, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
3
1
|
import * as ReactEmailComponents from "@react-email/components";
|
|
4
|
-
import { Button as Button$1, CodeBlock, Column, Row, Section as Section$1 } from "@react-email/components";
|
|
2
|
+
import { Body as Body$1, Button as Button$1, CodeBlock, Column, Head, Heading as Heading$1, Hr, Html, Link as Link$1, Preview, Row, Section as Section$1, pretty, render, toPlainText } from "@react-email/components";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { Extension, InputRule, Mark, Node as Node$1, findChildren, mergeAttributes } from "@tiptap/core";
|
|
5
|
+
import { UndoRedo } from "@tiptap/extensions";
|
|
6
|
+
import { NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer, ReactRenderer, useCurrentEditor, useEditor as useEditor$1, useEditorState } from "@tiptap/react";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import { useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
9
|
+
import TipTapStarterKit from "@tiptap/starter-kit";
|
|
10
|
+
import BlockquoteBase from "@tiptap/extension-blockquote";
|
|
11
|
+
import BoldBase from "@tiptap/extension-bold";
|
|
12
|
+
import BulletListBase from "@tiptap/extension-bullet-list";
|
|
13
|
+
import CodeBase from "@tiptap/extension-code";
|
|
5
14
|
import CodeBlock$1 from "@tiptap/extension-code-block";
|
|
6
|
-
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
15
|
+
import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
|
7
16
|
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
8
17
|
import { fromHtml } from "hast-util-from-html";
|
|
9
18
|
import Prism from "prismjs";
|
|
10
|
-
import
|
|
19
|
+
import HorizontalRule from "@tiptap/extension-horizontal-rule";
|
|
20
|
+
import HardBreakBase from "@tiptap/extension-hard-break";
|
|
21
|
+
import { Heading as Heading$2 } from "@tiptap/extension-heading";
|
|
22
|
+
import ItalicBase from "@tiptap/extension-italic";
|
|
23
|
+
import TiptapLink from "@tiptap/extension-link";
|
|
24
|
+
import ListItemBase from "@tiptap/extension-list-item";
|
|
25
|
+
import OrderedListBase from "@tiptap/extension-ordered-list";
|
|
26
|
+
import ParagraphBase from "@tiptap/extension-paragraph";
|
|
27
|
+
import TipTapPlaceholder from "@tiptap/extension-placeholder";
|
|
28
|
+
import StrikeBase from "@tiptap/extension-strike";
|
|
29
|
+
import SuperscriptBase from "@tiptap/extension-superscript";
|
|
30
|
+
import UnderlineBase from "@tiptap/extension-underline";
|
|
31
|
+
import { generateJSON } from "@tiptap/html";
|
|
32
|
+
import { AlignCenterIcon, AlignLeftIcon, AlignRightIcon, BoldIcon, CaseUpperIcon, Check, ChevronDown, Code as Code$1, CodeIcon, Columns2, Columns3, Columns4, ExternalLinkIcon, Heading1, Heading2, Heading3, ItalicIcon, LinkIcon, List, ListOrdered, MousePointer, PencilIcon, Rows2, SplitSquareVertical, SquareCode, StrikethroughIcon, Text, TextIcon, TextQuote, UnderlineIcon, UnlinkIcon } from "lucide-react";
|
|
33
|
+
import * as Popover from "@radix-ui/react-popover";
|
|
34
|
+
import { BubbleMenu as BubbleMenu$1 } from "@tiptap/react/menus";
|
|
35
|
+
import Suggestion from "@tiptap/suggestion";
|
|
36
|
+
import tippy from "tippy.js";
|
|
11
37
|
|
|
12
|
-
//#region src/core/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
38
|
+
//#region src/core/event-bus.ts
|
|
39
|
+
const EVENT_PREFIX = "@react-email/editor:";
|
|
40
|
+
var EditorEventBus = class {
|
|
41
|
+
prefixEventName(eventName) {
|
|
42
|
+
return `${EVENT_PREFIX}${String(eventName)}`;
|
|
16
43
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
44
|
+
dispatch(eventName, payload, options) {
|
|
45
|
+
const target = options?.target ?? window;
|
|
46
|
+
const prefixedEventName = this.prefixEventName(eventName);
|
|
47
|
+
const event = new CustomEvent(prefixedEventName, {
|
|
48
|
+
detail: payload,
|
|
49
|
+
bubbles: false,
|
|
50
|
+
cancelable: false
|
|
51
|
+
});
|
|
52
|
+
target.dispatchEvent(event);
|
|
23
53
|
}
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
54
|
+
on(eventName, handler, options) {
|
|
55
|
+
const target = options?.target ?? window;
|
|
56
|
+
const prefixedEventName = this.prefixEventName(eventName);
|
|
57
|
+
const abortController = new AbortController();
|
|
58
|
+
const wrappedHandler = (event) => {
|
|
59
|
+
const customEvent = event;
|
|
60
|
+
const result = handler(customEvent.detail);
|
|
61
|
+
if (result instanceof Promise) result.catch((error) => {
|
|
62
|
+
console.error(`Error in async event handler for ${prefixedEventName}:`, {
|
|
63
|
+
event: customEvent.detail,
|
|
64
|
+
error
|
|
65
|
+
});
|
|
66
|
+
});
|
|
30
67
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return super.extend(resolvedConfig);
|
|
68
|
+
target.addEventListener(prefixedEventName, wrappedHandler, {
|
|
69
|
+
...options,
|
|
70
|
+
signal: abortController.signal
|
|
71
|
+
});
|
|
72
|
+
return { unsubscribe: () => {
|
|
73
|
+
abortController.abort();
|
|
74
|
+
} };
|
|
39
75
|
}
|
|
40
76
|
};
|
|
77
|
+
const editorEventBus = new EditorEventBus();
|
|
41
78
|
|
|
42
79
|
//#endregion
|
|
43
|
-
//#region src/
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
};
|
|
56
|
-
},
|
|
57
|
-
addGlobalAttributes() {
|
|
58
|
-
return [{
|
|
59
|
-
types: this.options.types,
|
|
60
|
-
attributes: { alignment: {
|
|
61
|
-
parseHTML: (element) => {
|
|
62
|
-
const explicitAlign = element.getAttribute("align") || element.getAttribute("alignment") || element.style.textAlign;
|
|
63
|
-
if (explicitAlign && this.options.alignments.includes(explicitAlign)) return explicitAlign;
|
|
64
|
-
return null;
|
|
65
|
-
},
|
|
66
|
-
renderHTML: (attributes) => {
|
|
67
|
-
if (attributes.alignment === "left") return {};
|
|
68
|
-
return { alignment: attributes.alignment };
|
|
69
|
-
}
|
|
70
|
-
} }
|
|
71
|
-
}];
|
|
72
|
-
},
|
|
73
|
-
addCommands() {
|
|
74
|
-
return { setAlignment: (alignment) => ({ commands }) => {
|
|
75
|
-
if (!this.options.alignments.includes(alignment)) return false;
|
|
76
|
-
return this.options.types.every((type) => commands.updateAttributes(type, { alignment }));
|
|
77
|
-
} };
|
|
78
|
-
},
|
|
79
|
-
addKeyboardShortcuts() {
|
|
80
|
-
return {
|
|
81
|
-
Enter: () => {
|
|
82
|
-
const { from } = this.editor.state.selection;
|
|
83
|
-
const currentAlignment = this.editor.state.doc.nodeAt(from)?.attrs?.alignment;
|
|
84
|
-
if (currentAlignment) requestAnimationFrame(() => {
|
|
85
|
-
this.editor.commands.setAlignment(currentAlignment);
|
|
86
|
-
});
|
|
87
|
-
return false;
|
|
88
|
-
},
|
|
89
|
-
"Mod-Shift-l": () => this.editor.commands.setAlignment("left"),
|
|
90
|
-
"Mod-Shift-e": () => this.editor.commands.setAlignment("center"),
|
|
91
|
-
"Mod-Shift-r": () => this.editor.commands.setAlignment("right"),
|
|
92
|
-
"Mod-Shift-j": () => this.editor.commands.setAlignment("justify")
|
|
80
|
+
//#region src/core/is-document-visually-empty.ts
|
|
81
|
+
function isDocumentVisuallyEmpty(doc) {
|
|
82
|
+
let nonGlobalNodeCount = 0;
|
|
83
|
+
let firstNonGlobalNode = null;
|
|
84
|
+
for (let index = 0; index < doc.childCount; index += 1) {
|
|
85
|
+
const node = doc.child(index);
|
|
86
|
+
if (node.type.name === "globalContent") continue;
|
|
87
|
+
nonGlobalNodeCount += 1;
|
|
88
|
+
if (firstNonGlobalNode === null) firstNonGlobalNode = {
|
|
89
|
+
type: node.type,
|
|
90
|
+
textContent: node.textContent,
|
|
91
|
+
childCount: node.content.childCount
|
|
93
92
|
};
|
|
94
93
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
//#region src/utils/attribute-helpers.ts
|
|
99
|
-
/**
|
|
100
|
-
* Creates TipTap attribute definitions for a list of HTML attributes.
|
|
101
|
-
* Each attribute will have the same pattern:
|
|
102
|
-
* - default: null
|
|
103
|
-
* - parseHTML: extracts the attribute from the element
|
|
104
|
-
* - renderHTML: conditionally renders the attribute if it has a value
|
|
105
|
-
*
|
|
106
|
-
* @param attributeNames - Array of HTML attribute names to create definitions for
|
|
107
|
-
* @returns Object with TipTap attribute definitions
|
|
108
|
-
*
|
|
109
|
-
* @example
|
|
110
|
-
* const attrs = createStandardAttributes(['class', 'id', 'title']);
|
|
111
|
-
* // Returns:
|
|
112
|
-
* // {
|
|
113
|
-
* // class: {
|
|
114
|
-
* // default: null,
|
|
115
|
-
* // parseHTML: (element) => element.getAttribute('class'),
|
|
116
|
-
* // renderHTML: (attributes) => attributes.class ? { class: attributes.class } : {}
|
|
117
|
-
* // },
|
|
118
|
-
* // ...
|
|
119
|
-
* // }
|
|
120
|
-
*/
|
|
121
|
-
function createStandardAttributes(attributeNames) {
|
|
122
|
-
return Object.fromEntries(attributeNames.map((attr) => [attr, {
|
|
123
|
-
default: null,
|
|
124
|
-
parseHTML: (element) => element.getAttribute(attr),
|
|
125
|
-
renderHTML: (attributes) => {
|
|
126
|
-
if (!attributes[attr]) return {};
|
|
127
|
-
return { [attr]: attributes[attr] };
|
|
128
|
-
}
|
|
129
|
-
}]));
|
|
94
|
+
if (nonGlobalNodeCount === 0) return true;
|
|
95
|
+
if (nonGlobalNodeCount !== 1) return false;
|
|
96
|
+
return firstNonGlobalNode?.type.name === "paragraph" && firstNonGlobalNode.textContent.trim().length === 0 && firstNonGlobalNode.childCount === 0;
|
|
130
97
|
}
|
|
131
|
-
/**
|
|
132
|
-
* Common HTML attributes used across multiple extensions.
|
|
133
|
-
* These preserve attributes during HTML import and editing for better
|
|
134
|
-
* fidelity when importing existing email templates.
|
|
135
|
-
*/
|
|
136
|
-
const COMMON_HTML_ATTRIBUTES = [
|
|
137
|
-
"id",
|
|
138
|
-
"class",
|
|
139
|
-
"title",
|
|
140
|
-
"lang",
|
|
141
|
-
"dir",
|
|
142
|
-
"data-id"
|
|
143
|
-
];
|
|
144
|
-
/**
|
|
145
|
-
* Layout-specific HTML attributes used for positioning and sizing.
|
|
146
|
-
*/
|
|
147
|
-
const LAYOUT_ATTRIBUTES = [
|
|
148
|
-
"align",
|
|
149
|
-
"width",
|
|
150
|
-
"height"
|
|
151
|
-
];
|
|
152
|
-
/**
|
|
153
|
-
* Table-specific HTML attributes used for table layout and styling.
|
|
154
|
-
*/
|
|
155
|
-
const TABLE_ATTRIBUTES = [
|
|
156
|
-
"border",
|
|
157
|
-
"cellpadding",
|
|
158
|
-
"cellspacing"
|
|
159
|
-
];
|
|
160
|
-
/**
|
|
161
|
-
* Table cell-specific HTML attributes.
|
|
162
|
-
*/
|
|
163
|
-
const TABLE_CELL_ATTRIBUTES = [
|
|
164
|
-
"valign",
|
|
165
|
-
"bgcolor",
|
|
166
|
-
"colspan",
|
|
167
|
-
"rowspan"
|
|
168
|
-
];
|
|
169
|
-
/**
|
|
170
|
-
* Table header cell-specific HTML attributes.
|
|
171
|
-
* These are additional attributes that only apply to <th> elements.
|
|
172
|
-
*/
|
|
173
|
-
const TABLE_HEADER_ATTRIBUTES = [...TABLE_CELL_ATTRIBUTES, "scope"];
|
|
174
98
|
|
|
175
99
|
//#endregion
|
|
176
100
|
//#region src/utils/styles.ts
|
|
@@ -342,137 +266,418 @@ function resolveConflictingStyles(resetStyles, inlineStyles) {
|
|
|
342
266
|
}
|
|
343
267
|
|
|
344
268
|
//#endregion
|
|
345
|
-
//#region src/
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
];
|
|
375
|
-
},
|
|
376
|
-
renderToReactEmail({ children, node, styles }) {
|
|
377
|
-
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
378
|
-
return /* @__PURE__ */ jsx("div", {
|
|
379
|
-
className: node.attrs?.class || void 0,
|
|
380
|
-
style: {
|
|
381
|
-
...styles.reset,
|
|
382
|
-
...inlineStyles
|
|
383
|
-
},
|
|
384
|
-
children
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
});
|
|
269
|
+
//#region src/core/serializer/default-base-template.tsx
|
|
270
|
+
function DefaultBaseTemplate({ children, previewText }) {
|
|
271
|
+
return /* @__PURE__ */ jsxs(Html, { children: [
|
|
272
|
+
/* @__PURE__ */ jsxs(Head, { children: [
|
|
273
|
+
/* @__PURE__ */ jsx("meta", {
|
|
274
|
+
content: "width=device-width",
|
|
275
|
+
name: "viewport"
|
|
276
|
+
}),
|
|
277
|
+
/* @__PURE__ */ jsx("meta", {
|
|
278
|
+
content: "IE=edge",
|
|
279
|
+
httpEquiv: "X-UA-Compatible"
|
|
280
|
+
}),
|
|
281
|
+
/* @__PURE__ */ jsx("meta", { name: "x-apple-disable-message-reformatting" }),
|
|
282
|
+
/* @__PURE__ */ jsx("meta", {
|
|
283
|
+
content: "telephone=no,address=no,email=no,date=no,url=no",
|
|
284
|
+
name: "format-detection"
|
|
285
|
+
})
|
|
286
|
+
] }),
|
|
287
|
+
previewText && previewText !== "" && /* @__PURE__ */ jsx(Preview, { children: previewText }),
|
|
288
|
+
/* @__PURE__ */ jsx(Body$1, { children: /* @__PURE__ */ jsx(Section$1, {
|
|
289
|
+
width: "100%",
|
|
290
|
+
align: "center",
|
|
291
|
+
children: /* @__PURE__ */ jsx(Section$1, {
|
|
292
|
+
style: { width: "100%" },
|
|
293
|
+
children
|
|
294
|
+
})
|
|
295
|
+
}) })
|
|
296
|
+
] });
|
|
297
|
+
}
|
|
388
298
|
|
|
389
299
|
//#endregion
|
|
390
|
-
//#region src/
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
*
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
* This extension allows you to mark text as bold.
|
|
409
|
-
* @see https://tiptap.dev/api/marks/bold
|
|
410
|
-
*/
|
|
411
|
-
const Bold = Mark.create({
|
|
412
|
-
name: "bold",
|
|
413
|
-
addOptions() {
|
|
414
|
-
return { HTMLAttributes: {} };
|
|
415
|
-
},
|
|
416
|
-
parseHTML() {
|
|
417
|
-
return [
|
|
418
|
-
{ tag: "strong" },
|
|
419
|
-
{
|
|
420
|
-
tag: "b",
|
|
421
|
-
getAttrs: (node) => node.style.fontWeight !== "normal" && null
|
|
422
|
-
},
|
|
423
|
-
{
|
|
424
|
-
style: "font-weight=400",
|
|
425
|
-
clearMark: (mark) => mark.type.name === this.name
|
|
426
|
-
}
|
|
427
|
-
];
|
|
428
|
-
},
|
|
429
|
-
renderHTML({ HTMLAttributes }) {
|
|
430
|
-
return [
|
|
431
|
-
"strong",
|
|
432
|
-
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
|
433
|
-
0
|
|
434
|
-
];
|
|
435
|
-
},
|
|
436
|
-
addCommands() {
|
|
437
|
-
return {
|
|
438
|
-
setBold: () => ({ commands }) => {
|
|
439
|
-
return commands.setMark(this.name);
|
|
440
|
-
},
|
|
441
|
-
toggleBold: () => ({ commands }) => {
|
|
442
|
-
return commands.toggleMark(this.name);
|
|
443
|
-
},
|
|
444
|
-
unsetBold: () => ({ commands }) => {
|
|
445
|
-
return commands.unsetMark(this.name);
|
|
446
|
-
}
|
|
447
|
-
};
|
|
448
|
-
},
|
|
449
|
-
addKeyboardShortcuts() {
|
|
450
|
-
return {
|
|
451
|
-
"Mod-b": () => this.editor.commands.toggleBold(),
|
|
452
|
-
"Mod-B": () => this.editor.commands.toggleBold()
|
|
300
|
+
//#region src/core/serializer/email-mark.ts
|
|
301
|
+
var EmailMark = class EmailMark extends Mark {
|
|
302
|
+
constructor(config) {
|
|
303
|
+
super(config);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Create a new Mark instance
|
|
307
|
+
* @param config - Mark configuration object or a function that returns a configuration object
|
|
308
|
+
*/
|
|
309
|
+
static create(config) {
|
|
310
|
+
return new EmailMark(typeof config === "function" ? config() : config);
|
|
311
|
+
}
|
|
312
|
+
static from(mark, renderToReactEmail) {
|
|
313
|
+
const customMark = EmailMark.create({});
|
|
314
|
+
Object.assign(customMark, { ...mark });
|
|
315
|
+
customMark.config = {
|
|
316
|
+
...mark.config,
|
|
317
|
+
renderToReactEmail
|
|
453
318
|
};
|
|
454
|
-
|
|
455
|
-
addInputRules() {
|
|
456
|
-
return [markInputRule({
|
|
457
|
-
find: starInputRegex,
|
|
458
|
-
type: this.type
|
|
459
|
-
}), markInputRule({
|
|
460
|
-
find: underscoreInputRegex,
|
|
461
|
-
type: this.type
|
|
462
|
-
})];
|
|
463
|
-
},
|
|
464
|
-
addPasteRules() {
|
|
465
|
-
return [markPasteRule({
|
|
466
|
-
find: starPasteRegex,
|
|
467
|
-
type: this.type
|
|
468
|
-
}), markPasteRule({
|
|
469
|
-
find: underscorePasteRegex,
|
|
470
|
-
type: this.type
|
|
471
|
-
})];
|
|
319
|
+
return customMark;
|
|
472
320
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
321
|
+
configure(options) {
|
|
322
|
+
return super.configure(options);
|
|
323
|
+
}
|
|
324
|
+
extend(extendedConfig) {
|
|
325
|
+
const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
|
|
326
|
+
return super.extend(resolvedConfig);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/core/serializer/email-node.ts
|
|
332
|
+
var EmailNode = class EmailNode extends Node$1 {
|
|
333
|
+
constructor(config) {
|
|
334
|
+
super(config);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Create a new Node instance
|
|
338
|
+
* @param config - Node configuration object or a function that returns a configuration object
|
|
339
|
+
*/
|
|
340
|
+
static create(config) {
|
|
341
|
+
return new EmailNode(typeof config === "function" ? config() : config);
|
|
342
|
+
}
|
|
343
|
+
static from(node, renderToReactEmail) {
|
|
344
|
+
const customNode = EmailNode.create({});
|
|
345
|
+
Object.assign(customNode, { ...node });
|
|
346
|
+
customNode.config = {
|
|
347
|
+
...node.config,
|
|
348
|
+
renderToReactEmail
|
|
349
|
+
};
|
|
350
|
+
return customNode;
|
|
351
|
+
}
|
|
352
|
+
configure(options) {
|
|
353
|
+
return super.configure(options);
|
|
354
|
+
}
|
|
355
|
+
extend(extendedConfig) {
|
|
356
|
+
const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
|
|
357
|
+
return super.extend(resolvedConfig);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region src/core/serializer/compose-react-email.tsx
|
|
363
|
+
const MARK_ORDER = {
|
|
364
|
+
preservedStyle: 0,
|
|
365
|
+
italic: 1,
|
|
366
|
+
strike: 2,
|
|
367
|
+
underline: 3,
|
|
368
|
+
link: 4,
|
|
369
|
+
bold: 5,
|
|
370
|
+
code: 6
|
|
371
|
+
};
|
|
372
|
+
const NODES_WITH_INCREMENTED_CHILD_DEPTH = new Set(["bulletList", "orderedList"]);
|
|
373
|
+
function getOrderedMarks(marks) {
|
|
374
|
+
if (!marks) return [];
|
|
375
|
+
return [...marks].sort((a, b) => (MARK_ORDER[a.type] ?? Number.MAX_SAFE_INTEGER) - (MARK_ORDER[b.type] ?? Number.MAX_SAFE_INTEGER));
|
|
376
|
+
}
|
|
377
|
+
const composeReactEmail = async ({ editor, preview }) => {
|
|
378
|
+
const data = editor.getJSON();
|
|
379
|
+
const extensions = editor.extensionManager.extensions;
|
|
380
|
+
const serializerPlugin = extensions.map((ext) => ext.options?.serializerPlugin).filter((p) => Boolean(p)).at(-1);
|
|
381
|
+
const emailNodeComponentRegistry = Object.fromEntries(extensions.filter((ext) => ext instanceof EmailNode).map((extension) => [extension.name, extension.config.renderToReactEmail]));
|
|
382
|
+
const emailMarkComponentRegistry = Object.fromEntries(extensions.filter((ext) => ext instanceof EmailMark).map((extension) => [extension.name, extension.config.renderToReactEmail]));
|
|
383
|
+
function renderMark(mark, node, children, depth) {
|
|
384
|
+
const markStyle = serializerPlugin?.getNodeStyles({
|
|
385
|
+
type: mark.type,
|
|
386
|
+
attrs: mark.attrs ?? {}
|
|
387
|
+
}, depth, editor) ?? {};
|
|
388
|
+
const markRenderer = emailMarkComponentRegistry[mark.type];
|
|
389
|
+
if (markRenderer) return markRenderer({
|
|
390
|
+
mark,
|
|
391
|
+
node,
|
|
392
|
+
style: markStyle,
|
|
393
|
+
children
|
|
394
|
+
});
|
|
395
|
+
return children;
|
|
396
|
+
}
|
|
397
|
+
function parseContent(content, depth = 0) {
|
|
398
|
+
if (!content) return;
|
|
399
|
+
return content.map((node, index) => {
|
|
400
|
+
const style = serializerPlugin?.getNodeStyles(node, depth, editor) ?? {};
|
|
401
|
+
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
402
|
+
if (node.type && emailNodeComponentRegistry[node.type]) {
|
|
403
|
+
const Component = emailNodeComponentRegistry[node.type];
|
|
404
|
+
const childDepth = NODES_WITH_INCREMENTED_CHILD_DEPTH.has(node.type) ? depth + 1 : depth;
|
|
405
|
+
return /* @__PURE__ */ jsx(Component, {
|
|
406
|
+
node: node.type === "table" && inlineStyles.width && !node.attrs?.width ? {
|
|
407
|
+
...node,
|
|
408
|
+
attrs: {
|
|
409
|
+
...node.attrs,
|
|
410
|
+
width: inlineStyles.width
|
|
411
|
+
}
|
|
412
|
+
} : node,
|
|
413
|
+
style,
|
|
414
|
+
children: parseContent(node.content, childDepth)
|
|
415
|
+
}, index);
|
|
416
|
+
}
|
|
417
|
+
switch (node.type) {
|
|
418
|
+
case "text": {
|
|
419
|
+
let wrappedText = node.text;
|
|
420
|
+
getOrderedMarks(node.marks).forEach((mark) => {
|
|
421
|
+
wrappedText = renderMark(mark, node, wrappedText, depth);
|
|
422
|
+
});
|
|
423
|
+
const textAttributes = node.marks?.find((mark) => mark.type === "textStyle")?.attrs;
|
|
424
|
+
return /* @__PURE__ */ jsx("span", {
|
|
425
|
+
style: {
|
|
426
|
+
...textAttributes,
|
|
427
|
+
...style
|
|
428
|
+
},
|
|
429
|
+
children: wrappedText
|
|
430
|
+
}, index);
|
|
431
|
+
}
|
|
432
|
+
default: return null;
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
const unformattedHtml = await render(/* @__PURE__ */ jsx(serializerPlugin?.BaseTemplate ?? DefaultBaseTemplate, {
|
|
437
|
+
previewText: preview,
|
|
438
|
+
editor,
|
|
439
|
+
children: parseContent(data.content)
|
|
440
|
+
}));
|
|
441
|
+
const [prettyHtml, text] = await Promise.all([pretty(unformattedHtml), toPlainText(unformattedHtml)]);
|
|
442
|
+
return {
|
|
443
|
+
html: prettyHtml,
|
|
444
|
+
text
|
|
445
|
+
};
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/extensions/alignment-attribute.tsx
|
|
450
|
+
const AlignmentAttribute = Extension.create({
|
|
451
|
+
name: "alignmentAttribute",
|
|
452
|
+
addOptions() {
|
|
453
|
+
return {
|
|
454
|
+
types: [],
|
|
455
|
+
alignments: [
|
|
456
|
+
"left",
|
|
457
|
+
"center",
|
|
458
|
+
"right",
|
|
459
|
+
"justify"
|
|
460
|
+
]
|
|
461
|
+
};
|
|
462
|
+
},
|
|
463
|
+
addGlobalAttributes() {
|
|
464
|
+
return [{
|
|
465
|
+
types: this.options.types,
|
|
466
|
+
attributes: { alignment: {
|
|
467
|
+
parseHTML: (element) => {
|
|
468
|
+
const explicitAlign = element.getAttribute("align") || element.getAttribute("alignment") || element.style.textAlign;
|
|
469
|
+
if (explicitAlign && this.options.alignments.includes(explicitAlign)) return explicitAlign;
|
|
470
|
+
return null;
|
|
471
|
+
},
|
|
472
|
+
renderHTML: (attributes) => {
|
|
473
|
+
if (attributes.alignment === "left") return {};
|
|
474
|
+
return { alignment: attributes.alignment };
|
|
475
|
+
}
|
|
476
|
+
} }
|
|
477
|
+
}];
|
|
478
|
+
},
|
|
479
|
+
addCommands() {
|
|
480
|
+
return { setAlignment: (alignment) => ({ commands }) => {
|
|
481
|
+
if (!this.options.alignments.includes(alignment)) return false;
|
|
482
|
+
return this.options.types.every((type) => commands.updateAttributes(type, { alignment }));
|
|
483
|
+
} };
|
|
484
|
+
},
|
|
485
|
+
addKeyboardShortcuts() {
|
|
486
|
+
return {
|
|
487
|
+
Enter: () => {
|
|
488
|
+
const { from } = this.editor.state.selection;
|
|
489
|
+
const currentAlignment = this.editor.state.doc.nodeAt(from)?.attrs?.alignment;
|
|
490
|
+
if (currentAlignment) requestAnimationFrame(() => {
|
|
491
|
+
this.editor.commands.setAlignment(currentAlignment);
|
|
492
|
+
});
|
|
493
|
+
return false;
|
|
494
|
+
},
|
|
495
|
+
"Mod-Shift-l": () => this.editor.commands.setAlignment("left"),
|
|
496
|
+
"Mod-Shift-e": () => this.editor.commands.setAlignment("center"),
|
|
497
|
+
"Mod-Shift-r": () => this.editor.commands.setAlignment("right"),
|
|
498
|
+
"Mod-Shift-j": () => this.editor.commands.setAlignment("justify")
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
//#endregion
|
|
504
|
+
//#region src/utils/get-text-alignment.ts
|
|
505
|
+
function getTextAlignment(alignment) {
|
|
506
|
+
switch (alignment) {
|
|
507
|
+
case "left": return { textAlign: "left" };
|
|
508
|
+
case "center": return { textAlign: "center" };
|
|
509
|
+
case "right": return { textAlign: "right" };
|
|
510
|
+
default: return {};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
//#endregion
|
|
515
|
+
//#region src/extensions/blockquote.tsx
|
|
516
|
+
const Blockquote = EmailNode.from(BlockquoteBase, ({ children, node, style }) => /* @__PURE__ */ jsx("blockquote", {
|
|
517
|
+
className: node.attrs?.class || void 0,
|
|
518
|
+
style: {
|
|
519
|
+
...style,
|
|
520
|
+
...inlineCssToJs(node.attrs?.style),
|
|
521
|
+
...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
|
|
522
|
+
},
|
|
523
|
+
children
|
|
524
|
+
}));
|
|
525
|
+
|
|
526
|
+
//#endregion
|
|
527
|
+
//#region src/utils/attribute-helpers.ts
|
|
528
|
+
/**
|
|
529
|
+
* Creates TipTap attribute definitions for a list of HTML attributes.
|
|
530
|
+
* Each attribute will have the same pattern:
|
|
531
|
+
* - default: null
|
|
532
|
+
* - parseHTML: extracts the attribute from the element
|
|
533
|
+
* - renderHTML: conditionally renders the attribute if it has a value
|
|
534
|
+
*
|
|
535
|
+
* @param attributeNames - Array of HTML attribute names to create definitions for
|
|
536
|
+
* @returns Object with TipTap attribute definitions
|
|
537
|
+
*
|
|
538
|
+
* @example
|
|
539
|
+
* const attrs = createStandardAttributes(['class', 'id', 'title']);
|
|
540
|
+
* // Returns:
|
|
541
|
+
* // {
|
|
542
|
+
* // class: {
|
|
543
|
+
* // default: null,
|
|
544
|
+
* // parseHTML: (element) => element.getAttribute('class'),
|
|
545
|
+
* // renderHTML: (attributes) => attributes.class ? { class: attributes.class } : {}
|
|
546
|
+
* // },
|
|
547
|
+
* // ...
|
|
548
|
+
* // }
|
|
549
|
+
*/
|
|
550
|
+
function createStandardAttributes(attributeNames) {
|
|
551
|
+
return Object.fromEntries(attributeNames.map((attr) => [attr, {
|
|
552
|
+
default: null,
|
|
553
|
+
parseHTML: (element) => element.getAttribute(attr),
|
|
554
|
+
renderHTML: (attributes) => {
|
|
555
|
+
if (!attributes[attr]) return {};
|
|
556
|
+
return { [attr]: attributes[attr] };
|
|
557
|
+
}
|
|
558
|
+
}]));
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Common HTML attributes used across multiple extensions.
|
|
562
|
+
* These preserve attributes during HTML import and editing for better
|
|
563
|
+
* fidelity when importing existing email templates.
|
|
564
|
+
*/
|
|
565
|
+
const COMMON_HTML_ATTRIBUTES = [
|
|
566
|
+
"id",
|
|
567
|
+
"class",
|
|
568
|
+
"title",
|
|
569
|
+
"lang",
|
|
570
|
+
"dir",
|
|
571
|
+
"data-id"
|
|
572
|
+
];
|
|
573
|
+
/**
|
|
574
|
+
* Layout-specific HTML attributes used for positioning and sizing.
|
|
575
|
+
*/
|
|
576
|
+
const LAYOUT_ATTRIBUTES = [
|
|
577
|
+
"align",
|
|
578
|
+
"width",
|
|
579
|
+
"height"
|
|
580
|
+
];
|
|
581
|
+
/**
|
|
582
|
+
* Table-specific HTML attributes used for table layout and styling.
|
|
583
|
+
*/
|
|
584
|
+
const TABLE_ATTRIBUTES = [
|
|
585
|
+
"border",
|
|
586
|
+
"cellpadding",
|
|
587
|
+
"cellspacing"
|
|
588
|
+
];
|
|
589
|
+
/**
|
|
590
|
+
* Table cell-specific HTML attributes.
|
|
591
|
+
*/
|
|
592
|
+
const TABLE_CELL_ATTRIBUTES = [
|
|
593
|
+
"valign",
|
|
594
|
+
"bgcolor",
|
|
595
|
+
"colspan",
|
|
596
|
+
"rowspan"
|
|
597
|
+
];
|
|
598
|
+
/**
|
|
599
|
+
* Table header cell-specific HTML attributes.
|
|
600
|
+
* These are additional attributes that only apply to <th> elements.
|
|
601
|
+
*/
|
|
602
|
+
const TABLE_HEADER_ATTRIBUTES = [...TABLE_CELL_ATTRIBUTES, "scope"];
|
|
603
|
+
|
|
604
|
+
//#endregion
|
|
605
|
+
//#region src/extensions/body.tsx
|
|
606
|
+
const Body = EmailNode.create({
|
|
607
|
+
name: "body",
|
|
608
|
+
group: "block",
|
|
609
|
+
content: "block+",
|
|
610
|
+
defining: true,
|
|
611
|
+
isolating: true,
|
|
612
|
+
addAttributes() {
|
|
613
|
+
return { ...createStandardAttributes([...COMMON_HTML_ATTRIBUTES, ...LAYOUT_ATTRIBUTES]) };
|
|
614
|
+
},
|
|
615
|
+
parseHTML() {
|
|
616
|
+
return [{
|
|
617
|
+
tag: "body",
|
|
618
|
+
getAttrs: (node) => {
|
|
619
|
+
if (typeof node === "string") return false;
|
|
620
|
+
const element = node;
|
|
621
|
+
const attrs = {};
|
|
622
|
+
Array.from(element.attributes).forEach((attr) => {
|
|
623
|
+
attrs[attr.name] = attr.value;
|
|
624
|
+
});
|
|
625
|
+
return attrs;
|
|
626
|
+
}
|
|
627
|
+
}];
|
|
628
|
+
},
|
|
629
|
+
renderHTML({ HTMLAttributes }) {
|
|
630
|
+
return [
|
|
631
|
+
"div",
|
|
632
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
|
633
|
+
0
|
|
634
|
+
];
|
|
635
|
+
},
|
|
636
|
+
renderToReactEmail({ children, node, style }) {
|
|
637
|
+
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
638
|
+
return /* @__PURE__ */ jsx("div", {
|
|
639
|
+
className: node.attrs?.class || void 0,
|
|
640
|
+
style: {
|
|
641
|
+
...style,
|
|
642
|
+
...inlineStyles
|
|
643
|
+
},
|
|
644
|
+
children
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
//#endregion
|
|
650
|
+
//#region src/extensions/bold.tsx
|
|
651
|
+
const BoldWithoutFontWeightInference = BoldBase.extend({ parseHTML() {
|
|
652
|
+
return [
|
|
653
|
+
{ tag: "strong" },
|
|
654
|
+
{
|
|
655
|
+
tag: "b",
|
|
656
|
+
getAttrs: (node) => node.style.fontWeight !== "normal" && null
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
style: "font-weight=400",
|
|
660
|
+
clearMark: (mark) => mark.type.name === this.name
|
|
661
|
+
}
|
|
662
|
+
];
|
|
663
|
+
} });
|
|
664
|
+
const Bold = EmailMark.from(BoldWithoutFontWeightInference, ({ children, style }) => /* @__PURE__ */ jsx("strong", {
|
|
665
|
+
style,
|
|
666
|
+
children
|
|
667
|
+
}));
|
|
668
|
+
|
|
669
|
+
//#endregion
|
|
670
|
+
//#region src/extensions/bullet-list.tsx
|
|
671
|
+
const BulletList = EmailNode.from(BulletListBase, ({ children, node, style }) => /* @__PURE__ */ jsx("ul", {
|
|
672
|
+
className: node.attrs?.class || void 0,
|
|
673
|
+
style: {
|
|
674
|
+
...style,
|
|
675
|
+
...inlineCssToJs(node.attrs?.style)
|
|
676
|
+
},
|
|
677
|
+
children
|
|
678
|
+
}));
|
|
679
|
+
|
|
680
|
+
//#endregion
|
|
476
681
|
//#region src/extensions/button.tsx
|
|
477
682
|
const Button = EmailNode.create({
|
|
478
683
|
name: "button",
|
|
@@ -534,7 +739,7 @@ const Button = EmailNode.create({
|
|
|
534
739
|
}
|
|
535
740
|
};
|
|
536
741
|
},
|
|
537
|
-
renderToReactEmail({ children, node,
|
|
742
|
+
renderToReactEmail({ children, node, style }) {
|
|
538
743
|
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
539
744
|
return /* @__PURE__ */ jsx(Row, { children: /* @__PURE__ */ jsx(Column, {
|
|
540
745
|
align: node.attrs?.align || node.attrs?.alignment,
|
|
@@ -542,8 +747,7 @@ const Button = EmailNode.create({
|
|
|
542
747
|
className: node.attrs?.class || void 0,
|
|
543
748
|
href: node.attrs?.href,
|
|
544
749
|
style: {
|
|
545
|
-
...
|
|
546
|
-
...styles.button,
|
|
750
|
+
...style,
|
|
547
751
|
...inlineStyles
|
|
548
752
|
},
|
|
549
753
|
children
|
|
@@ -594,6 +798,16 @@ const ClassAttribute = Extension.create({
|
|
|
594
798
|
}
|
|
595
799
|
});
|
|
596
800
|
|
|
801
|
+
//#endregion
|
|
802
|
+
//#region src/extensions/code.tsx
|
|
803
|
+
const Code = EmailMark.from(CodeBase, ({ children, node, style }) => /* @__PURE__ */ jsx("code", {
|
|
804
|
+
style: {
|
|
805
|
+
...style,
|
|
806
|
+
...inlineCssToJs(node.attrs?.style)
|
|
807
|
+
},
|
|
808
|
+
children
|
|
809
|
+
}));
|
|
810
|
+
|
|
597
811
|
//#endregion
|
|
598
812
|
//#region src/utils/prism-utils.ts
|
|
599
813
|
const publicURL = "/styles/prism";
|
|
@@ -772,6 +986,25 @@ const CodeBlockPrism = EmailNode.from(CodeBlock$1.extend({
|
|
|
772
986
|
]
|
|
773
987
|
];
|
|
774
988
|
},
|
|
989
|
+
addKeyboardShortcuts() {
|
|
990
|
+
return {
|
|
991
|
+
...this.parent?.(),
|
|
992
|
+
"Mod-a": ({ editor }) => {
|
|
993
|
+
const { state } = editor;
|
|
994
|
+
const { selection } = state;
|
|
995
|
+
const { $from } = selection;
|
|
996
|
+
for (let depth = $from.depth; depth >= 1; depth--) if ($from.node(depth).type.name === this.name) {
|
|
997
|
+
const blockStart = $from.start(depth);
|
|
998
|
+
const blockEnd = $from.end(depth);
|
|
999
|
+
if (selection.from === blockStart && selection.to === blockEnd) return false;
|
|
1000
|
+
const tr = state.tr.setSelection(TextSelection.create(state.doc, blockStart, blockEnd));
|
|
1001
|
+
editor.view.dispatch(tr);
|
|
1002
|
+
return true;
|
|
1003
|
+
}
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
},
|
|
775
1008
|
addProseMirrorPlugins() {
|
|
776
1009
|
return [...this.parent?.() || [], PrismPlugin({
|
|
777
1010
|
name: this.name,
|
|
@@ -779,7 +1012,7 @@ const CodeBlockPrism = EmailNode.from(CodeBlock$1.extend({
|
|
|
779
1012
|
defaultTheme: this.options.defaultTheme
|
|
780
1013
|
})];
|
|
781
1014
|
}
|
|
782
|
-
}), ({ node,
|
|
1015
|
+
}), ({ node, style }) => {
|
|
783
1016
|
const language = node.attrs?.language ? `${node.attrs.language}` : "javascript";
|
|
784
1017
|
const userTheme = ReactEmailComponents[node.attrs?.theme];
|
|
785
1018
|
const theme = userTheme ? {
|
|
@@ -803,7 +1036,7 @@ const CodeBlockPrism = EmailNode.from(CodeBlock$1.extend({
|
|
|
803
1036
|
theme,
|
|
804
1037
|
style: {
|
|
805
1038
|
width: "auto",
|
|
806
|
-
...
|
|
1039
|
+
...style
|
|
807
1040
|
}
|
|
808
1041
|
});
|
|
809
1042
|
});
|
|
@@ -840,12 +1073,12 @@ const Div = EmailNode.create({
|
|
|
840
1073
|
addAttributes() {
|
|
841
1074
|
return { ...createStandardAttributes([...COMMON_HTML_ATTRIBUTES, ...LAYOUT_ATTRIBUTES]) };
|
|
842
1075
|
},
|
|
843
|
-
renderToReactEmail({ children, node,
|
|
1076
|
+
renderToReactEmail({ children, node, style }) {
|
|
844
1077
|
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
845
1078
|
return /* @__PURE__ */ jsx("div", {
|
|
846
1079
|
className: node.attrs?.class || void 0,
|
|
847
1080
|
style: {
|
|
848
|
-
...
|
|
1081
|
+
...style,
|
|
849
1082
|
...inlineStyles
|
|
850
1083
|
},
|
|
851
1084
|
children
|
|
@@ -854,97 +1087,162 @@ const Div = EmailNode.create({
|
|
|
854
1087
|
});
|
|
855
1088
|
|
|
856
1089
|
//#endregion
|
|
857
|
-
//#region src/extensions/
|
|
858
|
-
const
|
|
859
|
-
|
|
1090
|
+
//#region src/extensions/divider.tsx
|
|
1091
|
+
const Divider = EmailNode.from(HorizontalRule.extend({
|
|
1092
|
+
addAttributes() {
|
|
1093
|
+
return { class: { default: "divider" } };
|
|
1094
|
+
},
|
|
1095
|
+
addInputRules() {
|
|
1096
|
+
return [new InputRule({
|
|
1097
|
+
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
|
|
1098
|
+
handler: ({ state, range }) => {
|
|
1099
|
+
const attributes = {};
|
|
1100
|
+
const { tr } = state;
|
|
1101
|
+
const start = range.from;
|
|
1102
|
+
const end = range.to;
|
|
1103
|
+
tr.insert(start - 1, this.type.create(attributes)).delete(tr.mapping.map(start), tr.mapping.map(end));
|
|
1104
|
+
}
|
|
1105
|
+
})];
|
|
1106
|
+
},
|
|
1107
|
+
addNodeView() {
|
|
1108
|
+
return ReactNodeViewRenderer((props) => {
|
|
1109
|
+
const node = props.node;
|
|
1110
|
+
const { class: className, ...rest } = node.attrs;
|
|
1111
|
+
return /* @__PURE__ */ jsx(NodeViewWrapper, { children: /* @__PURE__ */ jsx(Hr, {
|
|
1112
|
+
...rest,
|
|
1113
|
+
className: "node-hr",
|
|
1114
|
+
style: inlineCssToJs(node.attrs.style)
|
|
1115
|
+
}) });
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
}), ({ node, style }) => {
|
|
1119
|
+
return /* @__PURE__ */ jsx(Hr, {
|
|
1120
|
+
className: node.attrs?.class || void 0,
|
|
1121
|
+
style: {
|
|
1122
|
+
...style,
|
|
1123
|
+
...inlineCssToJs(node.attrs?.style)
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
//#endregion
|
|
1129
|
+
//#region src/extensions/global-content.ts
|
|
1130
|
+
const GLOBAL_CONTENT_NODE_TYPE = "globalContent";
|
|
1131
|
+
let cachedGlobalPosition = null;
|
|
1132
|
+
function findGlobalContentPositions(doc) {
|
|
1133
|
+
const positions = [];
|
|
1134
|
+
doc.descendants((node, position) => {
|
|
1135
|
+
if (node.type.name === GLOBAL_CONTENT_NODE_TYPE) positions.push(position);
|
|
1136
|
+
});
|
|
1137
|
+
return positions;
|
|
1138
|
+
}
|
|
1139
|
+
function getCachedGlobalContentPosition(doc) {
|
|
1140
|
+
if (cachedGlobalPosition != null) try {
|
|
1141
|
+
if (doc.nodeAt(cachedGlobalPosition)?.type.name === GLOBAL_CONTENT_NODE_TYPE) return cachedGlobalPosition;
|
|
1142
|
+
} catch {
|
|
1143
|
+
cachedGlobalPosition = null;
|
|
1144
|
+
}
|
|
1145
|
+
cachedGlobalPosition = findGlobalContentPositions(doc)[0] ?? null;
|
|
1146
|
+
return cachedGlobalPosition;
|
|
1147
|
+
}
|
|
1148
|
+
function getGlobalContent(key, editor) {
|
|
1149
|
+
const position = getCachedGlobalContentPosition(editor.state.doc);
|
|
1150
|
+
if (cachedGlobalPosition == null) return null;
|
|
1151
|
+
return editor.state.doc.nodeAt(position)?.attrs.data[key] ?? null;
|
|
1152
|
+
}
|
|
1153
|
+
const GlobalContent = Node$1.create({
|
|
1154
|
+
name: GLOBAL_CONTENT_NODE_TYPE,
|
|
860
1155
|
addOptions() {
|
|
861
1156
|
return {
|
|
862
|
-
|
|
863
|
-
|
|
1157
|
+
key: GLOBAL_CONTENT_NODE_TYPE,
|
|
1158
|
+
data: {}
|
|
864
1159
|
};
|
|
865
1160
|
},
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
});
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
});
|
|
896
|
-
if (rangesToLift.length === 0) return null;
|
|
897
|
-
const tr = newState.tr;
|
|
898
|
-
for (let i = rangesToLift.length - 1; i >= 0; i--) {
|
|
899
|
-
const { range, target } = rangesToLift[i];
|
|
900
|
-
tr.lift(range, target);
|
|
1161
|
+
group: "block",
|
|
1162
|
+
selectable: false,
|
|
1163
|
+
draggable: false,
|
|
1164
|
+
atom: true,
|
|
1165
|
+
addAttributes() {
|
|
1166
|
+
return { data: { default: this.options.data } };
|
|
1167
|
+
},
|
|
1168
|
+
parseHTML() {
|
|
1169
|
+
return [{ tag: `div[data-type="${this.name}"]` }];
|
|
1170
|
+
},
|
|
1171
|
+
renderHTML({ HTMLAttributes }) {
|
|
1172
|
+
return ["div", mergeAttributes(HTMLAttributes, {
|
|
1173
|
+
"data-type": this.name,
|
|
1174
|
+
style: "width: 100%; height: 1px; visibility: hidden;"
|
|
1175
|
+
})];
|
|
1176
|
+
},
|
|
1177
|
+
addCommands() {
|
|
1178
|
+
return { setGlobalContent: (key, value) => ({ tr, dispatch }) => {
|
|
1179
|
+
const ensureGlobalPosition = () => {
|
|
1180
|
+
const positions = findGlobalContentPositions(tr.doc);
|
|
1181
|
+
for (let i = positions.length - 1; i > 0; i--) tr.delete(positions[i], positions[i] + 1);
|
|
1182
|
+
const pos = positions[0] ?? -1;
|
|
1183
|
+
if (pos >= 0) cachedGlobalPosition = pos;
|
|
1184
|
+
else {
|
|
1185
|
+
cachedGlobalPosition = 0;
|
|
1186
|
+
tr.insert(0, this.type.create());
|
|
901
1187
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
if (
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
if (wouldCreateDeepNesting) return false;
|
|
910
|
-
let depth = 0;
|
|
911
|
-
let currentPos = pos;
|
|
912
|
-
let currentNode = node;
|
|
913
|
-
while (currentNode && depth <= maxDepth) {
|
|
914
|
-
if (!nodeTypes || nodeTypes.includes(currentNode.type.name)) depth++;
|
|
915
|
-
const $pos = newDoc.resolve(currentPos);
|
|
916
|
-
if ($pos.depth === 0) break;
|
|
917
|
-
currentPos = $pos.before($pos.depth);
|
|
918
|
-
currentNode = newDoc.nodeAt(currentPos);
|
|
919
|
-
}
|
|
920
|
-
if (depth > maxDepth) {
|
|
921
|
-
wouldCreateDeepNesting = true;
|
|
922
|
-
return false;
|
|
923
|
-
}
|
|
1188
|
+
};
|
|
1189
|
+
if (dispatch) {
|
|
1190
|
+
ensureGlobalPosition();
|
|
1191
|
+
if (cachedGlobalPosition == null) return false;
|
|
1192
|
+
tr.setNodeAttribute(cachedGlobalPosition, "data", {
|
|
1193
|
+
...tr.doc.nodeAt(cachedGlobalPosition)?.attrs.data,
|
|
1194
|
+
[key]: value
|
|
924
1195
|
});
|
|
925
|
-
return !wouldCreateDeepNesting;
|
|
926
1196
|
}
|
|
927
|
-
|
|
1197
|
+
return true;
|
|
1198
|
+
} };
|
|
928
1199
|
}
|
|
929
1200
|
});
|
|
930
1201
|
|
|
931
1202
|
//#endregion
|
|
932
|
-
//#region src/extensions/
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1203
|
+
//#region src/extensions/hard-break.tsx
|
|
1204
|
+
const HardBreak = EmailNode.from(HardBreakBase, () => /* @__PURE__ */ jsx("br", {}));
|
|
1205
|
+
|
|
1206
|
+
//#endregion
|
|
1207
|
+
//#region src/extensions/heading.tsx
|
|
1208
|
+
const Heading = EmailNode.from(Heading$2.extend({ addNodeView() {
|
|
1209
|
+
return ReactNodeViewRenderer(({ node }) => {
|
|
1210
|
+
const level = node.attrs.level ?? 1;
|
|
1211
|
+
const { class: className, ...rest } = node.attrs;
|
|
1212
|
+
const attrs = {
|
|
1213
|
+
...rest,
|
|
1214
|
+
className: `node-h${level} ${className}`,
|
|
1215
|
+
style: inlineCssToJs(node.attrs.style)
|
|
1216
|
+
};
|
|
1217
|
+
return /* @__PURE__ */ jsx(NodeViewWrapper, { children: /* @__PURE__ */ jsx(Heading$1, {
|
|
1218
|
+
as: `h${level}`,
|
|
1219
|
+
...attrs,
|
|
1220
|
+
children: /* @__PURE__ */ jsx(NodeViewContent, {})
|
|
1221
|
+
}) });
|
|
1222
|
+
});
|
|
1223
|
+
} }), ({ children, node, style }) => {
|
|
1224
|
+
return /* @__PURE__ */ jsx(Heading$1, {
|
|
1225
|
+
as: `h${node.attrs?.level ?? 1}`,
|
|
1226
|
+
className: node.attrs?.class || void 0,
|
|
1227
|
+
style: {
|
|
1228
|
+
...style,
|
|
1229
|
+
...inlineCssToJs(node.attrs?.style),
|
|
1230
|
+
...getTextAlignment(node.attrs?.align ?? node.attrs?.alignment)
|
|
938
1231
|
},
|
|
939
|
-
|
|
940
|
-
...options
|
|
1232
|
+
children
|
|
941
1233
|
});
|
|
942
|
-
};
|
|
943
|
-
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
//#endregion
|
|
1237
|
+
//#region src/extensions/italic.tsx
|
|
1238
|
+
const Italic = EmailMark.from(ItalicBase, ({ children, style }) => /* @__PURE__ */ jsx("em", {
|
|
1239
|
+
style,
|
|
1240
|
+
children
|
|
1241
|
+
}));
|
|
944
1242
|
|
|
945
1243
|
//#endregion
|
|
946
|
-
//#region src/extensions/preserved-style.
|
|
947
|
-
const PreservedStyle =
|
|
1244
|
+
//#region src/extensions/preserved-style.tsx
|
|
1245
|
+
const PreservedStyle = EmailMark.create({
|
|
948
1246
|
name: "preservedStyle",
|
|
949
1247
|
addAttributes() {
|
|
950
1248
|
return { style: {
|
|
@@ -973,6 +1271,12 @@ const PreservedStyle = Mark.create({
|
|
|
973
1271
|
mergeAttributes(HTMLAttributes),
|
|
974
1272
|
0
|
|
975
1273
|
];
|
|
1274
|
+
},
|
|
1275
|
+
renderToReactEmail({ children, mark }) {
|
|
1276
|
+
return /* @__PURE__ */ jsx("span", {
|
|
1277
|
+
style: mark.attrs?.style ? inlineCssToJs(mark.attrs.style) : void 0,
|
|
1278
|
+
children
|
|
1279
|
+
});
|
|
976
1280
|
}
|
|
977
1281
|
});
|
|
978
1282
|
const LINK_INDICATOR_STYLES = [
|
|
@@ -1017,139 +1321,358 @@ function processStylesForUnlink(styleString) {
|
|
|
1017
1321
|
}
|
|
1018
1322
|
|
|
1019
1323
|
//#endregion
|
|
1020
|
-
//#region src/
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
content: "block+",
|
|
1036
|
-
isolating: true,
|
|
1037
|
-
defining: true,
|
|
1324
|
+
//#region src/extensions/link.tsx
|
|
1325
|
+
const Link = EmailMark.from(TiptapLink, ({ children, mark, style }) => {
|
|
1326
|
+
const linkMarkStyle = mark.attrs?.style ? inlineCssToJs(mark.attrs.style) : {};
|
|
1327
|
+
return /* @__PURE__ */ jsx(Link$1, {
|
|
1328
|
+
href: mark.attrs?.href ?? void 0,
|
|
1329
|
+
rel: mark.attrs?.rel ?? void 0,
|
|
1330
|
+
style: {
|
|
1331
|
+
...style,
|
|
1332
|
+
...linkMarkStyle
|
|
1333
|
+
},
|
|
1334
|
+
target: mark.attrs?.target ?? void 0,
|
|
1335
|
+
...mark.attrs?.["ses:no-track"] ? { "ses:no-track": mark.attrs["ses:no-track"] } : {},
|
|
1336
|
+
children
|
|
1337
|
+
});
|
|
1338
|
+
}).extend({
|
|
1038
1339
|
parseHTML() {
|
|
1039
|
-
return [{
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1340
|
+
return [{
|
|
1341
|
+
tag: "a[target]:not([data-id=\"react-email-button\"])",
|
|
1342
|
+
getAttrs: (node) => {
|
|
1343
|
+
if (typeof node === "string") return false;
|
|
1344
|
+
const element = node;
|
|
1345
|
+
const attrs = {};
|
|
1346
|
+
Array.from(element.attributes).forEach((attr) => {
|
|
1347
|
+
attrs[attr.name] = attr.value;
|
|
1348
|
+
});
|
|
1349
|
+
return attrs;
|
|
1350
|
+
}
|
|
1351
|
+
}, {
|
|
1352
|
+
tag: "a[href]:not([data-id=\"react-email-button\"])",
|
|
1353
|
+
getAttrs: (node) => {
|
|
1354
|
+
if (typeof node === "string") return false;
|
|
1355
|
+
const element = node;
|
|
1356
|
+
const attrs = {};
|
|
1357
|
+
Array.from(element.attributes).forEach((attr) => {
|
|
1358
|
+
attrs[attr.name] = attr.value;
|
|
1359
|
+
});
|
|
1360
|
+
return attrs;
|
|
1361
|
+
}
|
|
1362
|
+
}];
|
|
1061
1363
|
},
|
|
1062
|
-
|
|
1063
|
-
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
1064
|
-
const textAlign = node.attrs?.align || node.attrs?.alignment;
|
|
1065
|
-
return /* @__PURE__ */ jsx(Section$1, {
|
|
1066
|
-
className: node.attrs?.class || void 0,
|
|
1067
|
-
align: textAlign,
|
|
1068
|
-
style: {
|
|
1069
|
-
...styles.section,
|
|
1070
|
-
...inlineStyles,
|
|
1071
|
-
...getTextAlignment(textAlign)
|
|
1072
|
-
},
|
|
1073
|
-
children
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
1076
|
-
});
|
|
1077
|
-
|
|
1078
|
-
//#endregion
|
|
1079
|
-
//#region src/extensions/style-attribute.tsx
|
|
1080
|
-
const StyleAttribute = Extension.create({
|
|
1081
|
-
name: "styleAttribute",
|
|
1082
|
-
priority: 101,
|
|
1083
|
-
addOptions() {
|
|
1364
|
+
addAttributes() {
|
|
1084
1365
|
return {
|
|
1085
|
-
|
|
1086
|
-
|
|
1366
|
+
...this.parent?.(),
|
|
1367
|
+
"ses:no-track": {
|
|
1368
|
+
default: null,
|
|
1369
|
+
parseHTML: (element) => element.getAttribute("ses:no-track")
|
|
1370
|
+
}
|
|
1087
1371
|
};
|
|
1088
1372
|
},
|
|
1089
|
-
addGlobalAttributes() {
|
|
1090
|
-
return [{
|
|
1091
|
-
types: this.options.types,
|
|
1092
|
-
attributes: { style: {
|
|
1093
|
-
default: "",
|
|
1094
|
-
parseHTML: (element) => element.getAttribute("style") || "",
|
|
1095
|
-
renderHTML: (attributes) => {
|
|
1096
|
-
return { style: attributes.style ?? "" };
|
|
1097
|
-
}
|
|
1098
|
-
} }
|
|
1099
|
-
}];
|
|
1100
|
-
},
|
|
1101
1373
|
addCommands() {
|
|
1102
1374
|
return {
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1375
|
+
...this.parent?.(),
|
|
1376
|
+
unsetLink: () => ({ state, chain }) => {
|
|
1377
|
+
const { from } = state.selection;
|
|
1378
|
+
const linkStyle = state.doc.resolve(from).marks().find((m) => m.type.name === "link")?.attrs?.style ?? null;
|
|
1379
|
+
const preservedStyle = processStylesForUnlink(linkStyle);
|
|
1380
|
+
const shouldRemoveUnderline = preservedStyle !== linkStyle;
|
|
1381
|
+
if (preservedStyle) {
|
|
1382
|
+
const cmd = chain().extendMarkRange("link").unsetMark("link").setMark("preservedStyle", { style: preservedStyle });
|
|
1383
|
+
return shouldRemoveUnderline ? cmd.unsetMark("underline").run() : cmd.run();
|
|
1384
|
+
}
|
|
1385
|
+
return chain().extendMarkRange("link").unsetMark("link").unsetMark("underline").run();
|
|
1108
1386
|
}
|
|
1109
1387
|
};
|
|
1110
1388
|
},
|
|
1111
1389
|
addKeyboardShortcuts() {
|
|
1112
|
-
return {
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
const { $from } = selection;
|
|
1116
|
-
const textBefore = $from.nodeBefore?.text || "";
|
|
1117
|
-
if (textBefore.includes("{{") || textBefore.includes("{{{")) return false;
|
|
1118
|
-
requestAnimationFrame(() => {
|
|
1119
|
-
editor.commands.resetAttributes("paragraph", "style");
|
|
1120
|
-
});
|
|
1121
|
-
return false;
|
|
1390
|
+
return { "Mod-k": () => {
|
|
1391
|
+
editorEventBus.dispatch("bubble-menu:add-link", void 0);
|
|
1392
|
+
return this.editor.chain().focus().toggleLink({ href: "" }).run();
|
|
1122
1393
|
} };
|
|
1123
1394
|
}
|
|
1124
1395
|
});
|
|
1125
1396
|
|
|
1126
1397
|
//#endregion
|
|
1127
|
-
//#region src/extensions/
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
addOptions() {
|
|
1135
|
-
return { HTMLAttributes: {} };
|
|
1136
|
-
},
|
|
1137
|
-
parseHTML() {
|
|
1138
|
-
return [{ tag: "sup" }];
|
|
1139
|
-
},
|
|
1140
|
-
renderHTML({ HTMLAttributes }) {
|
|
1141
|
-
return [
|
|
1142
|
-
"sup",
|
|
1143
|
-
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
|
1144
|
-
0
|
|
1145
|
-
];
|
|
1398
|
+
//#region src/extensions/list-item.tsx
|
|
1399
|
+
const ListItem = EmailNode.from(ListItemBase, ({ children, node, style }) => /* @__PURE__ */ jsx("li", {
|
|
1400
|
+
className: node.attrs?.class || void 0,
|
|
1401
|
+
style: {
|
|
1402
|
+
...style,
|
|
1403
|
+
...inlineCssToJs(node.attrs?.style),
|
|
1404
|
+
...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
|
|
1146
1405
|
},
|
|
1147
|
-
|
|
1406
|
+
children
|
|
1407
|
+
}));
|
|
1408
|
+
|
|
1409
|
+
//#endregion
|
|
1410
|
+
//#region src/extensions/max-nesting.ts
|
|
1411
|
+
const MaxNesting = Extension.create({
|
|
1412
|
+
name: "maxNesting",
|
|
1413
|
+
addOptions() {
|
|
1148
1414
|
return {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1415
|
+
maxDepth: 3,
|
|
1416
|
+
nodeTypes: void 0
|
|
1417
|
+
};
|
|
1418
|
+
},
|
|
1419
|
+
addProseMirrorPlugins() {
|
|
1420
|
+
const { maxDepth, nodeTypes } = this.options;
|
|
1421
|
+
if (typeof maxDepth !== "number" || maxDepth < 1) throw new Error("maxDepth must be a positive number");
|
|
1422
|
+
return [new Plugin({
|
|
1423
|
+
key: new PluginKey("maxNesting"),
|
|
1424
|
+
appendTransaction(transactions, _oldState, newState) {
|
|
1425
|
+
if (!transactions.some((tr$1) => tr$1.docChanged)) return null;
|
|
1426
|
+
const rangesToLift = [];
|
|
1427
|
+
newState.doc.descendants((node, pos) => {
|
|
1428
|
+
let depth = 0;
|
|
1429
|
+
let currentPos = pos;
|
|
1430
|
+
let currentNode = node;
|
|
1431
|
+
while (currentNode && depth <= maxDepth) {
|
|
1432
|
+
if (!nodeTypes || nodeTypes.includes(currentNode.type.name)) depth++;
|
|
1433
|
+
const $pos = newState.doc.resolve(currentPos);
|
|
1434
|
+
if ($pos.depth === 0) break;
|
|
1435
|
+
currentPos = $pos.before($pos.depth);
|
|
1436
|
+
currentNode = newState.doc.nodeAt(currentPos);
|
|
1437
|
+
}
|
|
1438
|
+
if (depth > maxDepth) {
|
|
1439
|
+
const $pos = newState.doc.resolve(pos);
|
|
1440
|
+
if ($pos.depth > 0) {
|
|
1441
|
+
const range = $pos.blockRange();
|
|
1442
|
+
if (range && "canReplace" in newState.schema.nodes.doc && typeof newState.schema.nodes.doc.canReplace === "function" && newState.schema.nodes.doc.canReplace(range.start - 1, range.end + 1, newState.doc.slice(range.start, range.end).content)) rangesToLift.push({
|
|
1443
|
+
range,
|
|
1444
|
+
target: range.start - 1
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
if (rangesToLift.length === 0) return null;
|
|
1450
|
+
const tr = newState.tr;
|
|
1451
|
+
for (let i = rangesToLift.length - 1; i >= 0; i--) {
|
|
1452
|
+
const { range, target } = rangesToLift[i];
|
|
1453
|
+
tr.lift(range, target);
|
|
1454
|
+
}
|
|
1455
|
+
return tr;
|
|
1456
|
+
},
|
|
1457
|
+
filterTransaction(tr) {
|
|
1458
|
+
if (!tr.docChanged) return true;
|
|
1459
|
+
let wouldCreateDeepNesting = false;
|
|
1460
|
+
const newDoc = tr.doc;
|
|
1461
|
+
newDoc.descendants((node, pos) => {
|
|
1462
|
+
if (wouldCreateDeepNesting) return false;
|
|
1463
|
+
let depth = 0;
|
|
1464
|
+
let currentPos = pos;
|
|
1465
|
+
let currentNode = node;
|
|
1466
|
+
while (currentNode && depth <= maxDepth) {
|
|
1467
|
+
if (!nodeTypes || nodeTypes.includes(currentNode.type.name)) depth++;
|
|
1468
|
+
const $pos = newDoc.resolve(currentPos);
|
|
1469
|
+
if ($pos.depth === 0) break;
|
|
1470
|
+
currentPos = $pos.before($pos.depth);
|
|
1471
|
+
currentNode = newDoc.nodeAt(currentPos);
|
|
1472
|
+
}
|
|
1473
|
+
if (depth > maxDepth) {
|
|
1474
|
+
wouldCreateDeepNesting = true;
|
|
1475
|
+
return false;
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
return !wouldCreateDeepNesting;
|
|
1479
|
+
}
|
|
1480
|
+
})];
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
//#endregion
|
|
1485
|
+
//#region src/extensions/ordered-list.tsx
|
|
1486
|
+
const OrderedList = EmailNode.from(OrderedListBase, ({ children, node, style }) => /* @__PURE__ */ jsx("ol", {
|
|
1487
|
+
className: node.attrs?.class || void 0,
|
|
1488
|
+
start: node.attrs?.start,
|
|
1489
|
+
style: {
|
|
1490
|
+
...style,
|
|
1491
|
+
...inlineCssToJs(node.attrs?.style)
|
|
1492
|
+
},
|
|
1493
|
+
children
|
|
1494
|
+
}));
|
|
1495
|
+
|
|
1496
|
+
//#endregion
|
|
1497
|
+
//#region src/extensions/paragraph.tsx
|
|
1498
|
+
const Paragraph = EmailNode.from(ParagraphBase, ({ children, node, style }) => {
|
|
1499
|
+
const isEmpty = !node.content || node.content.length === 0;
|
|
1500
|
+
return /* @__PURE__ */ jsx("p", {
|
|
1501
|
+
className: node.attrs?.class || void 0,
|
|
1502
|
+
style: {
|
|
1503
|
+
...style,
|
|
1504
|
+
...inlineCssToJs(node.attrs?.style),
|
|
1505
|
+
...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
|
|
1506
|
+
},
|
|
1507
|
+
children: isEmpty ? /* @__PURE__ */ jsx("br", {}) : children
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
//#endregion
|
|
1512
|
+
//#region src/extensions/placeholder.ts
|
|
1513
|
+
const Placeholder = TipTapPlaceholder.configure({
|
|
1514
|
+
placeholder: ({ node }) => {
|
|
1515
|
+
if (node.type.name === "heading") return `Heading ${node.attrs.level}`;
|
|
1516
|
+
return "Press '/' for commands";
|
|
1517
|
+
},
|
|
1518
|
+
includeChildren: true
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
//#endregion
|
|
1522
|
+
//#region src/extensions/preview-text.ts
|
|
1523
|
+
const PreviewText = Node$1.create({
|
|
1524
|
+
name: "previewText",
|
|
1525
|
+
group: "block",
|
|
1526
|
+
selectable: false,
|
|
1527
|
+
draggable: false,
|
|
1528
|
+
atom: true,
|
|
1529
|
+
addOptions() {
|
|
1530
|
+
return { HTMLAttributes: {} };
|
|
1531
|
+
},
|
|
1532
|
+
addStorage() {
|
|
1533
|
+
return { previewText: null };
|
|
1534
|
+
},
|
|
1535
|
+
renderHTML() {
|
|
1536
|
+
return ["div", { style: "display: none" }];
|
|
1537
|
+
},
|
|
1538
|
+
parseHTML() {
|
|
1539
|
+
return [{
|
|
1540
|
+
tag: "div[data-skip-in-text=\"true\"]",
|
|
1541
|
+
getAttrs: (node) => {
|
|
1542
|
+
if (typeof node === "string") return false;
|
|
1543
|
+
const element = node;
|
|
1544
|
+
let directText = "";
|
|
1545
|
+
for (const child of element.childNodes) if (child.nodeType === 3) directText += child.textContent || "";
|
|
1546
|
+
const cleanText = directText.trim();
|
|
1547
|
+
if (cleanText) this.storage.previewText = cleanText;
|
|
1548
|
+
return false;
|
|
1549
|
+
}
|
|
1550
|
+
}, {
|
|
1551
|
+
tag: "span.preheader",
|
|
1552
|
+
getAttrs: (node) => {
|
|
1553
|
+
if (typeof node === "string") return false;
|
|
1554
|
+
const preheaderText = node.textContent?.trim();
|
|
1555
|
+
if (preheaderText) this.storage.previewText = preheaderText;
|
|
1556
|
+
return false;
|
|
1557
|
+
}
|
|
1558
|
+
}];
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
//#endregion
|
|
1563
|
+
//#region src/extensions/section.tsx
|
|
1564
|
+
const Section = EmailNode.create({
|
|
1565
|
+
name: "section",
|
|
1566
|
+
group: "block",
|
|
1567
|
+
content: "block+",
|
|
1568
|
+
isolating: true,
|
|
1569
|
+
defining: true,
|
|
1570
|
+
parseHTML() {
|
|
1571
|
+
return [{ tag: "section[data-type=\"section\"]" }];
|
|
1572
|
+
},
|
|
1573
|
+
renderHTML({ HTMLAttributes }) {
|
|
1574
|
+
return [
|
|
1575
|
+
"section",
|
|
1576
|
+
mergeAttributes({
|
|
1577
|
+
"data-type": "section",
|
|
1578
|
+
class: "node-section"
|
|
1579
|
+
}, HTMLAttributes),
|
|
1580
|
+
0
|
|
1581
|
+
];
|
|
1582
|
+
},
|
|
1583
|
+
addCommands() {
|
|
1584
|
+
return { insertSection: () => ({ commands }) => {
|
|
1585
|
+
return commands.insertContent({
|
|
1586
|
+
type: this.name,
|
|
1587
|
+
content: [{
|
|
1588
|
+
type: "paragraph",
|
|
1589
|
+
content: []
|
|
1590
|
+
}]
|
|
1591
|
+
});
|
|
1592
|
+
} };
|
|
1593
|
+
},
|
|
1594
|
+
renderToReactEmail({ children, node, style }) {
|
|
1595
|
+
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
1596
|
+
const textAlign = node.attrs?.align || node.attrs?.alignment;
|
|
1597
|
+
return /* @__PURE__ */ jsx(Section$1, {
|
|
1598
|
+
className: node.attrs?.class || void 0,
|
|
1599
|
+
align: textAlign,
|
|
1600
|
+
style: {
|
|
1601
|
+
...style,
|
|
1602
|
+
...inlineStyles,
|
|
1603
|
+
...getTextAlignment(textAlign)
|
|
1604
|
+
},
|
|
1605
|
+
children
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
//#endregion
|
|
1611
|
+
//#region src/extensions/strike.tsx
|
|
1612
|
+
const Strike = EmailMark.from(StrikeBase, ({ children, style }) => /* @__PURE__ */ jsx("s", {
|
|
1613
|
+
style,
|
|
1614
|
+
children
|
|
1615
|
+
}));
|
|
1616
|
+
|
|
1617
|
+
//#endregion
|
|
1618
|
+
//#region src/extensions/style-attribute.tsx
|
|
1619
|
+
const StyleAttribute = Extension.create({
|
|
1620
|
+
name: "styleAttribute",
|
|
1621
|
+
priority: 101,
|
|
1622
|
+
addOptions() {
|
|
1623
|
+
return {
|
|
1624
|
+
types: [],
|
|
1625
|
+
style: []
|
|
1626
|
+
};
|
|
1627
|
+
},
|
|
1628
|
+
addGlobalAttributes() {
|
|
1629
|
+
return [{
|
|
1630
|
+
types: this.options.types,
|
|
1631
|
+
attributes: { style: {
|
|
1632
|
+
default: "",
|
|
1633
|
+
parseHTML: (element) => element.getAttribute("style") || "",
|
|
1634
|
+
renderHTML: (attributes) => {
|
|
1635
|
+
return { style: attributes.style ?? "" };
|
|
1636
|
+
}
|
|
1637
|
+
} }
|
|
1638
|
+
}];
|
|
1639
|
+
},
|
|
1640
|
+
addCommands() {
|
|
1641
|
+
return {
|
|
1642
|
+
unsetStyle: () => ({ commands }) => {
|
|
1643
|
+
return this.options.types.every((type) => commands.resetAttributes(type, "style"));
|
|
1644
|
+
},
|
|
1645
|
+
setStyle: (style) => ({ commands }) => {
|
|
1646
|
+
return this.options.types.every((type) => commands.updateAttributes(type, { style }));
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
},
|
|
1650
|
+
addKeyboardShortcuts() {
|
|
1651
|
+
return { Enter: ({ editor }) => {
|
|
1652
|
+
const { state } = editor.view;
|
|
1653
|
+
const { selection } = state;
|
|
1654
|
+
const { $from } = selection;
|
|
1655
|
+
const textBefore = $from.nodeBefore?.text || "";
|
|
1656
|
+
if (textBefore.includes("{{") || textBefore.includes("{{{")) return false;
|
|
1657
|
+
requestAnimationFrame(() => {
|
|
1658
|
+
editor.commands.resetAttributes("paragraph", "style");
|
|
1659
|
+
});
|
|
1660
|
+
return false;
|
|
1661
|
+
} };
|
|
1662
|
+
}
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
//#endregion
|
|
1666
|
+
//#region src/extensions/sup.tsx
|
|
1667
|
+
const SupBase = SuperscriptBase.extend({
|
|
1668
|
+
name: "sup",
|
|
1669
|
+
addCommands() {
|
|
1670
|
+
return {
|
|
1671
|
+
...this.parent?.(),
|
|
1672
|
+
setSup: () => ({ commands }) => {
|
|
1673
|
+
return commands.setMark(this.name);
|
|
1674
|
+
},
|
|
1675
|
+
toggleSup: () => ({ commands }) => {
|
|
1153
1676
|
return commands.toggleMark(this.name);
|
|
1154
1677
|
},
|
|
1155
1678
|
unsetSup: () => ({ commands }) => {
|
|
@@ -1158,6 +1681,10 @@ const Sup = Mark.create({
|
|
|
1158
1681
|
};
|
|
1159
1682
|
}
|
|
1160
1683
|
});
|
|
1684
|
+
const Sup = EmailMark.from(SupBase, ({ children, style }) => /* @__PURE__ */ jsx("sup", {
|
|
1685
|
+
style,
|
|
1686
|
+
children
|
|
1687
|
+
}));
|
|
1161
1688
|
|
|
1162
1689
|
//#endregion
|
|
1163
1690
|
//#region src/extensions/table.tsx
|
|
@@ -1199,7 +1726,7 @@ const Table = EmailNode.create({
|
|
|
1199
1726
|
]
|
|
1200
1727
|
];
|
|
1201
1728
|
},
|
|
1202
|
-
renderToReactEmail({ children, node,
|
|
1729
|
+
renderToReactEmail({ children, node, style }) {
|
|
1203
1730
|
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
1204
1731
|
const alignment = node.attrs?.align || node.attrs?.alignment;
|
|
1205
1732
|
const width = node.attrs?.width;
|
|
@@ -1210,7 +1737,7 @@ const Table = EmailNode.create({
|
|
|
1210
1737
|
return /* @__PURE__ */ jsx(Section$1, {
|
|
1211
1738
|
className: node.attrs?.class || void 0,
|
|
1212
1739
|
align: alignment,
|
|
1213
|
-
style: resolveConflictingStyles(
|
|
1740
|
+
style: resolveConflictingStyles(style, {
|
|
1214
1741
|
...inlineStyles,
|
|
1215
1742
|
...centeringStyles
|
|
1216
1743
|
}),
|
|
@@ -1251,12 +1778,12 @@ const TableRow = EmailNode.create({
|
|
|
1251
1778
|
0
|
|
1252
1779
|
];
|
|
1253
1780
|
},
|
|
1254
|
-
renderToReactEmail({ children, node,
|
|
1781
|
+
renderToReactEmail({ children, node, style }) {
|
|
1255
1782
|
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
1256
1783
|
return /* @__PURE__ */ jsx("tr", {
|
|
1257
1784
|
className: node.attrs?.class || void 0,
|
|
1258
1785
|
style: {
|
|
1259
|
-
...
|
|
1786
|
+
...style,
|
|
1260
1787
|
...inlineStyles
|
|
1261
1788
|
},
|
|
1262
1789
|
children
|
|
@@ -1296,20 +1823,20 @@ const TableCell = EmailNode.create({
|
|
|
1296
1823
|
0
|
|
1297
1824
|
];
|
|
1298
1825
|
},
|
|
1299
|
-
renderToReactEmail({ children, node,
|
|
1826
|
+
renderToReactEmail({ children, node, style }) {
|
|
1300
1827
|
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
1301
1828
|
return /* @__PURE__ */ jsx(Column, {
|
|
1302
1829
|
className: node.attrs?.class || void 0,
|
|
1303
1830
|
align: node.attrs?.align || node.attrs?.alignment,
|
|
1304
1831
|
style: {
|
|
1305
|
-
...
|
|
1832
|
+
...style,
|
|
1306
1833
|
...inlineStyles
|
|
1307
1834
|
},
|
|
1308
1835
|
children
|
|
1309
1836
|
});
|
|
1310
1837
|
}
|
|
1311
1838
|
});
|
|
1312
|
-
const TableHeader = Node.create({
|
|
1839
|
+
const TableHeader = Node$1.create({
|
|
1313
1840
|
name: "tableHeader",
|
|
1314
1841
|
group: "tableCell",
|
|
1315
1842
|
content: "block+",
|
|
@@ -1346,5 +1873,2175 @@ const TableHeader = Node.create({
|
|
|
1346
1873
|
});
|
|
1347
1874
|
|
|
1348
1875
|
//#endregion
|
|
1349
|
-
|
|
1876
|
+
//#region src/extensions/underline.tsx
|
|
1877
|
+
const Underline = EmailMark.from(UnderlineBase, ({ children, style }) => /* @__PURE__ */ jsx("u", {
|
|
1878
|
+
style,
|
|
1879
|
+
children
|
|
1880
|
+
}));
|
|
1881
|
+
|
|
1882
|
+
//#endregion
|
|
1883
|
+
//#region src/extensions/uppercase.tsx
|
|
1884
|
+
const Uppercase = EmailMark.create({
|
|
1885
|
+
name: "uppercase",
|
|
1886
|
+
addOptions() {
|
|
1887
|
+
return { HTMLAttributes: {} };
|
|
1888
|
+
},
|
|
1889
|
+
parseHTML() {
|
|
1890
|
+
return [{
|
|
1891
|
+
tag: "span",
|
|
1892
|
+
getAttrs: (node) => {
|
|
1893
|
+
if (node.style.textTransform === "uppercase") return {};
|
|
1894
|
+
return false;
|
|
1895
|
+
}
|
|
1896
|
+
}];
|
|
1897
|
+
},
|
|
1898
|
+
renderHTML({ HTMLAttributes }) {
|
|
1899
|
+
return [
|
|
1900
|
+
"span",
|
|
1901
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { style: "text-transform: uppercase" }),
|
|
1902
|
+
0
|
|
1903
|
+
];
|
|
1904
|
+
},
|
|
1905
|
+
renderToReactEmail({ children, style }) {
|
|
1906
|
+
return /* @__PURE__ */ jsx("span", {
|
|
1907
|
+
style: {
|
|
1908
|
+
...style,
|
|
1909
|
+
textTransform: "uppercase"
|
|
1910
|
+
},
|
|
1911
|
+
children
|
|
1912
|
+
});
|
|
1913
|
+
},
|
|
1914
|
+
addCommands() {
|
|
1915
|
+
return {
|
|
1916
|
+
setUppercase: () => ({ commands }) => {
|
|
1917
|
+
return commands.setMark(this.name);
|
|
1918
|
+
},
|
|
1919
|
+
toggleUppercase: () => ({ commands }) => {
|
|
1920
|
+
return commands.toggleMark(this.name);
|
|
1921
|
+
},
|
|
1922
|
+
unsetUppercase: () => ({ commands }) => {
|
|
1923
|
+
return commands.unsetMark(this.name);
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
|
|
1929
|
+
//#endregion
|
|
1930
|
+
//#region src/extensions/columns.tsx
|
|
1931
|
+
const COLUMN_PARENT_TYPES = [
|
|
1932
|
+
"twoColumns",
|
|
1933
|
+
"threeColumns",
|
|
1934
|
+
"fourColumns"
|
|
1935
|
+
];
|
|
1936
|
+
const COLUMN_PARENT_SET = new Set(COLUMN_PARENT_TYPES);
|
|
1937
|
+
const MAX_COLUMNS_DEPTH = 3;
|
|
1938
|
+
function getColumnsDepth(doc, from) {
|
|
1939
|
+
const $from = doc.resolve(from);
|
|
1940
|
+
let depth = 0;
|
|
1941
|
+
for (let d = $from.depth; d > 0; d--) if (COLUMN_PARENT_SET.has($from.node(d).type.name)) depth++;
|
|
1942
|
+
return depth;
|
|
1943
|
+
}
|
|
1944
|
+
const VARIANTS = [
|
|
1945
|
+
{
|
|
1946
|
+
name: "twoColumns",
|
|
1947
|
+
columnCount: 2,
|
|
1948
|
+
content: "columnsColumn columnsColumn",
|
|
1949
|
+
dataType: "two-columns"
|
|
1950
|
+
},
|
|
1951
|
+
{
|
|
1952
|
+
name: "threeColumns",
|
|
1953
|
+
columnCount: 3,
|
|
1954
|
+
content: "columnsColumn columnsColumn columnsColumn",
|
|
1955
|
+
dataType: "three-columns"
|
|
1956
|
+
},
|
|
1957
|
+
{
|
|
1958
|
+
name: "fourColumns",
|
|
1959
|
+
columnCount: 4,
|
|
1960
|
+
content: "columnsColumn{4}",
|
|
1961
|
+
dataType: "four-columns"
|
|
1962
|
+
}
|
|
1963
|
+
];
|
|
1964
|
+
const NODE_TYPE_MAP = {
|
|
1965
|
+
2: "twoColumns",
|
|
1966
|
+
3: "threeColumns",
|
|
1967
|
+
4: "fourColumns"
|
|
1968
|
+
};
|
|
1969
|
+
function createColumnsNode(config, includeCommands) {
|
|
1970
|
+
return EmailNode.create({
|
|
1971
|
+
name: config.name,
|
|
1972
|
+
group: "block",
|
|
1973
|
+
content: config.content,
|
|
1974
|
+
isolating: true,
|
|
1975
|
+
defining: true,
|
|
1976
|
+
addAttributes() {
|
|
1977
|
+
return createStandardAttributes([...LAYOUT_ATTRIBUTES, ...COMMON_HTML_ATTRIBUTES]);
|
|
1978
|
+
},
|
|
1979
|
+
parseHTML() {
|
|
1980
|
+
return [{ tag: `div[data-type="${config.dataType}"]` }];
|
|
1981
|
+
},
|
|
1982
|
+
renderHTML({ HTMLAttributes }) {
|
|
1983
|
+
return [
|
|
1984
|
+
"div",
|
|
1985
|
+
mergeAttributes({
|
|
1986
|
+
"data-type": config.dataType,
|
|
1987
|
+
class: "node-columns"
|
|
1988
|
+
}, HTMLAttributes),
|
|
1989
|
+
0
|
|
1990
|
+
];
|
|
1991
|
+
},
|
|
1992
|
+
...includeCommands && { addCommands() {
|
|
1993
|
+
return { insertColumns: (count) => ({ commands, state }) => {
|
|
1994
|
+
if (getColumnsDepth(state.doc, state.selection.from) >= MAX_COLUMNS_DEPTH) return false;
|
|
1995
|
+
const nodeType = NODE_TYPE_MAP[count];
|
|
1996
|
+
const children = Array.from({ length: count }, () => ({
|
|
1997
|
+
type: "columnsColumn",
|
|
1998
|
+
content: [{
|
|
1999
|
+
type: "paragraph",
|
|
2000
|
+
content: []
|
|
2001
|
+
}]
|
|
2002
|
+
}));
|
|
2003
|
+
return commands.insertContent({
|
|
2004
|
+
type: nodeType,
|
|
2005
|
+
content: children
|
|
2006
|
+
});
|
|
2007
|
+
} };
|
|
2008
|
+
} },
|
|
2009
|
+
renderToReactEmail({ children, node, style }) {
|
|
2010
|
+
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
2011
|
+
return /* @__PURE__ */ jsx(Row, {
|
|
2012
|
+
className: node.attrs?.class || void 0,
|
|
2013
|
+
style: {
|
|
2014
|
+
...style,
|
|
2015
|
+
...inlineStyles
|
|
2016
|
+
},
|
|
2017
|
+
children
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
const TwoColumns = createColumnsNode(VARIANTS[0], true);
|
|
2023
|
+
const ThreeColumns = createColumnsNode(VARIANTS[1], false);
|
|
2024
|
+
const FourColumns = createColumnsNode(VARIANTS[2], false);
|
|
2025
|
+
const ColumnsColumn = EmailNode.create({
|
|
2026
|
+
name: "columnsColumn",
|
|
2027
|
+
group: "columnsColumn",
|
|
2028
|
+
content: "block+",
|
|
2029
|
+
isolating: true,
|
|
2030
|
+
addAttributes() {
|
|
2031
|
+
return { ...createStandardAttributes([...LAYOUT_ATTRIBUTES, ...COMMON_HTML_ATTRIBUTES]) };
|
|
2032
|
+
},
|
|
2033
|
+
parseHTML() {
|
|
2034
|
+
return [{ tag: "div[data-type=\"column\"]" }];
|
|
2035
|
+
},
|
|
2036
|
+
renderHTML({ HTMLAttributes }) {
|
|
2037
|
+
return [
|
|
2038
|
+
"div",
|
|
2039
|
+
mergeAttributes({
|
|
2040
|
+
"data-type": "column",
|
|
2041
|
+
class: "node-column"
|
|
2042
|
+
}, HTMLAttributes),
|
|
2043
|
+
0
|
|
2044
|
+
];
|
|
2045
|
+
},
|
|
2046
|
+
addKeyboardShortcuts() {
|
|
2047
|
+
return {
|
|
2048
|
+
Backspace: ({ editor }) => {
|
|
2049
|
+
const { state } = editor;
|
|
2050
|
+
const { selection } = state;
|
|
2051
|
+
const { empty, $from } = selection;
|
|
2052
|
+
if (!empty) return false;
|
|
2053
|
+
for (let depth = $from.depth; depth >= 1; depth--) {
|
|
2054
|
+
if ($from.pos !== $from.start(depth)) break;
|
|
2055
|
+
const indexInParent = $from.index(depth - 1);
|
|
2056
|
+
if (indexInParent === 0) continue;
|
|
2057
|
+
const prevNode = $from.node(depth - 1).child(indexInParent - 1);
|
|
2058
|
+
if (COLUMN_PARENT_SET.has(prevNode.type.name)) {
|
|
2059
|
+
const deleteFrom = $from.before(depth) - prevNode.nodeSize;
|
|
2060
|
+
const deleteTo = $from.before(depth);
|
|
2061
|
+
editor.view.dispatch(state.tr.delete(deleteFrom, deleteTo));
|
|
2062
|
+
return true;
|
|
2063
|
+
}
|
|
2064
|
+
break;
|
|
2065
|
+
}
|
|
2066
|
+
return false;
|
|
2067
|
+
},
|
|
2068
|
+
"Mod-a": ({ editor }) => {
|
|
2069
|
+
const { state } = editor;
|
|
2070
|
+
const { $from } = state.selection;
|
|
2071
|
+
for (let d = $from.depth; d > 0; d--) {
|
|
2072
|
+
if ($from.node(d).type.name !== "columnsColumn") continue;
|
|
2073
|
+
const columnStart = $from.start(d);
|
|
2074
|
+
const columnEnd = $from.end(d);
|
|
2075
|
+
const { from, to } = state.selection;
|
|
2076
|
+
if (from === columnStart && to === columnEnd) return false;
|
|
2077
|
+
editor.view.dispatch(state.tr.setSelection(TextSelection.create(state.doc, columnStart, columnEnd)));
|
|
2078
|
+
return true;
|
|
2079
|
+
}
|
|
2080
|
+
return false;
|
|
2081
|
+
}
|
|
2082
|
+
};
|
|
2083
|
+
},
|
|
2084
|
+
renderToReactEmail({ children, node, style }) {
|
|
2085
|
+
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
2086
|
+
const width = node.attrs?.width;
|
|
2087
|
+
return /* @__PURE__ */ jsx(Column, {
|
|
2088
|
+
className: node.attrs?.class || void 0,
|
|
2089
|
+
style: {
|
|
2090
|
+
...style,
|
|
2091
|
+
...inlineStyles,
|
|
2092
|
+
...width ? { width } : {}
|
|
2093
|
+
},
|
|
2094
|
+
children
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
});
|
|
2098
|
+
|
|
2099
|
+
//#endregion
|
|
2100
|
+
//#region src/extensions/index.ts
|
|
2101
|
+
const starterKitExtensions = {
|
|
2102
|
+
CodeBlockPrism,
|
|
2103
|
+
Code,
|
|
2104
|
+
Paragraph,
|
|
2105
|
+
BulletList,
|
|
2106
|
+
OrderedList,
|
|
2107
|
+
Blockquote,
|
|
2108
|
+
ListItem,
|
|
2109
|
+
HardBreak,
|
|
2110
|
+
Italic,
|
|
2111
|
+
Placeholder,
|
|
2112
|
+
PreviewText,
|
|
2113
|
+
Bold,
|
|
2114
|
+
Strike,
|
|
2115
|
+
Heading,
|
|
2116
|
+
Divider,
|
|
2117
|
+
Link,
|
|
2118
|
+
Sup,
|
|
2119
|
+
Underline,
|
|
2120
|
+
Uppercase,
|
|
2121
|
+
PreservedStyle,
|
|
2122
|
+
Table,
|
|
2123
|
+
TableRow,
|
|
2124
|
+
TableCell,
|
|
2125
|
+
TableHeader,
|
|
2126
|
+
Body,
|
|
2127
|
+
Div,
|
|
2128
|
+
Button,
|
|
2129
|
+
Section,
|
|
2130
|
+
GlobalContent,
|
|
2131
|
+
AlignmentAttribute,
|
|
2132
|
+
StyleAttribute,
|
|
2133
|
+
ClassAttribute,
|
|
2134
|
+
MaxNesting
|
|
2135
|
+
};
|
|
2136
|
+
const StarterKit = Extension.create({
|
|
2137
|
+
name: "reactEmailStarterKit",
|
|
2138
|
+
addOptions() {
|
|
2139
|
+
return {
|
|
2140
|
+
TiptapStarterKit: {},
|
|
2141
|
+
CodeBlockPrism: {
|
|
2142
|
+
defaultLanguage: "javascript",
|
|
2143
|
+
HTMLAttributes: { class: "prism node-codeBlock" }
|
|
2144
|
+
},
|
|
2145
|
+
Code: { HTMLAttributes: {
|
|
2146
|
+
class: "node-inlineCode",
|
|
2147
|
+
spellcheck: "false"
|
|
2148
|
+
} },
|
|
2149
|
+
Paragraph: { HTMLAttributes: { class: "node-paragraph" } },
|
|
2150
|
+
BulletList: { HTMLAttributes: { class: "node-bulletList" } },
|
|
2151
|
+
OrderedList: { HTMLAttributes: { class: "node-orderedList" } },
|
|
2152
|
+
Blockquote: { HTMLAttributes: { class: "node-blockquote" } },
|
|
2153
|
+
ListItem: {},
|
|
2154
|
+
HardBreak: {},
|
|
2155
|
+
Italic: {},
|
|
2156
|
+
Placeholder: {},
|
|
2157
|
+
PreviewText: {},
|
|
2158
|
+
Bold: {},
|
|
2159
|
+
Strike: {},
|
|
2160
|
+
Heading: {},
|
|
2161
|
+
Divider: {},
|
|
2162
|
+
Link: {},
|
|
2163
|
+
Sup: {},
|
|
2164
|
+
Underline: {},
|
|
2165
|
+
Uppercase: {},
|
|
2166
|
+
PreservedStyle: {},
|
|
2167
|
+
Table: {},
|
|
2168
|
+
TableRow: {},
|
|
2169
|
+
TableCell: {},
|
|
2170
|
+
TableHeader: {},
|
|
2171
|
+
Body: {},
|
|
2172
|
+
Div: {},
|
|
2173
|
+
Button: {},
|
|
2174
|
+
Section: {},
|
|
2175
|
+
GlobalContent: {},
|
|
2176
|
+
AlignmentAttribute: { types: [
|
|
2177
|
+
"heading",
|
|
2178
|
+
"paragraph",
|
|
2179
|
+
"image",
|
|
2180
|
+
"blockquote",
|
|
2181
|
+
"codeBlock",
|
|
2182
|
+
"bulletList",
|
|
2183
|
+
"orderedList",
|
|
2184
|
+
"listItem",
|
|
2185
|
+
"button",
|
|
2186
|
+
"youtube",
|
|
2187
|
+
"twitter",
|
|
2188
|
+
"table",
|
|
2189
|
+
"tableRow",
|
|
2190
|
+
"tableCell",
|
|
2191
|
+
"tableHeader",
|
|
2192
|
+
"columnsColumn"
|
|
2193
|
+
] },
|
|
2194
|
+
StyleAttribute: { types: [
|
|
2195
|
+
"heading",
|
|
2196
|
+
"paragraph",
|
|
2197
|
+
"image",
|
|
2198
|
+
"blockquote",
|
|
2199
|
+
"codeBlock",
|
|
2200
|
+
"bulletList",
|
|
2201
|
+
"orderedList",
|
|
2202
|
+
"listItem",
|
|
2203
|
+
"button",
|
|
2204
|
+
"youtube",
|
|
2205
|
+
"twitter",
|
|
2206
|
+
"horizontalRule",
|
|
2207
|
+
"footer",
|
|
2208
|
+
"section",
|
|
2209
|
+
"div",
|
|
2210
|
+
"body",
|
|
2211
|
+
"table",
|
|
2212
|
+
"tableRow",
|
|
2213
|
+
"tableCell",
|
|
2214
|
+
"tableHeader",
|
|
2215
|
+
"columnsColumn",
|
|
2216
|
+
"link"
|
|
2217
|
+
] },
|
|
2218
|
+
ClassAttribute: { types: [
|
|
2219
|
+
"heading",
|
|
2220
|
+
"paragraph",
|
|
2221
|
+
"image",
|
|
2222
|
+
"blockquote",
|
|
2223
|
+
"bulletList",
|
|
2224
|
+
"orderedList",
|
|
2225
|
+
"listItem",
|
|
2226
|
+
"button",
|
|
2227
|
+
"youtube",
|
|
2228
|
+
"twitter",
|
|
2229
|
+
"horizontalRule",
|
|
2230
|
+
"footer",
|
|
2231
|
+
"section",
|
|
2232
|
+
"div",
|
|
2233
|
+
"body",
|
|
2234
|
+
"table",
|
|
2235
|
+
"tableRow",
|
|
2236
|
+
"tableCell",
|
|
2237
|
+
"tableHeader",
|
|
2238
|
+
"columnsColumn",
|
|
2239
|
+
"link"
|
|
2240
|
+
] },
|
|
2241
|
+
MaxNesting: {
|
|
2242
|
+
maxDepth: 50,
|
|
2243
|
+
nodeTypes: [
|
|
2244
|
+
"section",
|
|
2245
|
+
"bulletList",
|
|
2246
|
+
"orderedList"
|
|
2247
|
+
]
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
},
|
|
2251
|
+
addExtensions() {
|
|
2252
|
+
const extensions = [];
|
|
2253
|
+
if (this.options.TiptapStarterKit !== false) extensions.push(TipTapStarterKit.configure({
|
|
2254
|
+
undoRedo: false,
|
|
2255
|
+
heading: false,
|
|
2256
|
+
link: false,
|
|
2257
|
+
underline: false,
|
|
2258
|
+
trailingNode: false,
|
|
2259
|
+
bold: false,
|
|
2260
|
+
italic: false,
|
|
2261
|
+
strike: false,
|
|
2262
|
+
code: false,
|
|
2263
|
+
paragraph: false,
|
|
2264
|
+
bulletList: false,
|
|
2265
|
+
orderedList: false,
|
|
2266
|
+
listItem: false,
|
|
2267
|
+
blockquote: false,
|
|
2268
|
+
hardBreak: false,
|
|
2269
|
+
gapcursor: false,
|
|
2270
|
+
codeBlock: false,
|
|
2271
|
+
horizontalRule: false,
|
|
2272
|
+
dropcursor: {
|
|
2273
|
+
color: "#61a8f8",
|
|
2274
|
+
class: "rounded-full animate-[fade-in_300ms_ease-in-out] !z-40",
|
|
2275
|
+
width: 4
|
|
2276
|
+
},
|
|
2277
|
+
...this.options.TiptapStarterKit
|
|
2278
|
+
}));
|
|
2279
|
+
for (const [name, extension] of Object.entries(starterKitExtensions)) {
|
|
2280
|
+
const key = name;
|
|
2281
|
+
const extensionOptions = this.options[key];
|
|
2282
|
+
if (extensionOptions !== false) extensions.push(extension.configure(extensionOptions));
|
|
2283
|
+
}
|
|
2284
|
+
return extensions;
|
|
2285
|
+
}
|
|
2286
|
+
});
|
|
2287
|
+
|
|
2288
|
+
//#endregion
|
|
2289
|
+
//#region src/core/create-drop-handler.ts
|
|
2290
|
+
function createDropHandler({ onPaste, onUploadImage }) {
|
|
2291
|
+
return (view, event, _slice, moved) => {
|
|
2292
|
+
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
|
|
2293
|
+
event.preventDefault();
|
|
2294
|
+
const file = event.dataTransfer.files[0];
|
|
2295
|
+
if (onPaste?.(file, view)) return true;
|
|
2296
|
+
if (file.type.includes("image/") && onUploadImage) {
|
|
2297
|
+
onUploadImage(file, view, (view.posAtCoords({
|
|
2298
|
+
left: event.clientX,
|
|
2299
|
+
top: event.clientY
|
|
2300
|
+
})?.pos || 0) - 1);
|
|
2301
|
+
return true;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
return false;
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
//#endregion
|
|
2309
|
+
//#region src/utils/paste-sanitizer.ts
|
|
2310
|
+
/**
|
|
2311
|
+
* Sanitizes pasted HTML.
|
|
2312
|
+
* - From editor (has node-* classes): pass through as-is
|
|
2313
|
+
* - From external: strip all styles/classes, keep only semantic HTML
|
|
2314
|
+
*/
|
|
2315
|
+
/**
|
|
2316
|
+
* Detects content from the Resend editor by checking for node-* class names.
|
|
2317
|
+
*/
|
|
2318
|
+
const EDITOR_CLASS_PATTERN = /class="[^"]*node-/;
|
|
2319
|
+
/**
|
|
2320
|
+
* Attributes to preserve on specific elements for EXTERNAL content.
|
|
2321
|
+
* Only functional attributes - NO style or class.
|
|
2322
|
+
*/
|
|
2323
|
+
const PRESERVED_ATTRIBUTES = {
|
|
2324
|
+
a: [
|
|
2325
|
+
"href",
|
|
2326
|
+
"target",
|
|
2327
|
+
"rel"
|
|
2328
|
+
],
|
|
2329
|
+
img: [
|
|
2330
|
+
"src",
|
|
2331
|
+
"alt",
|
|
2332
|
+
"width",
|
|
2333
|
+
"height"
|
|
2334
|
+
],
|
|
2335
|
+
td: ["colspan", "rowspan"],
|
|
2336
|
+
th: [
|
|
2337
|
+
"colspan",
|
|
2338
|
+
"rowspan",
|
|
2339
|
+
"scope"
|
|
2340
|
+
],
|
|
2341
|
+
table: [
|
|
2342
|
+
"border",
|
|
2343
|
+
"cellpadding",
|
|
2344
|
+
"cellspacing"
|
|
2345
|
+
],
|
|
2346
|
+
"*": ["id"]
|
|
2347
|
+
};
|
|
2348
|
+
function isFromEditor(html) {
|
|
2349
|
+
return EDITOR_CLASS_PATTERN.test(html);
|
|
2350
|
+
}
|
|
2351
|
+
function sanitizePastedHtml(html) {
|
|
2352
|
+
if (isFromEditor(html)) return html;
|
|
2353
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
2354
|
+
sanitizeNode(doc.body);
|
|
2355
|
+
return doc.body.innerHTML;
|
|
2356
|
+
}
|
|
2357
|
+
function sanitizeNode(node) {
|
|
2358
|
+
if (node.nodeType === Node.ELEMENT_NODE) sanitizeElement(node);
|
|
2359
|
+
for (const child of Array.from(node.childNodes)) sanitizeNode(child);
|
|
2360
|
+
}
|
|
2361
|
+
function sanitizeElement(el) {
|
|
2362
|
+
const allowedForTag = PRESERVED_ATTRIBUTES[el.tagName.toLowerCase()] || [];
|
|
2363
|
+
const allowedGlobal = PRESERVED_ATTRIBUTES["*"] || [];
|
|
2364
|
+
const allowed = new Set([...allowedForTag, ...allowedGlobal]);
|
|
2365
|
+
const attributesToRemove = [];
|
|
2366
|
+
for (const attr of Array.from(el.attributes)) {
|
|
2367
|
+
if (attr.name.startsWith("data-")) {
|
|
2368
|
+
attributesToRemove.push(attr.name);
|
|
2369
|
+
continue;
|
|
2370
|
+
}
|
|
2371
|
+
if (!allowed.has(attr.name)) attributesToRemove.push(attr.name);
|
|
2372
|
+
}
|
|
2373
|
+
for (const attr of attributesToRemove) el.removeAttribute(attr);
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
//#endregion
|
|
2377
|
+
//#region src/core/create-paste-handler.ts
|
|
2378
|
+
function createPasteHandler({ onPaste, onUploadImage, extensions }) {
|
|
2379
|
+
return (view, event, slice) => {
|
|
2380
|
+
const text = event.clipboardData?.getData("text/plain");
|
|
2381
|
+
if (text && onPaste?.(text, view)) {
|
|
2382
|
+
event.preventDefault();
|
|
2383
|
+
return true;
|
|
2384
|
+
}
|
|
2385
|
+
if (event.clipboardData?.files?.[0]) {
|
|
2386
|
+
const file = event.clipboardData.files[0];
|
|
2387
|
+
if (onPaste?.(file, view)) {
|
|
2388
|
+
event.preventDefault();
|
|
2389
|
+
return true;
|
|
2390
|
+
}
|
|
2391
|
+
if (file.type.includes("image/") && onUploadImage) {
|
|
2392
|
+
const pos = view.state.selection.from;
|
|
2393
|
+
onUploadImage(file, view, pos);
|
|
2394
|
+
return true;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
/**
|
|
2398
|
+
* If the coming content has a single child, we can assume
|
|
2399
|
+
* it's a plain text and doesn't need to be parsed and
|
|
2400
|
+
* be introduced in a new line
|
|
2401
|
+
*/
|
|
2402
|
+
if (slice.content.childCount === 1) return false;
|
|
2403
|
+
if (event.clipboardData?.getData?.("text/html")) {
|
|
2404
|
+
event.preventDefault();
|
|
2405
|
+
const jsonContent = generateJSON(sanitizePastedHtml(event.clipboardData.getData("text/html")), extensions);
|
|
2406
|
+
const node = view.state.schema.nodeFromJSON(jsonContent);
|
|
2407
|
+
const transaction = view.state.tr.replaceSelectionWith(node, false);
|
|
2408
|
+
view.dispatch(transaction);
|
|
2409
|
+
return true;
|
|
2410
|
+
}
|
|
2411
|
+
return false;
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
//#endregion
|
|
2416
|
+
//#region src/core/use-editor.ts
|
|
2417
|
+
const COLLABORATION_EXTENSION_NAMES = new Set(["liveblocksExtension", "collaboration"]);
|
|
2418
|
+
function hasCollaborationExtension(exts) {
|
|
2419
|
+
return exts.some((ext) => COLLABORATION_EXTENSION_NAMES.has(ext.name));
|
|
2420
|
+
}
|
|
2421
|
+
function useEditor({ content, extensions = [], onUpdate, onPaste, onUploadImage, onReady, editable = true, ...rest }) {
|
|
2422
|
+
const [contentError, setContentError] = React.useState(null);
|
|
2423
|
+
const isCollaborative = hasCollaborationExtension(extensions);
|
|
2424
|
+
const effectiveExtensions = React.useMemo(() => [
|
|
2425
|
+
StarterKit,
|
|
2426
|
+
...isCollaborative ? [] : [UndoRedo],
|
|
2427
|
+
...extensions
|
|
2428
|
+
], [extensions, isCollaborative]);
|
|
2429
|
+
const editor = useEditor$1({
|
|
2430
|
+
content: isCollaborative ? void 0 : content,
|
|
2431
|
+
extensions: effectiveExtensions,
|
|
2432
|
+
editable,
|
|
2433
|
+
immediatelyRender: false,
|
|
2434
|
+
enableContentCheck: true,
|
|
2435
|
+
onContentError({ editor: editor$1, error, disableCollaboration }) {
|
|
2436
|
+
disableCollaboration();
|
|
2437
|
+
setContentError(error);
|
|
2438
|
+
console.error(error);
|
|
2439
|
+
editor$1.setEditable(false);
|
|
2440
|
+
},
|
|
2441
|
+
onCreate({ editor: editor$1 }) {
|
|
2442
|
+
onReady?.(editor$1);
|
|
2443
|
+
},
|
|
2444
|
+
onUpdate({ editor: editor$1, transaction }) {
|
|
2445
|
+
onUpdate?.(editor$1, transaction);
|
|
2446
|
+
},
|
|
2447
|
+
editorProps: {
|
|
2448
|
+
handleDOMEvents: { click: (view, event) => {
|
|
2449
|
+
if (!view.editable) {
|
|
2450
|
+
if (event.target.closest("a")) {
|
|
2451
|
+
event.preventDefault();
|
|
2452
|
+
return true;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
return false;
|
|
2456
|
+
} },
|
|
2457
|
+
handlePaste: createPasteHandler({
|
|
2458
|
+
onPaste,
|
|
2459
|
+
onUploadImage,
|
|
2460
|
+
extensions: effectiveExtensions
|
|
2461
|
+
}),
|
|
2462
|
+
handleDrop: createDropHandler({
|
|
2463
|
+
onPaste,
|
|
2464
|
+
onUploadImage
|
|
2465
|
+
})
|
|
2466
|
+
},
|
|
2467
|
+
...rest
|
|
2468
|
+
});
|
|
2469
|
+
return {
|
|
2470
|
+
editor,
|
|
2471
|
+
isEditorEmpty: useEditorState({
|
|
2472
|
+
editor,
|
|
2473
|
+
selector: (context) => {
|
|
2474
|
+
if (!context.editor) return true;
|
|
2475
|
+
return isDocumentVisuallyEmpty(context.editor.state.doc);
|
|
2476
|
+
}
|
|
2477
|
+
}) ?? true,
|
|
2478
|
+
extensions: effectiveExtensions,
|
|
2479
|
+
contentError,
|
|
2480
|
+
isCollaborative
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
//#endregion
|
|
2485
|
+
//#region src/utils/set-text-alignment.ts
|
|
2486
|
+
function setTextAlignment(editor, alignment) {
|
|
2487
|
+
const { from, to } = editor.state.selection;
|
|
2488
|
+
const tr = editor.state.tr;
|
|
2489
|
+
editor.state.doc.nodesBetween(from, to, (node, pos) => {
|
|
2490
|
+
if (node.isTextblock) {
|
|
2491
|
+
const prop = "align" in node.attrs ? "align" : "alignment";
|
|
2492
|
+
tr.setNodeMarkup(pos, null, {
|
|
2493
|
+
...node.attrs,
|
|
2494
|
+
[prop]: alignment
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
});
|
|
2498
|
+
editor.view.dispatch(tr);
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
//#endregion
|
|
2502
|
+
//#region src/ui/bubble-menu/context.tsx
|
|
2503
|
+
const BubbleMenuContext = React.createContext(null);
|
|
2504
|
+
function useBubbleMenuContext() {
|
|
2505
|
+
const context = React.useContext(BubbleMenuContext);
|
|
2506
|
+
if (!context) throw new Error("BubbleMenu compound components must be used within <BubbleMenu.Root>");
|
|
2507
|
+
return context;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
//#endregion
|
|
2511
|
+
//#region src/ui/bubble-menu/item.tsx
|
|
2512
|
+
function BubbleMenuItem({ name, isActive, onCommand, className, children, ...rest }) {
|
|
2513
|
+
return /* @__PURE__ */ jsx("button", {
|
|
2514
|
+
type: "button",
|
|
2515
|
+
"aria-label": name,
|
|
2516
|
+
"aria-pressed": isActive,
|
|
2517
|
+
className,
|
|
2518
|
+
"data-re-bubble-menu-item": "",
|
|
2519
|
+
"data-item": name,
|
|
2520
|
+
...isActive ? { "data-active": "" } : {},
|
|
2521
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
2522
|
+
onClick: onCommand,
|
|
2523
|
+
...rest,
|
|
2524
|
+
children
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
//#endregion
|
|
2529
|
+
//#region src/ui/bubble-menu/align-center.tsx
|
|
2530
|
+
function BubbleMenuAlignCenter({ className, children }) {
|
|
2531
|
+
const { editor } = useBubbleMenuContext();
|
|
2532
|
+
return /* @__PURE__ */ jsx(BubbleMenuItem, {
|
|
2533
|
+
name: "align-center",
|
|
2534
|
+
isActive: useEditorState({
|
|
2535
|
+
editor,
|
|
2536
|
+
selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "center" }) ?? false
|
|
2537
|
+
}),
|
|
2538
|
+
onCommand: () => setTextAlignment(editor, "center"),
|
|
2539
|
+
className,
|
|
2540
|
+
children: children ?? /* @__PURE__ */ jsx(AlignCenterIcon, {})
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
//#endregion
|
|
2545
|
+
//#region src/ui/bubble-menu/align-left.tsx
|
|
2546
|
+
function BubbleMenuAlignLeft({ className, children }) {
|
|
2547
|
+
const { editor } = useBubbleMenuContext();
|
|
2548
|
+
return /* @__PURE__ */ jsx(BubbleMenuItem, {
|
|
2549
|
+
name: "align-left",
|
|
2550
|
+
isActive: useEditorState({
|
|
2551
|
+
editor,
|
|
2552
|
+
selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "left" }) ?? false
|
|
2553
|
+
}),
|
|
2554
|
+
onCommand: () => setTextAlignment(editor, "left"),
|
|
2555
|
+
className,
|
|
2556
|
+
children: children ?? /* @__PURE__ */ jsx(AlignLeftIcon, {})
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
//#endregion
|
|
2561
|
+
//#region src/ui/bubble-menu/align-right.tsx
|
|
2562
|
+
function BubbleMenuAlignRight({ className, children }) {
|
|
2563
|
+
const { editor } = useBubbleMenuContext();
|
|
2564
|
+
return /* @__PURE__ */ jsx(BubbleMenuItem, {
|
|
2565
|
+
name: "align-right",
|
|
2566
|
+
isActive: useEditorState({
|
|
2567
|
+
editor,
|
|
2568
|
+
selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "right" }) ?? false
|
|
2569
|
+
}),
|
|
2570
|
+
onCommand: () => setTextAlignment(editor, "right"),
|
|
2571
|
+
className,
|
|
2572
|
+
children: children ?? /* @__PURE__ */ jsx(AlignRightIcon, {})
|
|
2573
|
+
});
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
//#endregion
|
|
2577
|
+
//#region src/ui/bubble-menu/create-mark-bubble-item.tsx
|
|
2578
|
+
function createMarkBubbleItem(config) {
|
|
2579
|
+
function MarkBubbleItem({ className, children }) {
|
|
2580
|
+
const { editor } = useBubbleMenuContext();
|
|
2581
|
+
const isActive = useEditorState({
|
|
2582
|
+
editor,
|
|
2583
|
+
selector: ({ editor: editor$1 }) => {
|
|
2584
|
+
if (config.activeParams) return editor$1?.isActive(config.activeName, config.activeParams) ?? false;
|
|
2585
|
+
return editor$1?.isActive(config.activeName) ?? false;
|
|
2586
|
+
}
|
|
2587
|
+
});
|
|
2588
|
+
const handleCommand = () => {
|
|
2589
|
+
const chain = editor.chain().focus();
|
|
2590
|
+
const method = chain[config.command];
|
|
2591
|
+
if (method) method.call(chain).run();
|
|
2592
|
+
};
|
|
2593
|
+
return /* @__PURE__ */ jsx(BubbleMenuItem, {
|
|
2594
|
+
name: config.name,
|
|
2595
|
+
isActive,
|
|
2596
|
+
onCommand: handleCommand,
|
|
2597
|
+
className,
|
|
2598
|
+
children: children ?? config.icon
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
MarkBubbleItem.displayName = `BubbleMenu${config.name.charAt(0).toUpperCase() + config.name.slice(1)}`;
|
|
2602
|
+
return MarkBubbleItem;
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
//#endregion
|
|
2606
|
+
//#region src/ui/bubble-menu/bold.tsx
|
|
2607
|
+
const BubbleMenuBold = createMarkBubbleItem({
|
|
2608
|
+
name: "bold",
|
|
2609
|
+
activeName: "bold",
|
|
2610
|
+
command: "toggleBold",
|
|
2611
|
+
icon: /* @__PURE__ */ jsx(BoldIcon, {})
|
|
2612
|
+
});
|
|
2613
|
+
|
|
2614
|
+
//#endregion
|
|
2615
|
+
//#region src/ui/bubble-menu/code.tsx
|
|
2616
|
+
const BubbleMenuCode = createMarkBubbleItem({
|
|
2617
|
+
name: "code",
|
|
2618
|
+
activeName: "code",
|
|
2619
|
+
command: "toggleCode",
|
|
2620
|
+
icon: /* @__PURE__ */ jsx(CodeIcon, {})
|
|
2621
|
+
});
|
|
2622
|
+
|
|
2623
|
+
//#endregion
|
|
2624
|
+
//#region src/ui/bubble-menu/group.tsx
|
|
2625
|
+
function BubbleMenuItemGroup({ className, children }) {
|
|
2626
|
+
return /* @__PURE__ */ jsx("fieldset", {
|
|
2627
|
+
className,
|
|
2628
|
+
"data-re-bubble-menu-group": "",
|
|
2629
|
+
children
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
//#endregion
|
|
2634
|
+
//#region src/ui/bubble-menu/italic.tsx
|
|
2635
|
+
const BubbleMenuItalic = createMarkBubbleItem({
|
|
2636
|
+
name: "italic",
|
|
2637
|
+
activeName: "italic",
|
|
2638
|
+
command: "toggleItalic",
|
|
2639
|
+
icon: /* @__PURE__ */ jsx(ItalicIcon, {})
|
|
2640
|
+
});
|
|
2641
|
+
|
|
2642
|
+
//#endregion
|
|
2643
|
+
//#region src/ui/bubble-menu/utils.ts
|
|
2644
|
+
const SAFE_PROTOCOLS = new Set([
|
|
2645
|
+
"http:",
|
|
2646
|
+
"https:",
|
|
2647
|
+
"mailto:",
|
|
2648
|
+
"tel:"
|
|
2649
|
+
]);
|
|
2650
|
+
/**
|
|
2651
|
+
* Basic URL validation and auto-prefixing.
|
|
2652
|
+
* Rejects dangerous schemes (javascript:, data:, vbscript:, etc.).
|
|
2653
|
+
* Returns the valid URL string or null.
|
|
2654
|
+
*/
|
|
2655
|
+
function getUrlFromString(str) {
|
|
2656
|
+
if (str === "#") return str;
|
|
2657
|
+
try {
|
|
2658
|
+
const url = new URL(str);
|
|
2659
|
+
if (SAFE_PROTOCOLS.has(url.protocol)) return str;
|
|
2660
|
+
return null;
|
|
2661
|
+
} catch {}
|
|
2662
|
+
try {
|
|
2663
|
+
if (str.includes(".") && !str.includes(" ")) return new URL(`https://${str}`).toString();
|
|
2664
|
+
} catch {}
|
|
2665
|
+
return null;
|
|
2666
|
+
}
|
|
2667
|
+
function setLinkHref(editor, href) {
|
|
2668
|
+
if (href.length === 0) {
|
|
2669
|
+
editor.chain().unsetLink().run();
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
const { from, to } = editor.state.selection;
|
|
2673
|
+
if (from === to) {
|
|
2674
|
+
editor.chain().extendMarkRange("link").setLink({ href }).setTextSelection({
|
|
2675
|
+
from,
|
|
2676
|
+
to
|
|
2677
|
+
}).run();
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
editor.chain().setLink({ href }).run();
|
|
2681
|
+
}
|
|
2682
|
+
function focusEditor(editor) {
|
|
2683
|
+
setTimeout(() => {
|
|
2684
|
+
editor.commands.focus();
|
|
2685
|
+
}, 0);
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
//#endregion
|
|
2689
|
+
//#region src/ui/bubble-menu/link-selector.tsx
|
|
2690
|
+
function BubbleMenuLinkSelector({ className, showToggle = true, validateUrl, onLinkApply, onLinkRemove, children, open: controlledOpen, onOpenChange }) {
|
|
2691
|
+
const { editor } = useBubbleMenuContext();
|
|
2692
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
|
|
2693
|
+
const isControlled = controlledOpen !== void 0;
|
|
2694
|
+
const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
|
|
2695
|
+
const setIsOpen = React.useCallback((value) => {
|
|
2696
|
+
if (!isControlled) setUncontrolledOpen(value);
|
|
2697
|
+
onOpenChange?.(value);
|
|
2698
|
+
}, [isControlled, onOpenChange]);
|
|
2699
|
+
const editorState = useEditorState({
|
|
2700
|
+
editor,
|
|
2701
|
+
selector: ({ editor: editor$1 }) => ({
|
|
2702
|
+
isLinkActive: editor$1?.isActive("link") ?? false,
|
|
2703
|
+
hasLink: Boolean(editor$1?.getAttributes("link").href),
|
|
2704
|
+
currentHref: editor$1?.getAttributes("link").href || ""
|
|
2705
|
+
})
|
|
2706
|
+
});
|
|
2707
|
+
const setIsOpenRef = React.useRef(setIsOpen);
|
|
2708
|
+
setIsOpenRef.current = setIsOpen;
|
|
2709
|
+
React.useEffect(() => {
|
|
2710
|
+
const subscription = editorEventBus.on("bubble-menu:add-link", () => {
|
|
2711
|
+
setIsOpenRef.current(true);
|
|
2712
|
+
});
|
|
2713
|
+
return () => {
|
|
2714
|
+
setIsOpenRef.current(false);
|
|
2715
|
+
subscription.unsubscribe();
|
|
2716
|
+
};
|
|
2717
|
+
}, []);
|
|
2718
|
+
if (!editorState) return null;
|
|
2719
|
+
const handleOpenLink = () => {
|
|
2720
|
+
setIsOpen(!isOpen);
|
|
2721
|
+
};
|
|
2722
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2723
|
+
"data-re-link-selector": "",
|
|
2724
|
+
...isOpen ? { "data-open": "" } : {},
|
|
2725
|
+
...editorState.hasLink ? { "data-has-link": "" } : {},
|
|
2726
|
+
className,
|
|
2727
|
+
children: [showToggle && /* @__PURE__ */ jsx("button", {
|
|
2728
|
+
type: "button",
|
|
2729
|
+
"aria-expanded": isOpen,
|
|
2730
|
+
"aria-haspopup": "true",
|
|
2731
|
+
"aria-label": "Add link",
|
|
2732
|
+
"aria-pressed": editorState.isLinkActive && editorState.hasLink,
|
|
2733
|
+
"data-re-link-selector-trigger": "",
|
|
2734
|
+
onClick: handleOpenLink,
|
|
2735
|
+
children: /* @__PURE__ */ jsx(LinkIcon, {})
|
|
2736
|
+
}), isOpen && /* @__PURE__ */ jsx(LinkForm, {
|
|
2737
|
+
editor,
|
|
2738
|
+
currentHref: editorState.currentHref,
|
|
2739
|
+
validateUrl,
|
|
2740
|
+
onLinkApply,
|
|
2741
|
+
onLinkRemove,
|
|
2742
|
+
setIsOpen,
|
|
2743
|
+
children
|
|
2744
|
+
})]
|
|
2745
|
+
});
|
|
2746
|
+
}
|
|
2747
|
+
function LinkForm({ editor, currentHref, validateUrl, onLinkApply, onLinkRemove, setIsOpen, children }) {
|
|
2748
|
+
const inputRef = React.useRef(null);
|
|
2749
|
+
const formRef = React.useRef(null);
|
|
2750
|
+
const displayHref = currentHref === "#" ? "" : currentHref;
|
|
2751
|
+
const [inputValue, setInputValue] = React.useState(displayHref);
|
|
2752
|
+
React.useEffect(() => {
|
|
2753
|
+
const timeoutId = setTimeout(() => {
|
|
2754
|
+
inputRef.current?.focus();
|
|
2755
|
+
}, 0);
|
|
2756
|
+
return () => clearTimeout(timeoutId);
|
|
2757
|
+
}, []);
|
|
2758
|
+
React.useEffect(() => {
|
|
2759
|
+
const handleKeyDown = (event) => {
|
|
2760
|
+
if (event.key === "Escape") {
|
|
2761
|
+
if (editor.getAttributes("link").href === "#") editor.chain().unsetLink().run();
|
|
2762
|
+
setIsOpen(false);
|
|
2763
|
+
}
|
|
2764
|
+
};
|
|
2765
|
+
const handleClickOutside = (event) => {
|
|
2766
|
+
if (formRef.current && !formRef.current.contains(event.target)) {
|
|
2767
|
+
const form = formRef.current;
|
|
2768
|
+
const submitEvent = new Event("submit", {
|
|
2769
|
+
bubbles: true,
|
|
2770
|
+
cancelable: true
|
|
2771
|
+
});
|
|
2772
|
+
form.dispatchEvent(submitEvent);
|
|
2773
|
+
setIsOpen(false);
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
2777
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
2778
|
+
return () => {
|
|
2779
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
2780
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
2781
|
+
};
|
|
2782
|
+
}, [editor, setIsOpen]);
|
|
2783
|
+
function handleSubmit(e) {
|
|
2784
|
+
e.preventDefault();
|
|
2785
|
+
const value = inputValue.trim();
|
|
2786
|
+
if (value === "") {
|
|
2787
|
+
setLinkHref(editor, "");
|
|
2788
|
+
setIsOpen(false);
|
|
2789
|
+
focusEditor(editor);
|
|
2790
|
+
onLinkRemove?.();
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
const finalValue = (validateUrl ?? getUrlFromString)(value);
|
|
2794
|
+
if (!finalValue) {
|
|
2795
|
+
setLinkHref(editor, "");
|
|
2796
|
+
setIsOpen(false);
|
|
2797
|
+
focusEditor(editor);
|
|
2798
|
+
onLinkRemove?.();
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
setLinkHref(editor, finalValue);
|
|
2802
|
+
setIsOpen(false);
|
|
2803
|
+
focusEditor(editor);
|
|
2804
|
+
onLinkApply?.(finalValue);
|
|
2805
|
+
}
|
|
2806
|
+
function handleUnlink(e) {
|
|
2807
|
+
e.stopPropagation();
|
|
2808
|
+
setLinkHref(editor, "");
|
|
2809
|
+
setIsOpen(false);
|
|
2810
|
+
focusEditor(editor);
|
|
2811
|
+
onLinkRemove?.();
|
|
2812
|
+
}
|
|
2813
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
2814
|
+
ref: formRef,
|
|
2815
|
+
"data-re-link-selector-form": "",
|
|
2816
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
2817
|
+
onClick: (e) => e.stopPropagation(),
|
|
2818
|
+
onKeyDown: (e) => e.stopPropagation(),
|
|
2819
|
+
onSubmit: handleSubmit,
|
|
2820
|
+
children: [
|
|
2821
|
+
/* @__PURE__ */ jsx("input", {
|
|
2822
|
+
ref: inputRef,
|
|
2823
|
+
"data-re-link-selector-input": "",
|
|
2824
|
+
value: inputValue,
|
|
2825
|
+
onFocus: (e) => e.stopPropagation(),
|
|
2826
|
+
onChange: (e) => setInputValue(e.target.value),
|
|
2827
|
+
placeholder: "Paste a link",
|
|
2828
|
+
type: "text"
|
|
2829
|
+
}),
|
|
2830
|
+
children,
|
|
2831
|
+
displayHref ? /* @__PURE__ */ jsx("button", {
|
|
2832
|
+
type: "button",
|
|
2833
|
+
"aria-label": "Remove link",
|
|
2834
|
+
"data-re-link-selector-unlink": "",
|
|
2835
|
+
onClick: handleUnlink,
|
|
2836
|
+
children: /* @__PURE__ */ jsx(UnlinkIcon, {})
|
|
2837
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
2838
|
+
type: "submit",
|
|
2839
|
+
"aria-label": "Apply link",
|
|
2840
|
+
"data-re-link-selector-apply": "",
|
|
2841
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
2842
|
+
children: /* @__PURE__ */ jsx(Check, {})
|
|
2843
|
+
})
|
|
2844
|
+
]
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
//#endregion
|
|
2849
|
+
//#region src/ui/bubble-menu/node-selector.tsx
|
|
2850
|
+
const NodeSelectorContext = React.createContext(null);
|
|
2851
|
+
function useNodeSelectorContext() {
|
|
2852
|
+
const context = React.useContext(NodeSelectorContext);
|
|
2853
|
+
if (!context) throw new Error("NodeSelector compound components must be used within <NodeSelector.Root>");
|
|
2854
|
+
return context;
|
|
2855
|
+
}
|
|
2856
|
+
function NodeSelectorRoot({ omit = [], open: controlledOpen, onOpenChange, className, children }) {
|
|
2857
|
+
const { editor } = useBubbleMenuContext();
|
|
2858
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
|
|
2859
|
+
const isControlled = controlledOpen !== void 0;
|
|
2860
|
+
const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
|
|
2861
|
+
const setIsOpen = React.useCallback((value) => {
|
|
2862
|
+
if (!isControlled) setUncontrolledOpen(value);
|
|
2863
|
+
onOpenChange?.(value);
|
|
2864
|
+
}, [isControlled, onOpenChange]);
|
|
2865
|
+
const editorState = useEditorState({
|
|
2866
|
+
editor,
|
|
2867
|
+
selector: ({ editor: editor$1 }) => ({
|
|
2868
|
+
isParagraphActive: (editor$1?.isActive("paragraph") ?? false) && !editor$1?.isActive("bulletList") && !editor$1?.isActive("orderedList"),
|
|
2869
|
+
isHeading1Active: editor$1?.isActive("heading", { level: 1 }) ?? false,
|
|
2870
|
+
isHeading2Active: editor$1?.isActive("heading", { level: 2 }) ?? false,
|
|
2871
|
+
isHeading3Active: editor$1?.isActive("heading", { level: 3 }) ?? false,
|
|
2872
|
+
isBulletListActive: editor$1?.isActive("bulletList") ?? false,
|
|
2873
|
+
isOrderedListActive: editor$1?.isActive("orderedList") ?? false,
|
|
2874
|
+
isBlockquoteActive: editor$1?.isActive("blockquote") ?? false,
|
|
2875
|
+
isCodeBlockActive: editor$1?.isActive("codeBlock") ?? false
|
|
2876
|
+
})
|
|
2877
|
+
});
|
|
2878
|
+
const allItems = React.useMemo(() => [
|
|
2879
|
+
{
|
|
2880
|
+
name: "Text",
|
|
2881
|
+
icon: TextIcon,
|
|
2882
|
+
command: () => editor.chain().focus().clearNodes().toggleNode("paragraph", "paragraph").run(),
|
|
2883
|
+
isActive: editorState?.isParagraphActive ?? false
|
|
2884
|
+
},
|
|
2885
|
+
{
|
|
2886
|
+
name: "Title",
|
|
2887
|
+
icon: Heading1,
|
|
2888
|
+
command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 1 }).run(),
|
|
2889
|
+
isActive: editorState?.isHeading1Active ?? false
|
|
2890
|
+
},
|
|
2891
|
+
{
|
|
2892
|
+
name: "Subtitle",
|
|
2893
|
+
icon: Heading2,
|
|
2894
|
+
command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 2 }).run(),
|
|
2895
|
+
isActive: editorState?.isHeading2Active ?? false
|
|
2896
|
+
},
|
|
2897
|
+
{
|
|
2898
|
+
name: "Heading",
|
|
2899
|
+
icon: Heading3,
|
|
2900
|
+
command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 3 }).run(),
|
|
2901
|
+
isActive: editorState?.isHeading3Active ?? false
|
|
2902
|
+
},
|
|
2903
|
+
{
|
|
2904
|
+
name: "Bullet List",
|
|
2905
|
+
icon: List,
|
|
2906
|
+
command: () => editor.chain().focus().clearNodes().toggleBulletList().run(),
|
|
2907
|
+
isActive: editorState?.isBulletListActive ?? false
|
|
2908
|
+
},
|
|
2909
|
+
{
|
|
2910
|
+
name: "Numbered List",
|
|
2911
|
+
icon: ListOrdered,
|
|
2912
|
+
command: () => editor.chain().focus().clearNodes().toggleOrderedList().run(),
|
|
2913
|
+
isActive: editorState?.isOrderedListActive ?? false
|
|
2914
|
+
},
|
|
2915
|
+
{
|
|
2916
|
+
name: "Quote",
|
|
2917
|
+
icon: TextQuote,
|
|
2918
|
+
command: () => editor.chain().focus().clearNodes().toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
|
|
2919
|
+
isActive: editorState?.isBlockquoteActive ?? false
|
|
2920
|
+
},
|
|
2921
|
+
{
|
|
2922
|
+
name: "Code",
|
|
2923
|
+
icon: Code$1,
|
|
2924
|
+
command: () => editor.chain().focus().clearNodes().toggleCodeBlock().run(),
|
|
2925
|
+
isActive: editorState?.isCodeBlockActive ?? false
|
|
2926
|
+
}
|
|
2927
|
+
], [editor, editorState]);
|
|
2928
|
+
const items = React.useMemo(() => allItems.filter((item) => !omit.includes(item.name)), [allItems, omit]);
|
|
2929
|
+
const activeItem = React.useMemo(() => items.find((item) => item.isActive) ?? { name: "Multiple" }, [items]);
|
|
2930
|
+
const contextValue = React.useMemo(() => ({
|
|
2931
|
+
items,
|
|
2932
|
+
activeItem,
|
|
2933
|
+
isOpen,
|
|
2934
|
+
setIsOpen
|
|
2935
|
+
}), [
|
|
2936
|
+
items,
|
|
2937
|
+
activeItem,
|
|
2938
|
+
isOpen,
|
|
2939
|
+
setIsOpen
|
|
2940
|
+
]);
|
|
2941
|
+
if (!editorState || items.length === 0) return null;
|
|
2942
|
+
return /* @__PURE__ */ jsx(NodeSelectorContext.Provider, {
|
|
2943
|
+
value: contextValue,
|
|
2944
|
+
children: /* @__PURE__ */ jsx(Popover.Root, {
|
|
2945
|
+
open: isOpen,
|
|
2946
|
+
onOpenChange: setIsOpen,
|
|
2947
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
2948
|
+
"data-re-node-selector": "",
|
|
2949
|
+
...isOpen ? { "data-open": "" } : {},
|
|
2950
|
+
className,
|
|
2951
|
+
children
|
|
2952
|
+
})
|
|
2953
|
+
})
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
function NodeSelectorTrigger({ className, children }) {
|
|
2957
|
+
const { activeItem, isOpen, setIsOpen } = useNodeSelectorContext();
|
|
2958
|
+
return /* @__PURE__ */ jsx(Popover.Trigger, {
|
|
2959
|
+
"data-re-node-selector-trigger": "",
|
|
2960
|
+
className,
|
|
2961
|
+
onClick: () => setIsOpen(!isOpen),
|
|
2962
|
+
children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", { children: activeItem.name }), /* @__PURE__ */ jsx(ChevronDown, {})] })
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
function NodeSelectorContent({ className, align = "start", children }) {
|
|
2966
|
+
const { items, setIsOpen } = useNodeSelectorContext();
|
|
2967
|
+
return /* @__PURE__ */ jsx(Popover.Content, {
|
|
2968
|
+
align,
|
|
2969
|
+
"data-re-node-selector-content": "",
|
|
2970
|
+
className,
|
|
2971
|
+
children: children ? children(items, () => setIsOpen(false)) : items.map((item) => {
|
|
2972
|
+
const Icon = item.icon;
|
|
2973
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
2974
|
+
type: "button",
|
|
2975
|
+
"data-re-node-selector-item": "",
|
|
2976
|
+
...item.isActive ? { "data-active": "" } : {},
|
|
2977
|
+
onClick: () => {
|
|
2978
|
+
item.command();
|
|
2979
|
+
setIsOpen(false);
|
|
2980
|
+
},
|
|
2981
|
+
children: [
|
|
2982
|
+
/* @__PURE__ */ jsx(Icon, {}),
|
|
2983
|
+
/* @__PURE__ */ jsx("span", { children: item.name }),
|
|
2984
|
+
item.isActive && /* @__PURE__ */ jsx(Check, {})
|
|
2985
|
+
]
|
|
2986
|
+
}, item.name);
|
|
2987
|
+
})
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2990
|
+
function BubbleMenuNodeSelector({ omit = [], className, triggerContent, open, onOpenChange }) {
|
|
2991
|
+
return /* @__PURE__ */ jsxs(NodeSelectorRoot, {
|
|
2992
|
+
omit,
|
|
2993
|
+
open,
|
|
2994
|
+
onOpenChange,
|
|
2995
|
+
className,
|
|
2996
|
+
children: [/* @__PURE__ */ jsx(NodeSelectorTrigger, { children: triggerContent }), /* @__PURE__ */ jsx(NodeSelectorContent, {})]
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
//#endregion
|
|
3001
|
+
//#region src/ui/bubble-menu/root.tsx
|
|
3002
|
+
function BubbleMenuRoot({ excludeNodes = [], placement = "bottom", offset = 8, onHide, className, children }) {
|
|
3003
|
+
const { editor } = useCurrentEditor();
|
|
3004
|
+
if (!editor) return null;
|
|
3005
|
+
return /* @__PURE__ */ jsx(BubbleMenu$1, {
|
|
3006
|
+
editor,
|
|
3007
|
+
"data-re-bubble-menu": "",
|
|
3008
|
+
shouldShow: ({ editor: editor$1, view }) => {
|
|
3009
|
+
for (const node of excludeNodes) if (editor$1.isActive(node)) return false;
|
|
3010
|
+
if (view.dom.classList.contains("dragging")) return false;
|
|
3011
|
+
return editor$1.view.state.selection.content().size > 0;
|
|
3012
|
+
},
|
|
3013
|
+
options: {
|
|
3014
|
+
placement,
|
|
3015
|
+
offset,
|
|
3016
|
+
onHide
|
|
3017
|
+
},
|
|
3018
|
+
className,
|
|
3019
|
+
children: /* @__PURE__ */ jsx(BubbleMenuContext.Provider, {
|
|
3020
|
+
value: { editor },
|
|
3021
|
+
children
|
|
3022
|
+
})
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
//#endregion
|
|
3027
|
+
//#region src/ui/bubble-menu/strike.tsx
|
|
3028
|
+
const BubbleMenuStrike = createMarkBubbleItem({
|
|
3029
|
+
name: "strike",
|
|
3030
|
+
activeName: "strike",
|
|
3031
|
+
command: "toggleStrike",
|
|
3032
|
+
icon: /* @__PURE__ */ jsx(StrikethroughIcon, {})
|
|
3033
|
+
});
|
|
3034
|
+
|
|
3035
|
+
//#endregion
|
|
3036
|
+
//#region src/ui/bubble-menu/underline.tsx
|
|
3037
|
+
const BubbleMenuUnderline = createMarkBubbleItem({
|
|
3038
|
+
name: "underline",
|
|
3039
|
+
activeName: "underline",
|
|
3040
|
+
command: "toggleUnderline",
|
|
3041
|
+
icon: /* @__PURE__ */ jsx(UnderlineIcon, {})
|
|
3042
|
+
});
|
|
3043
|
+
|
|
3044
|
+
//#endregion
|
|
3045
|
+
//#region src/ui/bubble-menu/uppercase.tsx
|
|
3046
|
+
const BubbleMenuUppercase = createMarkBubbleItem({
|
|
3047
|
+
name: "uppercase",
|
|
3048
|
+
activeName: "uppercase",
|
|
3049
|
+
command: "toggleUppercase",
|
|
3050
|
+
icon: /* @__PURE__ */ jsx(CaseUpperIcon, {})
|
|
3051
|
+
});
|
|
3052
|
+
|
|
3053
|
+
//#endregion
|
|
3054
|
+
//#region src/ui/bubble-menu/default.tsx
|
|
3055
|
+
function BubbleMenuDefault({ excludeItems = [], excludeNodes, placement, offset, onHide, className }) {
|
|
3056
|
+
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = React.useState(false);
|
|
3057
|
+
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = React.useState(false);
|
|
3058
|
+
const has = (item) => !excludeItems.includes(item);
|
|
3059
|
+
const handleNodeSelectorOpenChange = React.useCallback((open) => {
|
|
3060
|
+
setIsNodeSelectorOpen(open);
|
|
3061
|
+
if (open) setIsLinkSelectorOpen(false);
|
|
3062
|
+
}, []);
|
|
3063
|
+
const handleLinkSelectorOpenChange = React.useCallback((open) => {
|
|
3064
|
+
setIsLinkSelectorOpen(open);
|
|
3065
|
+
if (open) setIsNodeSelectorOpen(false);
|
|
3066
|
+
}, []);
|
|
3067
|
+
const handleHide = React.useCallback(() => {
|
|
3068
|
+
setIsNodeSelectorOpen(false);
|
|
3069
|
+
setIsLinkSelectorOpen(false);
|
|
3070
|
+
onHide?.();
|
|
3071
|
+
}, [onHide]);
|
|
3072
|
+
const hasFormattingItems = has("bold") || has("italic") || has("underline") || has("strike") || has("code") || has("uppercase");
|
|
3073
|
+
const hasAlignmentItems = has("align-left") || has("align-center") || has("align-right");
|
|
3074
|
+
return /* @__PURE__ */ jsxs(BubbleMenuRoot, {
|
|
3075
|
+
excludeNodes,
|
|
3076
|
+
placement,
|
|
3077
|
+
offset,
|
|
3078
|
+
onHide: handleHide,
|
|
3079
|
+
className,
|
|
3080
|
+
children: [
|
|
3081
|
+
has("node-selector") && /* @__PURE__ */ jsx(BubbleMenuNodeSelector, {
|
|
3082
|
+
open: isNodeSelectorOpen,
|
|
3083
|
+
onOpenChange: handleNodeSelectorOpenChange
|
|
3084
|
+
}),
|
|
3085
|
+
has("link-selector") && /* @__PURE__ */ jsx(BubbleMenuLinkSelector, {
|
|
3086
|
+
open: isLinkSelectorOpen,
|
|
3087
|
+
onOpenChange: handleLinkSelectorOpenChange
|
|
3088
|
+
}),
|
|
3089
|
+
hasFormattingItems && /* @__PURE__ */ jsxs(BubbleMenuItemGroup, { children: [
|
|
3090
|
+
has("bold") && /* @__PURE__ */ jsx(BubbleMenuBold, {}),
|
|
3091
|
+
has("italic") && /* @__PURE__ */ jsx(BubbleMenuItalic, {}),
|
|
3092
|
+
has("underline") && /* @__PURE__ */ jsx(BubbleMenuUnderline, {}),
|
|
3093
|
+
has("strike") && /* @__PURE__ */ jsx(BubbleMenuStrike, {}),
|
|
3094
|
+
has("code") && /* @__PURE__ */ jsx(BubbleMenuCode, {}),
|
|
3095
|
+
has("uppercase") && /* @__PURE__ */ jsx(BubbleMenuUppercase, {})
|
|
3096
|
+
] }),
|
|
3097
|
+
hasAlignmentItems && /* @__PURE__ */ jsxs(BubbleMenuItemGroup, { children: [
|
|
3098
|
+
has("align-left") && /* @__PURE__ */ jsx(BubbleMenuAlignLeft, {}),
|
|
3099
|
+
has("align-center") && /* @__PURE__ */ jsx(BubbleMenuAlignCenter, {}),
|
|
3100
|
+
has("align-right") && /* @__PURE__ */ jsx(BubbleMenuAlignRight, {})
|
|
3101
|
+
] })
|
|
3102
|
+
]
|
|
3103
|
+
});
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
//#endregion
|
|
3107
|
+
//#region src/ui/bubble-menu/separator.tsx
|
|
3108
|
+
function BubbleMenuSeparator({ className }) {
|
|
3109
|
+
return /* @__PURE__ */ jsx("hr", {
|
|
3110
|
+
className,
|
|
3111
|
+
"data-re-bubble-menu-separator": ""
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
//#endregion
|
|
3116
|
+
//#region src/ui/bubble-menu/index.ts
|
|
3117
|
+
const BubbleMenu = {
|
|
3118
|
+
Root: BubbleMenuRoot,
|
|
3119
|
+
ItemGroup: BubbleMenuItemGroup,
|
|
3120
|
+
Separator: BubbleMenuSeparator,
|
|
3121
|
+
Item: BubbleMenuItem,
|
|
3122
|
+
Bold: BubbleMenuBold,
|
|
3123
|
+
Italic: BubbleMenuItalic,
|
|
3124
|
+
Underline: BubbleMenuUnderline,
|
|
3125
|
+
Strike: BubbleMenuStrike,
|
|
3126
|
+
Code: BubbleMenuCode,
|
|
3127
|
+
Uppercase: BubbleMenuUppercase,
|
|
3128
|
+
AlignLeft: BubbleMenuAlignLeft,
|
|
3129
|
+
AlignCenter: BubbleMenuAlignCenter,
|
|
3130
|
+
AlignRight: BubbleMenuAlignRight,
|
|
3131
|
+
NodeSelector: Object.assign(BubbleMenuNodeSelector, {
|
|
3132
|
+
Root: NodeSelectorRoot,
|
|
3133
|
+
Trigger: NodeSelectorTrigger,
|
|
3134
|
+
Content: NodeSelectorContent
|
|
3135
|
+
}),
|
|
3136
|
+
LinkSelector: BubbleMenuLinkSelector,
|
|
3137
|
+
Default: BubbleMenuDefault
|
|
3138
|
+
};
|
|
3139
|
+
|
|
3140
|
+
//#endregion
|
|
3141
|
+
//#region src/ui/button-bubble-menu/context.tsx
|
|
3142
|
+
const ButtonBubbleMenuContext = React.createContext(null);
|
|
3143
|
+
function useButtonBubbleMenuContext() {
|
|
3144
|
+
const context = React.useContext(ButtonBubbleMenuContext);
|
|
3145
|
+
if (!context) throw new Error("ButtonBubbleMenu compound components must be used within <ButtonBubbleMenu.Root>");
|
|
3146
|
+
return context;
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
//#endregion
|
|
3150
|
+
//#region src/ui/button-bubble-menu/edit-link.tsx
|
|
3151
|
+
function ButtonBubbleMenuEditLink({ className, children, onClick, onMouseDown, ...rest }) {
|
|
3152
|
+
const { setIsEditing } = useButtonBubbleMenuContext();
|
|
3153
|
+
return /* @__PURE__ */ jsx("button", {
|
|
3154
|
+
...rest,
|
|
3155
|
+
type: "button",
|
|
3156
|
+
"aria-label": "Edit link",
|
|
3157
|
+
"data-re-btn-bm-item": "",
|
|
3158
|
+
"data-item": "edit-link",
|
|
3159
|
+
className,
|
|
3160
|
+
onMouseDown: (e) => {
|
|
3161
|
+
e.preventDefault();
|
|
3162
|
+
onMouseDown?.(e);
|
|
3163
|
+
},
|
|
3164
|
+
onClick: (e) => {
|
|
3165
|
+
onClick?.(e);
|
|
3166
|
+
setIsEditing(true);
|
|
3167
|
+
},
|
|
3168
|
+
children: children ?? /* @__PURE__ */ jsx(LinkIcon, {})
|
|
3169
|
+
});
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
//#endregion
|
|
3173
|
+
//#region src/ui/button-bubble-menu/root.tsx
|
|
3174
|
+
function ButtonBubbleMenuRoot({ onHide, placement = "top", offset = 8, className, children }) {
|
|
3175
|
+
const { editor } = useCurrentEditor();
|
|
3176
|
+
const [isEditing, setIsEditing] = React.useState(false);
|
|
3177
|
+
if (!editor) return null;
|
|
3178
|
+
return /* @__PURE__ */ jsx(BubbleMenu$1, {
|
|
3179
|
+
editor,
|
|
3180
|
+
"data-re-btn-bm": "",
|
|
3181
|
+
shouldShow: ({ editor: e, view }) => e.isActive("button") && !view.dom.classList.contains("dragging"),
|
|
3182
|
+
options: {
|
|
3183
|
+
placement,
|
|
3184
|
+
offset,
|
|
3185
|
+
onHide: () => {
|
|
3186
|
+
setIsEditing(false);
|
|
3187
|
+
onHide?.();
|
|
3188
|
+
}
|
|
3189
|
+
},
|
|
3190
|
+
className,
|
|
3191
|
+
children: /* @__PURE__ */ jsx(ButtonBubbleMenuContext.Provider, {
|
|
3192
|
+
value: {
|
|
3193
|
+
editor,
|
|
3194
|
+
isEditing,
|
|
3195
|
+
setIsEditing
|
|
3196
|
+
},
|
|
3197
|
+
children
|
|
3198
|
+
})
|
|
3199
|
+
});
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
//#endregion
|
|
3203
|
+
//#region src/ui/button-bubble-menu/toolbar.tsx
|
|
3204
|
+
function ButtonBubbleMenuToolbar({ children, ...rest }) {
|
|
3205
|
+
const { isEditing } = useButtonBubbleMenuContext();
|
|
3206
|
+
if (isEditing) return null;
|
|
3207
|
+
return /* @__PURE__ */ jsx("div", {
|
|
3208
|
+
"data-re-btn-bm-toolbar": "",
|
|
3209
|
+
...rest,
|
|
3210
|
+
children
|
|
3211
|
+
});
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
//#endregion
|
|
3215
|
+
//#region src/ui/button-bubble-menu/default.tsx
|
|
3216
|
+
function ButtonBubbleMenuDefault({ excludeItems = [], placement, offset, onHide, className }) {
|
|
3217
|
+
return /* @__PURE__ */ jsx(ButtonBubbleMenuRoot, {
|
|
3218
|
+
placement,
|
|
3219
|
+
offset,
|
|
3220
|
+
onHide,
|
|
3221
|
+
className,
|
|
3222
|
+
children: !excludeItems.includes("edit-link") && /* @__PURE__ */ jsx(ButtonBubbleMenuToolbar, { children: /* @__PURE__ */ jsx(ButtonBubbleMenuEditLink, {}) })
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
//#endregion
|
|
3227
|
+
//#region src/ui/button-bubble-menu/index.ts
|
|
3228
|
+
const ButtonBubbleMenu = {
|
|
3229
|
+
Root: ButtonBubbleMenuRoot,
|
|
3230
|
+
Toolbar: ButtonBubbleMenuToolbar,
|
|
3231
|
+
EditLink: ButtonBubbleMenuEditLink,
|
|
3232
|
+
Default: ButtonBubbleMenuDefault
|
|
3233
|
+
};
|
|
3234
|
+
|
|
3235
|
+
//#endregion
|
|
3236
|
+
//#region src/ui/image-bubble-menu/context.tsx
|
|
3237
|
+
const ImageBubbleMenuContext = React.createContext(null);
|
|
3238
|
+
function useImageBubbleMenuContext() {
|
|
3239
|
+
const context = React.useContext(ImageBubbleMenuContext);
|
|
3240
|
+
if (!context) throw new Error("ImageBubbleMenu compound components must be used within <ImageBubbleMenu.Root>");
|
|
3241
|
+
return context;
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
//#endregion
|
|
3245
|
+
//#region src/ui/image-bubble-menu/edit-link.tsx
|
|
3246
|
+
function ImageBubbleMenuEditLink({ className, children, onClick, onMouseDown, ...rest }) {
|
|
3247
|
+
const { setIsEditing } = useImageBubbleMenuContext();
|
|
3248
|
+
return /* @__PURE__ */ jsx("button", {
|
|
3249
|
+
...rest,
|
|
3250
|
+
type: "button",
|
|
3251
|
+
"aria-label": "Edit link",
|
|
3252
|
+
"data-re-img-bm-item": "",
|
|
3253
|
+
"data-item": "edit-link",
|
|
3254
|
+
className,
|
|
3255
|
+
onMouseDown: (e) => {
|
|
3256
|
+
e.preventDefault();
|
|
3257
|
+
onMouseDown?.(e);
|
|
3258
|
+
},
|
|
3259
|
+
onClick: (e) => {
|
|
3260
|
+
onClick?.(e);
|
|
3261
|
+
setIsEditing(true);
|
|
3262
|
+
},
|
|
3263
|
+
children: children ?? /* @__PURE__ */ jsx(LinkIcon, {})
|
|
3264
|
+
});
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
//#endregion
|
|
3268
|
+
//#region src/ui/image-bubble-menu/root.tsx
|
|
3269
|
+
function ImageBubbleMenuRoot({ onHide, placement = "top", offset = 8, className, children }) {
|
|
3270
|
+
const { editor } = useCurrentEditor();
|
|
3271
|
+
const [isEditing, setIsEditing] = React.useState(false);
|
|
3272
|
+
if (!editor) return null;
|
|
3273
|
+
return /* @__PURE__ */ jsx(BubbleMenu$1, {
|
|
3274
|
+
editor,
|
|
3275
|
+
"data-re-img-bm": "",
|
|
3276
|
+
shouldShow: ({ editor: e, view }) => e.isActive("image") && !view.dom.classList.contains("dragging"),
|
|
3277
|
+
options: {
|
|
3278
|
+
placement,
|
|
3279
|
+
offset,
|
|
3280
|
+
onHide: () => {
|
|
3281
|
+
setIsEditing(false);
|
|
3282
|
+
onHide?.();
|
|
3283
|
+
}
|
|
3284
|
+
},
|
|
3285
|
+
className,
|
|
3286
|
+
children: /* @__PURE__ */ jsx(ImageBubbleMenuContext.Provider, {
|
|
3287
|
+
value: {
|
|
3288
|
+
editor,
|
|
3289
|
+
isEditing,
|
|
3290
|
+
setIsEditing
|
|
3291
|
+
},
|
|
3292
|
+
children
|
|
3293
|
+
})
|
|
3294
|
+
});
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
//#endregion
|
|
3298
|
+
//#region src/ui/image-bubble-menu/toolbar.tsx
|
|
3299
|
+
function ImageBubbleMenuToolbar({ children, ...rest }) {
|
|
3300
|
+
const { isEditing } = useImageBubbleMenuContext();
|
|
3301
|
+
if (isEditing) return null;
|
|
3302
|
+
return /* @__PURE__ */ jsx("div", {
|
|
3303
|
+
"data-re-img-bm-toolbar": "",
|
|
3304
|
+
...rest,
|
|
3305
|
+
children
|
|
3306
|
+
});
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
//#endregion
|
|
3310
|
+
//#region src/ui/image-bubble-menu/default.tsx
|
|
3311
|
+
function ImageBubbleMenuDefault({ excludeItems = [], placement, offset, onHide, className }) {
|
|
3312
|
+
return /* @__PURE__ */ jsx(ImageBubbleMenuRoot, {
|
|
3313
|
+
placement,
|
|
3314
|
+
offset,
|
|
3315
|
+
onHide,
|
|
3316
|
+
className,
|
|
3317
|
+
children: !excludeItems.includes("edit-link") && /* @__PURE__ */ jsx(ImageBubbleMenuToolbar, { children: /* @__PURE__ */ jsx(ImageBubbleMenuEditLink, {}) })
|
|
3318
|
+
});
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
//#endregion
|
|
3322
|
+
//#region src/ui/image-bubble-menu/index.ts
|
|
3323
|
+
const ImageBubbleMenu = {
|
|
3324
|
+
Root: ImageBubbleMenuRoot,
|
|
3325
|
+
Toolbar: ImageBubbleMenuToolbar,
|
|
3326
|
+
EditLink: ImageBubbleMenuEditLink,
|
|
3327
|
+
Default: ImageBubbleMenuDefault
|
|
3328
|
+
};
|
|
3329
|
+
|
|
3330
|
+
//#endregion
|
|
3331
|
+
//#region src/ui/link-bubble-menu/context.tsx
|
|
3332
|
+
const LinkBubbleMenuContext = React.createContext(null);
|
|
3333
|
+
function useLinkBubbleMenuContext() {
|
|
3334
|
+
const context = React.useContext(LinkBubbleMenuContext);
|
|
3335
|
+
if (!context) throw new Error("LinkBubbleMenu compound components must be used within <LinkBubbleMenu.Root>");
|
|
3336
|
+
return context;
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
//#endregion
|
|
3340
|
+
//#region src/ui/link-bubble-menu/edit-link.tsx
|
|
3341
|
+
function LinkBubbleMenuEditLink({ className, children, onClick, onMouseDown, ...rest }) {
|
|
3342
|
+
const { setIsEditing } = useLinkBubbleMenuContext();
|
|
3343
|
+
return /* @__PURE__ */ jsx("button", {
|
|
3344
|
+
type: "button",
|
|
3345
|
+
"aria-label": "Edit link",
|
|
3346
|
+
"data-re-link-bm-item": "",
|
|
3347
|
+
"data-item": "edit-link",
|
|
3348
|
+
className,
|
|
3349
|
+
onMouseDown: (e) => {
|
|
3350
|
+
e.preventDefault();
|
|
3351
|
+
onMouseDown?.(e);
|
|
3352
|
+
},
|
|
3353
|
+
onClick: (e) => {
|
|
3354
|
+
onClick?.(e);
|
|
3355
|
+
setIsEditing(true);
|
|
3356
|
+
},
|
|
3357
|
+
...rest,
|
|
3358
|
+
children: children ?? /* @__PURE__ */ jsx(PencilIcon, {})
|
|
3359
|
+
});
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
//#endregion
|
|
3363
|
+
//#region src/ui/link-bubble-menu/form.tsx
|
|
3364
|
+
function LinkBubbleMenuForm({ className, validateUrl, onLinkApply, onLinkRemove, children }) {
|
|
3365
|
+
const { editor, linkHref, isEditing, setIsEditing } = useLinkBubbleMenuContext();
|
|
3366
|
+
const inputRef = React.useRef(null);
|
|
3367
|
+
const formRef = React.useRef(null);
|
|
3368
|
+
const displayHref = linkHref === "#" ? "" : linkHref;
|
|
3369
|
+
const [inputValue, setInputValue] = React.useState(displayHref);
|
|
3370
|
+
React.useEffect(() => {
|
|
3371
|
+
if (!isEditing) return;
|
|
3372
|
+
const timeoutId = setTimeout(() => {
|
|
3373
|
+
inputRef.current?.focus();
|
|
3374
|
+
}, 0);
|
|
3375
|
+
return () => clearTimeout(timeoutId);
|
|
3376
|
+
}, [isEditing]);
|
|
3377
|
+
React.useEffect(() => {
|
|
3378
|
+
if (!isEditing) return;
|
|
3379
|
+
const handleKeyDown = (event) => {
|
|
3380
|
+
if (event.key === "Escape") setIsEditing(false);
|
|
3381
|
+
};
|
|
3382
|
+
const handleClickOutside = (event) => {
|
|
3383
|
+
if (formRef.current && !formRef.current.contains(event.target)) {
|
|
3384
|
+
const form = formRef.current;
|
|
3385
|
+
const submitEvent = new Event("submit", {
|
|
3386
|
+
bubbles: true,
|
|
3387
|
+
cancelable: true
|
|
3388
|
+
});
|
|
3389
|
+
form.dispatchEvent(submitEvent);
|
|
3390
|
+
setIsEditing(false);
|
|
3391
|
+
}
|
|
3392
|
+
};
|
|
3393
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
3394
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
3395
|
+
return () => {
|
|
3396
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
3397
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
3398
|
+
};
|
|
3399
|
+
}, [isEditing, setIsEditing]);
|
|
3400
|
+
if (!isEditing) return null;
|
|
3401
|
+
function handleSubmit(e) {
|
|
3402
|
+
e.preventDefault();
|
|
3403
|
+
const value = inputValue.trim();
|
|
3404
|
+
if (value === "") {
|
|
3405
|
+
setLinkHref(editor, "");
|
|
3406
|
+
setIsEditing(false);
|
|
3407
|
+
focusEditor(editor);
|
|
3408
|
+
onLinkRemove?.();
|
|
3409
|
+
return;
|
|
3410
|
+
}
|
|
3411
|
+
const finalValue = (validateUrl ?? getUrlFromString)(value);
|
|
3412
|
+
if (!finalValue) {
|
|
3413
|
+
setLinkHref(editor, "");
|
|
3414
|
+
setIsEditing(false);
|
|
3415
|
+
focusEditor(editor);
|
|
3416
|
+
onLinkRemove?.();
|
|
3417
|
+
return;
|
|
3418
|
+
}
|
|
3419
|
+
setLinkHref(editor, finalValue);
|
|
3420
|
+
setIsEditing(false);
|
|
3421
|
+
focusEditor(editor);
|
|
3422
|
+
onLinkApply?.(finalValue);
|
|
3423
|
+
}
|
|
3424
|
+
function handleUnlink(e) {
|
|
3425
|
+
e.stopPropagation();
|
|
3426
|
+
setLinkHref(editor, "");
|
|
3427
|
+
setIsEditing(false);
|
|
3428
|
+
focusEditor(editor);
|
|
3429
|
+
onLinkRemove?.();
|
|
3430
|
+
}
|
|
3431
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
3432
|
+
ref: formRef,
|
|
3433
|
+
"data-re-link-bm-form": "",
|
|
3434
|
+
className,
|
|
3435
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
3436
|
+
onClick: (e) => e.stopPropagation(),
|
|
3437
|
+
onKeyDown: (e) => e.stopPropagation(),
|
|
3438
|
+
onSubmit: handleSubmit,
|
|
3439
|
+
children: [
|
|
3440
|
+
/* @__PURE__ */ jsx("input", {
|
|
3441
|
+
ref: inputRef,
|
|
3442
|
+
"data-re-link-bm-input": "",
|
|
3443
|
+
value: inputValue,
|
|
3444
|
+
onFocus: (e) => e.stopPropagation(),
|
|
3445
|
+
onChange: (e) => setInputValue(e.target.value),
|
|
3446
|
+
placeholder: "Paste a link",
|
|
3447
|
+
type: "text"
|
|
3448
|
+
}),
|
|
3449
|
+
children,
|
|
3450
|
+
displayHref ? /* @__PURE__ */ jsx("button", {
|
|
3451
|
+
type: "button",
|
|
3452
|
+
"aria-label": "Remove link",
|
|
3453
|
+
"data-re-link-bm-unlink": "",
|
|
3454
|
+
onClick: handleUnlink,
|
|
3455
|
+
children: /* @__PURE__ */ jsx(UnlinkIcon, {})
|
|
3456
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
3457
|
+
type: "submit",
|
|
3458
|
+
"aria-label": "Apply link",
|
|
3459
|
+
"data-re-link-bm-apply": "",
|
|
3460
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
3461
|
+
children: /* @__PURE__ */ jsx(Check, {})
|
|
3462
|
+
})
|
|
3463
|
+
]
|
|
3464
|
+
});
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
//#endregion
|
|
3468
|
+
//#region src/ui/link-bubble-menu/open-link.tsx
|
|
3469
|
+
function LinkBubbleMenuOpenLink({ className, children, ...rest }) {
|
|
3470
|
+
const { linkHref } = useLinkBubbleMenuContext();
|
|
3471
|
+
return /* @__PURE__ */ jsx("a", {
|
|
3472
|
+
...rest,
|
|
3473
|
+
href: linkHref,
|
|
3474
|
+
target: "_blank",
|
|
3475
|
+
rel: "noopener noreferrer",
|
|
3476
|
+
"aria-label": "Open link",
|
|
3477
|
+
"data-re-link-bm-item": "",
|
|
3478
|
+
"data-item": "open-link",
|
|
3479
|
+
className,
|
|
3480
|
+
children: children ?? /* @__PURE__ */ jsx(ExternalLinkIcon, {})
|
|
3481
|
+
});
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
//#endregion
|
|
3485
|
+
//#region src/ui/link-bubble-menu/root.tsx
|
|
3486
|
+
function LinkBubbleMenuRoot({ onHide, placement = "top", offset = 8, className, children }) {
|
|
3487
|
+
const { editor } = useCurrentEditor();
|
|
3488
|
+
const [isEditing, setIsEditing] = React.useState(false);
|
|
3489
|
+
const linkHref = useEditorState({
|
|
3490
|
+
editor,
|
|
3491
|
+
selector: ({ editor: e }) => e?.getAttributes("link").href ?? ""
|
|
3492
|
+
});
|
|
3493
|
+
if (!editor) return null;
|
|
3494
|
+
return /* @__PURE__ */ jsx(BubbleMenu$1, {
|
|
3495
|
+
editor,
|
|
3496
|
+
"data-re-link-bm": "",
|
|
3497
|
+
shouldShow: ({ editor: e }) => e.isActive("link") && e.view.state.selection.content().size === 0,
|
|
3498
|
+
options: {
|
|
3499
|
+
placement,
|
|
3500
|
+
offset,
|
|
3501
|
+
onHide: () => {
|
|
3502
|
+
setIsEditing(false);
|
|
3503
|
+
onHide?.();
|
|
3504
|
+
}
|
|
3505
|
+
},
|
|
3506
|
+
className,
|
|
3507
|
+
children: /* @__PURE__ */ jsx(LinkBubbleMenuContext.Provider, {
|
|
3508
|
+
value: {
|
|
3509
|
+
editor,
|
|
3510
|
+
linkHref: linkHref ?? "",
|
|
3511
|
+
isEditing,
|
|
3512
|
+
setIsEditing
|
|
3513
|
+
},
|
|
3514
|
+
children
|
|
3515
|
+
})
|
|
3516
|
+
});
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
//#endregion
|
|
3520
|
+
//#region src/ui/link-bubble-menu/toolbar.tsx
|
|
3521
|
+
function LinkBubbleMenuToolbar({ children, ...rest }) {
|
|
3522
|
+
const { isEditing } = useLinkBubbleMenuContext();
|
|
3523
|
+
if (isEditing) return null;
|
|
3524
|
+
return /* @__PURE__ */ jsx("div", {
|
|
3525
|
+
"data-re-link-bm-toolbar": "",
|
|
3526
|
+
...rest,
|
|
3527
|
+
children
|
|
3528
|
+
});
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
//#endregion
|
|
3532
|
+
//#region src/ui/link-bubble-menu/unlink.tsx
|
|
3533
|
+
function LinkBubbleMenuUnlink({ className, children, onClick, onMouseDown, ...rest }) {
|
|
3534
|
+
const { editor } = useLinkBubbleMenuContext();
|
|
3535
|
+
return /* @__PURE__ */ jsx("button", {
|
|
3536
|
+
type: "button",
|
|
3537
|
+
"aria-label": "Remove link",
|
|
3538
|
+
"data-re-link-bm-item": "",
|
|
3539
|
+
"data-item": "unlink",
|
|
3540
|
+
className,
|
|
3541
|
+
onMouseDown: (e) => {
|
|
3542
|
+
e.preventDefault();
|
|
3543
|
+
onMouseDown?.(e);
|
|
3544
|
+
},
|
|
3545
|
+
onClick: (e) => {
|
|
3546
|
+
onClick?.(e);
|
|
3547
|
+
editor.chain().focus().unsetLink().run();
|
|
3548
|
+
},
|
|
3549
|
+
...rest,
|
|
3550
|
+
children: children ?? /* @__PURE__ */ jsx(UnlinkIcon, {})
|
|
3551
|
+
});
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
//#endregion
|
|
3555
|
+
//#region src/ui/link-bubble-menu/default.tsx
|
|
3556
|
+
function LinkBubbleMenuDefault({ excludeItems = [], placement, offset, onHide, className, validateUrl, onLinkApply, onLinkRemove }) {
|
|
3557
|
+
const has = (item) => !excludeItems.includes(item);
|
|
3558
|
+
return /* @__PURE__ */ jsxs(LinkBubbleMenuRoot, {
|
|
3559
|
+
placement,
|
|
3560
|
+
offset,
|
|
3561
|
+
onHide,
|
|
3562
|
+
className,
|
|
3563
|
+
children: [(has("edit-link") || has("open-link") || has("unlink")) && /* @__PURE__ */ jsxs(LinkBubbleMenuToolbar, { children: [
|
|
3564
|
+
has("edit-link") && /* @__PURE__ */ jsx(LinkBubbleMenuEditLink, {}),
|
|
3565
|
+
has("open-link") && /* @__PURE__ */ jsx(LinkBubbleMenuOpenLink, {}),
|
|
3566
|
+
has("unlink") && /* @__PURE__ */ jsx(LinkBubbleMenuUnlink, {})
|
|
3567
|
+
] }), /* @__PURE__ */ jsx(LinkBubbleMenuForm, {
|
|
3568
|
+
validateUrl,
|
|
3569
|
+
onLinkApply,
|
|
3570
|
+
onLinkRemove
|
|
3571
|
+
})]
|
|
3572
|
+
});
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
//#endregion
|
|
3576
|
+
//#region src/ui/link-bubble-menu/index.ts
|
|
3577
|
+
const LinkBubbleMenu = {
|
|
3578
|
+
Root: LinkBubbleMenuRoot,
|
|
3579
|
+
Toolbar: LinkBubbleMenuToolbar,
|
|
3580
|
+
Form: LinkBubbleMenuForm,
|
|
3581
|
+
EditLink: LinkBubbleMenuEditLink,
|
|
3582
|
+
Unlink: LinkBubbleMenuUnlink,
|
|
3583
|
+
OpenLink: LinkBubbleMenuOpenLink,
|
|
3584
|
+
Default: LinkBubbleMenuDefault
|
|
3585
|
+
};
|
|
3586
|
+
|
|
3587
|
+
//#endregion
|
|
3588
|
+
//#region src/ui/slash-command/utils.ts
|
|
3589
|
+
function isInsideNode(editor, type) {
|
|
3590
|
+
const { $from } = editor.state.selection;
|
|
3591
|
+
for (let d = $from.depth; d > 0; d--) if ($from.node(d).type.name === type) return true;
|
|
3592
|
+
return false;
|
|
3593
|
+
}
|
|
3594
|
+
function isAtMaxColumnsDepth(editor) {
|
|
3595
|
+
const { from } = editor.state.selection;
|
|
3596
|
+
return getColumnsDepth(editor.state.doc, from) >= MAX_COLUMNS_DEPTH;
|
|
3597
|
+
}
|
|
3598
|
+
function updateScrollView(container, item) {
|
|
3599
|
+
const containerRect = container.getBoundingClientRect();
|
|
3600
|
+
const itemRect = item.getBoundingClientRect();
|
|
3601
|
+
if (itemRect.top < containerRect.top) container.scrollTop -= containerRect.top - itemRect.top;
|
|
3602
|
+
else if (itemRect.bottom > containerRect.bottom) container.scrollTop += itemRect.bottom - containerRect.bottom;
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
//#endregion
|
|
3606
|
+
//#region src/ui/slash-command/command-list.tsx
|
|
3607
|
+
const CATEGORY_ORDER = [
|
|
3608
|
+
"Text",
|
|
3609
|
+
"Media",
|
|
3610
|
+
"Layout",
|
|
3611
|
+
"Utility"
|
|
3612
|
+
];
|
|
3613
|
+
function groupByCategory(items) {
|
|
3614
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3615
|
+
for (const item of items) {
|
|
3616
|
+
const existing = seen.get(item.category);
|
|
3617
|
+
if (existing) existing.push(item);
|
|
3618
|
+
else seen.set(item.category, [item]);
|
|
3619
|
+
}
|
|
3620
|
+
const ordered = [];
|
|
3621
|
+
for (const cat of CATEGORY_ORDER) {
|
|
3622
|
+
const group = seen.get(cat);
|
|
3623
|
+
if (group) {
|
|
3624
|
+
ordered.push({
|
|
3625
|
+
category: cat,
|
|
3626
|
+
items: group
|
|
3627
|
+
});
|
|
3628
|
+
seen.delete(cat);
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
for (const [category, group] of seen) ordered.push({
|
|
3632
|
+
category,
|
|
3633
|
+
items: group
|
|
3634
|
+
});
|
|
3635
|
+
return ordered;
|
|
3636
|
+
}
|
|
3637
|
+
function CommandItem({ item, selected, onSelect }) {
|
|
3638
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
3639
|
+
"data-re-slash-command-item": "",
|
|
3640
|
+
"data-selected": selected || void 0,
|
|
3641
|
+
onClick: onSelect,
|
|
3642
|
+
type: "button",
|
|
3643
|
+
children: [item.icon, /* @__PURE__ */ jsx("span", { children: item.title })]
|
|
3644
|
+
});
|
|
3645
|
+
}
|
|
3646
|
+
function CommandList({ items, command, query, ref }) {
|
|
3647
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
3648
|
+
const containerRef = useRef(null);
|
|
3649
|
+
useEffect(() => {
|
|
3650
|
+
setSelectedIndex(0);
|
|
3651
|
+
}, [items]);
|
|
3652
|
+
useLayoutEffect(() => {
|
|
3653
|
+
const container = containerRef.current;
|
|
3654
|
+
if (!container) return;
|
|
3655
|
+
const selected = container.querySelector("[data-selected]");
|
|
3656
|
+
if (selected) updateScrollView(container, selected);
|
|
3657
|
+
}, [selectedIndex]);
|
|
3658
|
+
const selectItem = useCallback((index) => {
|
|
3659
|
+
const item = items[index];
|
|
3660
|
+
if (item) command(item);
|
|
3661
|
+
}, [items, command]);
|
|
3662
|
+
useImperativeHandle(ref, () => ({ onKeyDown: ({ event }) => {
|
|
3663
|
+
if (items.length === 0) return false;
|
|
3664
|
+
if (event.key === "ArrowUp") {
|
|
3665
|
+
setSelectedIndex((i) => (i + items.length - 1) % items.length);
|
|
3666
|
+
return true;
|
|
3667
|
+
}
|
|
3668
|
+
if (event.key === "ArrowDown") {
|
|
3669
|
+
setSelectedIndex((i) => (i + 1) % items.length);
|
|
3670
|
+
return true;
|
|
3671
|
+
}
|
|
3672
|
+
if (event.key === "Enter") {
|
|
3673
|
+
selectItem(selectedIndex);
|
|
3674
|
+
return true;
|
|
3675
|
+
}
|
|
3676
|
+
return false;
|
|
3677
|
+
} }), [
|
|
3678
|
+
items.length,
|
|
3679
|
+
selectItem,
|
|
3680
|
+
selectedIndex
|
|
3681
|
+
]);
|
|
3682
|
+
if (items.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
3683
|
+
"data-re-slash-command": "",
|
|
3684
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
3685
|
+
"data-re-slash-command-empty": "",
|
|
3686
|
+
children: "No results"
|
|
3687
|
+
})
|
|
3688
|
+
});
|
|
3689
|
+
if (query.trim().length > 0) return /* @__PURE__ */ jsx("div", {
|
|
3690
|
+
"data-re-slash-command": "",
|
|
3691
|
+
ref: containerRef,
|
|
3692
|
+
children: items.map((item, index) => /* @__PURE__ */ jsx(CommandItem, {
|
|
3693
|
+
item,
|
|
3694
|
+
onSelect: () => selectItem(index),
|
|
3695
|
+
selected: index === selectedIndex
|
|
3696
|
+
}, item.title))
|
|
3697
|
+
});
|
|
3698
|
+
const groups = groupByCategory(items);
|
|
3699
|
+
let flatIndex = 0;
|
|
3700
|
+
return /* @__PURE__ */ jsx("div", {
|
|
3701
|
+
"data-re-slash-command": "",
|
|
3702
|
+
ref: containerRef,
|
|
3703
|
+
children: groups.map((group) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
|
|
3704
|
+
"data-re-slash-command-category": "",
|
|
3705
|
+
children: group.category
|
|
3706
|
+
}), group.items.map((item) => {
|
|
3707
|
+
const currentIndex = flatIndex++;
|
|
3708
|
+
return /* @__PURE__ */ jsx(CommandItem, {
|
|
3709
|
+
item,
|
|
3710
|
+
onSelect: () => selectItem(currentIndex),
|
|
3711
|
+
selected: currentIndex === selectedIndex
|
|
3712
|
+
}, item.title);
|
|
3713
|
+
})] }, group.category))
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
//#endregion
|
|
3718
|
+
//#region src/ui/slash-command/commands.tsx
|
|
3719
|
+
const TEXT = {
|
|
3720
|
+
title: "Text",
|
|
3721
|
+
description: "Plain text block",
|
|
3722
|
+
icon: /* @__PURE__ */ jsx(Text, { size: 20 }),
|
|
3723
|
+
category: "Text",
|
|
3724
|
+
searchTerms: ["p", "paragraph"],
|
|
3725
|
+
command: ({ editor, range }) => {
|
|
3726
|
+
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
|
|
3727
|
+
}
|
|
3728
|
+
};
|
|
3729
|
+
const H1 = {
|
|
3730
|
+
title: "Title",
|
|
3731
|
+
description: "Large heading",
|
|
3732
|
+
icon: /* @__PURE__ */ jsx(Heading1, { size: 20 }),
|
|
3733
|
+
category: "Text",
|
|
3734
|
+
searchTerms: [
|
|
3735
|
+
"title",
|
|
3736
|
+
"big",
|
|
3737
|
+
"large",
|
|
3738
|
+
"h1"
|
|
3739
|
+
],
|
|
3740
|
+
command: ({ editor, range }) => {
|
|
3741
|
+
editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
|
|
3742
|
+
}
|
|
3743
|
+
};
|
|
3744
|
+
const H2 = {
|
|
3745
|
+
title: "Subtitle",
|
|
3746
|
+
description: "Medium heading",
|
|
3747
|
+
icon: /* @__PURE__ */ jsx(Heading2, { size: 20 }),
|
|
3748
|
+
category: "Text",
|
|
3749
|
+
searchTerms: [
|
|
3750
|
+
"subtitle",
|
|
3751
|
+
"medium",
|
|
3752
|
+
"h2"
|
|
3753
|
+
],
|
|
3754
|
+
command: ({ editor, range }) => {
|
|
3755
|
+
editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
|
|
3756
|
+
}
|
|
3757
|
+
};
|
|
3758
|
+
const H3 = {
|
|
3759
|
+
title: "Heading",
|
|
3760
|
+
description: "Small heading",
|
|
3761
|
+
icon: /* @__PURE__ */ jsx(Heading3, { size: 20 }),
|
|
3762
|
+
category: "Text",
|
|
3763
|
+
searchTerms: [
|
|
3764
|
+
"subtitle",
|
|
3765
|
+
"small",
|
|
3766
|
+
"h3"
|
|
3767
|
+
],
|
|
3768
|
+
command: ({ editor, range }) => {
|
|
3769
|
+
editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
|
|
3770
|
+
}
|
|
3771
|
+
};
|
|
3772
|
+
const BULLET_LIST = {
|
|
3773
|
+
title: "Bullet list",
|
|
3774
|
+
description: "Unordered list",
|
|
3775
|
+
icon: /* @__PURE__ */ jsx(List, { size: 20 }),
|
|
3776
|
+
category: "Text",
|
|
3777
|
+
searchTerms: ["unordered", "point"],
|
|
3778
|
+
command: ({ editor, range }) => {
|
|
3779
|
+
editor.chain().focus().deleteRange(range).toggleBulletList().run();
|
|
3780
|
+
}
|
|
3781
|
+
};
|
|
3782
|
+
const NUMBERED_LIST = {
|
|
3783
|
+
title: "Numbered list",
|
|
3784
|
+
description: "Ordered list",
|
|
3785
|
+
icon: /* @__PURE__ */ jsx(ListOrdered, { size: 20 }),
|
|
3786
|
+
category: "Text",
|
|
3787
|
+
searchTerms: ["ordered"],
|
|
3788
|
+
command: ({ editor, range }) => {
|
|
3789
|
+
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
|
3790
|
+
}
|
|
3791
|
+
};
|
|
3792
|
+
const QUOTE = {
|
|
3793
|
+
title: "Quote",
|
|
3794
|
+
description: "Block quote",
|
|
3795
|
+
icon: /* @__PURE__ */ jsx(TextQuote, { size: 20 }),
|
|
3796
|
+
category: "Text",
|
|
3797
|
+
searchTerms: ["blockquote"],
|
|
3798
|
+
command: ({ editor, range }) => {
|
|
3799
|
+
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run();
|
|
3800
|
+
}
|
|
3801
|
+
};
|
|
3802
|
+
const CODE = {
|
|
3803
|
+
title: "Code block",
|
|
3804
|
+
description: "Code snippet",
|
|
3805
|
+
icon: /* @__PURE__ */ jsx(SquareCode, { size: 20 }),
|
|
3806
|
+
category: "Text",
|
|
3807
|
+
searchTerms: ["codeblock"],
|
|
3808
|
+
command: ({ editor, range }) => {
|
|
3809
|
+
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
|
|
3810
|
+
}
|
|
3811
|
+
};
|
|
3812
|
+
const BUTTON = {
|
|
3813
|
+
title: "Button",
|
|
3814
|
+
description: "Clickable button",
|
|
3815
|
+
icon: /* @__PURE__ */ jsx(MousePointer, { size: 20 }),
|
|
3816
|
+
category: "Layout",
|
|
3817
|
+
searchTerms: ["button"],
|
|
3818
|
+
command: ({ editor, range }) => {
|
|
3819
|
+
editor.chain().focus().deleteRange(range).setButton().run();
|
|
3820
|
+
}
|
|
3821
|
+
};
|
|
3822
|
+
const DIVIDER = {
|
|
3823
|
+
title: "Divider",
|
|
3824
|
+
description: "Horizontal separator",
|
|
3825
|
+
icon: /* @__PURE__ */ jsx(SplitSquareVertical, { size: 20 }),
|
|
3826
|
+
category: "Layout",
|
|
3827
|
+
searchTerms: [
|
|
3828
|
+
"hr",
|
|
3829
|
+
"divider",
|
|
3830
|
+
"separator"
|
|
3831
|
+
],
|
|
3832
|
+
command: ({ editor, range }) => {
|
|
3833
|
+
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
|
3834
|
+
}
|
|
3835
|
+
};
|
|
3836
|
+
const SECTION = {
|
|
3837
|
+
title: "Section",
|
|
3838
|
+
description: "Content section",
|
|
3839
|
+
icon: /* @__PURE__ */ jsx(Rows2, { size: 20 }),
|
|
3840
|
+
category: "Layout",
|
|
3841
|
+
searchTerms: [
|
|
3842
|
+
"section",
|
|
3843
|
+
"row",
|
|
3844
|
+
"container"
|
|
3845
|
+
],
|
|
3846
|
+
command: ({ editor, range }) => {
|
|
3847
|
+
editor.chain().focus().deleteRange(range).insertSection().run();
|
|
3848
|
+
}
|
|
3849
|
+
};
|
|
3850
|
+
const TWO_COLUMNS = {
|
|
3851
|
+
title: "2 columns",
|
|
3852
|
+
description: "Two column layout",
|
|
3853
|
+
icon: /* @__PURE__ */ jsx(Columns2, { size: 20 }),
|
|
3854
|
+
category: "Layout",
|
|
3855
|
+
searchTerms: [
|
|
3856
|
+
"columns",
|
|
3857
|
+
"column",
|
|
3858
|
+
"layout",
|
|
3859
|
+
"grid",
|
|
3860
|
+
"split",
|
|
3861
|
+
"side-by-side",
|
|
3862
|
+
"multi-column",
|
|
3863
|
+
"row",
|
|
3864
|
+
"two",
|
|
3865
|
+
"2"
|
|
3866
|
+
],
|
|
3867
|
+
command: ({ editor, range }) => {
|
|
3868
|
+
editor.chain().focus().deleteRange(range).insertColumns(2).run();
|
|
3869
|
+
}
|
|
3870
|
+
};
|
|
3871
|
+
const THREE_COLUMNS = {
|
|
3872
|
+
title: "3 columns",
|
|
3873
|
+
description: "Three column layout",
|
|
3874
|
+
icon: /* @__PURE__ */ jsx(Columns3, { size: 20 }),
|
|
3875
|
+
category: "Layout",
|
|
3876
|
+
searchTerms: [
|
|
3877
|
+
"columns",
|
|
3878
|
+
"column",
|
|
3879
|
+
"layout",
|
|
3880
|
+
"grid",
|
|
3881
|
+
"split",
|
|
3882
|
+
"multi-column",
|
|
3883
|
+
"row",
|
|
3884
|
+
"three",
|
|
3885
|
+
"3"
|
|
3886
|
+
],
|
|
3887
|
+
command: ({ editor, range }) => {
|
|
3888
|
+
editor.chain().focus().deleteRange(range).insertColumns(3).run();
|
|
3889
|
+
}
|
|
3890
|
+
};
|
|
3891
|
+
const FOUR_COLUMNS = {
|
|
3892
|
+
title: "4 columns",
|
|
3893
|
+
description: "Four column layout",
|
|
3894
|
+
icon: /* @__PURE__ */ jsx(Columns4, { size: 20 }),
|
|
3895
|
+
category: "Layout",
|
|
3896
|
+
searchTerms: [
|
|
3897
|
+
"columns",
|
|
3898
|
+
"column",
|
|
3899
|
+
"layout",
|
|
3900
|
+
"grid",
|
|
3901
|
+
"split",
|
|
3902
|
+
"multi-column",
|
|
3903
|
+
"row",
|
|
3904
|
+
"four",
|
|
3905
|
+
"4"
|
|
3906
|
+
],
|
|
3907
|
+
command: ({ editor, range }) => {
|
|
3908
|
+
editor.chain().focus().deleteRange(range).insertColumns(4).run();
|
|
3909
|
+
}
|
|
3910
|
+
};
|
|
3911
|
+
const defaultSlashCommands = [
|
|
3912
|
+
TEXT,
|
|
3913
|
+
H1,
|
|
3914
|
+
H2,
|
|
3915
|
+
H3,
|
|
3916
|
+
BULLET_LIST,
|
|
3917
|
+
NUMBERED_LIST,
|
|
3918
|
+
QUOTE,
|
|
3919
|
+
CODE,
|
|
3920
|
+
BUTTON,
|
|
3921
|
+
DIVIDER,
|
|
3922
|
+
SECTION,
|
|
3923
|
+
TWO_COLUMNS,
|
|
3924
|
+
THREE_COLUMNS,
|
|
3925
|
+
FOUR_COLUMNS
|
|
3926
|
+
];
|
|
3927
|
+
|
|
3928
|
+
//#endregion
|
|
3929
|
+
//#region src/ui/slash-command/extension.ts
|
|
3930
|
+
const SlashCommandExtension = Extension.create({
|
|
3931
|
+
name: "slash-command",
|
|
3932
|
+
addOptions() {
|
|
3933
|
+
return { suggestion: {
|
|
3934
|
+
char: "/",
|
|
3935
|
+
allow: ({ editor }) => !editor.isActive("codeBlock"),
|
|
3936
|
+
command: ({ editor, range, props }) => {
|
|
3937
|
+
props.command({
|
|
3938
|
+
editor,
|
|
3939
|
+
range
|
|
3940
|
+
});
|
|
3941
|
+
}
|
|
3942
|
+
} };
|
|
3943
|
+
},
|
|
3944
|
+
addProseMirrorPlugins() {
|
|
3945
|
+
return [Suggestion({
|
|
3946
|
+
pluginKey: new PluginKey("slash-command"),
|
|
3947
|
+
editor: this.editor,
|
|
3948
|
+
...this.options.suggestion
|
|
3949
|
+
})];
|
|
3950
|
+
}
|
|
3951
|
+
});
|
|
3952
|
+
|
|
3953
|
+
//#endregion
|
|
3954
|
+
//#region src/ui/slash-command/render.tsx
|
|
3955
|
+
function createRenderItems(component = CommandList) {
|
|
3956
|
+
return () => {
|
|
3957
|
+
let renderer = null;
|
|
3958
|
+
let popup = null;
|
|
3959
|
+
return {
|
|
3960
|
+
onStart: (props) => {
|
|
3961
|
+
renderer = new ReactRenderer(component, {
|
|
3962
|
+
props,
|
|
3963
|
+
editor: props.editor
|
|
3964
|
+
});
|
|
3965
|
+
if (!props.clientRect) return;
|
|
3966
|
+
popup = tippy("body", {
|
|
3967
|
+
getReferenceClientRect: props.clientRect,
|
|
3968
|
+
appendTo: () => document.body,
|
|
3969
|
+
content: renderer.element,
|
|
3970
|
+
showOnCreate: true,
|
|
3971
|
+
interactive: true,
|
|
3972
|
+
trigger: "manual",
|
|
3973
|
+
placement: "bottom-start"
|
|
3974
|
+
});
|
|
3975
|
+
},
|
|
3976
|
+
onUpdate: (props) => {
|
|
3977
|
+
if (!renderer) return;
|
|
3978
|
+
renderer.updateProps(props);
|
|
3979
|
+
if (popup?.[0] && props.clientRect) popup[0].setProps({ getReferenceClientRect: props.clientRect });
|
|
3980
|
+
},
|
|
3981
|
+
onKeyDown: (props) => {
|
|
3982
|
+
if (props.event.key === "Escape") {
|
|
3983
|
+
popup?.[0]?.hide();
|
|
3984
|
+
return true;
|
|
3985
|
+
}
|
|
3986
|
+
return renderer?.ref?.onKeyDown(props) ?? false;
|
|
3987
|
+
},
|
|
3988
|
+
onExit: () => {
|
|
3989
|
+
popup?.[0]?.destroy();
|
|
3990
|
+
renderer?.destroy();
|
|
3991
|
+
popup = null;
|
|
3992
|
+
renderer = null;
|
|
3993
|
+
}
|
|
3994
|
+
};
|
|
3995
|
+
};
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3998
|
+
//#endregion
|
|
3999
|
+
//#region src/ui/slash-command/search.ts
|
|
4000
|
+
function scoreItem(item, query) {
|
|
4001
|
+
if (!query) return 100;
|
|
4002
|
+
const q = query.toLowerCase();
|
|
4003
|
+
const title = item.title.toLowerCase();
|
|
4004
|
+
const description = item.description.toLowerCase();
|
|
4005
|
+
const terms = item.searchTerms?.map((t) => t.toLowerCase()) ?? [];
|
|
4006
|
+
if (title === q) return 100;
|
|
4007
|
+
if (title.startsWith(q)) return 90;
|
|
4008
|
+
if (title.split(/\s+/).some((w) => w.startsWith(q))) return 80;
|
|
4009
|
+
if (terms.some((t) => t === q)) return 70;
|
|
4010
|
+
if (terms.some((t) => t.startsWith(q))) return 60;
|
|
4011
|
+
if (title.includes(q)) return 40;
|
|
4012
|
+
if (terms.some((t) => t.includes(q))) return 30;
|
|
4013
|
+
if (description.includes(q)) return 20;
|
|
4014
|
+
return 0;
|
|
4015
|
+
}
|
|
4016
|
+
function filterAndRankItems(items, query) {
|
|
4017
|
+
const trimmed = query.trim();
|
|
4018
|
+
if (!trimmed) return items;
|
|
4019
|
+
const scored = items.map((item) => ({
|
|
4020
|
+
item,
|
|
4021
|
+
score: scoreItem(item, trimmed)
|
|
4022
|
+
})).filter(({ score }) => score > 0);
|
|
4023
|
+
scored.sort((a, b) => b.score - a.score);
|
|
4024
|
+
return scored.map(({ item }) => item);
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
//#endregion
|
|
4028
|
+
//#region src/ui/slash-command/create-slash-command.ts
|
|
4029
|
+
function defaultFilterItems(items, query, editor) {
|
|
4030
|
+
return filterAndRankItems(isAtMaxColumnsDepth(editor) ? items.filter((item) => item.category !== "Layout" || !item.title.includes("column")) : items, query);
|
|
4031
|
+
}
|
|
4032
|
+
function createSlashCommand(options) {
|
|
4033
|
+
const items = options?.items ?? defaultSlashCommands;
|
|
4034
|
+
const filterFn = options?.filterItems ?? defaultFilterItems;
|
|
4035
|
+
return SlashCommandExtension.configure({ suggestion: {
|
|
4036
|
+
items: ({ query, editor }) => filterFn(items, query, editor),
|
|
4037
|
+
render: createRenderItems(options?.component)
|
|
4038
|
+
} });
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
//#endregion
|
|
4042
|
+
//#region src/ui/slash-command/index.ts
|
|
4043
|
+
const SlashCommand = createSlashCommand();
|
|
4044
|
+
|
|
4045
|
+
//#endregion
|
|
4046
|
+
export { AlignmentAttribute, BULLET_LIST, BUTTON, Blockquote, Body, Bold, BubbleMenu, BubbleMenuAlignCenter, BubbleMenuAlignLeft, BubbleMenuAlignRight, BubbleMenuBold, BubbleMenuCode, BubbleMenuDefault, BubbleMenuItalic, BubbleMenuItem, BubbleMenuItemGroup, BubbleMenuLinkSelector, BubbleMenuNodeSelector, BubbleMenuRoot, BubbleMenuSeparator, BubbleMenuStrike, BubbleMenuUnderline, BubbleMenuUppercase, BulletList, Button, ButtonBubbleMenu, ButtonBubbleMenuDefault, ButtonBubbleMenuEditLink, ButtonBubbleMenuRoot, ButtonBubbleMenuToolbar, CODE, COLUMN_PARENT_TYPES, ClassAttribute, Code, CodeBlockPrism, ColumnsColumn, CommandList, DIVIDER, Div, Divider, EmailNode, FOUR_COLUMNS, FourColumns, GlobalContent, H1, H2, H3, HardBreak, Heading, ImageBubbleMenu, ImageBubbleMenuDefault, ImageBubbleMenuEditLink, ImageBubbleMenuRoot, ImageBubbleMenuToolbar, Italic, Link, LinkBubbleMenu, LinkBubbleMenuDefault, LinkBubbleMenuEditLink, LinkBubbleMenuForm, LinkBubbleMenuOpenLink, LinkBubbleMenuRoot, LinkBubbleMenuToolbar, LinkBubbleMenuUnlink, ListItem, MAX_COLUMNS_DEPTH, MaxNesting, NUMBERED_LIST, NodeSelectorContent, NodeSelectorRoot, NodeSelectorTrigger, OrderedList, Paragraph, Placeholder, PreservedStyle, PreviewText, QUOTE, SECTION, Section, SlashCommand, StarterKit, Strike, StyleAttribute, Sup, TEXT, THREE_COLUMNS, TWO_COLUMNS, Table, TableCell, TableHeader, TableRow, ThreeColumns, TwoColumns, Underline, Uppercase, composeReactEmail, createSlashCommand, defaultSlashCommands, editorEventBus, filterAndRankItems, getColumnsDepth, getGlobalContent, isAtMaxColumnsDepth, isDocumentVisuallyEmpty, isInsideNode, processStylesForUnlink, scoreItem, setTextAlignment, useButtonBubbleMenuContext, useEditor, useImageBubbleMenuContext, useLinkBubbleMenuContext };
|
|
1350
4047
|
//# sourceMappingURL=index.mjs.map
|