@react-email/editor 0.0.0-experimental.1 → 0.0.0-experimental.10
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.d.mts +686 -21
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +687 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2166 -12
- package/dist/index.mjs +2109 -15
- package/dist/index.mjs.map +1 -1
- package/dist/ui/bubble-menu/bubble-menu.css +131 -0
- package/dist/ui/link-bubble-menu/link-bubble-menu.css +66 -0
- package/package.json +21 -10
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
import { Node, findChildren, mergeAttributes } from "@tiptap/core";
|
|
2
|
-
import {
|
|
1
|
+
import { Extension, Mark, Node, findChildren, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
|
|
2
|
+
import { StarterKit } from "@tiptap/starter-kit";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
4
|
import * as ReactEmailComponents from "@react-email/components";
|
|
4
5
|
import { Button as Button$1, CodeBlock, Column, Row, Section as Section$1 } from "@react-email/components";
|
|
5
6
|
import CodeBlock$1 from "@tiptap/extension-code-block";
|
|
6
|
-
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
7
|
+
import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
|
7
8
|
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
8
9
|
import { fromHtml } from "hast-util-from-html";
|
|
9
10
|
import Prism from "prismjs";
|
|
11
|
+
import TipTapPlaceholder from "@tiptap/extension-placeholder";
|
|
12
|
+
import { useCurrentEditor, useEditorState } from "@tiptap/react";
|
|
13
|
+
import { AlignCenterIcon, AlignLeftIcon, AlignRightIcon, BoldIcon, CaseUpperIcon, Check, ChevronDown, Code, CodeIcon, ExternalLinkIcon, Heading1, Heading2, Heading3, ItalicIcon, LinkIcon, List, ListOrdered, PencilIcon, StrikethroughIcon, TextIcon, TextQuote, UnderlineIcon, UnlinkIcon } from "lucide-react";
|
|
14
|
+
import * as React from "react";
|
|
15
|
+
import * as Popover from "@radix-ui/react-popover";
|
|
16
|
+
import { BubbleMenu as BubbleMenu$1 } from "@tiptap/react/menus";
|
|
10
17
|
|
|
11
18
|
//#region src/core/email-node.ts
|
|
12
19
|
var EmailNode = class EmailNode extends Node {
|
|
@@ -38,6 +45,103 @@ var EmailNode = class EmailNode extends Node {
|
|
|
38
45
|
}
|
|
39
46
|
};
|
|
40
47
|
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/core/event-bus.ts
|
|
50
|
+
const EVENT_PREFIX = "@react-email/editor:";
|
|
51
|
+
var EditorEventBus = class {
|
|
52
|
+
prefixEventName(eventName) {
|
|
53
|
+
return `${EVENT_PREFIX}${String(eventName)}`;
|
|
54
|
+
}
|
|
55
|
+
dispatch(eventName, payload, options) {
|
|
56
|
+
const target = options?.target ?? window;
|
|
57
|
+
const prefixedEventName = this.prefixEventName(eventName);
|
|
58
|
+
const event = new CustomEvent(prefixedEventName, {
|
|
59
|
+
detail: payload,
|
|
60
|
+
bubbles: false,
|
|
61
|
+
cancelable: false
|
|
62
|
+
});
|
|
63
|
+
target.dispatchEvent(event);
|
|
64
|
+
}
|
|
65
|
+
on(eventName, handler, options) {
|
|
66
|
+
const target = options?.target ?? window;
|
|
67
|
+
const prefixedEventName = this.prefixEventName(eventName);
|
|
68
|
+
const abortController = new AbortController();
|
|
69
|
+
const wrappedHandler = (event) => {
|
|
70
|
+
const customEvent = event;
|
|
71
|
+
const result = handler(customEvent.detail);
|
|
72
|
+
if (result instanceof Promise) result.catch((error) => {
|
|
73
|
+
console.error(`Error in async event handler for ${prefixedEventName}:`, {
|
|
74
|
+
event: customEvent.detail,
|
|
75
|
+
error
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
target.addEventListener(prefixedEventName, wrappedHandler, {
|
|
80
|
+
...options,
|
|
81
|
+
signal: abortController.signal
|
|
82
|
+
});
|
|
83
|
+
return { unsubscribe: () => {
|
|
84
|
+
abortController.abort();
|
|
85
|
+
} };
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const editorEventBus = new EditorEventBus();
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/extensions/alignment-attribute.tsx
|
|
92
|
+
const AlignmentAttribute = Extension.create({
|
|
93
|
+
name: "alignmentAttribute",
|
|
94
|
+
addOptions() {
|
|
95
|
+
return {
|
|
96
|
+
types: [],
|
|
97
|
+
alignments: [
|
|
98
|
+
"left",
|
|
99
|
+
"center",
|
|
100
|
+
"right",
|
|
101
|
+
"justify"
|
|
102
|
+
]
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
addGlobalAttributes() {
|
|
106
|
+
return [{
|
|
107
|
+
types: this.options.types,
|
|
108
|
+
attributes: { alignment: {
|
|
109
|
+
parseHTML: (element) => {
|
|
110
|
+
const explicitAlign = element.getAttribute("align") || element.getAttribute("alignment") || element.style.textAlign;
|
|
111
|
+
if (explicitAlign && this.options.alignments.includes(explicitAlign)) return explicitAlign;
|
|
112
|
+
return null;
|
|
113
|
+
},
|
|
114
|
+
renderHTML: (attributes) => {
|
|
115
|
+
if (attributes.alignment === "left") return {};
|
|
116
|
+
return { alignment: attributes.alignment };
|
|
117
|
+
}
|
|
118
|
+
} }
|
|
119
|
+
}];
|
|
120
|
+
},
|
|
121
|
+
addCommands() {
|
|
122
|
+
return { setAlignment: (alignment) => ({ commands }) => {
|
|
123
|
+
if (!this.options.alignments.includes(alignment)) return false;
|
|
124
|
+
return this.options.types.every((type) => commands.updateAttributes(type, { alignment }));
|
|
125
|
+
} };
|
|
126
|
+
},
|
|
127
|
+
addKeyboardShortcuts() {
|
|
128
|
+
return {
|
|
129
|
+
Enter: () => {
|
|
130
|
+
const { from } = this.editor.state.selection;
|
|
131
|
+
const currentAlignment = this.editor.state.doc.nodeAt(from)?.attrs?.alignment;
|
|
132
|
+
if (currentAlignment) requestAnimationFrame(() => {
|
|
133
|
+
this.editor.commands.setAlignment(currentAlignment);
|
|
134
|
+
});
|
|
135
|
+
return false;
|
|
136
|
+
},
|
|
137
|
+
"Mod-Shift-l": () => this.editor.commands.setAlignment("left"),
|
|
138
|
+
"Mod-Shift-e": () => this.editor.commands.setAlignment("center"),
|
|
139
|
+
"Mod-Shift-r": () => this.editor.commands.setAlignment("right"),
|
|
140
|
+
"Mod-Shift-j": () => this.editor.commands.setAlignment("justify")
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
41
145
|
//#endregion
|
|
42
146
|
//#region src/utils/attribute-helpers.ts
|
|
43
147
|
/**
|
|
@@ -94,6 +198,14 @@ const LAYOUT_ATTRIBUTES = [
|
|
|
94
198
|
"height"
|
|
95
199
|
];
|
|
96
200
|
/**
|
|
201
|
+
* Table-specific HTML attributes used for table layout and styling.
|
|
202
|
+
*/
|
|
203
|
+
const TABLE_ATTRIBUTES = [
|
|
204
|
+
"border",
|
|
205
|
+
"cellpadding",
|
|
206
|
+
"cellspacing"
|
|
207
|
+
];
|
|
208
|
+
/**
|
|
97
209
|
* Table cell-specific HTML attributes.
|
|
98
210
|
*/
|
|
99
211
|
const TABLE_CELL_ATTRIBUTES = [
|
|
@@ -110,6 +222,7 @@ const TABLE_HEADER_ATTRIBUTES = [...TABLE_CELL_ATTRIBUTES, "scope"];
|
|
|
110
222
|
|
|
111
223
|
//#endregion
|
|
112
224
|
//#region src/utils/styles.ts
|
|
225
|
+
const WHITE_SPACE_REGEX = /\s+/;
|
|
113
226
|
const inlineCssToJs = (inlineStyle, options = {}) => {
|
|
114
227
|
const styleObject = {};
|
|
115
228
|
if (!inlineStyle || inlineStyle === "" || typeof inlineStyle === "object") return styleObject;
|
|
@@ -124,6 +237,157 @@ const inlineCssToJs = (inlineStyle, options = {}) => {
|
|
|
124
237
|
});
|
|
125
238
|
return styleObject;
|
|
126
239
|
};
|
|
240
|
+
/**
|
|
241
|
+
* Expands CSS shorthand properties (margin, padding) into their longhand equivalents.
|
|
242
|
+
* This prevents shorthand properties from overriding specific longhand properties in email clients.
|
|
243
|
+
*
|
|
244
|
+
* @param styles - Style object that may contain shorthand properties
|
|
245
|
+
* @returns New style object with shorthand properties expanded to longhand
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* expandShorthandProperties({ margin: '0', paddingTop: '10px' })
|
|
249
|
+
* // Returns: { marginTop: '0', marginRight: '0', marginBottom: '0', marginLeft: '0', paddingTop: '10px' }
|
|
250
|
+
*/
|
|
251
|
+
function expandShorthandProperties(styles) {
|
|
252
|
+
if (!styles || typeof styles !== "object") return {};
|
|
253
|
+
const expanded = {};
|
|
254
|
+
for (const key in styles) {
|
|
255
|
+
const value = styles[key];
|
|
256
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
257
|
+
switch (key) {
|
|
258
|
+
case "margin": {
|
|
259
|
+
const values = parseShorthandValue(value);
|
|
260
|
+
expanded.marginTop = values.top;
|
|
261
|
+
expanded.marginRight = values.right;
|
|
262
|
+
expanded.marginBottom = values.bottom;
|
|
263
|
+
expanded.marginLeft = values.left;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
case "padding": {
|
|
267
|
+
const values = parseShorthandValue(value);
|
|
268
|
+
expanded.paddingTop = values.top;
|
|
269
|
+
expanded.paddingRight = values.right;
|
|
270
|
+
expanded.paddingBottom = values.bottom;
|
|
271
|
+
expanded.paddingLeft = values.left;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
case "border": {
|
|
275
|
+
const values = convertBorderValue(value);
|
|
276
|
+
expanded.borderStyle = values.style;
|
|
277
|
+
expanded.borderWidth = values.width;
|
|
278
|
+
expanded.borderColor = values.color;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case "borderTopLeftRadius":
|
|
282
|
+
case "borderTopRightRadius":
|
|
283
|
+
case "borderBottomLeftRadius":
|
|
284
|
+
case "borderBottomRightRadius":
|
|
285
|
+
expanded[key] = value;
|
|
286
|
+
if (styles.borderTopLeftRadius && styles.borderTopRightRadius && styles.borderBottomLeftRadius && styles.borderBottomRightRadius) {
|
|
287
|
+
const values = [
|
|
288
|
+
styles.borderTopLeftRadius,
|
|
289
|
+
styles.borderTopRightRadius,
|
|
290
|
+
styles.borderBottomLeftRadius,
|
|
291
|
+
styles.borderBottomRightRadius
|
|
292
|
+
];
|
|
293
|
+
if (new Set(values).size === 1) expanded.borderRadius = values[0];
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
296
|
+
default: expanded[key] = value;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return expanded;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Parses CSS shorthand value (1-4 values) into individual side values.
|
|
303
|
+
* Follows CSS specification for shorthand property value parsing.
|
|
304
|
+
*
|
|
305
|
+
* @param value - Shorthand value string (e.g., '0', '10px 20px', '5px 10px 15px 20px')
|
|
306
|
+
* @returns Object with top, right, bottom, left values
|
|
307
|
+
*/
|
|
308
|
+
function parseShorthandValue(value) {
|
|
309
|
+
const stringValue = String(value).trim();
|
|
310
|
+
const parts = stringValue.split(WHITE_SPACE_REGEX);
|
|
311
|
+
const len = parts.length;
|
|
312
|
+
if (len === 1) return {
|
|
313
|
+
top: parts[0],
|
|
314
|
+
right: parts[0],
|
|
315
|
+
bottom: parts[0],
|
|
316
|
+
left: parts[0]
|
|
317
|
+
};
|
|
318
|
+
if (len === 2) return {
|
|
319
|
+
top: parts[0],
|
|
320
|
+
right: parts[1],
|
|
321
|
+
bottom: parts[0],
|
|
322
|
+
left: parts[1]
|
|
323
|
+
};
|
|
324
|
+
if (len === 3) return {
|
|
325
|
+
top: parts[0],
|
|
326
|
+
right: parts[1],
|
|
327
|
+
bottom: parts[2],
|
|
328
|
+
left: parts[1]
|
|
329
|
+
};
|
|
330
|
+
if (len === 4) return {
|
|
331
|
+
top: parts[0],
|
|
332
|
+
right: parts[1],
|
|
333
|
+
bottom: parts[2],
|
|
334
|
+
left: parts[3]
|
|
335
|
+
};
|
|
336
|
+
return {
|
|
337
|
+
top: stringValue,
|
|
338
|
+
right: stringValue,
|
|
339
|
+
bottom: stringValue,
|
|
340
|
+
left: stringValue
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function convertBorderValue(value) {
|
|
344
|
+
const stringValue = String(value).trim();
|
|
345
|
+
const parts = stringValue.split(WHITE_SPACE_REGEX);
|
|
346
|
+
switch (parts.length) {
|
|
347
|
+
case 1: return {
|
|
348
|
+
style: "solid",
|
|
349
|
+
width: parts[0],
|
|
350
|
+
color: "black"
|
|
351
|
+
};
|
|
352
|
+
case 2: return {
|
|
353
|
+
style: parts[1],
|
|
354
|
+
width: parts[0],
|
|
355
|
+
color: "black"
|
|
356
|
+
};
|
|
357
|
+
case 3: return {
|
|
358
|
+
style: parts[1],
|
|
359
|
+
width: parts[0],
|
|
360
|
+
color: parts[2]
|
|
361
|
+
};
|
|
362
|
+
case 4: return {
|
|
363
|
+
style: parts[1],
|
|
364
|
+
width: parts[0],
|
|
365
|
+
color: parts[2]
|
|
366
|
+
};
|
|
367
|
+
default: return {
|
|
368
|
+
style: "solid",
|
|
369
|
+
width: stringValue,
|
|
370
|
+
color: "black"
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Resolves conflicts between reset styles and inline styles by expanding
|
|
376
|
+
* shorthand properties (margin, padding) to longhand before merging.
|
|
377
|
+
* This prevents shorthand properties from overriding specific longhand properties.
|
|
378
|
+
*
|
|
379
|
+
* @param resetStyles - Base reset styles that may contain shorthand properties
|
|
380
|
+
* @param inlineStyles - Inline styles that should override reset styles
|
|
381
|
+
* @returns Merged styles with inline styles taking precedence
|
|
382
|
+
*/
|
|
383
|
+
function resolveConflictingStyles(resetStyles, inlineStyles) {
|
|
384
|
+
const expandedResetStyles = expandShorthandProperties(resetStyles);
|
|
385
|
+
const expandedInlineStyles = expandShorthandProperties(inlineStyles);
|
|
386
|
+
return {
|
|
387
|
+
...expandedResetStyles,
|
|
388
|
+
...expandedInlineStyles
|
|
389
|
+
};
|
|
390
|
+
}
|
|
127
391
|
|
|
128
392
|
//#endregion
|
|
129
393
|
//#region src/extensions/body.tsx
|
|
@@ -157,12 +421,12 @@ const Body = EmailNode.create({
|
|
|
157
421
|
0
|
|
158
422
|
];
|
|
159
423
|
},
|
|
160
|
-
renderToReactEmail({ children, node,
|
|
424
|
+
renderToReactEmail({ children, node, style }) {
|
|
161
425
|
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
162
426
|
return /* @__PURE__ */ jsx("div", {
|
|
163
427
|
className: node.attrs?.class || void 0,
|
|
164
428
|
style: {
|
|
165
|
-
...
|
|
429
|
+
...style,
|
|
166
430
|
...inlineStyles
|
|
167
431
|
},
|
|
168
432
|
children
|
|
@@ -170,6 +434,92 @@ const Body = EmailNode.create({
|
|
|
170
434
|
}
|
|
171
435
|
});
|
|
172
436
|
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/extensions/bold.ts
|
|
439
|
+
/**
|
|
440
|
+
* Matches bold text via `**` as input.
|
|
441
|
+
*/
|
|
442
|
+
const starInputRegex = /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))$/;
|
|
443
|
+
/**
|
|
444
|
+
* Matches bold text via `**` while pasting.
|
|
445
|
+
*/
|
|
446
|
+
const starPasteRegex = /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))/g;
|
|
447
|
+
/**
|
|
448
|
+
* Matches bold text via `__` as input.
|
|
449
|
+
*/
|
|
450
|
+
const underscoreInputRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))$/;
|
|
451
|
+
/**
|
|
452
|
+
* Matches bold text via `__` while pasting.
|
|
453
|
+
*/
|
|
454
|
+
const underscorePasteRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))/g;
|
|
455
|
+
/**
|
|
456
|
+
* This extension allows you to mark text as bold.
|
|
457
|
+
* @see https://tiptap.dev/api/marks/bold
|
|
458
|
+
*/
|
|
459
|
+
const Bold = Mark.create({
|
|
460
|
+
name: "bold",
|
|
461
|
+
addOptions() {
|
|
462
|
+
return { HTMLAttributes: {} };
|
|
463
|
+
},
|
|
464
|
+
parseHTML() {
|
|
465
|
+
return [
|
|
466
|
+
{ tag: "strong" },
|
|
467
|
+
{
|
|
468
|
+
tag: "b",
|
|
469
|
+
getAttrs: (node) => node.style.fontWeight !== "normal" && null
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
style: "font-weight=400",
|
|
473
|
+
clearMark: (mark) => mark.type.name === this.name
|
|
474
|
+
}
|
|
475
|
+
];
|
|
476
|
+
},
|
|
477
|
+
renderHTML({ HTMLAttributes }) {
|
|
478
|
+
return [
|
|
479
|
+
"strong",
|
|
480
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
|
481
|
+
0
|
|
482
|
+
];
|
|
483
|
+
},
|
|
484
|
+
addCommands() {
|
|
485
|
+
return {
|
|
486
|
+
setBold: () => ({ commands }) => {
|
|
487
|
+
return commands.setMark(this.name);
|
|
488
|
+
},
|
|
489
|
+
toggleBold: () => ({ commands }) => {
|
|
490
|
+
return commands.toggleMark(this.name);
|
|
491
|
+
},
|
|
492
|
+
unsetBold: () => ({ commands }) => {
|
|
493
|
+
return commands.unsetMark(this.name);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
},
|
|
497
|
+
addKeyboardShortcuts() {
|
|
498
|
+
return {
|
|
499
|
+
"Mod-b": () => this.editor.commands.toggleBold(),
|
|
500
|
+
"Mod-B": () => this.editor.commands.toggleBold()
|
|
501
|
+
};
|
|
502
|
+
},
|
|
503
|
+
addInputRules() {
|
|
504
|
+
return [markInputRule({
|
|
505
|
+
find: starInputRegex,
|
|
506
|
+
type: this.type
|
|
507
|
+
}), markInputRule({
|
|
508
|
+
find: underscoreInputRegex,
|
|
509
|
+
type: this.type
|
|
510
|
+
})];
|
|
511
|
+
},
|
|
512
|
+
addPasteRules() {
|
|
513
|
+
return [markPasteRule({
|
|
514
|
+
find: starPasteRegex,
|
|
515
|
+
type: this.type
|
|
516
|
+
}), markPasteRule({
|
|
517
|
+
find: underscorePasteRegex,
|
|
518
|
+
type: this.type
|
|
519
|
+
})];
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
173
523
|
//#endregion
|
|
174
524
|
//#region src/extensions/button.tsx
|
|
175
525
|
const Button = EmailNode.create({
|
|
@@ -232,7 +582,7 @@ const Button = EmailNode.create({
|
|
|
232
582
|
}
|
|
233
583
|
};
|
|
234
584
|
},
|
|
235
|
-
renderToReactEmail({ children, node,
|
|
585
|
+
renderToReactEmail({ children, node, style }) {
|
|
236
586
|
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
237
587
|
return /* @__PURE__ */ jsx(Row, { children: /* @__PURE__ */ jsx(Column, {
|
|
238
588
|
align: node.attrs?.align || node.attrs?.alignment,
|
|
@@ -240,8 +590,7 @@ const Button = EmailNode.create({
|
|
|
240
590
|
className: node.attrs?.class || void 0,
|
|
241
591
|
href: node.attrs?.href,
|
|
242
592
|
style: {
|
|
243
|
-
...
|
|
244
|
-
...styles.button,
|
|
593
|
+
...style,
|
|
245
594
|
...inlineStyles
|
|
246
595
|
},
|
|
247
596
|
children
|
|
@@ -250,6 +599,48 @@ const Button = EmailNode.create({
|
|
|
250
599
|
}
|
|
251
600
|
});
|
|
252
601
|
|
|
602
|
+
//#endregion
|
|
603
|
+
//#region src/extensions/class-attribute.tsx
|
|
604
|
+
const ClassAttribute = Extension.create({
|
|
605
|
+
name: "classAttribute",
|
|
606
|
+
addOptions() {
|
|
607
|
+
return {
|
|
608
|
+
types: [],
|
|
609
|
+
class: []
|
|
610
|
+
};
|
|
611
|
+
},
|
|
612
|
+
addGlobalAttributes() {
|
|
613
|
+
return [{
|
|
614
|
+
types: this.options.types,
|
|
615
|
+
attributes: { class: {
|
|
616
|
+
default: "",
|
|
617
|
+
parseHTML: (element) => element.className || "",
|
|
618
|
+
renderHTML: (attributes) => {
|
|
619
|
+
return attributes.class ? { class: attributes.class } : {};
|
|
620
|
+
}
|
|
621
|
+
} }
|
|
622
|
+
}];
|
|
623
|
+
},
|
|
624
|
+
addCommands() {
|
|
625
|
+
return {
|
|
626
|
+
unsetClass: () => ({ commands }) => {
|
|
627
|
+
return this.options.types.every((type) => commands.resetAttributes(type, "class"));
|
|
628
|
+
},
|
|
629
|
+
setClass: (classList) => ({ commands }) => {
|
|
630
|
+
return this.options.types.every((type) => commands.updateAttributes(type, { class: classList }));
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
},
|
|
634
|
+
addKeyboardShortcuts() {
|
|
635
|
+
return { Enter: ({ editor }) => {
|
|
636
|
+
requestAnimationFrame(() => {
|
|
637
|
+
editor.commands.resetAttributes("paragraph", "class");
|
|
638
|
+
});
|
|
639
|
+
return false;
|
|
640
|
+
} };
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
|
|
253
644
|
//#endregion
|
|
254
645
|
//#region src/utils/prism-utils.ts
|
|
255
646
|
const publicURL = "/styles/prism";
|
|
@@ -428,6 +819,25 @@ const CodeBlockPrism = EmailNode.from(CodeBlock$1.extend({
|
|
|
428
819
|
]
|
|
429
820
|
];
|
|
430
821
|
},
|
|
822
|
+
addKeyboardShortcuts() {
|
|
823
|
+
return {
|
|
824
|
+
...this.parent?.(),
|
|
825
|
+
"Mod-a": ({ editor }) => {
|
|
826
|
+
const { state } = editor;
|
|
827
|
+
const { selection } = state;
|
|
828
|
+
const { $from } = selection;
|
|
829
|
+
for (let depth = $from.depth; depth >= 1; depth--) if ($from.node(depth).type.name === this.name) {
|
|
830
|
+
const blockStart = $from.start(depth);
|
|
831
|
+
const blockEnd = $from.end(depth);
|
|
832
|
+
if (selection.from === blockStart && selection.to === blockEnd) return false;
|
|
833
|
+
const tr = state.tr.setSelection(TextSelection.create(state.doc, blockStart, blockEnd));
|
|
834
|
+
editor.view.dispatch(tr);
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
},
|
|
431
841
|
addProseMirrorPlugins() {
|
|
432
842
|
return [...this.parent?.() || [], PrismPlugin({
|
|
433
843
|
name: this.name,
|
|
@@ -435,7 +845,7 @@ const CodeBlockPrism = EmailNode.from(CodeBlock$1.extend({
|
|
|
435
845
|
defaultTheme: this.options.defaultTheme
|
|
436
846
|
})];
|
|
437
847
|
}
|
|
438
|
-
}), ({ node,
|
|
848
|
+
}), ({ node, style }) => {
|
|
439
849
|
const language = node.attrs?.language ? `${node.attrs.language}` : "javascript";
|
|
440
850
|
const userTheme = ReactEmailComponents[node.attrs?.theme];
|
|
441
851
|
const theme = userTheme ? {
|
|
@@ -459,7 +869,7 @@ const CodeBlockPrism = EmailNode.from(CodeBlock$1.extend({
|
|
|
459
869
|
theme,
|
|
460
870
|
style: {
|
|
461
871
|
width: "auto",
|
|
462
|
-
...
|
|
872
|
+
...style
|
|
463
873
|
}
|
|
464
874
|
});
|
|
465
875
|
});
|
|
@@ -496,12 +906,12 @@ const Div = EmailNode.create({
|
|
|
496
906
|
addAttributes() {
|
|
497
907
|
return { ...createStandardAttributes([...COMMON_HTML_ATTRIBUTES, ...LAYOUT_ATTRIBUTES]) };
|
|
498
908
|
},
|
|
499
|
-
renderToReactEmail({ children, node,
|
|
909
|
+
renderToReactEmail({ children, node, style }) {
|
|
500
910
|
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
501
911
|
return /* @__PURE__ */ jsx("div", {
|
|
502
912
|
className: node.attrs?.class || void 0,
|
|
503
913
|
style: {
|
|
504
|
-
...
|
|
914
|
+
...style,
|
|
505
915
|
...inlineStyles
|
|
506
916
|
},
|
|
507
917
|
children
|
|
@@ -509,6 +919,206 @@ const Div = EmailNode.create({
|
|
|
509
919
|
}
|
|
510
920
|
});
|
|
511
921
|
|
|
922
|
+
//#endregion
|
|
923
|
+
//#region src/extensions/max-nesting.ts
|
|
924
|
+
const MaxNesting = Extension.create({
|
|
925
|
+
name: "maxNesting",
|
|
926
|
+
addOptions() {
|
|
927
|
+
return {
|
|
928
|
+
maxDepth: 3,
|
|
929
|
+
nodeTypes: void 0
|
|
930
|
+
};
|
|
931
|
+
},
|
|
932
|
+
addProseMirrorPlugins() {
|
|
933
|
+
const { maxDepth, nodeTypes } = this.options;
|
|
934
|
+
if (typeof maxDepth !== "number" || maxDepth < 1) throw new Error("maxDepth must be a positive number");
|
|
935
|
+
return [new Plugin({
|
|
936
|
+
key: new PluginKey("maxNesting"),
|
|
937
|
+
appendTransaction(transactions, _oldState, newState) {
|
|
938
|
+
if (!transactions.some((tr$1) => tr$1.docChanged)) return null;
|
|
939
|
+
const rangesToLift = [];
|
|
940
|
+
newState.doc.descendants((node, pos) => {
|
|
941
|
+
let depth = 0;
|
|
942
|
+
let currentPos = pos;
|
|
943
|
+
let currentNode = node;
|
|
944
|
+
while (currentNode && depth <= maxDepth) {
|
|
945
|
+
if (!nodeTypes || nodeTypes.includes(currentNode.type.name)) depth++;
|
|
946
|
+
const $pos = newState.doc.resolve(currentPos);
|
|
947
|
+
if ($pos.depth === 0) break;
|
|
948
|
+
currentPos = $pos.before($pos.depth);
|
|
949
|
+
currentNode = newState.doc.nodeAt(currentPos);
|
|
950
|
+
}
|
|
951
|
+
if (depth > maxDepth) {
|
|
952
|
+
const $pos = newState.doc.resolve(pos);
|
|
953
|
+
if ($pos.depth > 0) {
|
|
954
|
+
const range = $pos.blockRange();
|
|
955
|
+
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({
|
|
956
|
+
range,
|
|
957
|
+
target: range.start - 1
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
if (rangesToLift.length === 0) return null;
|
|
963
|
+
const tr = newState.tr;
|
|
964
|
+
for (let i = rangesToLift.length - 1; i >= 0; i--) {
|
|
965
|
+
const { range, target } = rangesToLift[i];
|
|
966
|
+
tr.lift(range, target);
|
|
967
|
+
}
|
|
968
|
+
return tr;
|
|
969
|
+
},
|
|
970
|
+
filterTransaction(tr) {
|
|
971
|
+
if (!tr.docChanged) return true;
|
|
972
|
+
let wouldCreateDeepNesting = false;
|
|
973
|
+
const newDoc = tr.doc;
|
|
974
|
+
newDoc.descendants((node, pos) => {
|
|
975
|
+
if (wouldCreateDeepNesting) return false;
|
|
976
|
+
let depth = 0;
|
|
977
|
+
let currentPos = pos;
|
|
978
|
+
let currentNode = node;
|
|
979
|
+
while (currentNode && depth <= maxDepth) {
|
|
980
|
+
if (!nodeTypes || nodeTypes.includes(currentNode.type.name)) depth++;
|
|
981
|
+
const $pos = newDoc.resolve(currentPos);
|
|
982
|
+
if ($pos.depth === 0) break;
|
|
983
|
+
currentPos = $pos.before($pos.depth);
|
|
984
|
+
currentNode = newDoc.nodeAt(currentPos);
|
|
985
|
+
}
|
|
986
|
+
if (depth > maxDepth) {
|
|
987
|
+
wouldCreateDeepNesting = true;
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
return !wouldCreateDeepNesting;
|
|
992
|
+
}
|
|
993
|
+
})];
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
//#endregion
|
|
998
|
+
//#region src/extensions/placeholder.ts
|
|
999
|
+
const Placeholder = TipTapPlaceholder.configure({
|
|
1000
|
+
placeholder: ({ node }) => {
|
|
1001
|
+
if (node.type.name === "heading") return `Heading ${node.attrs.level}`;
|
|
1002
|
+
return "Press '/' for commands";
|
|
1003
|
+
},
|
|
1004
|
+
includeChildren: true
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
//#endregion
|
|
1008
|
+
//#region src/extensions/preserved-style.ts
|
|
1009
|
+
const PreservedStyle = Mark.create({
|
|
1010
|
+
name: "preservedStyle",
|
|
1011
|
+
addAttributes() {
|
|
1012
|
+
return { style: {
|
|
1013
|
+
default: null,
|
|
1014
|
+
parseHTML: (element) => element.getAttribute("style"),
|
|
1015
|
+
renderHTML: (attributes) => {
|
|
1016
|
+
if (!attributes.style) return {};
|
|
1017
|
+
return { style: attributes.style };
|
|
1018
|
+
}
|
|
1019
|
+
} };
|
|
1020
|
+
},
|
|
1021
|
+
parseHTML() {
|
|
1022
|
+
return [{
|
|
1023
|
+
tag: "span[style]",
|
|
1024
|
+
getAttrs: (element) => {
|
|
1025
|
+
if (typeof element === "string") return false;
|
|
1026
|
+
const style = element.getAttribute("style");
|
|
1027
|
+
if (style && hasPreservableStyles(style)) return { style };
|
|
1028
|
+
return false;
|
|
1029
|
+
}
|
|
1030
|
+
}];
|
|
1031
|
+
},
|
|
1032
|
+
renderHTML({ HTMLAttributes }) {
|
|
1033
|
+
return [
|
|
1034
|
+
"span",
|
|
1035
|
+
mergeAttributes(HTMLAttributes),
|
|
1036
|
+
0
|
|
1037
|
+
];
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
const LINK_INDICATOR_STYLES = [
|
|
1041
|
+
"color",
|
|
1042
|
+
"text-decoration",
|
|
1043
|
+
"text-decoration-line",
|
|
1044
|
+
"text-decoration-color",
|
|
1045
|
+
"text-decoration-style"
|
|
1046
|
+
];
|
|
1047
|
+
function parseStyleString(styleString) {
|
|
1048
|
+
const temp = document.createElement("div");
|
|
1049
|
+
temp.style.cssText = styleString;
|
|
1050
|
+
return temp.style;
|
|
1051
|
+
}
|
|
1052
|
+
function hasBackground(style) {
|
|
1053
|
+
const bgColor = style.backgroundColor;
|
|
1054
|
+
const bg = style.background;
|
|
1055
|
+
if (bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)") return true;
|
|
1056
|
+
if (bg && bg !== "transparent" && bg !== "none" && bg !== "rgba(0, 0, 0, 0)") return true;
|
|
1057
|
+
return false;
|
|
1058
|
+
}
|
|
1059
|
+
function hasPreservableStyles(styleString) {
|
|
1060
|
+
return processStylesForUnlink(styleString) !== null;
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Processes styles when unlinking:
|
|
1064
|
+
* - Has background (button-like): preserve all styles
|
|
1065
|
+
* - No background: strip link-indicator styles (color, text-decoration), keep the rest
|
|
1066
|
+
*/
|
|
1067
|
+
function processStylesForUnlink(styleString) {
|
|
1068
|
+
if (!styleString) return null;
|
|
1069
|
+
const style = parseStyleString(styleString);
|
|
1070
|
+
if (hasBackground(style)) return styleString;
|
|
1071
|
+
const filtered = [];
|
|
1072
|
+
for (let i = 0; i < style.length; i++) {
|
|
1073
|
+
const prop = style[i];
|
|
1074
|
+
if (LINK_INDICATOR_STYLES.includes(prop)) continue;
|
|
1075
|
+
const value = style.getPropertyValue(prop);
|
|
1076
|
+
if (value) filtered.push(`${prop}: ${value}`);
|
|
1077
|
+
}
|
|
1078
|
+
return filtered.length > 0 ? filtered.join("; ") : null;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
//#endregion
|
|
1082
|
+
//#region src/extensions/preview-text.ts
|
|
1083
|
+
const PreviewText = Node.create({
|
|
1084
|
+
name: "previewText",
|
|
1085
|
+
group: "block",
|
|
1086
|
+
selectable: false,
|
|
1087
|
+
draggable: false,
|
|
1088
|
+
atom: true,
|
|
1089
|
+
addOptions() {
|
|
1090
|
+
return { HTMLAttributes: {} };
|
|
1091
|
+
},
|
|
1092
|
+
addStorage() {
|
|
1093
|
+
return { previewText: null };
|
|
1094
|
+
},
|
|
1095
|
+
renderHTML() {
|
|
1096
|
+
return ["div", { style: "display: none" }];
|
|
1097
|
+
},
|
|
1098
|
+
parseHTML() {
|
|
1099
|
+
return [{
|
|
1100
|
+
tag: "div[data-skip-in-text=\"true\"]",
|
|
1101
|
+
getAttrs: (node) => {
|
|
1102
|
+
if (typeof node === "string") return false;
|
|
1103
|
+
const element = node;
|
|
1104
|
+
let directText = "";
|
|
1105
|
+
for (const child of element.childNodes) if (child.nodeType === 3) directText += child.textContent || "";
|
|
1106
|
+
const cleanText = directText.trim();
|
|
1107
|
+
if (cleanText) this.storage.previewText = cleanText;
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
}, {
|
|
1111
|
+
tag: "span.preheader",
|
|
1112
|
+
getAttrs: (node) => {
|
|
1113
|
+
if (typeof node === "string") return false;
|
|
1114
|
+
const preheaderText = node.textContent?.trim();
|
|
1115
|
+
if (preheaderText) this.storage.previewText = preheaderText;
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
}];
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
|
|
512
1122
|
//#endregion
|
|
513
1123
|
//#region src/utils/get-text-alignment.ts
|
|
514
1124
|
function getTextAlignment(alignment) {
|
|
@@ -552,14 +1162,14 @@ const Section = EmailNode.create({
|
|
|
552
1162
|
});
|
|
553
1163
|
} };
|
|
554
1164
|
},
|
|
555
|
-
renderToReactEmail({ children, node,
|
|
1165
|
+
renderToReactEmail({ children, node, style }) {
|
|
556
1166
|
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
557
1167
|
const textAlign = node.attrs?.align || node.attrs?.alignment;
|
|
558
1168
|
return /* @__PURE__ */ jsx(Section$1, {
|
|
559
1169
|
className: node.attrs?.class || void 0,
|
|
560
1170
|
align: textAlign,
|
|
561
1171
|
style: {
|
|
562
|
-
...
|
|
1172
|
+
...style,
|
|
563
1173
|
...inlineStyles,
|
|
564
1174
|
...getTextAlignment(textAlign)
|
|
565
1175
|
},
|
|
@@ -569,5 +1179,1489 @@ const Section = EmailNode.create({
|
|
|
569
1179
|
});
|
|
570
1180
|
|
|
571
1181
|
//#endregion
|
|
572
|
-
|
|
1182
|
+
//#region src/extensions/style-attribute.tsx
|
|
1183
|
+
const StyleAttribute = Extension.create({
|
|
1184
|
+
name: "styleAttribute",
|
|
1185
|
+
priority: 101,
|
|
1186
|
+
addOptions() {
|
|
1187
|
+
return {
|
|
1188
|
+
types: [],
|
|
1189
|
+
style: []
|
|
1190
|
+
};
|
|
1191
|
+
},
|
|
1192
|
+
addGlobalAttributes() {
|
|
1193
|
+
return [{
|
|
1194
|
+
types: this.options.types,
|
|
1195
|
+
attributes: { style: {
|
|
1196
|
+
default: "",
|
|
1197
|
+
parseHTML: (element) => element.getAttribute("style") || "",
|
|
1198
|
+
renderHTML: (attributes) => {
|
|
1199
|
+
return { style: attributes.style ?? "" };
|
|
1200
|
+
}
|
|
1201
|
+
} }
|
|
1202
|
+
}];
|
|
1203
|
+
},
|
|
1204
|
+
addCommands() {
|
|
1205
|
+
return {
|
|
1206
|
+
unsetStyle: () => ({ commands }) => {
|
|
1207
|
+
return this.options.types.every((type) => commands.resetAttributes(type, "style"));
|
|
1208
|
+
},
|
|
1209
|
+
setStyle: (style) => ({ commands }) => {
|
|
1210
|
+
return this.options.types.every((type) => commands.updateAttributes(type, { style }));
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
},
|
|
1214
|
+
addKeyboardShortcuts() {
|
|
1215
|
+
return { Enter: ({ editor }) => {
|
|
1216
|
+
const { state } = editor.view;
|
|
1217
|
+
const { selection } = state;
|
|
1218
|
+
const { $from } = selection;
|
|
1219
|
+
const textBefore = $from.nodeBefore?.text || "";
|
|
1220
|
+
if (textBefore.includes("{{") || textBefore.includes("{{{")) return false;
|
|
1221
|
+
requestAnimationFrame(() => {
|
|
1222
|
+
editor.commands.resetAttributes("paragraph", "style");
|
|
1223
|
+
});
|
|
1224
|
+
return false;
|
|
1225
|
+
} };
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
//#endregion
|
|
1230
|
+
//#region src/extensions/sup.ts
|
|
1231
|
+
/**
|
|
1232
|
+
* This extension allows you to mark text as superscript.
|
|
1233
|
+
* @see https://tiptap.dev/api/marks/superscript
|
|
1234
|
+
*/
|
|
1235
|
+
const Sup = Mark.create({
|
|
1236
|
+
name: "sup",
|
|
1237
|
+
addOptions() {
|
|
1238
|
+
return { HTMLAttributes: {} };
|
|
1239
|
+
},
|
|
1240
|
+
parseHTML() {
|
|
1241
|
+
return [{ tag: "sup" }];
|
|
1242
|
+
},
|
|
1243
|
+
renderHTML({ HTMLAttributes }) {
|
|
1244
|
+
return [
|
|
1245
|
+
"sup",
|
|
1246
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
|
1247
|
+
0
|
|
1248
|
+
];
|
|
1249
|
+
},
|
|
1250
|
+
addCommands() {
|
|
1251
|
+
return {
|
|
1252
|
+
setSup: () => ({ commands }) => {
|
|
1253
|
+
return commands.setMark(this.name);
|
|
1254
|
+
},
|
|
1255
|
+
toggleSup: () => ({ commands }) => {
|
|
1256
|
+
return commands.toggleMark(this.name);
|
|
1257
|
+
},
|
|
1258
|
+
unsetSup: () => ({ commands }) => {
|
|
1259
|
+
return commands.unsetMark(this.name);
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
//#endregion
|
|
1266
|
+
//#region src/extensions/table.tsx
|
|
1267
|
+
const Table = EmailNode.create({
|
|
1268
|
+
name: "table",
|
|
1269
|
+
group: "block",
|
|
1270
|
+
content: "tableRow+",
|
|
1271
|
+
isolating: true,
|
|
1272
|
+
tableRole: "table",
|
|
1273
|
+
addAttributes() {
|
|
1274
|
+
return { ...createStandardAttributes([
|
|
1275
|
+
...TABLE_ATTRIBUTES,
|
|
1276
|
+
...LAYOUT_ATTRIBUTES,
|
|
1277
|
+
...COMMON_HTML_ATTRIBUTES
|
|
1278
|
+
]) };
|
|
1279
|
+
},
|
|
1280
|
+
parseHTML() {
|
|
1281
|
+
return [{
|
|
1282
|
+
tag: "table",
|
|
1283
|
+
getAttrs: (node) => {
|
|
1284
|
+
if (typeof node === "string") return false;
|
|
1285
|
+
const element = node;
|
|
1286
|
+
const attrs = {};
|
|
1287
|
+
Array.from(element.attributes).forEach((attr) => {
|
|
1288
|
+
attrs[attr.name] = attr.value;
|
|
1289
|
+
});
|
|
1290
|
+
return attrs;
|
|
1291
|
+
}
|
|
1292
|
+
}];
|
|
1293
|
+
},
|
|
1294
|
+
renderHTML({ HTMLAttributes }) {
|
|
1295
|
+
return [
|
|
1296
|
+
"table",
|
|
1297
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
|
1298
|
+
[
|
|
1299
|
+
"tbody",
|
|
1300
|
+
{},
|
|
1301
|
+
0
|
|
1302
|
+
]
|
|
1303
|
+
];
|
|
1304
|
+
},
|
|
1305
|
+
renderToReactEmail({ children, node, style }) {
|
|
1306
|
+
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
1307
|
+
const alignment = node.attrs?.align || node.attrs?.alignment;
|
|
1308
|
+
const width = node.attrs?.width;
|
|
1309
|
+
const centeringStyles = alignment === "center" ? {
|
|
1310
|
+
marginLeft: "auto",
|
|
1311
|
+
marginRight: "auto"
|
|
1312
|
+
} : {};
|
|
1313
|
+
return /* @__PURE__ */ jsx(Section$1, {
|
|
1314
|
+
className: node.attrs?.class || void 0,
|
|
1315
|
+
align: alignment,
|
|
1316
|
+
style: resolveConflictingStyles(style, {
|
|
1317
|
+
...inlineStyles,
|
|
1318
|
+
...centeringStyles
|
|
1319
|
+
}),
|
|
1320
|
+
...width !== void 0 ? { width } : {},
|
|
1321
|
+
children
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
const TableRow = EmailNode.create({
|
|
1326
|
+
name: "tableRow",
|
|
1327
|
+
group: "tableRow",
|
|
1328
|
+
content: "(tableCell | tableHeader)+",
|
|
1329
|
+
addAttributes() {
|
|
1330
|
+
return { ...createStandardAttributes([
|
|
1331
|
+
...TABLE_CELL_ATTRIBUTES,
|
|
1332
|
+
...LAYOUT_ATTRIBUTES,
|
|
1333
|
+
...COMMON_HTML_ATTRIBUTES
|
|
1334
|
+
]) };
|
|
1335
|
+
},
|
|
1336
|
+
parseHTML() {
|
|
1337
|
+
return [{
|
|
1338
|
+
tag: "tr",
|
|
1339
|
+
getAttrs: (node) => {
|
|
1340
|
+
if (typeof node === "string") return false;
|
|
1341
|
+
const element = node;
|
|
1342
|
+
const attrs = {};
|
|
1343
|
+
Array.from(element.attributes).forEach((attr) => {
|
|
1344
|
+
attrs[attr.name] = attr.value;
|
|
1345
|
+
});
|
|
1346
|
+
return attrs;
|
|
1347
|
+
}
|
|
1348
|
+
}];
|
|
1349
|
+
},
|
|
1350
|
+
renderHTML({ HTMLAttributes }) {
|
|
1351
|
+
return [
|
|
1352
|
+
"tr",
|
|
1353
|
+
HTMLAttributes,
|
|
1354
|
+
0
|
|
1355
|
+
];
|
|
1356
|
+
},
|
|
1357
|
+
renderToReactEmail({ children, node, style }) {
|
|
1358
|
+
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
1359
|
+
return /* @__PURE__ */ jsx("tr", {
|
|
1360
|
+
className: node.attrs?.class || void 0,
|
|
1361
|
+
style: {
|
|
1362
|
+
...style,
|
|
1363
|
+
...inlineStyles
|
|
1364
|
+
},
|
|
1365
|
+
children
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
const TableCell = EmailNode.create({
|
|
1370
|
+
name: "tableCell",
|
|
1371
|
+
group: "tableCell",
|
|
1372
|
+
content: "block+",
|
|
1373
|
+
isolating: true,
|
|
1374
|
+
addAttributes() {
|
|
1375
|
+
return { ...createStandardAttributes([
|
|
1376
|
+
...TABLE_CELL_ATTRIBUTES,
|
|
1377
|
+
...LAYOUT_ATTRIBUTES,
|
|
1378
|
+
...COMMON_HTML_ATTRIBUTES
|
|
1379
|
+
]) };
|
|
1380
|
+
},
|
|
1381
|
+
parseHTML() {
|
|
1382
|
+
return [{
|
|
1383
|
+
tag: "td",
|
|
1384
|
+
getAttrs: (node) => {
|
|
1385
|
+
if (typeof node === "string") return false;
|
|
1386
|
+
const element = node;
|
|
1387
|
+
const attrs = {};
|
|
1388
|
+
Array.from(element.attributes).forEach((attr) => {
|
|
1389
|
+
attrs[attr.name] = attr.value;
|
|
1390
|
+
});
|
|
1391
|
+
return attrs;
|
|
1392
|
+
}
|
|
1393
|
+
}];
|
|
1394
|
+
},
|
|
1395
|
+
renderHTML({ HTMLAttributes }) {
|
|
1396
|
+
return [
|
|
1397
|
+
"td",
|
|
1398
|
+
HTMLAttributes,
|
|
1399
|
+
0
|
|
1400
|
+
];
|
|
1401
|
+
},
|
|
1402
|
+
renderToReactEmail({ children, node, style }) {
|
|
1403
|
+
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
1404
|
+
return /* @__PURE__ */ jsx(Column, {
|
|
1405
|
+
className: node.attrs?.class || void 0,
|
|
1406
|
+
align: node.attrs?.align || node.attrs?.alignment,
|
|
1407
|
+
style: {
|
|
1408
|
+
...style,
|
|
1409
|
+
...inlineStyles
|
|
1410
|
+
},
|
|
1411
|
+
children
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
const TableHeader = Node.create({
|
|
1416
|
+
name: "tableHeader",
|
|
1417
|
+
group: "tableCell",
|
|
1418
|
+
content: "block+",
|
|
1419
|
+
isolating: true,
|
|
1420
|
+
addAttributes() {
|
|
1421
|
+
return { ...createStandardAttributes([
|
|
1422
|
+
...TABLE_HEADER_ATTRIBUTES,
|
|
1423
|
+
...TABLE_CELL_ATTRIBUTES,
|
|
1424
|
+
...LAYOUT_ATTRIBUTES,
|
|
1425
|
+
...COMMON_HTML_ATTRIBUTES
|
|
1426
|
+
]) };
|
|
1427
|
+
},
|
|
1428
|
+
parseHTML() {
|
|
1429
|
+
return [{
|
|
1430
|
+
tag: "th",
|
|
1431
|
+
getAttrs: (node) => {
|
|
1432
|
+
if (typeof node === "string") return false;
|
|
1433
|
+
const element = node;
|
|
1434
|
+
const attrs = {};
|
|
1435
|
+
Array.from(element.attributes).forEach((attr) => {
|
|
1436
|
+
attrs[attr.name] = attr.value;
|
|
1437
|
+
});
|
|
1438
|
+
return attrs;
|
|
1439
|
+
}
|
|
1440
|
+
}];
|
|
1441
|
+
},
|
|
1442
|
+
renderHTML({ HTMLAttributes }) {
|
|
1443
|
+
return [
|
|
1444
|
+
"th",
|
|
1445
|
+
HTMLAttributes,
|
|
1446
|
+
0
|
|
1447
|
+
];
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
//#endregion
|
|
1452
|
+
//#region src/extensions/uppercase.ts
|
|
1453
|
+
const Uppercase = Mark.create({
|
|
1454
|
+
name: "uppercase",
|
|
1455
|
+
addOptions() {
|
|
1456
|
+
return { HTMLAttributes: {} };
|
|
1457
|
+
},
|
|
1458
|
+
parseHTML() {
|
|
1459
|
+
return [{
|
|
1460
|
+
tag: "span",
|
|
1461
|
+
getAttrs: (node) => {
|
|
1462
|
+
if (node.style.textTransform === "uppercase") return {};
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1465
|
+
}];
|
|
1466
|
+
},
|
|
1467
|
+
renderHTML({ HTMLAttributes }) {
|
|
1468
|
+
return [
|
|
1469
|
+
"span",
|
|
1470
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { style: "text-transform: uppercase" }),
|
|
1471
|
+
0
|
|
1472
|
+
];
|
|
1473
|
+
},
|
|
1474
|
+
addCommands() {
|
|
1475
|
+
return {
|
|
1476
|
+
setUppercase: () => ({ commands }) => {
|
|
1477
|
+
return commands.setMark(this.name);
|
|
1478
|
+
},
|
|
1479
|
+
toggleUppercase: () => ({ commands }) => {
|
|
1480
|
+
return commands.toggleMark(this.name);
|
|
1481
|
+
},
|
|
1482
|
+
unsetUppercase: () => ({ commands }) => {
|
|
1483
|
+
return commands.unsetMark(this.name);
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
//#endregion
|
|
1490
|
+
//#region src/extensions/columns.tsx
|
|
1491
|
+
const COLUMN_PARENT_TYPES = [
|
|
1492
|
+
"twoColumns",
|
|
1493
|
+
"threeColumns",
|
|
1494
|
+
"fourColumns"
|
|
1495
|
+
];
|
|
1496
|
+
const COLUMN_PARENT_SET = new Set(COLUMN_PARENT_TYPES);
|
|
1497
|
+
const MAX_COLUMNS_DEPTH = 3;
|
|
1498
|
+
function getColumnsDepth(doc, from) {
|
|
1499
|
+
const $from = doc.resolve(from);
|
|
1500
|
+
let depth = 0;
|
|
1501
|
+
for (let d = $from.depth; d > 0; d--) if (COLUMN_PARENT_SET.has($from.node(d).type.name)) depth++;
|
|
1502
|
+
return depth;
|
|
1503
|
+
}
|
|
1504
|
+
const VARIANTS = [
|
|
1505
|
+
{
|
|
1506
|
+
name: "twoColumns",
|
|
1507
|
+
columnCount: 2,
|
|
1508
|
+
content: "columnsColumn columnsColumn",
|
|
1509
|
+
dataType: "two-columns"
|
|
1510
|
+
},
|
|
1511
|
+
{
|
|
1512
|
+
name: "threeColumns",
|
|
1513
|
+
columnCount: 3,
|
|
1514
|
+
content: "columnsColumn columnsColumn columnsColumn",
|
|
1515
|
+
dataType: "three-columns"
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
name: "fourColumns",
|
|
1519
|
+
columnCount: 4,
|
|
1520
|
+
content: "columnsColumn{4}",
|
|
1521
|
+
dataType: "four-columns"
|
|
1522
|
+
}
|
|
1523
|
+
];
|
|
1524
|
+
const NODE_TYPE_MAP = {
|
|
1525
|
+
2: "twoColumns",
|
|
1526
|
+
3: "threeColumns",
|
|
1527
|
+
4: "fourColumns"
|
|
1528
|
+
};
|
|
1529
|
+
function createColumnsNode(config, includeCommands) {
|
|
1530
|
+
return EmailNode.create({
|
|
1531
|
+
name: config.name,
|
|
1532
|
+
group: "block",
|
|
1533
|
+
content: config.content,
|
|
1534
|
+
isolating: true,
|
|
1535
|
+
defining: true,
|
|
1536
|
+
addAttributes() {
|
|
1537
|
+
return createStandardAttributes([...LAYOUT_ATTRIBUTES, ...COMMON_HTML_ATTRIBUTES]);
|
|
1538
|
+
},
|
|
1539
|
+
parseHTML() {
|
|
1540
|
+
return [{ tag: `div[data-type="${config.dataType}"]` }];
|
|
1541
|
+
},
|
|
1542
|
+
renderHTML({ HTMLAttributes }) {
|
|
1543
|
+
return [
|
|
1544
|
+
"div",
|
|
1545
|
+
mergeAttributes({
|
|
1546
|
+
"data-type": config.dataType,
|
|
1547
|
+
class: "node-columns"
|
|
1548
|
+
}, HTMLAttributes),
|
|
1549
|
+
0
|
|
1550
|
+
];
|
|
1551
|
+
},
|
|
1552
|
+
...includeCommands && { addCommands() {
|
|
1553
|
+
return { insertColumns: (count) => ({ commands, state }) => {
|
|
1554
|
+
if (getColumnsDepth(state.doc, state.selection.from) >= MAX_COLUMNS_DEPTH) return false;
|
|
1555
|
+
const nodeType = NODE_TYPE_MAP[count];
|
|
1556
|
+
const children = Array.from({ length: count }, () => ({
|
|
1557
|
+
type: "columnsColumn",
|
|
1558
|
+
content: [{
|
|
1559
|
+
type: "paragraph",
|
|
1560
|
+
content: []
|
|
1561
|
+
}]
|
|
1562
|
+
}));
|
|
1563
|
+
return commands.insertContent({
|
|
1564
|
+
type: nodeType,
|
|
1565
|
+
content: children
|
|
1566
|
+
});
|
|
1567
|
+
} };
|
|
1568
|
+
} },
|
|
1569
|
+
renderToReactEmail({ children, node, style }) {
|
|
1570
|
+
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
1571
|
+
return /* @__PURE__ */ jsx(Row, {
|
|
1572
|
+
className: node.attrs?.class || void 0,
|
|
1573
|
+
style: {
|
|
1574
|
+
...style,
|
|
1575
|
+
...inlineStyles
|
|
1576
|
+
},
|
|
1577
|
+
children
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
const TwoColumns = createColumnsNode(VARIANTS[0], true);
|
|
1583
|
+
const ThreeColumns = createColumnsNode(VARIANTS[1], false);
|
|
1584
|
+
const FourColumns = createColumnsNode(VARIANTS[2], false);
|
|
1585
|
+
const ColumnsColumn = EmailNode.create({
|
|
1586
|
+
name: "columnsColumn",
|
|
1587
|
+
group: "columnsColumn",
|
|
1588
|
+
content: "block+",
|
|
1589
|
+
isolating: true,
|
|
1590
|
+
addAttributes() {
|
|
1591
|
+
return { ...createStandardAttributes([...LAYOUT_ATTRIBUTES, ...COMMON_HTML_ATTRIBUTES]) };
|
|
1592
|
+
},
|
|
1593
|
+
parseHTML() {
|
|
1594
|
+
return [{ tag: "div[data-type=\"column\"]" }];
|
|
1595
|
+
},
|
|
1596
|
+
renderHTML({ HTMLAttributes }) {
|
|
1597
|
+
return [
|
|
1598
|
+
"div",
|
|
1599
|
+
mergeAttributes({
|
|
1600
|
+
"data-type": "column",
|
|
1601
|
+
class: "node-column"
|
|
1602
|
+
}, HTMLAttributes),
|
|
1603
|
+
0
|
|
1604
|
+
];
|
|
1605
|
+
},
|
|
1606
|
+
addKeyboardShortcuts() {
|
|
1607
|
+
return {
|
|
1608
|
+
Backspace: ({ editor }) => {
|
|
1609
|
+
const { state } = editor;
|
|
1610
|
+
const { selection } = state;
|
|
1611
|
+
const { empty, $from } = selection;
|
|
1612
|
+
if (!empty) return false;
|
|
1613
|
+
for (let depth = $from.depth; depth >= 1; depth--) {
|
|
1614
|
+
if ($from.pos !== $from.start(depth)) break;
|
|
1615
|
+
const indexInParent = $from.index(depth - 1);
|
|
1616
|
+
if (indexInParent === 0) continue;
|
|
1617
|
+
const prevNode = $from.node(depth - 1).child(indexInParent - 1);
|
|
1618
|
+
if (COLUMN_PARENT_SET.has(prevNode.type.name)) {
|
|
1619
|
+
const deleteFrom = $from.before(depth) - prevNode.nodeSize;
|
|
1620
|
+
const deleteTo = $from.before(depth);
|
|
1621
|
+
editor.view.dispatch(state.tr.delete(deleteFrom, deleteTo));
|
|
1622
|
+
return true;
|
|
1623
|
+
}
|
|
1624
|
+
break;
|
|
1625
|
+
}
|
|
1626
|
+
return false;
|
|
1627
|
+
},
|
|
1628
|
+
"Mod-a": ({ editor }) => {
|
|
1629
|
+
const { state } = editor;
|
|
1630
|
+
const { $from } = state.selection;
|
|
1631
|
+
for (let d = $from.depth; d > 0; d--) {
|
|
1632
|
+
if ($from.node(d).type.name !== "columnsColumn") continue;
|
|
1633
|
+
const columnStart = $from.start(d);
|
|
1634
|
+
const columnEnd = $from.end(d);
|
|
1635
|
+
const { from, to } = state.selection;
|
|
1636
|
+
if (from === columnStart && to === columnEnd) return false;
|
|
1637
|
+
editor.view.dispatch(state.tr.setSelection(TextSelection.create(state.doc, columnStart, columnEnd)));
|
|
1638
|
+
return true;
|
|
1639
|
+
}
|
|
1640
|
+
return false;
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
},
|
|
1644
|
+
renderToReactEmail({ children, node, style }) {
|
|
1645
|
+
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
1646
|
+
const width = node.attrs?.width;
|
|
1647
|
+
return /* @__PURE__ */ jsx(Column, {
|
|
1648
|
+
className: node.attrs?.class || void 0,
|
|
1649
|
+
style: {
|
|
1650
|
+
...style,
|
|
1651
|
+
...inlineStyles,
|
|
1652
|
+
...width ? { width } : {}
|
|
1653
|
+
},
|
|
1654
|
+
children
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
//#endregion
|
|
1660
|
+
//#region src/extensions/index.ts
|
|
1661
|
+
const coreExtensions = [
|
|
1662
|
+
StarterKit.configure({
|
|
1663
|
+
undoRedo: false,
|
|
1664
|
+
heading: false,
|
|
1665
|
+
link: false,
|
|
1666
|
+
underline: false,
|
|
1667
|
+
trailingNode: false,
|
|
1668
|
+
bold: false,
|
|
1669
|
+
gapcursor: false,
|
|
1670
|
+
listItem: {},
|
|
1671
|
+
bulletList: { HTMLAttributes: { class: "node-bulletList" } },
|
|
1672
|
+
paragraph: { HTMLAttributes: { class: "node-paragraph" } },
|
|
1673
|
+
orderedList: { HTMLAttributes: { class: "node-orderedList" } },
|
|
1674
|
+
blockquote: { HTMLAttributes: { class: "node-blockquote" } },
|
|
1675
|
+
codeBlock: false,
|
|
1676
|
+
code: { HTMLAttributes: {
|
|
1677
|
+
class: "node-inlineCode",
|
|
1678
|
+
spellcheck: "false"
|
|
1679
|
+
} },
|
|
1680
|
+
horizontalRule: false,
|
|
1681
|
+
dropcursor: {
|
|
1682
|
+
color: "#61a8f8",
|
|
1683
|
+
class: "rounded-full animate-[fade-in_300ms_ease-in-out] !z-40",
|
|
1684
|
+
width: 4
|
|
1685
|
+
}
|
|
1686
|
+
}),
|
|
1687
|
+
CodeBlockPrism.configure({
|
|
1688
|
+
defaultLanguage: "javascript",
|
|
1689
|
+
HTMLAttributes: { class: "prism node-codeBlock" }
|
|
1690
|
+
}),
|
|
1691
|
+
Placeholder,
|
|
1692
|
+
PreviewText,
|
|
1693
|
+
Bold,
|
|
1694
|
+
Sup,
|
|
1695
|
+
Uppercase,
|
|
1696
|
+
PreservedStyle,
|
|
1697
|
+
Table,
|
|
1698
|
+
TableRow,
|
|
1699
|
+
TableCell,
|
|
1700
|
+
TableHeader,
|
|
1701
|
+
Body,
|
|
1702
|
+
Div,
|
|
1703
|
+
Button,
|
|
1704
|
+
Section,
|
|
1705
|
+
AlignmentAttribute.configure({ types: [
|
|
1706
|
+
"heading",
|
|
1707
|
+
"paragraph",
|
|
1708
|
+
"image",
|
|
1709
|
+
"blockquote",
|
|
1710
|
+
"codeBlock",
|
|
1711
|
+
"bulletList",
|
|
1712
|
+
"orderedList",
|
|
1713
|
+
"listItem",
|
|
1714
|
+
"button",
|
|
1715
|
+
"youtube",
|
|
1716
|
+
"twitter",
|
|
1717
|
+
"table",
|
|
1718
|
+
"tableRow",
|
|
1719
|
+
"tableCell",
|
|
1720
|
+
"tableHeader",
|
|
1721
|
+
"columnsColumn"
|
|
1722
|
+
] }),
|
|
1723
|
+
StyleAttribute.configure({ types: [
|
|
1724
|
+
"heading",
|
|
1725
|
+
"paragraph",
|
|
1726
|
+
"image",
|
|
1727
|
+
"blockquote",
|
|
1728
|
+
"codeBlock",
|
|
1729
|
+
"bulletList",
|
|
1730
|
+
"orderedList",
|
|
1731
|
+
"listItem",
|
|
1732
|
+
"button",
|
|
1733
|
+
"youtube",
|
|
1734
|
+
"twitter",
|
|
1735
|
+
"horizontalRule",
|
|
1736
|
+
"footer",
|
|
1737
|
+
"section",
|
|
1738
|
+
"div",
|
|
1739
|
+
"body",
|
|
1740
|
+
"table",
|
|
1741
|
+
"tableRow",
|
|
1742
|
+
"tableCell",
|
|
1743
|
+
"tableHeader",
|
|
1744
|
+
"columnsColumn",
|
|
1745
|
+
"link"
|
|
1746
|
+
] }),
|
|
1747
|
+
ClassAttribute.configure({ types: [
|
|
1748
|
+
"heading",
|
|
1749
|
+
"paragraph",
|
|
1750
|
+
"image",
|
|
1751
|
+
"blockquote",
|
|
1752
|
+
"bulletList",
|
|
1753
|
+
"orderedList",
|
|
1754
|
+
"listItem",
|
|
1755
|
+
"button",
|
|
1756
|
+
"youtube",
|
|
1757
|
+
"twitter",
|
|
1758
|
+
"horizontalRule",
|
|
1759
|
+
"footer",
|
|
1760
|
+
"section",
|
|
1761
|
+
"div",
|
|
1762
|
+
"body",
|
|
1763
|
+
"table",
|
|
1764
|
+
"tableRow",
|
|
1765
|
+
"tableCell",
|
|
1766
|
+
"tableHeader",
|
|
1767
|
+
"columnsColumn",
|
|
1768
|
+
"link"
|
|
1769
|
+
] }),
|
|
1770
|
+
MaxNesting.configure({
|
|
1771
|
+
maxDepth: 50,
|
|
1772
|
+
nodeTypes: [
|
|
1773
|
+
"section",
|
|
1774
|
+
"bulletList",
|
|
1775
|
+
"orderedList"
|
|
1776
|
+
]
|
|
1777
|
+
})
|
|
1778
|
+
];
|
|
1779
|
+
|
|
1780
|
+
//#endregion
|
|
1781
|
+
//#region src/utils/set-text-alignment.ts
|
|
1782
|
+
function setTextAlignment(editor, alignment) {
|
|
1783
|
+
const { from, to } = editor.state.selection;
|
|
1784
|
+
const tr = editor.state.tr;
|
|
1785
|
+
editor.state.doc.nodesBetween(from, to, (node, pos) => {
|
|
1786
|
+
if (node.isTextblock) {
|
|
1787
|
+
const prop = "align" in node.attrs ? "align" : "alignment";
|
|
1788
|
+
tr.setNodeMarkup(pos, null, {
|
|
1789
|
+
...node.attrs,
|
|
1790
|
+
[prop]: alignment
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
editor.view.dispatch(tr);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
//#endregion
|
|
1798
|
+
//#region src/ui/bubble-menu/context.tsx
|
|
1799
|
+
const BubbleMenuContext = React.createContext(null);
|
|
1800
|
+
function useBubbleMenuContext() {
|
|
1801
|
+
const context = React.useContext(BubbleMenuContext);
|
|
1802
|
+
if (!context) throw new Error("BubbleMenu compound components must be used within <BubbleMenu.Root>");
|
|
1803
|
+
return context;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
//#endregion
|
|
1807
|
+
//#region src/ui/bubble-menu/item.tsx
|
|
1808
|
+
function BubbleMenuItem({ name, isActive, onCommand, className, children,...rest }) {
|
|
1809
|
+
return /* @__PURE__ */ jsx("button", {
|
|
1810
|
+
type: "button",
|
|
1811
|
+
"aria-label": name,
|
|
1812
|
+
"aria-pressed": isActive,
|
|
1813
|
+
className,
|
|
1814
|
+
"data-re-bubble-menu-item": "",
|
|
1815
|
+
"data-item": name,
|
|
1816
|
+
...isActive ? { "data-active": "" } : {},
|
|
1817
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
1818
|
+
onClick: onCommand,
|
|
1819
|
+
...rest,
|
|
1820
|
+
children
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
//#endregion
|
|
1825
|
+
//#region src/ui/bubble-menu/align-center.tsx
|
|
1826
|
+
function BubbleMenuAlignCenter({ className, children }) {
|
|
1827
|
+
const { editor } = useBubbleMenuContext();
|
|
1828
|
+
return /* @__PURE__ */ jsx(BubbleMenuItem, {
|
|
1829
|
+
name: "align-center",
|
|
1830
|
+
isActive: useEditorState({
|
|
1831
|
+
editor,
|
|
1832
|
+
selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "center" }) ?? false
|
|
1833
|
+
}),
|
|
1834
|
+
onCommand: () => setTextAlignment(editor, "center"),
|
|
1835
|
+
className,
|
|
1836
|
+
children: children ?? /* @__PURE__ */ jsx(AlignCenterIcon, {})
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
//#endregion
|
|
1841
|
+
//#region src/ui/bubble-menu/align-left.tsx
|
|
1842
|
+
function BubbleMenuAlignLeft({ className, children }) {
|
|
1843
|
+
const { editor } = useBubbleMenuContext();
|
|
1844
|
+
return /* @__PURE__ */ jsx(BubbleMenuItem, {
|
|
1845
|
+
name: "align-left",
|
|
1846
|
+
isActive: useEditorState({
|
|
1847
|
+
editor,
|
|
1848
|
+
selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "left" }) ?? false
|
|
1849
|
+
}),
|
|
1850
|
+
onCommand: () => setTextAlignment(editor, "left"),
|
|
1851
|
+
className,
|
|
1852
|
+
children: children ?? /* @__PURE__ */ jsx(AlignLeftIcon, {})
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
//#endregion
|
|
1857
|
+
//#region src/ui/bubble-menu/align-right.tsx
|
|
1858
|
+
function BubbleMenuAlignRight({ className, children }) {
|
|
1859
|
+
const { editor } = useBubbleMenuContext();
|
|
1860
|
+
return /* @__PURE__ */ jsx(BubbleMenuItem, {
|
|
1861
|
+
name: "align-right",
|
|
1862
|
+
isActive: useEditorState({
|
|
1863
|
+
editor,
|
|
1864
|
+
selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "right" }) ?? false
|
|
1865
|
+
}),
|
|
1866
|
+
onCommand: () => setTextAlignment(editor, "right"),
|
|
1867
|
+
className,
|
|
1868
|
+
children: children ?? /* @__PURE__ */ jsx(AlignRightIcon, {})
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
//#endregion
|
|
1873
|
+
//#region src/ui/bubble-menu/create-mark-bubble-item.tsx
|
|
1874
|
+
function createMarkBubbleItem(config) {
|
|
1875
|
+
function MarkBubbleItem({ className, children }) {
|
|
1876
|
+
const { editor } = useBubbleMenuContext();
|
|
1877
|
+
const isActive = useEditorState({
|
|
1878
|
+
editor,
|
|
1879
|
+
selector: ({ editor: editor$1 }) => {
|
|
1880
|
+
if (config.activeParams) return editor$1?.isActive(config.activeName, config.activeParams) ?? false;
|
|
1881
|
+
return editor$1?.isActive(config.activeName) ?? false;
|
|
1882
|
+
}
|
|
1883
|
+
});
|
|
1884
|
+
const handleCommand = () => {
|
|
1885
|
+
const chain = editor.chain().focus();
|
|
1886
|
+
const method = chain[config.command];
|
|
1887
|
+
if (method) method.call(chain).run();
|
|
1888
|
+
};
|
|
1889
|
+
return /* @__PURE__ */ jsx(BubbleMenuItem, {
|
|
1890
|
+
name: config.name,
|
|
1891
|
+
isActive,
|
|
1892
|
+
onCommand: handleCommand,
|
|
1893
|
+
className,
|
|
1894
|
+
children: children ?? config.icon
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
MarkBubbleItem.displayName = `BubbleMenu${config.name.charAt(0).toUpperCase() + config.name.slice(1)}`;
|
|
1898
|
+
return MarkBubbleItem;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
//#endregion
|
|
1902
|
+
//#region src/ui/bubble-menu/bold.tsx
|
|
1903
|
+
const BubbleMenuBold = createMarkBubbleItem({
|
|
1904
|
+
name: "bold",
|
|
1905
|
+
activeName: "bold",
|
|
1906
|
+
command: "toggleBold",
|
|
1907
|
+
icon: /* @__PURE__ */ jsx(BoldIcon, {})
|
|
1908
|
+
});
|
|
1909
|
+
|
|
1910
|
+
//#endregion
|
|
1911
|
+
//#region src/ui/bubble-menu/code.tsx
|
|
1912
|
+
const BubbleMenuCode = createMarkBubbleItem({
|
|
1913
|
+
name: "code",
|
|
1914
|
+
activeName: "code",
|
|
1915
|
+
command: "toggleCode",
|
|
1916
|
+
icon: /* @__PURE__ */ jsx(CodeIcon, {})
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
//#endregion
|
|
1920
|
+
//#region src/ui/bubble-menu/group.tsx
|
|
1921
|
+
function BubbleMenuItemGroup({ className, children }) {
|
|
1922
|
+
return /* @__PURE__ */ jsx("fieldset", {
|
|
1923
|
+
className,
|
|
1924
|
+
"data-re-bubble-menu-group": "",
|
|
1925
|
+
children
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
//#endregion
|
|
1930
|
+
//#region src/ui/bubble-menu/italic.tsx
|
|
1931
|
+
const BubbleMenuItalic = createMarkBubbleItem({
|
|
1932
|
+
name: "italic",
|
|
1933
|
+
activeName: "italic",
|
|
1934
|
+
command: "toggleItalic",
|
|
1935
|
+
icon: /* @__PURE__ */ jsx(ItalicIcon, {})
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
//#endregion
|
|
1939
|
+
//#region src/ui/bubble-menu/utils.ts
|
|
1940
|
+
const SAFE_PROTOCOLS = new Set([
|
|
1941
|
+
"http:",
|
|
1942
|
+
"https:",
|
|
1943
|
+
"mailto:",
|
|
1944
|
+
"tel:"
|
|
1945
|
+
]);
|
|
1946
|
+
/**
|
|
1947
|
+
* Basic URL validation and auto-prefixing.
|
|
1948
|
+
* Rejects dangerous schemes (javascript:, data:, vbscript:, etc.).
|
|
1949
|
+
* Returns the valid URL string or null.
|
|
1950
|
+
*/
|
|
1951
|
+
function getUrlFromString(str) {
|
|
1952
|
+
if (str === "#") return str;
|
|
1953
|
+
try {
|
|
1954
|
+
const url = new URL(str);
|
|
1955
|
+
if (SAFE_PROTOCOLS.has(url.protocol)) return str;
|
|
1956
|
+
return null;
|
|
1957
|
+
} catch {}
|
|
1958
|
+
try {
|
|
1959
|
+
if (str.includes(".") && !str.includes(" ")) return new URL(`https://${str}`).toString();
|
|
1960
|
+
} catch {}
|
|
1961
|
+
return null;
|
|
1962
|
+
}
|
|
1963
|
+
function setLinkHref(editor, href) {
|
|
1964
|
+
if (href.length === 0) {
|
|
1965
|
+
editor.chain().unsetLink().run();
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
const { from, to } = editor.state.selection;
|
|
1969
|
+
if (from === to) {
|
|
1970
|
+
editor.chain().extendMarkRange("link").setLink({ href }).setTextSelection({
|
|
1971
|
+
from,
|
|
1972
|
+
to
|
|
1973
|
+
}).run();
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
editor.chain().setLink({ href }).run();
|
|
1977
|
+
}
|
|
1978
|
+
function focusEditor(editor) {
|
|
1979
|
+
setTimeout(() => {
|
|
1980
|
+
editor.commands.focus();
|
|
1981
|
+
}, 0);
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
//#endregion
|
|
1985
|
+
//#region src/ui/bubble-menu/link-selector.tsx
|
|
1986
|
+
function BubbleMenuLinkSelector({ className, showToggle = true, validateUrl, onLinkApply, onLinkRemove, children, open: controlledOpen, onOpenChange }) {
|
|
1987
|
+
const { editor } = useBubbleMenuContext();
|
|
1988
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
|
|
1989
|
+
const isControlled = controlledOpen !== void 0;
|
|
1990
|
+
const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
|
|
1991
|
+
const setIsOpen = React.useCallback((value) => {
|
|
1992
|
+
if (!isControlled) setUncontrolledOpen(value);
|
|
1993
|
+
onOpenChange?.(value);
|
|
1994
|
+
}, [isControlled, onOpenChange]);
|
|
1995
|
+
const editorState = useEditorState({
|
|
1996
|
+
editor,
|
|
1997
|
+
selector: ({ editor: editor$1 }) => ({
|
|
1998
|
+
isLinkActive: editor$1?.isActive("link") ?? false,
|
|
1999
|
+
hasLink: Boolean(editor$1?.getAttributes("link").href),
|
|
2000
|
+
currentHref: editor$1?.getAttributes("link").href || ""
|
|
2001
|
+
})
|
|
2002
|
+
});
|
|
2003
|
+
const setIsOpenRef = React.useRef(setIsOpen);
|
|
2004
|
+
setIsOpenRef.current = setIsOpen;
|
|
2005
|
+
React.useEffect(() => {
|
|
2006
|
+
const subscription = editorEventBus.on("bubble-menu:add-link", () => {
|
|
2007
|
+
setIsOpenRef.current(true);
|
|
2008
|
+
});
|
|
2009
|
+
return () => {
|
|
2010
|
+
setIsOpenRef.current(false);
|
|
2011
|
+
subscription.unsubscribe();
|
|
2012
|
+
};
|
|
2013
|
+
}, []);
|
|
2014
|
+
if (!editorState) return null;
|
|
2015
|
+
const handleOpenLink = () => {
|
|
2016
|
+
setIsOpen(!isOpen);
|
|
2017
|
+
};
|
|
2018
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2019
|
+
"data-re-link-selector": "",
|
|
2020
|
+
...isOpen ? { "data-open": "" } : {},
|
|
2021
|
+
...editorState.hasLink ? { "data-has-link": "" } : {},
|
|
2022
|
+
className,
|
|
2023
|
+
children: [showToggle && /* @__PURE__ */ jsx("button", {
|
|
2024
|
+
type: "button",
|
|
2025
|
+
"aria-expanded": isOpen,
|
|
2026
|
+
"aria-haspopup": "true",
|
|
2027
|
+
"aria-label": "Add link",
|
|
2028
|
+
"aria-pressed": editorState.isLinkActive && editorState.hasLink,
|
|
2029
|
+
"data-re-link-selector-trigger": "",
|
|
2030
|
+
onClick: handleOpenLink,
|
|
2031
|
+
children: /* @__PURE__ */ jsx(LinkIcon, {})
|
|
2032
|
+
}), isOpen && /* @__PURE__ */ jsx(LinkForm, {
|
|
2033
|
+
editor,
|
|
2034
|
+
currentHref: editorState.currentHref,
|
|
2035
|
+
validateUrl,
|
|
2036
|
+
onLinkApply,
|
|
2037
|
+
onLinkRemove,
|
|
2038
|
+
setIsOpen,
|
|
2039
|
+
children
|
|
2040
|
+
})]
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
function LinkForm({ editor, currentHref, validateUrl, onLinkApply, onLinkRemove, setIsOpen, children }) {
|
|
2044
|
+
const inputRef = React.useRef(null);
|
|
2045
|
+
const formRef = React.useRef(null);
|
|
2046
|
+
const displayHref = currentHref === "#" ? "" : currentHref;
|
|
2047
|
+
const [inputValue, setInputValue] = React.useState(displayHref);
|
|
2048
|
+
React.useEffect(() => {
|
|
2049
|
+
const timeoutId = setTimeout(() => {
|
|
2050
|
+
inputRef.current?.focus();
|
|
2051
|
+
}, 0);
|
|
2052
|
+
return () => clearTimeout(timeoutId);
|
|
2053
|
+
}, []);
|
|
2054
|
+
React.useEffect(() => {
|
|
2055
|
+
const handleKeyDown = (event) => {
|
|
2056
|
+
if (event.key === "Escape") {
|
|
2057
|
+
if (editor.getAttributes("link").href === "#") editor.chain().unsetLink().run();
|
|
2058
|
+
setIsOpen(false);
|
|
2059
|
+
}
|
|
2060
|
+
};
|
|
2061
|
+
const handleClickOutside = (event) => {
|
|
2062
|
+
if (formRef.current && !formRef.current.contains(event.target)) {
|
|
2063
|
+
const form = formRef.current;
|
|
2064
|
+
const submitEvent = new Event("submit", {
|
|
2065
|
+
bubbles: true,
|
|
2066
|
+
cancelable: true
|
|
2067
|
+
});
|
|
2068
|
+
form.dispatchEvent(submitEvent);
|
|
2069
|
+
setIsOpen(false);
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
2072
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
2073
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
2074
|
+
return () => {
|
|
2075
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
2076
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
2077
|
+
};
|
|
2078
|
+
}, [editor, setIsOpen]);
|
|
2079
|
+
function handleSubmit(e) {
|
|
2080
|
+
e.preventDefault();
|
|
2081
|
+
const value = inputValue.trim();
|
|
2082
|
+
if (value === "") {
|
|
2083
|
+
setLinkHref(editor, "");
|
|
2084
|
+
setIsOpen(false);
|
|
2085
|
+
focusEditor(editor);
|
|
2086
|
+
onLinkRemove?.();
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
const finalValue = (validateUrl ?? getUrlFromString)(value);
|
|
2090
|
+
if (!finalValue) {
|
|
2091
|
+
setLinkHref(editor, "");
|
|
2092
|
+
setIsOpen(false);
|
|
2093
|
+
focusEditor(editor);
|
|
2094
|
+
onLinkRemove?.();
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
setLinkHref(editor, finalValue);
|
|
2098
|
+
setIsOpen(false);
|
|
2099
|
+
focusEditor(editor);
|
|
2100
|
+
onLinkApply?.(finalValue);
|
|
2101
|
+
}
|
|
2102
|
+
function handleUnlink(e) {
|
|
2103
|
+
e.stopPropagation();
|
|
2104
|
+
setLinkHref(editor, "");
|
|
2105
|
+
setIsOpen(false);
|
|
2106
|
+
focusEditor(editor);
|
|
2107
|
+
onLinkRemove?.();
|
|
2108
|
+
}
|
|
2109
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
2110
|
+
ref: formRef,
|
|
2111
|
+
"data-re-link-selector-form": "",
|
|
2112
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
2113
|
+
onClick: (e) => e.stopPropagation(),
|
|
2114
|
+
onKeyDown: (e) => e.stopPropagation(),
|
|
2115
|
+
onSubmit: handleSubmit,
|
|
2116
|
+
children: [
|
|
2117
|
+
/* @__PURE__ */ jsx("input", {
|
|
2118
|
+
ref: inputRef,
|
|
2119
|
+
"data-re-link-selector-input": "",
|
|
2120
|
+
value: inputValue,
|
|
2121
|
+
onFocus: (e) => e.stopPropagation(),
|
|
2122
|
+
onChange: (e) => setInputValue(e.target.value),
|
|
2123
|
+
placeholder: "Paste a link",
|
|
2124
|
+
type: "text"
|
|
2125
|
+
}),
|
|
2126
|
+
children,
|
|
2127
|
+
displayHref ? /* @__PURE__ */ jsx("button", {
|
|
2128
|
+
type: "button",
|
|
2129
|
+
"aria-label": "Remove link",
|
|
2130
|
+
"data-re-link-selector-unlink": "",
|
|
2131
|
+
onClick: handleUnlink,
|
|
2132
|
+
children: /* @__PURE__ */ jsx(UnlinkIcon, {})
|
|
2133
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
2134
|
+
type: "submit",
|
|
2135
|
+
"aria-label": "Apply link",
|
|
2136
|
+
"data-re-link-selector-apply": "",
|
|
2137
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
2138
|
+
children: /* @__PURE__ */ jsx(Check, {})
|
|
2139
|
+
})
|
|
2140
|
+
]
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
//#endregion
|
|
2145
|
+
//#region src/ui/bubble-menu/node-selector.tsx
|
|
2146
|
+
const NodeSelectorContext = React.createContext(null);
|
|
2147
|
+
function useNodeSelectorContext() {
|
|
2148
|
+
const context = React.useContext(NodeSelectorContext);
|
|
2149
|
+
if (!context) throw new Error("NodeSelector compound components must be used within <NodeSelector.Root>");
|
|
2150
|
+
return context;
|
|
2151
|
+
}
|
|
2152
|
+
function NodeSelectorRoot({ omit = [], open: controlledOpen, onOpenChange, className, children }) {
|
|
2153
|
+
const { editor } = useBubbleMenuContext();
|
|
2154
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
|
|
2155
|
+
const isControlled = controlledOpen !== void 0;
|
|
2156
|
+
const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
|
|
2157
|
+
const setIsOpen = React.useCallback((value) => {
|
|
2158
|
+
if (!isControlled) setUncontrolledOpen(value);
|
|
2159
|
+
onOpenChange?.(value);
|
|
2160
|
+
}, [isControlled, onOpenChange]);
|
|
2161
|
+
const editorState = useEditorState({
|
|
2162
|
+
editor,
|
|
2163
|
+
selector: ({ editor: editor$1 }) => ({
|
|
2164
|
+
isParagraphActive: (editor$1?.isActive("paragraph") ?? false) && !editor$1?.isActive("bulletList") && !editor$1?.isActive("orderedList"),
|
|
2165
|
+
isHeading1Active: editor$1?.isActive("heading", { level: 1 }) ?? false,
|
|
2166
|
+
isHeading2Active: editor$1?.isActive("heading", { level: 2 }) ?? false,
|
|
2167
|
+
isHeading3Active: editor$1?.isActive("heading", { level: 3 }) ?? false,
|
|
2168
|
+
isBulletListActive: editor$1?.isActive("bulletList") ?? false,
|
|
2169
|
+
isOrderedListActive: editor$1?.isActive("orderedList") ?? false,
|
|
2170
|
+
isBlockquoteActive: editor$1?.isActive("blockquote") ?? false,
|
|
2171
|
+
isCodeBlockActive: editor$1?.isActive("codeBlock") ?? false
|
|
2172
|
+
})
|
|
2173
|
+
});
|
|
2174
|
+
const allItems = React.useMemo(() => [
|
|
2175
|
+
{
|
|
2176
|
+
name: "Text",
|
|
2177
|
+
icon: TextIcon,
|
|
2178
|
+
command: () => editor.chain().focus().clearNodes().toggleNode("paragraph", "paragraph").run(),
|
|
2179
|
+
isActive: editorState?.isParagraphActive ?? false
|
|
2180
|
+
},
|
|
2181
|
+
{
|
|
2182
|
+
name: "Title",
|
|
2183
|
+
icon: Heading1,
|
|
2184
|
+
command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 1 }).run(),
|
|
2185
|
+
isActive: editorState?.isHeading1Active ?? false
|
|
2186
|
+
},
|
|
2187
|
+
{
|
|
2188
|
+
name: "Subtitle",
|
|
2189
|
+
icon: Heading2,
|
|
2190
|
+
command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 2 }).run(),
|
|
2191
|
+
isActive: editorState?.isHeading2Active ?? false
|
|
2192
|
+
},
|
|
2193
|
+
{
|
|
2194
|
+
name: "Heading",
|
|
2195
|
+
icon: Heading3,
|
|
2196
|
+
command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 3 }).run(),
|
|
2197
|
+
isActive: editorState?.isHeading3Active ?? false
|
|
2198
|
+
},
|
|
2199
|
+
{
|
|
2200
|
+
name: "Bullet List",
|
|
2201
|
+
icon: List,
|
|
2202
|
+
command: () => editor.chain().focus().clearNodes().toggleBulletList().run(),
|
|
2203
|
+
isActive: editorState?.isBulletListActive ?? false
|
|
2204
|
+
},
|
|
2205
|
+
{
|
|
2206
|
+
name: "Numbered List",
|
|
2207
|
+
icon: ListOrdered,
|
|
2208
|
+
command: () => editor.chain().focus().clearNodes().toggleOrderedList().run(),
|
|
2209
|
+
isActive: editorState?.isOrderedListActive ?? false
|
|
2210
|
+
},
|
|
2211
|
+
{
|
|
2212
|
+
name: "Quote",
|
|
2213
|
+
icon: TextQuote,
|
|
2214
|
+
command: () => editor.chain().focus().clearNodes().toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
|
|
2215
|
+
isActive: editorState?.isBlockquoteActive ?? false
|
|
2216
|
+
},
|
|
2217
|
+
{
|
|
2218
|
+
name: "Code",
|
|
2219
|
+
icon: Code,
|
|
2220
|
+
command: () => editor.chain().focus().clearNodes().toggleCodeBlock().run(),
|
|
2221
|
+
isActive: editorState?.isCodeBlockActive ?? false
|
|
2222
|
+
}
|
|
2223
|
+
], [editor, editorState]);
|
|
2224
|
+
const items = React.useMemo(() => allItems.filter((item) => !omit.includes(item.name)), [allItems, omit]);
|
|
2225
|
+
const activeItem = React.useMemo(() => items.find((item) => item.isActive) ?? { name: "Multiple" }, [items]);
|
|
2226
|
+
const contextValue = React.useMemo(() => ({
|
|
2227
|
+
items,
|
|
2228
|
+
activeItem,
|
|
2229
|
+
isOpen,
|
|
2230
|
+
setIsOpen
|
|
2231
|
+
}), [
|
|
2232
|
+
items,
|
|
2233
|
+
activeItem,
|
|
2234
|
+
isOpen,
|
|
2235
|
+
setIsOpen
|
|
2236
|
+
]);
|
|
2237
|
+
if (!editorState || items.length === 0) return null;
|
|
2238
|
+
return /* @__PURE__ */ jsx(NodeSelectorContext.Provider, {
|
|
2239
|
+
value: contextValue,
|
|
2240
|
+
children: /* @__PURE__ */ jsx(Popover.Root, {
|
|
2241
|
+
open: isOpen,
|
|
2242
|
+
onOpenChange: setIsOpen,
|
|
2243
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
2244
|
+
"data-re-node-selector": "",
|
|
2245
|
+
...isOpen ? { "data-open": "" } : {},
|
|
2246
|
+
className,
|
|
2247
|
+
children
|
|
2248
|
+
})
|
|
2249
|
+
})
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
function NodeSelectorTrigger({ className, children }) {
|
|
2253
|
+
const { activeItem, isOpen, setIsOpen } = useNodeSelectorContext();
|
|
2254
|
+
return /* @__PURE__ */ jsx(Popover.Trigger, {
|
|
2255
|
+
"data-re-node-selector-trigger": "",
|
|
2256
|
+
className,
|
|
2257
|
+
onClick: () => setIsOpen(!isOpen),
|
|
2258
|
+
children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", { children: activeItem.name }), /* @__PURE__ */ jsx(ChevronDown, {})] })
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
function NodeSelectorContent({ className, align = "start", children }) {
|
|
2262
|
+
const { items, setIsOpen } = useNodeSelectorContext();
|
|
2263
|
+
return /* @__PURE__ */ jsx(Popover.Content, {
|
|
2264
|
+
align,
|
|
2265
|
+
"data-re-node-selector-content": "",
|
|
2266
|
+
className,
|
|
2267
|
+
children: children ? children(items, () => setIsOpen(false)) : items.map((item) => {
|
|
2268
|
+
const Icon = item.icon;
|
|
2269
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
2270
|
+
type: "button",
|
|
2271
|
+
"data-re-node-selector-item": "",
|
|
2272
|
+
...item.isActive ? { "data-active": "" } : {},
|
|
2273
|
+
onClick: () => {
|
|
2274
|
+
item.command();
|
|
2275
|
+
setIsOpen(false);
|
|
2276
|
+
},
|
|
2277
|
+
children: [
|
|
2278
|
+
/* @__PURE__ */ jsx(Icon, {}),
|
|
2279
|
+
/* @__PURE__ */ jsx("span", { children: item.name }),
|
|
2280
|
+
item.isActive && /* @__PURE__ */ jsx(Check, {})
|
|
2281
|
+
]
|
|
2282
|
+
}, item.name);
|
|
2283
|
+
})
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
function BubbleMenuNodeSelector({ omit = [], className, triggerContent, open, onOpenChange }) {
|
|
2287
|
+
return /* @__PURE__ */ jsxs(NodeSelectorRoot, {
|
|
2288
|
+
omit,
|
|
2289
|
+
open,
|
|
2290
|
+
onOpenChange,
|
|
2291
|
+
className,
|
|
2292
|
+
children: [/* @__PURE__ */ jsx(NodeSelectorTrigger, { children: triggerContent }), /* @__PURE__ */ jsx(NodeSelectorContent, {})]
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
//#endregion
|
|
2297
|
+
//#region src/ui/bubble-menu/root.tsx
|
|
2298
|
+
function BubbleMenuRoot({ excludeNodes = [], placement = "bottom", offset = 8, onHide, className, children }) {
|
|
2299
|
+
const { editor } = useCurrentEditor();
|
|
2300
|
+
if (!editor) return null;
|
|
2301
|
+
return /* @__PURE__ */ jsx(BubbleMenu$1, {
|
|
2302
|
+
editor,
|
|
2303
|
+
"data-re-bubble-menu": "",
|
|
2304
|
+
shouldShow: ({ editor: editor$1, view }) => {
|
|
2305
|
+
for (const node of excludeNodes) if (editor$1.isActive(node)) return false;
|
|
2306
|
+
if (view.dom.classList.contains("dragging")) return false;
|
|
2307
|
+
return editor$1.view.state.selection.content().size > 0;
|
|
2308
|
+
},
|
|
2309
|
+
options: {
|
|
2310
|
+
placement,
|
|
2311
|
+
offset,
|
|
2312
|
+
onHide
|
|
2313
|
+
},
|
|
2314
|
+
className,
|
|
2315
|
+
children: /* @__PURE__ */ jsx(BubbleMenuContext.Provider, {
|
|
2316
|
+
value: { editor },
|
|
2317
|
+
children
|
|
2318
|
+
})
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
//#endregion
|
|
2323
|
+
//#region src/ui/bubble-menu/strike.tsx
|
|
2324
|
+
const BubbleMenuStrike = createMarkBubbleItem({
|
|
2325
|
+
name: "strike",
|
|
2326
|
+
activeName: "strike",
|
|
2327
|
+
command: "toggleStrike",
|
|
2328
|
+
icon: /* @__PURE__ */ jsx(StrikethroughIcon, {})
|
|
2329
|
+
});
|
|
2330
|
+
|
|
2331
|
+
//#endregion
|
|
2332
|
+
//#region src/ui/bubble-menu/underline.tsx
|
|
2333
|
+
const BubbleMenuUnderline = createMarkBubbleItem({
|
|
2334
|
+
name: "underline",
|
|
2335
|
+
activeName: "underline",
|
|
2336
|
+
command: "toggleUnderline",
|
|
2337
|
+
icon: /* @__PURE__ */ jsx(UnderlineIcon, {})
|
|
2338
|
+
});
|
|
2339
|
+
|
|
2340
|
+
//#endregion
|
|
2341
|
+
//#region src/ui/bubble-menu/uppercase.tsx
|
|
2342
|
+
const BubbleMenuUppercase = createMarkBubbleItem({
|
|
2343
|
+
name: "uppercase",
|
|
2344
|
+
activeName: "uppercase",
|
|
2345
|
+
command: "toggleUppercase",
|
|
2346
|
+
icon: /* @__PURE__ */ jsx(CaseUpperIcon, {})
|
|
2347
|
+
});
|
|
2348
|
+
|
|
2349
|
+
//#endregion
|
|
2350
|
+
//#region src/ui/bubble-menu/default.tsx
|
|
2351
|
+
function BubbleMenuDefault({ excludeItems = [], excludeNodes, placement, offset, onHide, className }) {
|
|
2352
|
+
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = React.useState(false);
|
|
2353
|
+
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = React.useState(false);
|
|
2354
|
+
const has = (item) => !excludeItems.includes(item);
|
|
2355
|
+
const handleNodeSelectorOpenChange = React.useCallback((open) => {
|
|
2356
|
+
setIsNodeSelectorOpen(open);
|
|
2357
|
+
if (open) setIsLinkSelectorOpen(false);
|
|
2358
|
+
}, []);
|
|
2359
|
+
const handleLinkSelectorOpenChange = React.useCallback((open) => {
|
|
2360
|
+
setIsLinkSelectorOpen(open);
|
|
2361
|
+
if (open) setIsNodeSelectorOpen(false);
|
|
2362
|
+
}, []);
|
|
2363
|
+
const handleHide = React.useCallback(() => {
|
|
2364
|
+
setIsNodeSelectorOpen(false);
|
|
2365
|
+
setIsLinkSelectorOpen(false);
|
|
2366
|
+
onHide?.();
|
|
2367
|
+
}, [onHide]);
|
|
2368
|
+
const hasFormattingItems = has("bold") || has("italic") || has("underline") || has("strike") || has("code") || has("uppercase");
|
|
2369
|
+
const hasAlignmentItems = has("align-left") || has("align-center") || has("align-right");
|
|
2370
|
+
return /* @__PURE__ */ jsxs(BubbleMenuRoot, {
|
|
2371
|
+
excludeNodes,
|
|
2372
|
+
placement,
|
|
2373
|
+
offset,
|
|
2374
|
+
onHide: handleHide,
|
|
2375
|
+
className,
|
|
2376
|
+
children: [
|
|
2377
|
+
has("node-selector") && /* @__PURE__ */ jsx(BubbleMenuNodeSelector, {
|
|
2378
|
+
open: isNodeSelectorOpen,
|
|
2379
|
+
onOpenChange: handleNodeSelectorOpenChange
|
|
2380
|
+
}),
|
|
2381
|
+
has("link-selector") && /* @__PURE__ */ jsx(BubbleMenuLinkSelector, {
|
|
2382
|
+
open: isLinkSelectorOpen,
|
|
2383
|
+
onOpenChange: handleLinkSelectorOpenChange
|
|
2384
|
+
}),
|
|
2385
|
+
hasFormattingItems && /* @__PURE__ */ jsxs(BubbleMenuItemGroup, { children: [
|
|
2386
|
+
has("bold") && /* @__PURE__ */ jsx(BubbleMenuBold, {}),
|
|
2387
|
+
has("italic") && /* @__PURE__ */ jsx(BubbleMenuItalic, {}),
|
|
2388
|
+
has("underline") && /* @__PURE__ */ jsx(BubbleMenuUnderline, {}),
|
|
2389
|
+
has("strike") && /* @__PURE__ */ jsx(BubbleMenuStrike, {}),
|
|
2390
|
+
has("code") && /* @__PURE__ */ jsx(BubbleMenuCode, {}),
|
|
2391
|
+
has("uppercase") && /* @__PURE__ */ jsx(BubbleMenuUppercase, {})
|
|
2392
|
+
] }),
|
|
2393
|
+
hasAlignmentItems && /* @__PURE__ */ jsxs(BubbleMenuItemGroup, { children: [
|
|
2394
|
+
has("align-left") && /* @__PURE__ */ jsx(BubbleMenuAlignLeft, {}),
|
|
2395
|
+
has("align-center") && /* @__PURE__ */ jsx(BubbleMenuAlignCenter, {}),
|
|
2396
|
+
has("align-right") && /* @__PURE__ */ jsx(BubbleMenuAlignRight, {})
|
|
2397
|
+
] })
|
|
2398
|
+
]
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
//#endregion
|
|
2403
|
+
//#region src/ui/bubble-menu/separator.tsx
|
|
2404
|
+
function BubbleMenuSeparator({ className }) {
|
|
2405
|
+
return /* @__PURE__ */ jsx("hr", {
|
|
2406
|
+
className,
|
|
2407
|
+
"data-re-bubble-menu-separator": ""
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
//#endregion
|
|
2412
|
+
//#region src/ui/bubble-menu/index.ts
|
|
2413
|
+
const BubbleMenu = {
|
|
2414
|
+
Root: BubbleMenuRoot,
|
|
2415
|
+
ItemGroup: BubbleMenuItemGroup,
|
|
2416
|
+
Separator: BubbleMenuSeparator,
|
|
2417
|
+
Item: BubbleMenuItem,
|
|
2418
|
+
Bold: BubbleMenuBold,
|
|
2419
|
+
Italic: BubbleMenuItalic,
|
|
2420
|
+
Underline: BubbleMenuUnderline,
|
|
2421
|
+
Strike: BubbleMenuStrike,
|
|
2422
|
+
Code: BubbleMenuCode,
|
|
2423
|
+
Uppercase: BubbleMenuUppercase,
|
|
2424
|
+
AlignLeft: BubbleMenuAlignLeft,
|
|
2425
|
+
AlignCenter: BubbleMenuAlignCenter,
|
|
2426
|
+
AlignRight: BubbleMenuAlignRight,
|
|
2427
|
+
NodeSelector: Object.assign(BubbleMenuNodeSelector, {
|
|
2428
|
+
Root: NodeSelectorRoot,
|
|
2429
|
+
Trigger: NodeSelectorTrigger,
|
|
2430
|
+
Content: NodeSelectorContent
|
|
2431
|
+
}),
|
|
2432
|
+
LinkSelector: BubbleMenuLinkSelector,
|
|
2433
|
+
Default: BubbleMenuDefault
|
|
2434
|
+
};
|
|
2435
|
+
|
|
2436
|
+
//#endregion
|
|
2437
|
+
//#region src/ui/link-bubble-menu/context.tsx
|
|
2438
|
+
const LinkBubbleMenuContext = React.createContext(null);
|
|
2439
|
+
function useLinkBubbleMenuContext() {
|
|
2440
|
+
const context = React.useContext(LinkBubbleMenuContext);
|
|
2441
|
+
if (!context) throw new Error("LinkBubbleMenu compound components must be used within <LinkBubbleMenu.Root>");
|
|
2442
|
+
return context;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
//#endregion
|
|
2446
|
+
//#region src/ui/link-bubble-menu/edit-link.tsx
|
|
2447
|
+
function LinkBubbleMenuEditLink({ className, children, onClick,...rest }) {
|
|
2448
|
+
const { setIsEditing } = useLinkBubbleMenuContext();
|
|
2449
|
+
return /* @__PURE__ */ jsx("button", {
|
|
2450
|
+
type: "button",
|
|
2451
|
+
"aria-label": "Edit link",
|
|
2452
|
+
"data-re-link-bm-item": "",
|
|
2453
|
+
"data-item": "edit-link",
|
|
2454
|
+
className,
|
|
2455
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
2456
|
+
onClick: (e) => {
|
|
2457
|
+
onClick?.(e);
|
|
2458
|
+
setIsEditing(true);
|
|
2459
|
+
},
|
|
2460
|
+
...rest,
|
|
2461
|
+
children: children ?? /* @__PURE__ */ jsx(PencilIcon, {})
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
//#endregion
|
|
2466
|
+
//#region src/ui/link-bubble-menu/form.tsx
|
|
2467
|
+
function LinkBubbleMenuForm({ className, validateUrl, onLinkApply, onLinkRemove, children }) {
|
|
2468
|
+
const { editor, linkHref, isEditing, setIsEditing } = useLinkBubbleMenuContext();
|
|
2469
|
+
const inputRef = React.useRef(null);
|
|
2470
|
+
const formRef = React.useRef(null);
|
|
2471
|
+
const displayHref = linkHref === "#" ? "" : linkHref;
|
|
2472
|
+
const [inputValue, setInputValue] = React.useState(displayHref);
|
|
2473
|
+
React.useEffect(() => {
|
|
2474
|
+
if (!isEditing) return;
|
|
2475
|
+
const timeoutId = setTimeout(() => {
|
|
2476
|
+
inputRef.current?.focus();
|
|
2477
|
+
}, 0);
|
|
2478
|
+
return () => clearTimeout(timeoutId);
|
|
2479
|
+
}, [isEditing]);
|
|
2480
|
+
React.useEffect(() => {
|
|
2481
|
+
if (!isEditing) return;
|
|
2482
|
+
const handleKeyDown = (event) => {
|
|
2483
|
+
if (event.key === "Escape") setIsEditing(false);
|
|
2484
|
+
};
|
|
2485
|
+
const handleClickOutside = (event) => {
|
|
2486
|
+
if (formRef.current && !formRef.current.contains(event.target)) {
|
|
2487
|
+
const form = formRef.current;
|
|
2488
|
+
const submitEvent = new Event("submit", {
|
|
2489
|
+
bubbles: true,
|
|
2490
|
+
cancelable: true
|
|
2491
|
+
});
|
|
2492
|
+
form.dispatchEvent(submitEvent);
|
|
2493
|
+
setIsEditing(false);
|
|
2494
|
+
}
|
|
2495
|
+
};
|
|
2496
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
2497
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
2498
|
+
return () => {
|
|
2499
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
2500
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
2501
|
+
};
|
|
2502
|
+
}, [isEditing, setIsEditing]);
|
|
2503
|
+
if (!isEditing) return null;
|
|
2504
|
+
function handleSubmit(e) {
|
|
2505
|
+
e.preventDefault();
|
|
2506
|
+
const value = inputValue.trim();
|
|
2507
|
+
if (value === "") {
|
|
2508
|
+
setLinkHref(editor, "");
|
|
2509
|
+
setIsEditing(false);
|
|
2510
|
+
focusEditor(editor);
|
|
2511
|
+
onLinkRemove?.();
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
const finalValue = (validateUrl ?? getUrlFromString)(value);
|
|
2515
|
+
if (!finalValue) {
|
|
2516
|
+
setLinkHref(editor, "");
|
|
2517
|
+
setIsEditing(false);
|
|
2518
|
+
focusEditor(editor);
|
|
2519
|
+
onLinkRemove?.();
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
setLinkHref(editor, finalValue);
|
|
2523
|
+
setIsEditing(false);
|
|
2524
|
+
focusEditor(editor);
|
|
2525
|
+
onLinkApply?.(finalValue);
|
|
2526
|
+
}
|
|
2527
|
+
function handleUnlink(e) {
|
|
2528
|
+
e.stopPropagation();
|
|
2529
|
+
setLinkHref(editor, "");
|
|
2530
|
+
setIsEditing(false);
|
|
2531
|
+
focusEditor(editor);
|
|
2532
|
+
onLinkRemove?.();
|
|
2533
|
+
}
|
|
2534
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
2535
|
+
ref: formRef,
|
|
2536
|
+
"data-re-link-bm-form": "",
|
|
2537
|
+
className,
|
|
2538
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
2539
|
+
onClick: (e) => e.stopPropagation(),
|
|
2540
|
+
onKeyDown: (e) => e.stopPropagation(),
|
|
2541
|
+
onSubmit: handleSubmit,
|
|
2542
|
+
children: [
|
|
2543
|
+
/* @__PURE__ */ jsx("input", {
|
|
2544
|
+
ref: inputRef,
|
|
2545
|
+
"data-re-link-bm-input": "",
|
|
2546
|
+
value: inputValue,
|
|
2547
|
+
onFocus: (e) => e.stopPropagation(),
|
|
2548
|
+
onChange: (e) => setInputValue(e.target.value),
|
|
2549
|
+
placeholder: "Paste a link",
|
|
2550
|
+
type: "text"
|
|
2551
|
+
}),
|
|
2552
|
+
children,
|
|
2553
|
+
displayHref ? /* @__PURE__ */ jsx("button", {
|
|
2554
|
+
type: "button",
|
|
2555
|
+
"aria-label": "Remove link",
|
|
2556
|
+
"data-re-link-bm-unlink": "",
|
|
2557
|
+
onClick: handleUnlink,
|
|
2558
|
+
children: /* @__PURE__ */ jsx(UnlinkIcon, {})
|
|
2559
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
2560
|
+
type: "submit",
|
|
2561
|
+
"aria-label": "Apply link",
|
|
2562
|
+
"data-re-link-bm-apply": "",
|
|
2563
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
2564
|
+
children: /* @__PURE__ */ jsx(Check, {})
|
|
2565
|
+
})
|
|
2566
|
+
]
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
//#endregion
|
|
2571
|
+
//#region src/ui/link-bubble-menu/open-link.tsx
|
|
2572
|
+
function LinkBubbleMenuOpenLink({ className, children,...rest }) {
|
|
2573
|
+
const { linkHref } = useLinkBubbleMenuContext();
|
|
2574
|
+
return /* @__PURE__ */ jsx("a", {
|
|
2575
|
+
...rest,
|
|
2576
|
+
href: linkHref,
|
|
2577
|
+
target: "_blank",
|
|
2578
|
+
rel: "noopener noreferrer",
|
|
2579
|
+
"aria-label": "Open link",
|
|
2580
|
+
"data-re-link-bm-item": "",
|
|
2581
|
+
"data-item": "open-link",
|
|
2582
|
+
className,
|
|
2583
|
+
children: children ?? /* @__PURE__ */ jsx(ExternalLinkIcon, {})
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
//#endregion
|
|
2588
|
+
//#region src/ui/link-bubble-menu/root.tsx
|
|
2589
|
+
function LinkBubbleMenuRoot({ onHide, placement = "bottom", offset = 8, className, children }) {
|
|
2590
|
+
const { editor } = useCurrentEditor();
|
|
2591
|
+
const [isEditing, setIsEditing] = React.useState(false);
|
|
2592
|
+
const linkHref = useEditorState({
|
|
2593
|
+
editor,
|
|
2594
|
+
selector: ({ editor: e }) => e?.getAttributes("link").href ?? ""
|
|
2595
|
+
});
|
|
2596
|
+
if (!editor) return null;
|
|
2597
|
+
return /* @__PURE__ */ jsx(BubbleMenu$1, {
|
|
2598
|
+
editor,
|
|
2599
|
+
"data-re-link-bm": "",
|
|
2600
|
+
shouldShow: ({ editor: e }) => e.isActive("link") && e.view.state.selection.content().size === 0,
|
|
2601
|
+
options: {
|
|
2602
|
+
placement,
|
|
2603
|
+
offset,
|
|
2604
|
+
onHide: () => {
|
|
2605
|
+
setIsEditing(false);
|
|
2606
|
+
onHide?.();
|
|
2607
|
+
}
|
|
2608
|
+
},
|
|
2609
|
+
className,
|
|
2610
|
+
children: /* @__PURE__ */ jsx(LinkBubbleMenuContext.Provider, {
|
|
2611
|
+
value: {
|
|
2612
|
+
editor,
|
|
2613
|
+
linkHref: linkHref ?? "",
|
|
2614
|
+
isEditing,
|
|
2615
|
+
setIsEditing
|
|
2616
|
+
},
|
|
2617
|
+
children
|
|
2618
|
+
})
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
//#endregion
|
|
2623
|
+
//#region src/ui/link-bubble-menu/toolbar.tsx
|
|
2624
|
+
function LinkBubbleMenuToolbar({ className, children }) {
|
|
2625
|
+
const { isEditing } = useLinkBubbleMenuContext();
|
|
2626
|
+
if (isEditing) return null;
|
|
2627
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2628
|
+
"data-re-link-bm-toolbar": "",
|
|
2629
|
+
className,
|
|
2630
|
+
children
|
|
2631
|
+
});
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
//#endregion
|
|
2635
|
+
//#region src/ui/link-bubble-menu/unlink.tsx
|
|
2636
|
+
function LinkBubbleMenuUnlink({ className, children, onClick,...rest }) {
|
|
2637
|
+
const { editor } = useLinkBubbleMenuContext();
|
|
2638
|
+
return /* @__PURE__ */ jsx("button", {
|
|
2639
|
+
type: "button",
|
|
2640
|
+
"aria-label": "Remove link",
|
|
2641
|
+
"data-re-link-bm-item": "",
|
|
2642
|
+
"data-item": "unlink",
|
|
2643
|
+
className,
|
|
2644
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
2645
|
+
onClick: (e) => {
|
|
2646
|
+
onClick?.(e);
|
|
2647
|
+
editor.chain().focus().unsetLink().run();
|
|
2648
|
+
},
|
|
2649
|
+
...rest,
|
|
2650
|
+
children: children ?? /* @__PURE__ */ jsx(UnlinkIcon, {})
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
//#endregion
|
|
2655
|
+
//#region src/ui/link-bubble-menu/index.ts
|
|
2656
|
+
const LinkBubbleMenu = {
|
|
2657
|
+
Root: LinkBubbleMenuRoot,
|
|
2658
|
+
Toolbar: LinkBubbleMenuToolbar,
|
|
2659
|
+
Form: LinkBubbleMenuForm,
|
|
2660
|
+
EditLink: LinkBubbleMenuEditLink,
|
|
2661
|
+
Unlink: LinkBubbleMenuUnlink,
|
|
2662
|
+
OpenLink: LinkBubbleMenuOpenLink
|
|
2663
|
+
};
|
|
2664
|
+
|
|
2665
|
+
//#endregion
|
|
2666
|
+
export { AlignmentAttribute, Body, Bold, BubbleMenu, BubbleMenuAlignCenter, BubbleMenuAlignLeft, BubbleMenuAlignRight, BubbleMenuBold, BubbleMenuCode, BubbleMenuDefault, BubbleMenuItalic, BubbleMenuItem, BubbleMenuItemGroup, BubbleMenuLinkSelector, BubbleMenuNodeSelector, BubbleMenuRoot, BubbleMenuSeparator, BubbleMenuStrike, BubbleMenuUnderline, BubbleMenuUppercase, Button, COLUMN_PARENT_TYPES, ClassAttribute, CodeBlockPrism, ColumnsColumn, Div, EmailNode, FourColumns, LinkBubbleMenu, LinkBubbleMenuEditLink, LinkBubbleMenuForm, LinkBubbleMenuOpenLink, LinkBubbleMenuRoot, LinkBubbleMenuToolbar, LinkBubbleMenuUnlink, MAX_COLUMNS_DEPTH, MaxNesting, NodeSelectorContent, NodeSelectorRoot, NodeSelectorTrigger, Placeholder, PreservedStyle, PreviewText, Section, StyleAttribute, Sup, Table, TableCell, TableHeader, TableRow, ThreeColumns, TwoColumns, Uppercase, coreExtensions, editorEventBus, getColumnsDepth, processStylesForUnlink, setTextAlignment, useLinkBubbleMenuContext };
|
|
573
2667
|
//# sourceMappingURL=index.mjs.map
|