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