@react-email/editor 0.0.0-experimental.14 → 0.0.0-experimental.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1221 -213
- package/dist/index.d.cts +274 -80
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +275 -81
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1183 -218
- package/dist/index.mjs.map +1 -1
- package/dist/ui/bubble-menu/bubble-menu.css +32 -17
- package/dist/ui/slash-command/slash-command.css +44 -0
- package/dist/ui/themes/default.css +241 -31
- package/package.json +20 -4
package/dist/index.mjs
CHANGED
|
@@ -1,51 +1,34 @@
|
|
|
1
|
-
import { Extension, Mark, Node, findChildren, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
|
|
2
|
-
import { StarterKit } from "@tiptap/starter-kit";
|
|
3
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
1
|
import * as ReactEmailComponents from "@react-email/components";
|
|
5
|
-
import { Button as Button$1, CodeBlock, Column, Row, Section as Section$1 } from "@react-email/components";
|
|
2
|
+
import { Body as Body$1, Button as Button$1, CodeBlock, Column, Head, Html, 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, Mark, Node as Node$1, findChildren, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
|
|
5
|
+
import { UndoRedo } from "@tiptap/extensions";
|
|
6
|
+
import { ReactRenderer, useCurrentEditor, useEditor as useEditor$1, useEditorState } from "@tiptap/react";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import { useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
9
|
+
import { StarterKit } from "@tiptap/starter-kit";
|
|
10
|
+
import BlockquoteBase from "@tiptap/extension-blockquote";
|
|
11
|
+
import BulletListBase from "@tiptap/extension-bullet-list";
|
|
12
|
+
import CodeBase from "@tiptap/extension-code";
|
|
6
13
|
import CodeBlock$1 from "@tiptap/extension-code-block";
|
|
7
14
|
import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
|
8
15
|
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
9
16
|
import { fromHtml } from "hast-util-from-html";
|
|
10
17
|
import Prism from "prismjs";
|
|
18
|
+
import HardBreakBase from "@tiptap/extension-hard-break";
|
|
19
|
+
import ItalicBase from "@tiptap/extension-italic";
|
|
20
|
+
import ListItemBase from "@tiptap/extension-list-item";
|
|
21
|
+
import OrderedListBase from "@tiptap/extension-ordered-list";
|
|
22
|
+
import ParagraphBase from "@tiptap/extension-paragraph";
|
|
11
23
|
import TipTapPlaceholder from "@tiptap/extension-placeholder";
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
24
|
+
import StrikeBase from "@tiptap/extension-strike";
|
|
25
|
+
import { generateJSON } from "@tiptap/html";
|
|
26
|
+
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";
|
|
15
27
|
import * as Popover from "@radix-ui/react-popover";
|
|
16
28
|
import { BubbleMenu as BubbleMenu$1 } from "@tiptap/react/menus";
|
|
29
|
+
import Suggestion from "@tiptap/suggestion";
|
|
30
|
+
import tippy from "tippy.js";
|
|
17
31
|
|
|
18
|
-
//#region src/core/email-node.ts
|
|
19
|
-
var EmailNode = class EmailNode extends Node {
|
|
20
|
-
constructor(config) {
|
|
21
|
-
super(config);
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Create a new Node instance
|
|
25
|
-
* @param config - Node configuration object or a function that returns a configuration object
|
|
26
|
-
*/
|
|
27
|
-
static create(config) {
|
|
28
|
-
return new EmailNode(typeof config === "function" ? config() : config);
|
|
29
|
-
}
|
|
30
|
-
static from(node, renderToReactEmail) {
|
|
31
|
-
const customNode = EmailNode.create({});
|
|
32
|
-
Object.assign(customNode, { ...node });
|
|
33
|
-
customNode.config = {
|
|
34
|
-
...node.config,
|
|
35
|
-
renderToReactEmail
|
|
36
|
-
};
|
|
37
|
-
return customNode;
|
|
38
|
-
}
|
|
39
|
-
configure(options) {
|
|
40
|
-
return super.configure(options);
|
|
41
|
-
}
|
|
42
|
-
extend(extendedConfig) {
|
|
43
|
-
const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
|
|
44
|
-
return super.extend(resolvedConfig);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
//#endregion
|
|
49
32
|
//#region src/core/event-bus.ts
|
|
50
33
|
const EVENT_PREFIX = "@react-email/editor:";
|
|
51
34
|
var EditorEventBus = class {
|
|
@@ -87,139 +70,6 @@ var EditorEventBus = class {
|
|
|
87
70
|
};
|
|
88
71
|
const editorEventBus = new EditorEventBus();
|
|
89
72
|
|
|
90
|
-
//#endregion
|
|
91
|
-
//#region src/extensions/alignment-attribute.tsx
|
|
92
|
-
const AlignmentAttribute = Extension.create({
|
|
93
|
-
name: "alignmentAttribute",
|
|
94
|
-
addOptions() {
|
|
95
|
-
return {
|
|
96
|
-
types: [],
|
|
97
|
-
alignments: [
|
|
98
|
-
"left",
|
|
99
|
-
"center",
|
|
100
|
-
"right",
|
|
101
|
-
"justify"
|
|
102
|
-
]
|
|
103
|
-
};
|
|
104
|
-
},
|
|
105
|
-
addGlobalAttributes() {
|
|
106
|
-
return [{
|
|
107
|
-
types: this.options.types,
|
|
108
|
-
attributes: { alignment: {
|
|
109
|
-
parseHTML: (element) => {
|
|
110
|
-
const explicitAlign = element.getAttribute("align") || element.getAttribute("alignment") || element.style.textAlign;
|
|
111
|
-
if (explicitAlign && this.options.alignments.includes(explicitAlign)) return explicitAlign;
|
|
112
|
-
return null;
|
|
113
|
-
},
|
|
114
|
-
renderHTML: (attributes) => {
|
|
115
|
-
if (attributes.alignment === "left") return {};
|
|
116
|
-
return { alignment: attributes.alignment };
|
|
117
|
-
}
|
|
118
|
-
} }
|
|
119
|
-
}];
|
|
120
|
-
},
|
|
121
|
-
addCommands() {
|
|
122
|
-
return { setAlignment: (alignment) => ({ commands }) => {
|
|
123
|
-
if (!this.options.alignments.includes(alignment)) return false;
|
|
124
|
-
return this.options.types.every((type) => commands.updateAttributes(type, { alignment }));
|
|
125
|
-
} };
|
|
126
|
-
},
|
|
127
|
-
addKeyboardShortcuts() {
|
|
128
|
-
return {
|
|
129
|
-
Enter: () => {
|
|
130
|
-
const { from } = this.editor.state.selection;
|
|
131
|
-
const currentAlignment = this.editor.state.doc.nodeAt(from)?.attrs?.alignment;
|
|
132
|
-
if (currentAlignment) requestAnimationFrame(() => {
|
|
133
|
-
this.editor.commands.setAlignment(currentAlignment);
|
|
134
|
-
});
|
|
135
|
-
return false;
|
|
136
|
-
},
|
|
137
|
-
"Mod-Shift-l": () => this.editor.commands.setAlignment("left"),
|
|
138
|
-
"Mod-Shift-e": () => this.editor.commands.setAlignment("center"),
|
|
139
|
-
"Mod-Shift-r": () => this.editor.commands.setAlignment("right"),
|
|
140
|
-
"Mod-Shift-j": () => this.editor.commands.setAlignment("justify")
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
//#endregion
|
|
146
|
-
//#region src/utils/attribute-helpers.ts
|
|
147
|
-
/**
|
|
148
|
-
* Creates TipTap attribute definitions for a list of HTML attributes.
|
|
149
|
-
* Each attribute will have the same pattern:
|
|
150
|
-
* - default: null
|
|
151
|
-
* - parseHTML: extracts the attribute from the element
|
|
152
|
-
* - renderHTML: conditionally renders the attribute if it has a value
|
|
153
|
-
*
|
|
154
|
-
* @param attributeNames - Array of HTML attribute names to create definitions for
|
|
155
|
-
* @returns Object with TipTap attribute definitions
|
|
156
|
-
*
|
|
157
|
-
* @example
|
|
158
|
-
* const attrs = createStandardAttributes(['class', 'id', 'title']);
|
|
159
|
-
* // Returns:
|
|
160
|
-
* // {
|
|
161
|
-
* // class: {
|
|
162
|
-
* // default: null,
|
|
163
|
-
* // parseHTML: (element) => element.getAttribute('class'),
|
|
164
|
-
* // renderHTML: (attributes) => attributes.class ? { class: attributes.class } : {}
|
|
165
|
-
* // },
|
|
166
|
-
* // ...
|
|
167
|
-
* // }
|
|
168
|
-
*/
|
|
169
|
-
function createStandardAttributes(attributeNames) {
|
|
170
|
-
return Object.fromEntries(attributeNames.map((attr) => [attr, {
|
|
171
|
-
default: null,
|
|
172
|
-
parseHTML: (element) => element.getAttribute(attr),
|
|
173
|
-
renderHTML: (attributes) => {
|
|
174
|
-
if (!attributes[attr]) return {};
|
|
175
|
-
return { [attr]: attributes[attr] };
|
|
176
|
-
}
|
|
177
|
-
}]));
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Common HTML attributes used across multiple extensions.
|
|
181
|
-
* These preserve attributes during HTML import and editing for better
|
|
182
|
-
* fidelity when importing existing email templates.
|
|
183
|
-
*/
|
|
184
|
-
const COMMON_HTML_ATTRIBUTES = [
|
|
185
|
-
"id",
|
|
186
|
-
"class",
|
|
187
|
-
"title",
|
|
188
|
-
"lang",
|
|
189
|
-
"dir",
|
|
190
|
-
"data-id"
|
|
191
|
-
];
|
|
192
|
-
/**
|
|
193
|
-
* Layout-specific HTML attributes used for positioning and sizing.
|
|
194
|
-
*/
|
|
195
|
-
const LAYOUT_ATTRIBUTES = [
|
|
196
|
-
"align",
|
|
197
|
-
"width",
|
|
198
|
-
"height"
|
|
199
|
-
];
|
|
200
|
-
/**
|
|
201
|
-
* Table-specific HTML attributes used for table layout and styling.
|
|
202
|
-
*/
|
|
203
|
-
const TABLE_ATTRIBUTES = [
|
|
204
|
-
"border",
|
|
205
|
-
"cellpadding",
|
|
206
|
-
"cellspacing"
|
|
207
|
-
];
|
|
208
|
-
/**
|
|
209
|
-
* Table cell-specific HTML attributes.
|
|
210
|
-
*/
|
|
211
|
-
const TABLE_CELL_ATTRIBUTES = [
|
|
212
|
-
"valign",
|
|
213
|
-
"bgcolor",
|
|
214
|
-
"colspan",
|
|
215
|
-
"rowspan"
|
|
216
|
-
];
|
|
217
|
-
/**
|
|
218
|
-
* Table header cell-specific HTML attributes.
|
|
219
|
-
* These are additional attributes that only apply to <th> elements.
|
|
220
|
-
*/
|
|
221
|
-
const TABLE_HEADER_ATTRIBUTES = [...TABLE_CELL_ATTRIBUTES, "scope"];
|
|
222
|
-
|
|
223
73
|
//#endregion
|
|
224
74
|
//#region src/utils/styles.ts
|
|
225
75
|
const WHITE_SPACE_REGEX = /\s+/;
|
|
@@ -372,22 +222,358 @@ function convertBorderValue(value) {
|
|
|
372
222
|
}
|
|
373
223
|
}
|
|
374
224
|
/**
|
|
375
|
-
* Resolves conflicts between reset styles and inline styles by expanding
|
|
376
|
-
* shorthand properties (margin, padding) to longhand before merging.
|
|
377
|
-
* This prevents shorthand properties from overriding specific longhand properties.
|
|
378
|
-
*
|
|
379
|
-
* @param resetStyles - Base reset styles that may contain shorthand properties
|
|
380
|
-
* @param inlineStyles - Inline styles that should override reset styles
|
|
381
|
-
* @returns Merged styles with inline styles taking precedence
|
|
225
|
+
* Resolves conflicts between reset styles and inline styles by expanding
|
|
226
|
+
* shorthand properties (margin, padding) to longhand before merging.
|
|
227
|
+
* This prevents shorthand properties from overriding specific longhand properties.
|
|
228
|
+
*
|
|
229
|
+
* @param resetStyles - Base reset styles that may contain shorthand properties
|
|
230
|
+
* @param inlineStyles - Inline styles that should override reset styles
|
|
231
|
+
* @returns Merged styles with inline styles taking precedence
|
|
232
|
+
*/
|
|
233
|
+
function resolveConflictingStyles(resetStyles, inlineStyles) {
|
|
234
|
+
const expandedResetStyles = expandShorthandProperties(resetStyles);
|
|
235
|
+
const expandedInlineStyles = expandShorthandProperties(inlineStyles);
|
|
236
|
+
return {
|
|
237
|
+
...expandedResetStyles,
|
|
238
|
+
...expandedInlineStyles
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/core/serializer/default-base-template.tsx
|
|
244
|
+
function DefaultBaseTemplate({ children, previewText }) {
|
|
245
|
+
return /* @__PURE__ */ jsxs(Html, { children: [
|
|
246
|
+
/* @__PURE__ */ jsxs(Head, { children: [
|
|
247
|
+
/* @__PURE__ */ jsx("meta", {
|
|
248
|
+
content: "width=device-width",
|
|
249
|
+
name: "viewport"
|
|
250
|
+
}),
|
|
251
|
+
/* @__PURE__ */ jsx("meta", {
|
|
252
|
+
content: "IE=edge",
|
|
253
|
+
httpEquiv: "X-UA-Compatible"
|
|
254
|
+
}),
|
|
255
|
+
/* @__PURE__ */ jsx("meta", { name: "x-apple-disable-message-reformatting" }),
|
|
256
|
+
/* @__PURE__ */ jsx("meta", {
|
|
257
|
+
content: "telephone=no,address=no,email=no,date=no,url=no",
|
|
258
|
+
name: "format-detection"
|
|
259
|
+
})
|
|
260
|
+
] }),
|
|
261
|
+
previewText && previewText !== "" && /* @__PURE__ */ jsx(Preview, { children: previewText }),
|
|
262
|
+
/* @__PURE__ */ jsx(Body$1, { children: /* @__PURE__ */ jsx(Section$1, {
|
|
263
|
+
width: "100%",
|
|
264
|
+
align: "center",
|
|
265
|
+
children: /* @__PURE__ */ jsx(Section$1, {
|
|
266
|
+
style: { width: "100%" },
|
|
267
|
+
children
|
|
268
|
+
})
|
|
269
|
+
}) })
|
|
270
|
+
] });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/core/serializer/email-mark.ts
|
|
275
|
+
var EmailMark = class EmailMark extends Mark {
|
|
276
|
+
constructor(config) {
|
|
277
|
+
super(config);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Create a new Mark instance
|
|
281
|
+
* @param config - Mark configuration object or a function that returns a configuration object
|
|
282
|
+
*/
|
|
283
|
+
static create(config) {
|
|
284
|
+
return new EmailMark(typeof config === "function" ? config() : config);
|
|
285
|
+
}
|
|
286
|
+
static from(mark, renderToReactEmail) {
|
|
287
|
+
const customMark = EmailMark.create({});
|
|
288
|
+
Object.assign(customMark, { ...mark });
|
|
289
|
+
customMark.config = {
|
|
290
|
+
...mark.config,
|
|
291
|
+
renderToReactEmail
|
|
292
|
+
};
|
|
293
|
+
return customMark;
|
|
294
|
+
}
|
|
295
|
+
configure(options) {
|
|
296
|
+
return super.configure(options);
|
|
297
|
+
}
|
|
298
|
+
extend(extendedConfig) {
|
|
299
|
+
const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
|
|
300
|
+
return super.extend(resolvedConfig);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
//#endregion
|
|
305
|
+
//#region src/core/serializer/email-node.ts
|
|
306
|
+
var EmailNode = class EmailNode extends Node$1 {
|
|
307
|
+
constructor(config) {
|
|
308
|
+
super(config);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Create a new Node instance
|
|
312
|
+
* @param config - Node configuration object or a function that returns a configuration object
|
|
313
|
+
*/
|
|
314
|
+
static create(config) {
|
|
315
|
+
return new EmailNode(typeof config === "function" ? config() : config);
|
|
316
|
+
}
|
|
317
|
+
static from(node, renderToReactEmail) {
|
|
318
|
+
const customNode = EmailNode.create({});
|
|
319
|
+
Object.assign(customNode, { ...node });
|
|
320
|
+
customNode.config = {
|
|
321
|
+
...node.config,
|
|
322
|
+
renderToReactEmail
|
|
323
|
+
};
|
|
324
|
+
return customNode;
|
|
325
|
+
}
|
|
326
|
+
configure(options) {
|
|
327
|
+
return super.configure(options);
|
|
328
|
+
}
|
|
329
|
+
extend(extendedConfig) {
|
|
330
|
+
const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
|
|
331
|
+
return super.extend(resolvedConfig);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
//#endregion
|
|
336
|
+
//#region src/core/serializer/compose-react-email.tsx
|
|
337
|
+
const MARK_ORDER = {
|
|
338
|
+
preservedStyle: 0,
|
|
339
|
+
italic: 1,
|
|
340
|
+
strike: 2,
|
|
341
|
+
underline: 3,
|
|
342
|
+
link: 4,
|
|
343
|
+
bold: 5,
|
|
344
|
+
code: 6
|
|
345
|
+
};
|
|
346
|
+
const NODES_WITH_INCREMENTED_CHILD_DEPTH = new Set(["bulletList", "orderedList"]);
|
|
347
|
+
function getOrderedMarks(marks) {
|
|
348
|
+
if (!marks) return [];
|
|
349
|
+
return [...marks].sort((a, b) => (MARK_ORDER[a.type] ?? Number.MAX_SAFE_INTEGER) - (MARK_ORDER[b.type] ?? Number.MAX_SAFE_INTEGER));
|
|
350
|
+
}
|
|
351
|
+
const composeReactEmail = async ({ editor, preview }) => {
|
|
352
|
+
const data = editor.getJSON();
|
|
353
|
+
const extensions = editor.extensionManager.extensions;
|
|
354
|
+
const serializerPlugin = extensions.map((ext) => ext.options?.serializerPlugin).filter((p) => Boolean(p)).at(-1);
|
|
355
|
+
const emailNodeComponentRegistry = Object.fromEntries(extensions.filter((ext) => ext instanceof EmailNode).map((extension) => [extension.name, extension.config.renderToReactEmail]));
|
|
356
|
+
const emailMarkComponentRegistry = Object.fromEntries(extensions.filter((ext) => ext instanceof EmailMark).map((extension) => [extension.name, extension.config.renderToReactEmail]));
|
|
357
|
+
function renderMark(mark, node, children, depth) {
|
|
358
|
+
const markStyle = serializerPlugin?.getNodeStyles({
|
|
359
|
+
type: mark.type,
|
|
360
|
+
attrs: mark.attrs ?? {}
|
|
361
|
+
}, depth, editor) ?? {};
|
|
362
|
+
const markRenderer = emailMarkComponentRegistry[mark.type];
|
|
363
|
+
if (markRenderer) return markRenderer({
|
|
364
|
+
mark,
|
|
365
|
+
node,
|
|
366
|
+
style: markStyle,
|
|
367
|
+
children
|
|
368
|
+
});
|
|
369
|
+
return children;
|
|
370
|
+
}
|
|
371
|
+
function parseContent(content, depth = 0) {
|
|
372
|
+
if (!content) return;
|
|
373
|
+
return content.map((node, index) => {
|
|
374
|
+
const style = serializerPlugin?.getNodeStyles(node, depth, editor) ?? {};
|
|
375
|
+
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
376
|
+
if (node.type && emailNodeComponentRegistry[node.type]) {
|
|
377
|
+
const Component = emailNodeComponentRegistry[node.type];
|
|
378
|
+
const childDepth = NODES_WITH_INCREMENTED_CHILD_DEPTH.has(node.type) ? depth + 1 : depth;
|
|
379
|
+
return /* @__PURE__ */ jsx(Component, {
|
|
380
|
+
node: node.type === "table" && inlineStyles.width && !node.attrs?.width ? {
|
|
381
|
+
...node,
|
|
382
|
+
attrs: {
|
|
383
|
+
...node.attrs,
|
|
384
|
+
width: inlineStyles.width
|
|
385
|
+
}
|
|
386
|
+
} : node,
|
|
387
|
+
style,
|
|
388
|
+
children: parseContent(node.content, childDepth)
|
|
389
|
+
}, index);
|
|
390
|
+
}
|
|
391
|
+
switch (node.type) {
|
|
392
|
+
case "text": {
|
|
393
|
+
let wrappedText = node.text;
|
|
394
|
+
getOrderedMarks(node.marks).forEach((mark) => {
|
|
395
|
+
wrappedText = renderMark(mark, node, wrappedText, depth);
|
|
396
|
+
});
|
|
397
|
+
const textAttributes = node.marks?.find((mark) => mark.type === "textStyle")?.attrs;
|
|
398
|
+
return /* @__PURE__ */ jsx("span", {
|
|
399
|
+
style: {
|
|
400
|
+
...textAttributes,
|
|
401
|
+
...style
|
|
402
|
+
},
|
|
403
|
+
children: wrappedText
|
|
404
|
+
}, index);
|
|
405
|
+
}
|
|
406
|
+
default: return null;
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
const unformattedHtml = await render(/* @__PURE__ */ jsx(serializerPlugin?.BaseTemplate ?? DefaultBaseTemplate, {
|
|
411
|
+
previewText: preview,
|
|
412
|
+
editor,
|
|
413
|
+
children: parseContent(data.content)
|
|
414
|
+
}));
|
|
415
|
+
const [prettyHtml, text] = await Promise.all([pretty(unformattedHtml), toPlainText(unformattedHtml)]);
|
|
416
|
+
return {
|
|
417
|
+
html: prettyHtml,
|
|
418
|
+
text
|
|
419
|
+
};
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/extensions/alignment-attribute.tsx
|
|
424
|
+
const AlignmentAttribute = Extension.create({
|
|
425
|
+
name: "alignmentAttribute",
|
|
426
|
+
addOptions() {
|
|
427
|
+
return {
|
|
428
|
+
types: [],
|
|
429
|
+
alignments: [
|
|
430
|
+
"left",
|
|
431
|
+
"center",
|
|
432
|
+
"right",
|
|
433
|
+
"justify"
|
|
434
|
+
]
|
|
435
|
+
};
|
|
436
|
+
},
|
|
437
|
+
addGlobalAttributes() {
|
|
438
|
+
return [{
|
|
439
|
+
types: this.options.types,
|
|
440
|
+
attributes: { alignment: {
|
|
441
|
+
parseHTML: (element) => {
|
|
442
|
+
const explicitAlign = element.getAttribute("align") || element.getAttribute("alignment") || element.style.textAlign;
|
|
443
|
+
if (explicitAlign && this.options.alignments.includes(explicitAlign)) return explicitAlign;
|
|
444
|
+
return null;
|
|
445
|
+
},
|
|
446
|
+
renderHTML: (attributes) => {
|
|
447
|
+
if (attributes.alignment === "left") return {};
|
|
448
|
+
return { alignment: attributes.alignment };
|
|
449
|
+
}
|
|
450
|
+
} }
|
|
451
|
+
}];
|
|
452
|
+
},
|
|
453
|
+
addCommands() {
|
|
454
|
+
return { setAlignment: (alignment) => ({ commands }) => {
|
|
455
|
+
if (!this.options.alignments.includes(alignment)) return false;
|
|
456
|
+
return this.options.types.every((type) => commands.updateAttributes(type, { alignment }));
|
|
457
|
+
} };
|
|
458
|
+
},
|
|
459
|
+
addKeyboardShortcuts() {
|
|
460
|
+
return {
|
|
461
|
+
Enter: () => {
|
|
462
|
+
const { from } = this.editor.state.selection;
|
|
463
|
+
const currentAlignment = this.editor.state.doc.nodeAt(from)?.attrs?.alignment;
|
|
464
|
+
if (currentAlignment) requestAnimationFrame(() => {
|
|
465
|
+
this.editor.commands.setAlignment(currentAlignment);
|
|
466
|
+
});
|
|
467
|
+
return false;
|
|
468
|
+
},
|
|
469
|
+
"Mod-Shift-l": () => this.editor.commands.setAlignment("left"),
|
|
470
|
+
"Mod-Shift-e": () => this.editor.commands.setAlignment("center"),
|
|
471
|
+
"Mod-Shift-r": () => this.editor.commands.setAlignment("right"),
|
|
472
|
+
"Mod-Shift-j": () => this.editor.commands.setAlignment("justify")
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
//#endregion
|
|
478
|
+
//#region src/utils/get-text-alignment.ts
|
|
479
|
+
function getTextAlignment(alignment) {
|
|
480
|
+
switch (alignment) {
|
|
481
|
+
case "left": return { textAlign: "left" };
|
|
482
|
+
case "center": return { textAlign: "center" };
|
|
483
|
+
case "right": return { textAlign: "right" };
|
|
484
|
+
default: return {};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
//#endregion
|
|
489
|
+
//#region src/extensions/blockquote.tsx
|
|
490
|
+
const Blockquote = EmailNode.from(BlockquoteBase, ({ children, node, style }) => /* @__PURE__ */ jsx("blockquote", {
|
|
491
|
+
className: node.attrs?.class || void 0,
|
|
492
|
+
style: {
|
|
493
|
+
...style,
|
|
494
|
+
...inlineCssToJs(node.attrs?.style),
|
|
495
|
+
...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
|
|
496
|
+
},
|
|
497
|
+
children
|
|
498
|
+
}));
|
|
499
|
+
|
|
500
|
+
//#endregion
|
|
501
|
+
//#region src/utils/attribute-helpers.ts
|
|
502
|
+
/**
|
|
503
|
+
* Creates TipTap attribute definitions for a list of HTML attributes.
|
|
504
|
+
* Each attribute will have the same pattern:
|
|
505
|
+
* - default: null
|
|
506
|
+
* - parseHTML: extracts the attribute from the element
|
|
507
|
+
* - renderHTML: conditionally renders the attribute if it has a value
|
|
508
|
+
*
|
|
509
|
+
* @param attributeNames - Array of HTML attribute names to create definitions for
|
|
510
|
+
* @returns Object with TipTap attribute definitions
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* const attrs = createStandardAttributes(['class', 'id', 'title']);
|
|
514
|
+
* // Returns:
|
|
515
|
+
* // {
|
|
516
|
+
* // class: {
|
|
517
|
+
* // default: null,
|
|
518
|
+
* // parseHTML: (element) => element.getAttribute('class'),
|
|
519
|
+
* // renderHTML: (attributes) => attributes.class ? { class: attributes.class } : {}
|
|
520
|
+
* // },
|
|
521
|
+
* // ...
|
|
522
|
+
* // }
|
|
523
|
+
*/
|
|
524
|
+
function createStandardAttributes(attributeNames) {
|
|
525
|
+
return Object.fromEntries(attributeNames.map((attr) => [attr, {
|
|
526
|
+
default: null,
|
|
527
|
+
parseHTML: (element) => element.getAttribute(attr),
|
|
528
|
+
renderHTML: (attributes) => {
|
|
529
|
+
if (!attributes[attr]) return {};
|
|
530
|
+
return { [attr]: attributes[attr] };
|
|
531
|
+
}
|
|
532
|
+
}]));
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Common HTML attributes used across multiple extensions.
|
|
536
|
+
* These preserve attributes during HTML import and editing for better
|
|
537
|
+
* fidelity when importing existing email templates.
|
|
538
|
+
*/
|
|
539
|
+
const COMMON_HTML_ATTRIBUTES = [
|
|
540
|
+
"id",
|
|
541
|
+
"class",
|
|
542
|
+
"title",
|
|
543
|
+
"lang",
|
|
544
|
+
"dir",
|
|
545
|
+
"data-id"
|
|
546
|
+
];
|
|
547
|
+
/**
|
|
548
|
+
* Layout-specific HTML attributes used for positioning and sizing.
|
|
382
549
|
*/
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
550
|
+
const LAYOUT_ATTRIBUTES = [
|
|
551
|
+
"align",
|
|
552
|
+
"width",
|
|
553
|
+
"height"
|
|
554
|
+
];
|
|
555
|
+
/**
|
|
556
|
+
* Table-specific HTML attributes used for table layout and styling.
|
|
557
|
+
*/
|
|
558
|
+
const TABLE_ATTRIBUTES = [
|
|
559
|
+
"border",
|
|
560
|
+
"cellpadding",
|
|
561
|
+
"cellspacing"
|
|
562
|
+
];
|
|
563
|
+
/**
|
|
564
|
+
* Table cell-specific HTML attributes.
|
|
565
|
+
*/
|
|
566
|
+
const TABLE_CELL_ATTRIBUTES = [
|
|
567
|
+
"valign",
|
|
568
|
+
"bgcolor",
|
|
569
|
+
"colspan",
|
|
570
|
+
"rowspan"
|
|
571
|
+
];
|
|
572
|
+
/**
|
|
573
|
+
* Table header cell-specific HTML attributes.
|
|
574
|
+
* These are additional attributes that only apply to <th> elements.
|
|
575
|
+
*/
|
|
576
|
+
const TABLE_HEADER_ATTRIBUTES = [...TABLE_CELL_ATTRIBUTES, "scope"];
|
|
391
577
|
|
|
392
578
|
//#endregion
|
|
393
579
|
//#region src/extensions/body.tsx
|
|
@@ -435,7 +621,7 @@ const Body = EmailNode.create({
|
|
|
435
621
|
});
|
|
436
622
|
|
|
437
623
|
//#endregion
|
|
438
|
-
//#region src/extensions/bold.
|
|
624
|
+
//#region src/extensions/bold.tsx
|
|
439
625
|
/**
|
|
440
626
|
* Matches bold text via `**` as input.
|
|
441
627
|
*/
|
|
@@ -456,7 +642,7 @@ const underscorePasteRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))/g;
|
|
|
456
642
|
* This extension allows you to mark text as bold.
|
|
457
643
|
* @see https://tiptap.dev/api/marks/bold
|
|
458
644
|
*/
|
|
459
|
-
const Bold =
|
|
645
|
+
const Bold = EmailMark.create({
|
|
460
646
|
name: "bold",
|
|
461
647
|
addOptions() {
|
|
462
648
|
return { HTMLAttributes: {} };
|
|
@@ -481,6 +667,12 @@ const Bold = Mark.create({
|
|
|
481
667
|
0
|
|
482
668
|
];
|
|
483
669
|
},
|
|
670
|
+
renderToReactEmail({ children, style }) {
|
|
671
|
+
return /* @__PURE__ */ jsx("strong", {
|
|
672
|
+
style,
|
|
673
|
+
children
|
|
674
|
+
});
|
|
675
|
+
},
|
|
484
676
|
addCommands() {
|
|
485
677
|
return {
|
|
486
678
|
setBold: () => ({ commands }) => {
|
|
@@ -520,6 +712,17 @@ const Bold = Mark.create({
|
|
|
520
712
|
}
|
|
521
713
|
});
|
|
522
714
|
|
|
715
|
+
//#endregion
|
|
716
|
+
//#region src/extensions/bullet-list.tsx
|
|
717
|
+
const BulletList = EmailNode.from(BulletListBase, ({ children, node, style }) => /* @__PURE__ */ jsx("ul", {
|
|
718
|
+
className: node.attrs?.class || void 0,
|
|
719
|
+
style: {
|
|
720
|
+
...style,
|
|
721
|
+
...inlineCssToJs(node.attrs?.style)
|
|
722
|
+
},
|
|
723
|
+
children
|
|
724
|
+
}));
|
|
725
|
+
|
|
523
726
|
//#endregion
|
|
524
727
|
//#region src/extensions/button.tsx
|
|
525
728
|
const Button = EmailNode.create({
|
|
@@ -641,6 +844,16 @@ const ClassAttribute = Extension.create({
|
|
|
641
844
|
}
|
|
642
845
|
});
|
|
643
846
|
|
|
847
|
+
//#endregion
|
|
848
|
+
//#region src/extensions/code.tsx
|
|
849
|
+
const Code = EmailMark.from(CodeBase, ({ children, node, style }) => /* @__PURE__ */ jsx("code", {
|
|
850
|
+
style: {
|
|
851
|
+
...style,
|
|
852
|
+
...inlineCssToJs(node.attrs?.style)
|
|
853
|
+
},
|
|
854
|
+
children
|
|
855
|
+
}));
|
|
856
|
+
|
|
644
857
|
//#endregion
|
|
645
858
|
//#region src/utils/prism-utils.ts
|
|
646
859
|
const publicURL = "/styles/prism";
|
|
@@ -919,6 +1132,29 @@ const Div = EmailNode.create({
|
|
|
919
1132
|
}
|
|
920
1133
|
});
|
|
921
1134
|
|
|
1135
|
+
//#endregion
|
|
1136
|
+
//#region src/extensions/hard-break.tsx
|
|
1137
|
+
const HardBreak = EmailNode.from(HardBreakBase, () => /* @__PURE__ */ jsx("br", {}));
|
|
1138
|
+
|
|
1139
|
+
//#endregion
|
|
1140
|
+
//#region src/extensions/italic.tsx
|
|
1141
|
+
const Italic = EmailMark.from(ItalicBase, ({ children, style }) => /* @__PURE__ */ jsx("em", {
|
|
1142
|
+
style,
|
|
1143
|
+
children
|
|
1144
|
+
}));
|
|
1145
|
+
|
|
1146
|
+
//#endregion
|
|
1147
|
+
//#region src/extensions/list-item.tsx
|
|
1148
|
+
const ListItem = EmailNode.from(ListItemBase, ({ children, node, style }) => /* @__PURE__ */ jsx("li", {
|
|
1149
|
+
className: node.attrs?.class || void 0,
|
|
1150
|
+
style: {
|
|
1151
|
+
...style,
|
|
1152
|
+
...inlineCssToJs(node.attrs?.style),
|
|
1153
|
+
...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
|
|
1154
|
+
},
|
|
1155
|
+
children
|
|
1156
|
+
}));
|
|
1157
|
+
|
|
922
1158
|
//#endregion
|
|
923
1159
|
//#region src/extensions/max-nesting.ts
|
|
924
1160
|
const MaxNesting = Extension.create({
|
|
@@ -994,6 +1230,33 @@ const MaxNesting = Extension.create({
|
|
|
994
1230
|
}
|
|
995
1231
|
});
|
|
996
1232
|
|
|
1233
|
+
//#endregion
|
|
1234
|
+
//#region src/extensions/ordered-list.tsx
|
|
1235
|
+
const OrderedList = EmailNode.from(OrderedListBase, ({ children, node, style }) => /* @__PURE__ */ jsx("ol", {
|
|
1236
|
+
className: node.attrs?.class || void 0,
|
|
1237
|
+
start: node.attrs?.start,
|
|
1238
|
+
style: {
|
|
1239
|
+
...style,
|
|
1240
|
+
...inlineCssToJs(node.attrs?.style)
|
|
1241
|
+
},
|
|
1242
|
+
children
|
|
1243
|
+
}));
|
|
1244
|
+
|
|
1245
|
+
//#endregion
|
|
1246
|
+
//#region src/extensions/paragraph.tsx
|
|
1247
|
+
const Paragraph = EmailNode.from(ParagraphBase, ({ children, node, style }) => {
|
|
1248
|
+
const isEmpty = !node.content || node.content.length === 0;
|
|
1249
|
+
return /* @__PURE__ */ jsx("p", {
|
|
1250
|
+
className: node.attrs?.class || void 0,
|
|
1251
|
+
style: {
|
|
1252
|
+
...style,
|
|
1253
|
+
...inlineCssToJs(node.attrs?.style),
|
|
1254
|
+
...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
|
|
1255
|
+
},
|
|
1256
|
+
children: isEmpty ? /* @__PURE__ */ jsx("br", {}) : children
|
|
1257
|
+
});
|
|
1258
|
+
});
|
|
1259
|
+
|
|
997
1260
|
//#endregion
|
|
998
1261
|
//#region src/extensions/placeholder.ts
|
|
999
1262
|
const Placeholder = TipTapPlaceholder.configure({
|
|
@@ -1005,8 +1268,8 @@ const Placeholder = TipTapPlaceholder.configure({
|
|
|
1005
1268
|
});
|
|
1006
1269
|
|
|
1007
1270
|
//#endregion
|
|
1008
|
-
//#region src/extensions/preserved-style.
|
|
1009
|
-
const PreservedStyle =
|
|
1271
|
+
//#region src/extensions/preserved-style.tsx
|
|
1272
|
+
const PreservedStyle = EmailMark.create({
|
|
1010
1273
|
name: "preservedStyle",
|
|
1011
1274
|
addAttributes() {
|
|
1012
1275
|
return { style: {
|
|
@@ -1035,6 +1298,12 @@ const PreservedStyle = Mark.create({
|
|
|
1035
1298
|
mergeAttributes(HTMLAttributes),
|
|
1036
1299
|
0
|
|
1037
1300
|
];
|
|
1301
|
+
},
|
|
1302
|
+
renderToReactEmail({ children, mark }) {
|
|
1303
|
+
return /* @__PURE__ */ jsx("span", {
|
|
1304
|
+
style: mark.attrs?.style ? inlineCssToJs(mark.attrs.style) : void 0,
|
|
1305
|
+
children
|
|
1306
|
+
});
|
|
1038
1307
|
}
|
|
1039
1308
|
});
|
|
1040
1309
|
const LINK_INDICATOR_STYLES = [
|
|
@@ -1080,7 +1349,7 @@ function processStylesForUnlink(styleString) {
|
|
|
1080
1349
|
|
|
1081
1350
|
//#endregion
|
|
1082
1351
|
//#region src/extensions/preview-text.ts
|
|
1083
|
-
const PreviewText = Node.create({
|
|
1352
|
+
const PreviewText = Node$1.create({
|
|
1084
1353
|
name: "previewText",
|
|
1085
1354
|
group: "block",
|
|
1086
1355
|
selectable: false,
|
|
@@ -1119,17 +1388,6 @@ const PreviewText = Node.create({
|
|
|
1119
1388
|
}
|
|
1120
1389
|
});
|
|
1121
1390
|
|
|
1122
|
-
//#endregion
|
|
1123
|
-
//#region src/utils/get-text-alignment.ts
|
|
1124
|
-
function getTextAlignment(alignment) {
|
|
1125
|
-
switch (alignment) {
|
|
1126
|
-
case "left": return { textAlign: "left" };
|
|
1127
|
-
case "center": return { textAlign: "center" };
|
|
1128
|
-
case "right": return { textAlign: "right" };
|
|
1129
|
-
default: return {};
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
1391
|
//#endregion
|
|
1134
1392
|
//#region src/extensions/section.tsx
|
|
1135
1393
|
const Section = EmailNode.create({
|
|
@@ -1178,6 +1436,13 @@ const Section = EmailNode.create({
|
|
|
1178
1436
|
}
|
|
1179
1437
|
});
|
|
1180
1438
|
|
|
1439
|
+
//#endregion
|
|
1440
|
+
//#region src/extensions/strike.tsx
|
|
1441
|
+
const Strike = EmailMark.from(StrikeBase, ({ children, style }) => /* @__PURE__ */ jsx("s", {
|
|
1442
|
+
style,
|
|
1443
|
+
children
|
|
1444
|
+
}));
|
|
1445
|
+
|
|
1181
1446
|
//#endregion
|
|
1182
1447
|
//#region src/extensions/style-attribute.tsx
|
|
1183
1448
|
const StyleAttribute = Extension.create({
|
|
@@ -1227,12 +1492,12 @@ const StyleAttribute = Extension.create({
|
|
|
1227
1492
|
});
|
|
1228
1493
|
|
|
1229
1494
|
//#endregion
|
|
1230
|
-
//#region src/extensions/sup.
|
|
1495
|
+
//#region src/extensions/sup.tsx
|
|
1231
1496
|
/**
|
|
1232
1497
|
* This extension allows you to mark text as superscript.
|
|
1233
1498
|
* @see https://tiptap.dev/api/marks/superscript
|
|
1234
1499
|
*/
|
|
1235
|
-
const Sup =
|
|
1500
|
+
const Sup = EmailMark.create({
|
|
1236
1501
|
name: "sup",
|
|
1237
1502
|
addOptions() {
|
|
1238
1503
|
return { HTMLAttributes: {} };
|
|
@@ -1247,6 +1512,12 @@ const Sup = Mark.create({
|
|
|
1247
1512
|
0
|
|
1248
1513
|
];
|
|
1249
1514
|
},
|
|
1515
|
+
renderToReactEmail({ children, style }) {
|
|
1516
|
+
return /* @__PURE__ */ jsx("sup", {
|
|
1517
|
+
style,
|
|
1518
|
+
children
|
|
1519
|
+
});
|
|
1520
|
+
},
|
|
1250
1521
|
addCommands() {
|
|
1251
1522
|
return {
|
|
1252
1523
|
setSup: () => ({ commands }) => {
|
|
@@ -1412,7 +1683,7 @@ const TableCell = EmailNode.create({
|
|
|
1412
1683
|
});
|
|
1413
1684
|
}
|
|
1414
1685
|
});
|
|
1415
|
-
const TableHeader = Node.create({
|
|
1686
|
+
const TableHeader = Node$1.create({
|
|
1416
1687
|
name: "tableHeader",
|
|
1417
1688
|
group: "tableCell",
|
|
1418
1689
|
content: "block+",
|
|
@@ -1449,8 +1720,8 @@ const TableHeader = Node.create({
|
|
|
1449
1720
|
});
|
|
1450
1721
|
|
|
1451
1722
|
//#endregion
|
|
1452
|
-
//#region src/extensions/uppercase.
|
|
1453
|
-
const Uppercase =
|
|
1723
|
+
//#region src/extensions/uppercase.tsx
|
|
1724
|
+
const Uppercase = EmailMark.create({
|
|
1454
1725
|
name: "uppercase",
|
|
1455
1726
|
addOptions() {
|
|
1456
1727
|
return { HTMLAttributes: {} };
|
|
@@ -1471,6 +1742,15 @@ const Uppercase = Mark.create({
|
|
|
1471
1742
|
0
|
|
1472
1743
|
];
|
|
1473
1744
|
},
|
|
1745
|
+
renderToReactEmail({ children, style }) {
|
|
1746
|
+
return /* @__PURE__ */ jsx("span", {
|
|
1747
|
+
style: {
|
|
1748
|
+
...style,
|
|
1749
|
+
textTransform: "uppercase"
|
|
1750
|
+
},
|
|
1751
|
+
children
|
|
1752
|
+
});
|
|
1753
|
+
},
|
|
1474
1754
|
addCommands() {
|
|
1475
1755
|
return {
|
|
1476
1756
|
setUppercase: () => ({ commands }) => {
|
|
@@ -1666,17 +1946,17 @@ const coreExtensions = [
|
|
|
1666
1946
|
underline: false,
|
|
1667
1947
|
trailingNode: false,
|
|
1668
1948
|
bold: false,
|
|
1949
|
+
italic: false,
|
|
1950
|
+
strike: false,
|
|
1951
|
+
code: false,
|
|
1952
|
+
paragraph: false,
|
|
1953
|
+
bulletList: false,
|
|
1954
|
+
orderedList: false,
|
|
1955
|
+
listItem: false,
|
|
1956
|
+
blockquote: false,
|
|
1957
|
+
hardBreak: false,
|
|
1669
1958
|
gapcursor: false,
|
|
1670
|
-
listItem: {},
|
|
1671
|
-
bulletList: { HTMLAttributes: { class: "node-bulletList" } },
|
|
1672
|
-
paragraph: { HTMLAttributes: { class: "node-paragraph" } },
|
|
1673
|
-
orderedList: { HTMLAttributes: { class: "node-orderedList" } },
|
|
1674
|
-
blockquote: { HTMLAttributes: { class: "node-blockquote" } },
|
|
1675
1959
|
codeBlock: false,
|
|
1676
|
-
code: { HTMLAttributes: {
|
|
1677
|
-
class: "node-inlineCode",
|
|
1678
|
-
spellcheck: "false"
|
|
1679
|
-
} },
|
|
1680
1960
|
horizontalRule: false,
|
|
1681
1961
|
dropcursor: {
|
|
1682
1962
|
color: "#61a8f8",
|
|
@@ -1688,9 +1968,21 @@ const coreExtensions = [
|
|
|
1688
1968
|
defaultLanguage: "javascript",
|
|
1689
1969
|
HTMLAttributes: { class: "prism node-codeBlock" }
|
|
1690
1970
|
}),
|
|
1971
|
+
Code.configure({ HTMLAttributes: {
|
|
1972
|
+
class: "node-inlineCode",
|
|
1973
|
+
spellcheck: "false"
|
|
1974
|
+
} }),
|
|
1975
|
+
Paragraph.configure({ HTMLAttributes: { class: "node-paragraph" } }),
|
|
1976
|
+
BulletList.configure({ HTMLAttributes: { class: "node-bulletList" } }),
|
|
1977
|
+
OrderedList.configure({ HTMLAttributes: { class: "node-orderedList" } }),
|
|
1978
|
+
Blockquote.configure({ HTMLAttributes: { class: "node-blockquote" } }),
|
|
1979
|
+
ListItem,
|
|
1980
|
+
HardBreak,
|
|
1981
|
+
Italic,
|
|
1691
1982
|
Placeholder,
|
|
1692
1983
|
PreviewText,
|
|
1693
1984
|
Bold,
|
|
1985
|
+
Strike,
|
|
1694
1986
|
Sup,
|
|
1695
1987
|
Uppercase,
|
|
1696
1988
|
PreservedStyle,
|
|
@@ -1777,6 +2069,221 @@ const coreExtensions = [
|
|
|
1777
2069
|
})
|
|
1778
2070
|
];
|
|
1779
2071
|
|
|
2072
|
+
//#endregion
|
|
2073
|
+
//#region src/core/create-drop-handler.ts
|
|
2074
|
+
function createDropHandler({ onPaste, onUploadImage }) {
|
|
2075
|
+
return (view, event, _slice, moved) => {
|
|
2076
|
+
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
|
|
2077
|
+
event.preventDefault();
|
|
2078
|
+
const file = event.dataTransfer.files[0];
|
|
2079
|
+
if (onPaste?.(file, view)) return true;
|
|
2080
|
+
if (file.type.includes("image/") && onUploadImage) {
|
|
2081
|
+
onUploadImage(file, view, (view.posAtCoords({
|
|
2082
|
+
left: event.clientX,
|
|
2083
|
+
top: event.clientY
|
|
2084
|
+
})?.pos || 0) - 1);
|
|
2085
|
+
return true;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
return false;
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
//#endregion
|
|
2093
|
+
//#region src/utils/paste-sanitizer.ts
|
|
2094
|
+
/**
|
|
2095
|
+
* Sanitizes pasted HTML.
|
|
2096
|
+
* - From editor (has node-* classes): pass through as-is
|
|
2097
|
+
* - From external: strip all styles/classes, keep only semantic HTML
|
|
2098
|
+
*/
|
|
2099
|
+
/**
|
|
2100
|
+
* Detects content from the Resend editor by checking for node-* class names.
|
|
2101
|
+
*/
|
|
2102
|
+
const EDITOR_CLASS_PATTERN = /class="[^"]*node-/;
|
|
2103
|
+
/**
|
|
2104
|
+
* Attributes to preserve on specific elements for EXTERNAL content.
|
|
2105
|
+
* Only functional attributes - NO style or class.
|
|
2106
|
+
*/
|
|
2107
|
+
const PRESERVED_ATTRIBUTES = {
|
|
2108
|
+
a: [
|
|
2109
|
+
"href",
|
|
2110
|
+
"target",
|
|
2111
|
+
"rel"
|
|
2112
|
+
],
|
|
2113
|
+
img: [
|
|
2114
|
+
"src",
|
|
2115
|
+
"alt",
|
|
2116
|
+
"width",
|
|
2117
|
+
"height"
|
|
2118
|
+
],
|
|
2119
|
+
td: ["colspan", "rowspan"],
|
|
2120
|
+
th: [
|
|
2121
|
+
"colspan",
|
|
2122
|
+
"rowspan",
|
|
2123
|
+
"scope"
|
|
2124
|
+
],
|
|
2125
|
+
table: [
|
|
2126
|
+
"border",
|
|
2127
|
+
"cellpadding",
|
|
2128
|
+
"cellspacing"
|
|
2129
|
+
],
|
|
2130
|
+
"*": ["id"]
|
|
2131
|
+
};
|
|
2132
|
+
function isFromEditor(html) {
|
|
2133
|
+
return EDITOR_CLASS_PATTERN.test(html);
|
|
2134
|
+
}
|
|
2135
|
+
function sanitizePastedHtml(html) {
|
|
2136
|
+
if (isFromEditor(html)) return html;
|
|
2137
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
2138
|
+
sanitizeNode(doc.body);
|
|
2139
|
+
return doc.body.innerHTML;
|
|
2140
|
+
}
|
|
2141
|
+
function sanitizeNode(node) {
|
|
2142
|
+
if (node.nodeType === Node.ELEMENT_NODE) sanitizeElement(node);
|
|
2143
|
+
for (const child of Array.from(node.childNodes)) sanitizeNode(child);
|
|
2144
|
+
}
|
|
2145
|
+
function sanitizeElement(el) {
|
|
2146
|
+
const allowedForTag = PRESERVED_ATTRIBUTES[el.tagName.toLowerCase()] || [];
|
|
2147
|
+
const allowedGlobal = PRESERVED_ATTRIBUTES["*"] || [];
|
|
2148
|
+
const allowed = new Set([...allowedForTag, ...allowedGlobal]);
|
|
2149
|
+
const attributesToRemove = [];
|
|
2150
|
+
for (const attr of Array.from(el.attributes)) {
|
|
2151
|
+
if (attr.name.startsWith("data-")) {
|
|
2152
|
+
attributesToRemove.push(attr.name);
|
|
2153
|
+
continue;
|
|
2154
|
+
}
|
|
2155
|
+
if (!allowed.has(attr.name)) attributesToRemove.push(attr.name);
|
|
2156
|
+
}
|
|
2157
|
+
for (const attr of attributesToRemove) el.removeAttribute(attr);
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
//#endregion
|
|
2161
|
+
//#region src/core/create-paste-handler.ts
|
|
2162
|
+
function createPasteHandler({ onPaste, onUploadImage, extensions }) {
|
|
2163
|
+
return (view, event, slice) => {
|
|
2164
|
+
const text = event.clipboardData?.getData("text/plain");
|
|
2165
|
+
if (text && onPaste?.(text, view)) {
|
|
2166
|
+
event.preventDefault();
|
|
2167
|
+
return true;
|
|
2168
|
+
}
|
|
2169
|
+
if (event.clipboardData?.files?.[0]) {
|
|
2170
|
+
const file = event.clipboardData.files[0];
|
|
2171
|
+
if (onPaste?.(file, view)) {
|
|
2172
|
+
event.preventDefault();
|
|
2173
|
+
return true;
|
|
2174
|
+
}
|
|
2175
|
+
if (file.type.includes("image/") && onUploadImage) {
|
|
2176
|
+
const pos = view.state.selection.from;
|
|
2177
|
+
onUploadImage(file, view, pos);
|
|
2178
|
+
return true;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* If the coming content has a single child, we can assume
|
|
2183
|
+
* it's a plain text and doesn't need to be parsed and
|
|
2184
|
+
* be introduced in a new line
|
|
2185
|
+
*/
|
|
2186
|
+
if (slice.content.childCount === 1) return false;
|
|
2187
|
+
if (event.clipboardData?.getData?.("text/html")) {
|
|
2188
|
+
event.preventDefault();
|
|
2189
|
+
const jsonContent = generateJSON(sanitizePastedHtml(event.clipboardData.getData("text/html")), extensions);
|
|
2190
|
+
const node = view.state.schema.nodeFromJSON(jsonContent);
|
|
2191
|
+
const transaction = view.state.tr.replaceSelectionWith(node, false);
|
|
2192
|
+
view.dispatch(transaction);
|
|
2193
|
+
return true;
|
|
2194
|
+
}
|
|
2195
|
+
return false;
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
//#endregion
|
|
2200
|
+
//#region src/core/is-document-visually-empty.ts
|
|
2201
|
+
function isDocumentVisuallyEmpty(doc) {
|
|
2202
|
+
let nonGlobalNodeCount = 0;
|
|
2203
|
+
let firstNonGlobalNode = null;
|
|
2204
|
+
for (let index = 0; index < doc.childCount; index += 1) {
|
|
2205
|
+
const node = doc.child(index);
|
|
2206
|
+
if (node.type.name === "globalContent") continue;
|
|
2207
|
+
nonGlobalNodeCount += 1;
|
|
2208
|
+
if (firstNonGlobalNode === null) firstNonGlobalNode = {
|
|
2209
|
+
type: node.type,
|
|
2210
|
+
textContent: node.textContent,
|
|
2211
|
+
childCount: node.content.childCount
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
if (nonGlobalNodeCount === 0) return true;
|
|
2215
|
+
if (nonGlobalNodeCount !== 1) return false;
|
|
2216
|
+
return firstNonGlobalNode?.type.name === "paragraph" && firstNonGlobalNode.textContent.trim().length === 0 && firstNonGlobalNode.childCount === 0;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
//#endregion
|
|
2220
|
+
//#region src/core/use-editor.ts
|
|
2221
|
+
const COLLABORATION_EXTENSION_NAMES = new Set(["liveblocksExtension", "collaboration"]);
|
|
2222
|
+
function hasCollaborationExtension(exts) {
|
|
2223
|
+
return exts.some((ext) => COLLABORATION_EXTENSION_NAMES.has(ext.name));
|
|
2224
|
+
}
|
|
2225
|
+
function useEditor({ content, extensions = [], onUpdate, onPaste, onUploadImage, onReady, editable = true, ...rest }) {
|
|
2226
|
+
const [contentError, setContentError] = React.useState(null);
|
|
2227
|
+
const isCollaborative = hasCollaborationExtension(extensions);
|
|
2228
|
+
const effectiveExtensions = React.useMemo(() => [
|
|
2229
|
+
...coreExtensions,
|
|
2230
|
+
...isCollaborative ? [] : [UndoRedo],
|
|
2231
|
+
...extensions
|
|
2232
|
+
], [extensions, isCollaborative]);
|
|
2233
|
+
const editor = useEditor$1({
|
|
2234
|
+
content: isCollaborative ? void 0 : content,
|
|
2235
|
+
extensions: effectiveExtensions,
|
|
2236
|
+
immediatelyRender: false,
|
|
2237
|
+
enableContentCheck: true,
|
|
2238
|
+
onContentError({ editor: editor$1, error, disableCollaboration }) {
|
|
2239
|
+
disableCollaboration();
|
|
2240
|
+
setContentError(error);
|
|
2241
|
+
console.error(error);
|
|
2242
|
+
editor$1.setEditable(false);
|
|
2243
|
+
},
|
|
2244
|
+
onCreate({ editor: editor$1 }) {
|
|
2245
|
+
onReady?.(editor$1);
|
|
2246
|
+
},
|
|
2247
|
+
onUpdate({ editor: editor$1, transaction }) {
|
|
2248
|
+
onUpdate?.(editor$1, transaction);
|
|
2249
|
+
},
|
|
2250
|
+
editorProps: {
|
|
2251
|
+
handleDOMEvents: { click: (view, event) => {
|
|
2252
|
+
if (!view.editable) {
|
|
2253
|
+
if (event.target.closest("a")) {
|
|
2254
|
+
event.preventDefault();
|
|
2255
|
+
return true;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
return false;
|
|
2259
|
+
} },
|
|
2260
|
+
handlePaste: createPasteHandler({
|
|
2261
|
+
onPaste,
|
|
2262
|
+
onUploadImage,
|
|
2263
|
+
extensions: effectiveExtensions
|
|
2264
|
+
}),
|
|
2265
|
+
handleDrop: createDropHandler({
|
|
2266
|
+
onPaste,
|
|
2267
|
+
onUploadImage
|
|
2268
|
+
})
|
|
2269
|
+
},
|
|
2270
|
+
...rest
|
|
2271
|
+
});
|
|
2272
|
+
return {
|
|
2273
|
+
editor,
|
|
2274
|
+
isEditorEmpty: useEditorState({
|
|
2275
|
+
editor,
|
|
2276
|
+
selector: (context) => {
|
|
2277
|
+
if (!context.editor) return true;
|
|
2278
|
+
return isDocumentVisuallyEmpty(context.editor.state.doc);
|
|
2279
|
+
}
|
|
2280
|
+
}) ?? true,
|
|
2281
|
+
extensions: effectiveExtensions,
|
|
2282
|
+
contentError,
|
|
2283
|
+
isCollaborative
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
|
|
1780
2287
|
//#endregion
|
|
1781
2288
|
//#region src/utils/set-text-alignment.ts
|
|
1782
2289
|
function setTextAlignment(editor, alignment) {
|
|
@@ -2216,7 +2723,7 @@ function NodeSelectorRoot({ omit = [], open: controlledOpen, onOpenChange, class
|
|
|
2216
2723
|
},
|
|
2217
2724
|
{
|
|
2218
2725
|
name: "Code",
|
|
2219
|
-
icon: Code,
|
|
2726
|
+
icon: Code$1,
|
|
2220
2727
|
command: () => editor.chain().focus().clearNodes().toggleCodeBlock().run(),
|
|
2221
2728
|
isActive: editorState?.isCodeBlockActive ?? false
|
|
2222
2729
|
}
|
|
@@ -2881,5 +3388,463 @@ const LinkBubbleMenu = {
|
|
|
2881
3388
|
};
|
|
2882
3389
|
|
|
2883
3390
|
//#endregion
|
|
2884
|
-
|
|
3391
|
+
//#region src/ui/slash-command/utils.ts
|
|
3392
|
+
function isInsideNode(editor, type) {
|
|
3393
|
+
const { $from } = editor.state.selection;
|
|
3394
|
+
for (let d = $from.depth; d > 0; d--) if ($from.node(d).type.name === type) return true;
|
|
3395
|
+
return false;
|
|
3396
|
+
}
|
|
3397
|
+
function isAtMaxColumnsDepth(editor) {
|
|
3398
|
+
const { from } = editor.state.selection;
|
|
3399
|
+
return getColumnsDepth(editor.state.doc, from) >= MAX_COLUMNS_DEPTH;
|
|
3400
|
+
}
|
|
3401
|
+
function updateScrollView(container, item) {
|
|
3402
|
+
const containerRect = container.getBoundingClientRect();
|
|
3403
|
+
const itemRect = item.getBoundingClientRect();
|
|
3404
|
+
if (itemRect.top < containerRect.top) container.scrollTop -= containerRect.top - itemRect.top;
|
|
3405
|
+
else if (itemRect.bottom > containerRect.bottom) container.scrollTop += itemRect.bottom - containerRect.bottom;
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
//#endregion
|
|
3409
|
+
//#region src/ui/slash-command/command-list.tsx
|
|
3410
|
+
const CATEGORY_ORDER = [
|
|
3411
|
+
"Text",
|
|
3412
|
+
"Media",
|
|
3413
|
+
"Layout",
|
|
3414
|
+
"Utility"
|
|
3415
|
+
];
|
|
3416
|
+
function groupByCategory(items) {
|
|
3417
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3418
|
+
for (const item of items) {
|
|
3419
|
+
const existing = seen.get(item.category);
|
|
3420
|
+
if (existing) existing.push(item);
|
|
3421
|
+
else seen.set(item.category, [item]);
|
|
3422
|
+
}
|
|
3423
|
+
const ordered = [];
|
|
3424
|
+
for (const cat of CATEGORY_ORDER) {
|
|
3425
|
+
const group = seen.get(cat);
|
|
3426
|
+
if (group) {
|
|
3427
|
+
ordered.push({
|
|
3428
|
+
category: cat,
|
|
3429
|
+
items: group
|
|
3430
|
+
});
|
|
3431
|
+
seen.delete(cat);
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
for (const [category, group] of seen) ordered.push({
|
|
3435
|
+
category,
|
|
3436
|
+
items: group
|
|
3437
|
+
});
|
|
3438
|
+
return ordered;
|
|
3439
|
+
}
|
|
3440
|
+
function CommandItem({ item, selected, onSelect }) {
|
|
3441
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
3442
|
+
"data-re-slash-command-item": "",
|
|
3443
|
+
"data-selected": selected || void 0,
|
|
3444
|
+
onClick: onSelect,
|
|
3445
|
+
type: "button",
|
|
3446
|
+
children: [item.icon, /* @__PURE__ */ jsx("span", { children: item.title })]
|
|
3447
|
+
});
|
|
3448
|
+
}
|
|
3449
|
+
function CommandList({ items, command, query, ref }) {
|
|
3450
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
3451
|
+
const containerRef = useRef(null);
|
|
3452
|
+
useEffect(() => {
|
|
3453
|
+
setSelectedIndex(0);
|
|
3454
|
+
}, [items]);
|
|
3455
|
+
useLayoutEffect(() => {
|
|
3456
|
+
const container = containerRef.current;
|
|
3457
|
+
if (!container) return;
|
|
3458
|
+
const selected = container.querySelector("[data-selected]");
|
|
3459
|
+
if (selected) updateScrollView(container, selected);
|
|
3460
|
+
}, [selectedIndex]);
|
|
3461
|
+
const selectItem = useCallback((index) => {
|
|
3462
|
+
const item = items[index];
|
|
3463
|
+
if (item) command(item);
|
|
3464
|
+
}, [items, command]);
|
|
3465
|
+
useImperativeHandle(ref, () => ({ onKeyDown: ({ event }) => {
|
|
3466
|
+
if (items.length === 0) return false;
|
|
3467
|
+
if (event.key === "ArrowUp") {
|
|
3468
|
+
setSelectedIndex((i) => (i + items.length - 1) % items.length);
|
|
3469
|
+
return true;
|
|
3470
|
+
}
|
|
3471
|
+
if (event.key === "ArrowDown") {
|
|
3472
|
+
setSelectedIndex((i) => (i + 1) % items.length);
|
|
3473
|
+
return true;
|
|
3474
|
+
}
|
|
3475
|
+
if (event.key === "Enter") {
|
|
3476
|
+
selectItem(selectedIndex);
|
|
3477
|
+
return true;
|
|
3478
|
+
}
|
|
3479
|
+
return false;
|
|
3480
|
+
} }), [
|
|
3481
|
+
items.length,
|
|
3482
|
+
selectItem,
|
|
3483
|
+
selectedIndex
|
|
3484
|
+
]);
|
|
3485
|
+
if (items.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
3486
|
+
"data-re-slash-command": "",
|
|
3487
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
3488
|
+
"data-re-slash-command-empty": "",
|
|
3489
|
+
children: "No results"
|
|
3490
|
+
})
|
|
3491
|
+
});
|
|
3492
|
+
if (query.trim().length > 0) return /* @__PURE__ */ jsx("div", {
|
|
3493
|
+
"data-re-slash-command": "",
|
|
3494
|
+
ref: containerRef,
|
|
3495
|
+
children: items.map((item, index) => /* @__PURE__ */ jsx(CommandItem, {
|
|
3496
|
+
item,
|
|
3497
|
+
onSelect: () => selectItem(index),
|
|
3498
|
+
selected: index === selectedIndex
|
|
3499
|
+
}, item.title))
|
|
3500
|
+
});
|
|
3501
|
+
const groups = groupByCategory(items);
|
|
3502
|
+
let flatIndex = 0;
|
|
3503
|
+
return /* @__PURE__ */ jsx("div", {
|
|
3504
|
+
"data-re-slash-command": "",
|
|
3505
|
+
ref: containerRef,
|
|
3506
|
+
children: groups.map((group) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
|
|
3507
|
+
"data-re-slash-command-category": "",
|
|
3508
|
+
children: group.category
|
|
3509
|
+
}), group.items.map((item) => {
|
|
3510
|
+
const currentIndex = flatIndex++;
|
|
3511
|
+
return /* @__PURE__ */ jsx(CommandItem, {
|
|
3512
|
+
item,
|
|
3513
|
+
onSelect: () => selectItem(currentIndex),
|
|
3514
|
+
selected: currentIndex === selectedIndex
|
|
3515
|
+
}, item.title);
|
|
3516
|
+
})] }, group.category))
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
//#endregion
|
|
3521
|
+
//#region src/ui/slash-command/commands.tsx
|
|
3522
|
+
const TEXT = {
|
|
3523
|
+
title: "Text",
|
|
3524
|
+
description: "Plain text block",
|
|
3525
|
+
icon: /* @__PURE__ */ jsx(Text, { size: 20 }),
|
|
3526
|
+
category: "Text",
|
|
3527
|
+
searchTerms: ["p", "paragraph"],
|
|
3528
|
+
command: ({ editor, range }) => {
|
|
3529
|
+
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
|
|
3530
|
+
}
|
|
3531
|
+
};
|
|
3532
|
+
const H1 = {
|
|
3533
|
+
title: "Title",
|
|
3534
|
+
description: "Large heading",
|
|
3535
|
+
icon: /* @__PURE__ */ jsx(Heading1, { size: 20 }),
|
|
3536
|
+
category: "Text",
|
|
3537
|
+
searchTerms: [
|
|
3538
|
+
"title",
|
|
3539
|
+
"big",
|
|
3540
|
+
"large",
|
|
3541
|
+
"h1"
|
|
3542
|
+
],
|
|
3543
|
+
command: ({ editor, range }) => {
|
|
3544
|
+
editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
|
|
3545
|
+
}
|
|
3546
|
+
};
|
|
3547
|
+
const H2 = {
|
|
3548
|
+
title: "Subtitle",
|
|
3549
|
+
description: "Medium heading",
|
|
3550
|
+
icon: /* @__PURE__ */ jsx(Heading2, { size: 20 }),
|
|
3551
|
+
category: "Text",
|
|
3552
|
+
searchTerms: [
|
|
3553
|
+
"subtitle",
|
|
3554
|
+
"medium",
|
|
3555
|
+
"h2"
|
|
3556
|
+
],
|
|
3557
|
+
command: ({ editor, range }) => {
|
|
3558
|
+
editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
|
|
3559
|
+
}
|
|
3560
|
+
};
|
|
3561
|
+
const H3 = {
|
|
3562
|
+
title: "Heading",
|
|
3563
|
+
description: "Small heading",
|
|
3564
|
+
icon: /* @__PURE__ */ jsx(Heading3, { size: 20 }),
|
|
3565
|
+
category: "Text",
|
|
3566
|
+
searchTerms: [
|
|
3567
|
+
"subtitle",
|
|
3568
|
+
"small",
|
|
3569
|
+
"h3"
|
|
3570
|
+
],
|
|
3571
|
+
command: ({ editor, range }) => {
|
|
3572
|
+
editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
|
|
3573
|
+
}
|
|
3574
|
+
};
|
|
3575
|
+
const BULLET_LIST = {
|
|
3576
|
+
title: "Bullet list",
|
|
3577
|
+
description: "Unordered list",
|
|
3578
|
+
icon: /* @__PURE__ */ jsx(List, { size: 20 }),
|
|
3579
|
+
category: "Text",
|
|
3580
|
+
searchTerms: ["unordered", "point"],
|
|
3581
|
+
command: ({ editor, range }) => {
|
|
3582
|
+
editor.chain().focus().deleteRange(range).toggleBulletList().run();
|
|
3583
|
+
}
|
|
3584
|
+
};
|
|
3585
|
+
const NUMBERED_LIST = {
|
|
3586
|
+
title: "Numbered list",
|
|
3587
|
+
description: "Ordered list",
|
|
3588
|
+
icon: /* @__PURE__ */ jsx(ListOrdered, { size: 20 }),
|
|
3589
|
+
category: "Text",
|
|
3590
|
+
searchTerms: ["ordered"],
|
|
3591
|
+
command: ({ editor, range }) => {
|
|
3592
|
+
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
|
3593
|
+
}
|
|
3594
|
+
};
|
|
3595
|
+
const QUOTE = {
|
|
3596
|
+
title: "Quote",
|
|
3597
|
+
description: "Block quote",
|
|
3598
|
+
icon: /* @__PURE__ */ jsx(TextQuote, { size: 20 }),
|
|
3599
|
+
category: "Text",
|
|
3600
|
+
searchTerms: ["blockquote"],
|
|
3601
|
+
command: ({ editor, range }) => {
|
|
3602
|
+
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run();
|
|
3603
|
+
}
|
|
3604
|
+
};
|
|
3605
|
+
const CODE = {
|
|
3606
|
+
title: "Code block",
|
|
3607
|
+
description: "Code snippet",
|
|
3608
|
+
icon: /* @__PURE__ */ jsx(SquareCode, { size: 20 }),
|
|
3609
|
+
category: "Text",
|
|
3610
|
+
searchTerms: ["codeblock"],
|
|
3611
|
+
command: ({ editor, range }) => {
|
|
3612
|
+
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
|
|
3613
|
+
}
|
|
3614
|
+
};
|
|
3615
|
+
const BUTTON = {
|
|
3616
|
+
title: "Button",
|
|
3617
|
+
description: "Clickable button",
|
|
3618
|
+
icon: /* @__PURE__ */ jsx(MousePointer, { size: 20 }),
|
|
3619
|
+
category: "Layout",
|
|
3620
|
+
searchTerms: ["button"],
|
|
3621
|
+
command: ({ editor, range }) => {
|
|
3622
|
+
editor.chain().focus().deleteRange(range).setButton().run();
|
|
3623
|
+
}
|
|
3624
|
+
};
|
|
3625
|
+
const DIVIDER = {
|
|
3626
|
+
title: "Divider",
|
|
3627
|
+
description: "Horizontal separator",
|
|
3628
|
+
icon: /* @__PURE__ */ jsx(SplitSquareVertical, { size: 20 }),
|
|
3629
|
+
category: "Layout",
|
|
3630
|
+
searchTerms: [
|
|
3631
|
+
"hr",
|
|
3632
|
+
"divider",
|
|
3633
|
+
"separator"
|
|
3634
|
+
],
|
|
3635
|
+
command: ({ editor, range }) => {
|
|
3636
|
+
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
|
3637
|
+
}
|
|
3638
|
+
};
|
|
3639
|
+
const SECTION = {
|
|
3640
|
+
title: "Section",
|
|
3641
|
+
description: "Content section",
|
|
3642
|
+
icon: /* @__PURE__ */ jsx(Rows2, { size: 20 }),
|
|
3643
|
+
category: "Layout",
|
|
3644
|
+
searchTerms: [
|
|
3645
|
+
"section",
|
|
3646
|
+
"row",
|
|
3647
|
+
"container"
|
|
3648
|
+
],
|
|
3649
|
+
command: ({ editor, range }) => {
|
|
3650
|
+
editor.chain().focus().deleteRange(range).insertSection().run();
|
|
3651
|
+
}
|
|
3652
|
+
};
|
|
3653
|
+
const TWO_COLUMNS = {
|
|
3654
|
+
title: "2 columns",
|
|
3655
|
+
description: "Two column layout",
|
|
3656
|
+
icon: /* @__PURE__ */ jsx(Columns2, { size: 20 }),
|
|
3657
|
+
category: "Layout",
|
|
3658
|
+
searchTerms: [
|
|
3659
|
+
"columns",
|
|
3660
|
+
"column",
|
|
3661
|
+
"layout",
|
|
3662
|
+
"grid",
|
|
3663
|
+
"split",
|
|
3664
|
+
"side-by-side",
|
|
3665
|
+
"multi-column",
|
|
3666
|
+
"row",
|
|
3667
|
+
"two",
|
|
3668
|
+
"2"
|
|
3669
|
+
],
|
|
3670
|
+
command: ({ editor, range }) => {
|
|
3671
|
+
editor.chain().focus().deleteRange(range).insertColumns(2).run();
|
|
3672
|
+
}
|
|
3673
|
+
};
|
|
3674
|
+
const THREE_COLUMNS = {
|
|
3675
|
+
title: "3 columns",
|
|
3676
|
+
description: "Three column layout",
|
|
3677
|
+
icon: /* @__PURE__ */ jsx(Columns3, { size: 20 }),
|
|
3678
|
+
category: "Layout",
|
|
3679
|
+
searchTerms: [
|
|
3680
|
+
"columns",
|
|
3681
|
+
"column",
|
|
3682
|
+
"layout",
|
|
3683
|
+
"grid",
|
|
3684
|
+
"split",
|
|
3685
|
+
"multi-column",
|
|
3686
|
+
"row",
|
|
3687
|
+
"three",
|
|
3688
|
+
"3"
|
|
3689
|
+
],
|
|
3690
|
+
command: ({ editor, range }) => {
|
|
3691
|
+
editor.chain().focus().deleteRange(range).insertColumns(3).run();
|
|
3692
|
+
}
|
|
3693
|
+
};
|
|
3694
|
+
const FOUR_COLUMNS = {
|
|
3695
|
+
title: "4 columns",
|
|
3696
|
+
description: "Four column layout",
|
|
3697
|
+
icon: /* @__PURE__ */ jsx(Columns4, { size: 20 }),
|
|
3698
|
+
category: "Layout",
|
|
3699
|
+
searchTerms: [
|
|
3700
|
+
"columns",
|
|
3701
|
+
"column",
|
|
3702
|
+
"layout",
|
|
3703
|
+
"grid",
|
|
3704
|
+
"split",
|
|
3705
|
+
"multi-column",
|
|
3706
|
+
"row",
|
|
3707
|
+
"four",
|
|
3708
|
+
"4"
|
|
3709
|
+
],
|
|
3710
|
+
command: ({ editor, range }) => {
|
|
3711
|
+
editor.chain().focus().deleteRange(range).insertColumns(4).run();
|
|
3712
|
+
}
|
|
3713
|
+
};
|
|
3714
|
+
const defaultSlashCommands = [
|
|
3715
|
+
TEXT,
|
|
3716
|
+
H1,
|
|
3717
|
+
H2,
|
|
3718
|
+
H3,
|
|
3719
|
+
BULLET_LIST,
|
|
3720
|
+
NUMBERED_LIST,
|
|
3721
|
+
QUOTE,
|
|
3722
|
+
CODE,
|
|
3723
|
+
BUTTON,
|
|
3724
|
+
DIVIDER,
|
|
3725
|
+
SECTION,
|
|
3726
|
+
TWO_COLUMNS,
|
|
3727
|
+
THREE_COLUMNS,
|
|
3728
|
+
FOUR_COLUMNS
|
|
3729
|
+
];
|
|
3730
|
+
|
|
3731
|
+
//#endregion
|
|
3732
|
+
//#region src/ui/slash-command/extension.ts
|
|
3733
|
+
const SlashCommandExtension = Extension.create({
|
|
3734
|
+
name: "slash-command",
|
|
3735
|
+
addOptions() {
|
|
3736
|
+
return { suggestion: {
|
|
3737
|
+
char: "/",
|
|
3738
|
+
allow: ({ editor }) => !editor.isActive("codeBlock"),
|
|
3739
|
+
command: ({ editor, range, props }) => {
|
|
3740
|
+
props.command({
|
|
3741
|
+
editor,
|
|
3742
|
+
range
|
|
3743
|
+
});
|
|
3744
|
+
}
|
|
3745
|
+
} };
|
|
3746
|
+
},
|
|
3747
|
+
addProseMirrorPlugins() {
|
|
3748
|
+
return [Suggestion({
|
|
3749
|
+
pluginKey: new PluginKey("slash-command"),
|
|
3750
|
+
editor: this.editor,
|
|
3751
|
+
...this.options.suggestion
|
|
3752
|
+
})];
|
|
3753
|
+
}
|
|
3754
|
+
});
|
|
3755
|
+
|
|
3756
|
+
//#endregion
|
|
3757
|
+
//#region src/ui/slash-command/render.tsx
|
|
3758
|
+
function createRenderItems(component = CommandList) {
|
|
3759
|
+
return () => {
|
|
3760
|
+
let renderer = null;
|
|
3761
|
+
let popup = null;
|
|
3762
|
+
return {
|
|
3763
|
+
onStart: (props) => {
|
|
3764
|
+
renderer = new ReactRenderer(component, {
|
|
3765
|
+
props,
|
|
3766
|
+
editor: props.editor
|
|
3767
|
+
});
|
|
3768
|
+
if (!props.clientRect) return;
|
|
3769
|
+
popup = tippy("body", {
|
|
3770
|
+
getReferenceClientRect: props.clientRect,
|
|
3771
|
+
appendTo: () => document.body,
|
|
3772
|
+
content: renderer.element,
|
|
3773
|
+
showOnCreate: true,
|
|
3774
|
+
interactive: true,
|
|
3775
|
+
trigger: "manual",
|
|
3776
|
+
placement: "bottom-start"
|
|
3777
|
+
});
|
|
3778
|
+
},
|
|
3779
|
+
onUpdate: (props) => {
|
|
3780
|
+
if (!renderer) return;
|
|
3781
|
+
renderer.updateProps(props);
|
|
3782
|
+
if (popup?.[0] && props.clientRect) popup[0].setProps({ getReferenceClientRect: props.clientRect });
|
|
3783
|
+
},
|
|
3784
|
+
onKeyDown: (props) => {
|
|
3785
|
+
if (props.event.key === "Escape") {
|
|
3786
|
+
popup?.[0]?.hide();
|
|
3787
|
+
return true;
|
|
3788
|
+
}
|
|
3789
|
+
return renderer?.ref?.onKeyDown(props) ?? false;
|
|
3790
|
+
},
|
|
3791
|
+
onExit: () => {
|
|
3792
|
+
popup?.[0]?.destroy();
|
|
3793
|
+
renderer?.destroy();
|
|
3794
|
+
popup = null;
|
|
3795
|
+
renderer = null;
|
|
3796
|
+
}
|
|
3797
|
+
};
|
|
3798
|
+
};
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
//#endregion
|
|
3802
|
+
//#region src/ui/slash-command/search.ts
|
|
3803
|
+
function scoreItem(item, query) {
|
|
3804
|
+
if (!query) return 100;
|
|
3805
|
+
const q = query.toLowerCase();
|
|
3806
|
+
const title = item.title.toLowerCase();
|
|
3807
|
+
const description = item.description.toLowerCase();
|
|
3808
|
+
const terms = item.searchTerms?.map((t) => t.toLowerCase()) ?? [];
|
|
3809
|
+
if (title === q) return 100;
|
|
3810
|
+
if (title.startsWith(q)) return 90;
|
|
3811
|
+
if (title.split(/\s+/).some((w) => w.startsWith(q))) return 80;
|
|
3812
|
+
if (terms.some((t) => t === q)) return 70;
|
|
3813
|
+
if (terms.some((t) => t.startsWith(q))) return 60;
|
|
3814
|
+
if (title.includes(q)) return 40;
|
|
3815
|
+
if (terms.some((t) => t.includes(q))) return 30;
|
|
3816
|
+
if (description.includes(q)) return 20;
|
|
3817
|
+
return 0;
|
|
3818
|
+
}
|
|
3819
|
+
function filterAndRankItems(items, query) {
|
|
3820
|
+
const trimmed = query.trim();
|
|
3821
|
+
if (!trimmed) return items;
|
|
3822
|
+
const scored = items.map((item) => ({
|
|
3823
|
+
item,
|
|
3824
|
+
score: scoreItem(item, trimmed)
|
|
3825
|
+
})).filter(({ score }) => score > 0);
|
|
3826
|
+
scored.sort((a, b) => b.score - a.score);
|
|
3827
|
+
return scored.map(({ item }) => item);
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
//#endregion
|
|
3831
|
+
//#region src/ui/slash-command/create-slash-command.ts
|
|
3832
|
+
function defaultFilterItems(items, query, editor) {
|
|
3833
|
+
return filterAndRankItems(isAtMaxColumnsDepth(editor) ? items.filter((item) => item.category !== "Layout" || !item.title.includes("column")) : items, query);
|
|
3834
|
+
}
|
|
3835
|
+
function createSlashCommand(options) {
|
|
3836
|
+
const items = options?.items ?? defaultSlashCommands;
|
|
3837
|
+
const filterFn = options?.filterItems ?? defaultFilterItems;
|
|
3838
|
+
return SlashCommandExtension.configure({ suggestion: {
|
|
3839
|
+
items: ({ query, editor }) => filterFn(items, query, editor),
|
|
3840
|
+
render: createRenderItems(options?.component)
|
|
3841
|
+
} });
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3844
|
+
//#endregion
|
|
3845
|
+
//#region src/ui/slash-command/index.ts
|
|
3846
|
+
const SlashCommand = createSlashCommand();
|
|
3847
|
+
|
|
3848
|
+
//#endregion
|
|
3849
|
+
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, EmailNode, FOUR_COLUMNS, FourColumns, H1, H2, H3, HardBreak, ImageBubbleMenu, ImageBubbleMenuDefault, ImageBubbleMenuEditLink, ImageBubbleMenuRoot, ImageBubbleMenuToolbar, Italic, 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, Strike, StyleAttribute, Sup, TEXT, THREE_COLUMNS, TWO_COLUMNS, Table, TableCell, TableHeader, TableRow, ThreeColumns, TwoColumns, Uppercase, composeReactEmail, coreExtensions, createSlashCommand, defaultSlashCommands, editorEventBus, filterAndRankItems, getColumnsDepth, isAtMaxColumnsDepth, isInsideNode, processStylesForUnlink, scoreItem, setTextAlignment, useButtonBubbleMenuContext, useEditor, useImageBubbleMenuContext, useLinkBubbleMenuContext };
|
|
2885
3850
|
//# sourceMappingURL=index.mjs.map
|