@react-email/editor 0.0.0-experimental.22 → 0.0.0-experimental.25

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