@react-email/editor 0.0.0-experimental.21 → 0.0.0-experimental.24

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