@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.cjs
CHANGED
|
@@ -25,11 +25,21 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
25
|
}) : target, mod));
|
|
26
26
|
|
|
27
27
|
//#endregion
|
|
28
|
-
let _tiptap_core = require("@tiptap/core");
|
|
29
|
-
let _tiptap_starter_kit = require("@tiptap/starter-kit");
|
|
30
|
-
let react_jsx_runtime = require("react/jsx-runtime");
|
|
31
28
|
let _react_email_components = require("@react-email/components");
|
|
32
29
|
_react_email_components = __toESM(_react_email_components);
|
|
30
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
31
|
+
let _tiptap_core = require("@tiptap/core");
|
|
32
|
+
let _tiptap_extensions = require("@tiptap/extensions");
|
|
33
|
+
let _tiptap_react = require("@tiptap/react");
|
|
34
|
+
let react = require("react");
|
|
35
|
+
react = __toESM(react);
|
|
36
|
+
let _tiptap_starter_kit = require("@tiptap/starter-kit");
|
|
37
|
+
let _tiptap_extension_blockquote = require("@tiptap/extension-blockquote");
|
|
38
|
+
_tiptap_extension_blockquote = __toESM(_tiptap_extension_blockquote);
|
|
39
|
+
let _tiptap_extension_bullet_list = require("@tiptap/extension-bullet-list");
|
|
40
|
+
_tiptap_extension_bullet_list = __toESM(_tiptap_extension_bullet_list);
|
|
41
|
+
let _tiptap_extension_code = require("@tiptap/extension-code");
|
|
42
|
+
_tiptap_extension_code = __toESM(_tiptap_extension_code);
|
|
33
43
|
let _tiptap_extension_code_block = require("@tiptap/extension-code-block");
|
|
34
44
|
_tiptap_extension_code_block = __toESM(_tiptap_extension_code_block);
|
|
35
45
|
let _tiptap_pm_state = require("@tiptap/pm/state");
|
|
@@ -37,47 +47,30 @@ let _tiptap_pm_view = require("@tiptap/pm/view");
|
|
|
37
47
|
let hast_util_from_html = require("hast-util-from-html");
|
|
38
48
|
let prismjs = require("prismjs");
|
|
39
49
|
prismjs = __toESM(prismjs);
|
|
50
|
+
let _tiptap_extension_hard_break = require("@tiptap/extension-hard-break");
|
|
51
|
+
_tiptap_extension_hard_break = __toESM(_tiptap_extension_hard_break);
|
|
52
|
+
let _tiptap_extension_italic = require("@tiptap/extension-italic");
|
|
53
|
+
_tiptap_extension_italic = __toESM(_tiptap_extension_italic);
|
|
54
|
+
let _tiptap_extension_list_item = require("@tiptap/extension-list-item");
|
|
55
|
+
_tiptap_extension_list_item = __toESM(_tiptap_extension_list_item);
|
|
56
|
+
let _tiptap_extension_ordered_list = require("@tiptap/extension-ordered-list");
|
|
57
|
+
_tiptap_extension_ordered_list = __toESM(_tiptap_extension_ordered_list);
|
|
58
|
+
let _tiptap_extension_paragraph = require("@tiptap/extension-paragraph");
|
|
59
|
+
_tiptap_extension_paragraph = __toESM(_tiptap_extension_paragraph);
|
|
40
60
|
let _tiptap_extension_placeholder = require("@tiptap/extension-placeholder");
|
|
41
61
|
_tiptap_extension_placeholder = __toESM(_tiptap_extension_placeholder);
|
|
42
|
-
let
|
|
62
|
+
let _tiptap_extension_strike = require("@tiptap/extension-strike");
|
|
63
|
+
_tiptap_extension_strike = __toESM(_tiptap_extension_strike);
|
|
64
|
+
let _tiptap_html = require("@tiptap/html");
|
|
43
65
|
let lucide_react = require("lucide-react");
|
|
44
|
-
let react = require("react");
|
|
45
|
-
react = __toESM(react);
|
|
46
66
|
let _radix_ui_react_popover = require("@radix-ui/react-popover");
|
|
47
67
|
_radix_ui_react_popover = __toESM(_radix_ui_react_popover);
|
|
48
68
|
let _tiptap_react_menus = require("@tiptap/react/menus");
|
|
69
|
+
let _tiptap_suggestion = require("@tiptap/suggestion");
|
|
70
|
+
_tiptap_suggestion = __toESM(_tiptap_suggestion);
|
|
71
|
+
let tippy_js = require("tippy.js");
|
|
72
|
+
tippy_js = __toESM(tippy_js);
|
|
49
73
|
|
|
50
|
-
//#region src/core/email-node.ts
|
|
51
|
-
var EmailNode = class EmailNode extends _tiptap_core.Node {
|
|
52
|
-
constructor(config) {
|
|
53
|
-
super(config);
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Create a new Node instance
|
|
57
|
-
* @param config - Node configuration object or a function that returns a configuration object
|
|
58
|
-
*/
|
|
59
|
-
static create(config) {
|
|
60
|
-
return new EmailNode(typeof config === "function" ? config() : config);
|
|
61
|
-
}
|
|
62
|
-
static from(node, renderToReactEmail) {
|
|
63
|
-
const customNode = EmailNode.create({});
|
|
64
|
-
Object.assign(customNode, { ...node });
|
|
65
|
-
customNode.config = {
|
|
66
|
-
...node.config,
|
|
67
|
-
renderToReactEmail
|
|
68
|
-
};
|
|
69
|
-
return customNode;
|
|
70
|
-
}
|
|
71
|
-
configure(options) {
|
|
72
|
-
return super.configure(options);
|
|
73
|
-
}
|
|
74
|
-
extend(extendedConfig) {
|
|
75
|
-
const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
|
|
76
|
-
return super.extend(resolvedConfig);
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
//#endregion
|
|
81
74
|
//#region src/core/event-bus.ts
|
|
82
75
|
const EVENT_PREFIX = "@react-email/editor:";
|
|
83
76
|
var EditorEventBus = class {
|
|
@@ -119,139 +112,6 @@ var EditorEventBus = class {
|
|
|
119
112
|
};
|
|
120
113
|
const editorEventBus = new EditorEventBus();
|
|
121
114
|
|
|
122
|
-
//#endregion
|
|
123
|
-
//#region src/extensions/alignment-attribute.tsx
|
|
124
|
-
const AlignmentAttribute = _tiptap_core.Extension.create({
|
|
125
|
-
name: "alignmentAttribute",
|
|
126
|
-
addOptions() {
|
|
127
|
-
return {
|
|
128
|
-
types: [],
|
|
129
|
-
alignments: [
|
|
130
|
-
"left",
|
|
131
|
-
"center",
|
|
132
|
-
"right",
|
|
133
|
-
"justify"
|
|
134
|
-
]
|
|
135
|
-
};
|
|
136
|
-
},
|
|
137
|
-
addGlobalAttributes() {
|
|
138
|
-
return [{
|
|
139
|
-
types: this.options.types,
|
|
140
|
-
attributes: { alignment: {
|
|
141
|
-
parseHTML: (element) => {
|
|
142
|
-
const explicitAlign = element.getAttribute("align") || element.getAttribute("alignment") || element.style.textAlign;
|
|
143
|
-
if (explicitAlign && this.options.alignments.includes(explicitAlign)) return explicitAlign;
|
|
144
|
-
return null;
|
|
145
|
-
},
|
|
146
|
-
renderHTML: (attributes) => {
|
|
147
|
-
if (attributes.alignment === "left") return {};
|
|
148
|
-
return { alignment: attributes.alignment };
|
|
149
|
-
}
|
|
150
|
-
} }
|
|
151
|
-
}];
|
|
152
|
-
},
|
|
153
|
-
addCommands() {
|
|
154
|
-
return { setAlignment: (alignment) => ({ commands }) => {
|
|
155
|
-
if (!this.options.alignments.includes(alignment)) return false;
|
|
156
|
-
return this.options.types.every((type) => commands.updateAttributes(type, { alignment }));
|
|
157
|
-
} };
|
|
158
|
-
},
|
|
159
|
-
addKeyboardShortcuts() {
|
|
160
|
-
return {
|
|
161
|
-
Enter: () => {
|
|
162
|
-
const { from } = this.editor.state.selection;
|
|
163
|
-
const currentAlignment = this.editor.state.doc.nodeAt(from)?.attrs?.alignment;
|
|
164
|
-
if (currentAlignment) requestAnimationFrame(() => {
|
|
165
|
-
this.editor.commands.setAlignment(currentAlignment);
|
|
166
|
-
});
|
|
167
|
-
return false;
|
|
168
|
-
},
|
|
169
|
-
"Mod-Shift-l": () => this.editor.commands.setAlignment("left"),
|
|
170
|
-
"Mod-Shift-e": () => this.editor.commands.setAlignment("center"),
|
|
171
|
-
"Mod-Shift-r": () => this.editor.commands.setAlignment("right"),
|
|
172
|
-
"Mod-Shift-j": () => this.editor.commands.setAlignment("justify")
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
//#endregion
|
|
178
|
-
//#region src/utils/attribute-helpers.ts
|
|
179
|
-
/**
|
|
180
|
-
* Creates TipTap attribute definitions for a list of HTML attributes.
|
|
181
|
-
* Each attribute will have the same pattern:
|
|
182
|
-
* - default: null
|
|
183
|
-
* - parseHTML: extracts the attribute from the element
|
|
184
|
-
* - renderHTML: conditionally renders the attribute if it has a value
|
|
185
|
-
*
|
|
186
|
-
* @param attributeNames - Array of HTML attribute names to create definitions for
|
|
187
|
-
* @returns Object with TipTap attribute definitions
|
|
188
|
-
*
|
|
189
|
-
* @example
|
|
190
|
-
* const attrs = createStandardAttributes(['class', 'id', 'title']);
|
|
191
|
-
* // Returns:
|
|
192
|
-
* // {
|
|
193
|
-
* // class: {
|
|
194
|
-
* // default: null,
|
|
195
|
-
* // parseHTML: (element) => element.getAttribute('class'),
|
|
196
|
-
* // renderHTML: (attributes) => attributes.class ? { class: attributes.class } : {}
|
|
197
|
-
* // },
|
|
198
|
-
* // ...
|
|
199
|
-
* // }
|
|
200
|
-
*/
|
|
201
|
-
function createStandardAttributes(attributeNames) {
|
|
202
|
-
return Object.fromEntries(attributeNames.map((attr) => [attr, {
|
|
203
|
-
default: null,
|
|
204
|
-
parseHTML: (element) => element.getAttribute(attr),
|
|
205
|
-
renderHTML: (attributes) => {
|
|
206
|
-
if (!attributes[attr]) return {};
|
|
207
|
-
return { [attr]: attributes[attr] };
|
|
208
|
-
}
|
|
209
|
-
}]));
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Common HTML attributes used across multiple extensions.
|
|
213
|
-
* These preserve attributes during HTML import and editing for better
|
|
214
|
-
* fidelity when importing existing email templates.
|
|
215
|
-
*/
|
|
216
|
-
const COMMON_HTML_ATTRIBUTES = [
|
|
217
|
-
"id",
|
|
218
|
-
"class",
|
|
219
|
-
"title",
|
|
220
|
-
"lang",
|
|
221
|
-
"dir",
|
|
222
|
-
"data-id"
|
|
223
|
-
];
|
|
224
|
-
/**
|
|
225
|
-
* Layout-specific HTML attributes used for positioning and sizing.
|
|
226
|
-
*/
|
|
227
|
-
const LAYOUT_ATTRIBUTES = [
|
|
228
|
-
"align",
|
|
229
|
-
"width",
|
|
230
|
-
"height"
|
|
231
|
-
];
|
|
232
|
-
/**
|
|
233
|
-
* Table-specific HTML attributes used for table layout and styling.
|
|
234
|
-
*/
|
|
235
|
-
const TABLE_ATTRIBUTES = [
|
|
236
|
-
"border",
|
|
237
|
-
"cellpadding",
|
|
238
|
-
"cellspacing"
|
|
239
|
-
];
|
|
240
|
-
/**
|
|
241
|
-
* Table cell-specific HTML attributes.
|
|
242
|
-
*/
|
|
243
|
-
const TABLE_CELL_ATTRIBUTES = [
|
|
244
|
-
"valign",
|
|
245
|
-
"bgcolor",
|
|
246
|
-
"colspan",
|
|
247
|
-
"rowspan"
|
|
248
|
-
];
|
|
249
|
-
/**
|
|
250
|
-
* Table header cell-specific HTML attributes.
|
|
251
|
-
* These are additional attributes that only apply to <th> elements.
|
|
252
|
-
*/
|
|
253
|
-
const TABLE_HEADER_ATTRIBUTES = [...TABLE_CELL_ATTRIBUTES, "scope"];
|
|
254
|
-
|
|
255
115
|
//#endregion
|
|
256
116
|
//#region src/utils/styles.ts
|
|
257
117
|
const WHITE_SPACE_REGEX = /\s+/;
|
|
@@ -404,22 +264,358 @@ function convertBorderValue(value) {
|
|
|
404
264
|
}
|
|
405
265
|
}
|
|
406
266
|
/**
|
|
407
|
-
* Resolves conflicts between reset styles and inline styles by expanding
|
|
408
|
-
* shorthand properties (margin, padding) to longhand before merging.
|
|
409
|
-
* This prevents shorthand properties from overriding specific longhand properties.
|
|
410
|
-
*
|
|
411
|
-
* @param resetStyles - Base reset styles that may contain shorthand properties
|
|
412
|
-
* @param inlineStyles - Inline styles that should override reset styles
|
|
413
|
-
* @returns Merged styles with inline styles taking precedence
|
|
267
|
+
* Resolves conflicts between reset styles and inline styles by expanding
|
|
268
|
+
* shorthand properties (margin, padding) to longhand before merging.
|
|
269
|
+
* This prevents shorthand properties from overriding specific longhand properties.
|
|
270
|
+
*
|
|
271
|
+
* @param resetStyles - Base reset styles that may contain shorthand properties
|
|
272
|
+
* @param inlineStyles - Inline styles that should override reset styles
|
|
273
|
+
* @returns Merged styles with inline styles taking precedence
|
|
274
|
+
*/
|
|
275
|
+
function resolveConflictingStyles(resetStyles, inlineStyles) {
|
|
276
|
+
const expandedResetStyles = expandShorthandProperties(resetStyles);
|
|
277
|
+
const expandedInlineStyles = expandShorthandProperties(inlineStyles);
|
|
278
|
+
return {
|
|
279
|
+
...expandedResetStyles,
|
|
280
|
+
...expandedInlineStyles
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region src/core/serializer/default-base-template.tsx
|
|
286
|
+
function DefaultBaseTemplate({ children, previewText }) {
|
|
287
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_react_email_components.Html, { children: [
|
|
288
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_react_email_components.Head, { children: [
|
|
289
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("meta", {
|
|
290
|
+
content: "width=device-width",
|
|
291
|
+
name: "viewport"
|
|
292
|
+
}),
|
|
293
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("meta", {
|
|
294
|
+
content: "IE=edge",
|
|
295
|
+
httpEquiv: "X-UA-Compatible"
|
|
296
|
+
}),
|
|
297
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("meta", { name: "x-apple-disable-message-reformatting" }),
|
|
298
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("meta", {
|
|
299
|
+
content: "telephone=no,address=no,email=no,date=no,url=no",
|
|
300
|
+
name: "format-detection"
|
|
301
|
+
})
|
|
302
|
+
] }),
|
|
303
|
+
previewText && previewText !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_email_components.Preview, { children: previewText }),
|
|
304
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_email_components.Body, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_email_components.Section, {
|
|
305
|
+
width: "100%",
|
|
306
|
+
align: "center",
|
|
307
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_email_components.Section, {
|
|
308
|
+
style: { width: "100%" },
|
|
309
|
+
children
|
|
310
|
+
})
|
|
311
|
+
}) })
|
|
312
|
+
] });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region src/core/serializer/email-mark.ts
|
|
317
|
+
var EmailMark = class EmailMark extends _tiptap_core.Mark {
|
|
318
|
+
constructor(config) {
|
|
319
|
+
super(config);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Create a new Mark instance
|
|
323
|
+
* @param config - Mark configuration object or a function that returns a configuration object
|
|
324
|
+
*/
|
|
325
|
+
static create(config) {
|
|
326
|
+
return new EmailMark(typeof config === "function" ? config() : config);
|
|
327
|
+
}
|
|
328
|
+
static from(mark, renderToReactEmail) {
|
|
329
|
+
const customMark = EmailMark.create({});
|
|
330
|
+
Object.assign(customMark, { ...mark });
|
|
331
|
+
customMark.config = {
|
|
332
|
+
...mark.config,
|
|
333
|
+
renderToReactEmail
|
|
334
|
+
};
|
|
335
|
+
return customMark;
|
|
336
|
+
}
|
|
337
|
+
configure(options) {
|
|
338
|
+
return super.configure(options);
|
|
339
|
+
}
|
|
340
|
+
extend(extendedConfig) {
|
|
341
|
+
const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
|
|
342
|
+
return super.extend(resolvedConfig);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region src/core/serializer/email-node.ts
|
|
348
|
+
var EmailNode = class EmailNode extends _tiptap_core.Node {
|
|
349
|
+
constructor(config) {
|
|
350
|
+
super(config);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Create a new Node instance
|
|
354
|
+
* @param config - Node configuration object or a function that returns a configuration object
|
|
355
|
+
*/
|
|
356
|
+
static create(config) {
|
|
357
|
+
return new EmailNode(typeof config === "function" ? config() : config);
|
|
358
|
+
}
|
|
359
|
+
static from(node, renderToReactEmail) {
|
|
360
|
+
const customNode = EmailNode.create({});
|
|
361
|
+
Object.assign(customNode, { ...node });
|
|
362
|
+
customNode.config = {
|
|
363
|
+
...node.config,
|
|
364
|
+
renderToReactEmail
|
|
365
|
+
};
|
|
366
|
+
return customNode;
|
|
367
|
+
}
|
|
368
|
+
configure(options) {
|
|
369
|
+
return super.configure(options);
|
|
370
|
+
}
|
|
371
|
+
extend(extendedConfig) {
|
|
372
|
+
const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
|
|
373
|
+
return super.extend(resolvedConfig);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
//#endregion
|
|
378
|
+
//#region src/core/serializer/compose-react-email.tsx
|
|
379
|
+
const MARK_ORDER = {
|
|
380
|
+
preservedStyle: 0,
|
|
381
|
+
italic: 1,
|
|
382
|
+
strike: 2,
|
|
383
|
+
underline: 3,
|
|
384
|
+
link: 4,
|
|
385
|
+
bold: 5,
|
|
386
|
+
code: 6
|
|
387
|
+
};
|
|
388
|
+
const NODES_WITH_INCREMENTED_CHILD_DEPTH = new Set(["bulletList", "orderedList"]);
|
|
389
|
+
function getOrderedMarks(marks) {
|
|
390
|
+
if (!marks) return [];
|
|
391
|
+
return [...marks].sort((a, b) => (MARK_ORDER[a.type] ?? Number.MAX_SAFE_INTEGER) - (MARK_ORDER[b.type] ?? Number.MAX_SAFE_INTEGER));
|
|
392
|
+
}
|
|
393
|
+
const composeReactEmail = async ({ editor, preview }) => {
|
|
394
|
+
const data = editor.getJSON();
|
|
395
|
+
const extensions = editor.extensionManager.extensions;
|
|
396
|
+
const serializerPlugin = extensions.map((ext) => ext.options?.serializerPlugin).filter((p) => Boolean(p)).at(-1);
|
|
397
|
+
const emailNodeComponentRegistry = Object.fromEntries(extensions.filter((ext) => ext instanceof EmailNode).map((extension) => [extension.name, extension.config.renderToReactEmail]));
|
|
398
|
+
const emailMarkComponentRegistry = Object.fromEntries(extensions.filter((ext) => ext instanceof EmailMark).map((extension) => [extension.name, extension.config.renderToReactEmail]));
|
|
399
|
+
function renderMark(mark, node, children, depth) {
|
|
400
|
+
const markStyle = serializerPlugin?.getNodeStyles({
|
|
401
|
+
type: mark.type,
|
|
402
|
+
attrs: mark.attrs ?? {}
|
|
403
|
+
}, depth, editor) ?? {};
|
|
404
|
+
const markRenderer = emailMarkComponentRegistry[mark.type];
|
|
405
|
+
if (markRenderer) return markRenderer({
|
|
406
|
+
mark,
|
|
407
|
+
node,
|
|
408
|
+
style: markStyle,
|
|
409
|
+
children
|
|
410
|
+
});
|
|
411
|
+
return children;
|
|
412
|
+
}
|
|
413
|
+
function parseContent(content, depth = 0) {
|
|
414
|
+
if (!content) return;
|
|
415
|
+
return content.map((node, index) => {
|
|
416
|
+
const style = serializerPlugin?.getNodeStyles(node, depth, editor) ?? {};
|
|
417
|
+
const inlineStyles = inlineCssToJs(node.attrs?.style);
|
|
418
|
+
if (node.type && emailNodeComponentRegistry[node.type]) {
|
|
419
|
+
const Component = emailNodeComponentRegistry[node.type];
|
|
420
|
+
const childDepth = NODES_WITH_INCREMENTED_CHILD_DEPTH.has(node.type) ? depth + 1 : depth;
|
|
421
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
|
|
422
|
+
node: node.type === "table" && inlineStyles.width && !node.attrs?.width ? {
|
|
423
|
+
...node,
|
|
424
|
+
attrs: {
|
|
425
|
+
...node.attrs,
|
|
426
|
+
width: inlineStyles.width
|
|
427
|
+
}
|
|
428
|
+
} : node,
|
|
429
|
+
style,
|
|
430
|
+
children: parseContent(node.content, childDepth)
|
|
431
|
+
}, index);
|
|
432
|
+
}
|
|
433
|
+
switch (node.type) {
|
|
434
|
+
case "text": {
|
|
435
|
+
let wrappedText = node.text;
|
|
436
|
+
getOrderedMarks(node.marks).forEach((mark) => {
|
|
437
|
+
wrappedText = renderMark(mark, node, wrappedText, depth);
|
|
438
|
+
});
|
|
439
|
+
const textAttributes = node.marks?.find((mark) => mark.type === "textStyle")?.attrs;
|
|
440
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
441
|
+
style: {
|
|
442
|
+
...textAttributes,
|
|
443
|
+
...style
|
|
444
|
+
},
|
|
445
|
+
children: wrappedText
|
|
446
|
+
}, index);
|
|
447
|
+
}
|
|
448
|
+
default: return null;
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
const unformattedHtml = await (0, _react_email_components.render)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(serializerPlugin?.BaseTemplate ?? DefaultBaseTemplate, {
|
|
453
|
+
previewText: preview,
|
|
454
|
+
editor,
|
|
455
|
+
children: parseContent(data.content)
|
|
456
|
+
}));
|
|
457
|
+
const [prettyHtml, text] = await Promise.all([(0, _react_email_components.pretty)(unformattedHtml), (0, _react_email_components.toPlainText)(unformattedHtml)]);
|
|
458
|
+
return {
|
|
459
|
+
html: prettyHtml,
|
|
460
|
+
text
|
|
461
|
+
};
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region src/extensions/alignment-attribute.tsx
|
|
466
|
+
const AlignmentAttribute = _tiptap_core.Extension.create({
|
|
467
|
+
name: "alignmentAttribute",
|
|
468
|
+
addOptions() {
|
|
469
|
+
return {
|
|
470
|
+
types: [],
|
|
471
|
+
alignments: [
|
|
472
|
+
"left",
|
|
473
|
+
"center",
|
|
474
|
+
"right",
|
|
475
|
+
"justify"
|
|
476
|
+
]
|
|
477
|
+
};
|
|
478
|
+
},
|
|
479
|
+
addGlobalAttributes() {
|
|
480
|
+
return [{
|
|
481
|
+
types: this.options.types,
|
|
482
|
+
attributes: { alignment: {
|
|
483
|
+
parseHTML: (element) => {
|
|
484
|
+
const explicitAlign = element.getAttribute("align") || element.getAttribute("alignment") || element.style.textAlign;
|
|
485
|
+
if (explicitAlign && this.options.alignments.includes(explicitAlign)) return explicitAlign;
|
|
486
|
+
return null;
|
|
487
|
+
},
|
|
488
|
+
renderHTML: (attributes) => {
|
|
489
|
+
if (attributes.alignment === "left") return {};
|
|
490
|
+
return { alignment: attributes.alignment };
|
|
491
|
+
}
|
|
492
|
+
} }
|
|
493
|
+
}];
|
|
494
|
+
},
|
|
495
|
+
addCommands() {
|
|
496
|
+
return { setAlignment: (alignment) => ({ commands }) => {
|
|
497
|
+
if (!this.options.alignments.includes(alignment)) return false;
|
|
498
|
+
return this.options.types.every((type) => commands.updateAttributes(type, { alignment }));
|
|
499
|
+
} };
|
|
500
|
+
},
|
|
501
|
+
addKeyboardShortcuts() {
|
|
502
|
+
return {
|
|
503
|
+
Enter: () => {
|
|
504
|
+
const { from } = this.editor.state.selection;
|
|
505
|
+
const currentAlignment = this.editor.state.doc.nodeAt(from)?.attrs?.alignment;
|
|
506
|
+
if (currentAlignment) requestAnimationFrame(() => {
|
|
507
|
+
this.editor.commands.setAlignment(currentAlignment);
|
|
508
|
+
});
|
|
509
|
+
return false;
|
|
510
|
+
},
|
|
511
|
+
"Mod-Shift-l": () => this.editor.commands.setAlignment("left"),
|
|
512
|
+
"Mod-Shift-e": () => this.editor.commands.setAlignment("center"),
|
|
513
|
+
"Mod-Shift-r": () => this.editor.commands.setAlignment("right"),
|
|
514
|
+
"Mod-Shift-j": () => this.editor.commands.setAlignment("justify")
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/utils/get-text-alignment.ts
|
|
521
|
+
function getTextAlignment(alignment) {
|
|
522
|
+
switch (alignment) {
|
|
523
|
+
case "left": return { textAlign: "left" };
|
|
524
|
+
case "center": return { textAlign: "center" };
|
|
525
|
+
case "right": return { textAlign: "right" };
|
|
526
|
+
default: return {};
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
//#endregion
|
|
531
|
+
//#region src/extensions/blockquote.tsx
|
|
532
|
+
const Blockquote = EmailNode.from(_tiptap_extension_blockquote.default, ({ children, node, style }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("blockquote", {
|
|
533
|
+
className: node.attrs?.class || void 0,
|
|
534
|
+
style: {
|
|
535
|
+
...style,
|
|
536
|
+
...inlineCssToJs(node.attrs?.style),
|
|
537
|
+
...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
|
|
538
|
+
},
|
|
539
|
+
children
|
|
540
|
+
}));
|
|
541
|
+
|
|
542
|
+
//#endregion
|
|
543
|
+
//#region src/utils/attribute-helpers.ts
|
|
544
|
+
/**
|
|
545
|
+
* Creates TipTap attribute definitions for a list of HTML attributes.
|
|
546
|
+
* Each attribute will have the same pattern:
|
|
547
|
+
* - default: null
|
|
548
|
+
* - parseHTML: extracts the attribute from the element
|
|
549
|
+
* - renderHTML: conditionally renders the attribute if it has a value
|
|
550
|
+
*
|
|
551
|
+
* @param attributeNames - Array of HTML attribute names to create definitions for
|
|
552
|
+
* @returns Object with TipTap attribute definitions
|
|
553
|
+
*
|
|
554
|
+
* @example
|
|
555
|
+
* const attrs = createStandardAttributes(['class', 'id', 'title']);
|
|
556
|
+
* // Returns:
|
|
557
|
+
* // {
|
|
558
|
+
* // class: {
|
|
559
|
+
* // default: null,
|
|
560
|
+
* // parseHTML: (element) => element.getAttribute('class'),
|
|
561
|
+
* // renderHTML: (attributes) => attributes.class ? { class: attributes.class } : {}
|
|
562
|
+
* // },
|
|
563
|
+
* // ...
|
|
564
|
+
* // }
|
|
565
|
+
*/
|
|
566
|
+
function createStandardAttributes(attributeNames) {
|
|
567
|
+
return Object.fromEntries(attributeNames.map((attr) => [attr, {
|
|
568
|
+
default: null,
|
|
569
|
+
parseHTML: (element) => element.getAttribute(attr),
|
|
570
|
+
renderHTML: (attributes) => {
|
|
571
|
+
if (!attributes[attr]) return {};
|
|
572
|
+
return { [attr]: attributes[attr] };
|
|
573
|
+
}
|
|
574
|
+
}]));
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Common HTML attributes used across multiple extensions.
|
|
578
|
+
* These preserve attributes during HTML import and editing for better
|
|
579
|
+
* fidelity when importing existing email templates.
|
|
580
|
+
*/
|
|
581
|
+
const COMMON_HTML_ATTRIBUTES = [
|
|
582
|
+
"id",
|
|
583
|
+
"class",
|
|
584
|
+
"title",
|
|
585
|
+
"lang",
|
|
586
|
+
"dir",
|
|
587
|
+
"data-id"
|
|
588
|
+
];
|
|
589
|
+
/**
|
|
590
|
+
* Layout-specific HTML attributes used for positioning and sizing.
|
|
414
591
|
*/
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
592
|
+
const LAYOUT_ATTRIBUTES = [
|
|
593
|
+
"align",
|
|
594
|
+
"width",
|
|
595
|
+
"height"
|
|
596
|
+
];
|
|
597
|
+
/**
|
|
598
|
+
* Table-specific HTML attributes used for table layout and styling.
|
|
599
|
+
*/
|
|
600
|
+
const TABLE_ATTRIBUTES = [
|
|
601
|
+
"border",
|
|
602
|
+
"cellpadding",
|
|
603
|
+
"cellspacing"
|
|
604
|
+
];
|
|
605
|
+
/**
|
|
606
|
+
* Table cell-specific HTML attributes.
|
|
607
|
+
*/
|
|
608
|
+
const TABLE_CELL_ATTRIBUTES = [
|
|
609
|
+
"valign",
|
|
610
|
+
"bgcolor",
|
|
611
|
+
"colspan",
|
|
612
|
+
"rowspan"
|
|
613
|
+
];
|
|
614
|
+
/**
|
|
615
|
+
* Table header cell-specific HTML attributes.
|
|
616
|
+
* These are additional attributes that only apply to <th> elements.
|
|
617
|
+
*/
|
|
618
|
+
const TABLE_HEADER_ATTRIBUTES = [...TABLE_CELL_ATTRIBUTES, "scope"];
|
|
423
619
|
|
|
424
620
|
//#endregion
|
|
425
621
|
//#region src/extensions/body.tsx
|
|
@@ -467,7 +663,7 @@ const Body = EmailNode.create({
|
|
|
467
663
|
});
|
|
468
664
|
|
|
469
665
|
//#endregion
|
|
470
|
-
//#region src/extensions/bold.
|
|
666
|
+
//#region src/extensions/bold.tsx
|
|
471
667
|
/**
|
|
472
668
|
* Matches bold text via `**` as input.
|
|
473
669
|
*/
|
|
@@ -488,7 +684,7 @@ const underscorePasteRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))/g;
|
|
|
488
684
|
* This extension allows you to mark text as bold.
|
|
489
685
|
* @see https://tiptap.dev/api/marks/bold
|
|
490
686
|
*/
|
|
491
|
-
const Bold =
|
|
687
|
+
const Bold = EmailMark.create({
|
|
492
688
|
name: "bold",
|
|
493
689
|
addOptions() {
|
|
494
690
|
return { HTMLAttributes: {} };
|
|
@@ -513,6 +709,12 @@ const Bold = _tiptap_core.Mark.create({
|
|
|
513
709
|
0
|
|
514
710
|
];
|
|
515
711
|
},
|
|
712
|
+
renderToReactEmail({ children, style }) {
|
|
713
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
714
|
+
style,
|
|
715
|
+
children
|
|
716
|
+
});
|
|
717
|
+
},
|
|
516
718
|
addCommands() {
|
|
517
719
|
return {
|
|
518
720
|
setBold: () => ({ commands }) => {
|
|
@@ -552,6 +754,17 @@ const Bold = _tiptap_core.Mark.create({
|
|
|
552
754
|
}
|
|
553
755
|
});
|
|
554
756
|
|
|
757
|
+
//#endregion
|
|
758
|
+
//#region src/extensions/bullet-list.tsx
|
|
759
|
+
const BulletList = EmailNode.from(_tiptap_extension_bullet_list.default, ({ children, node, style }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
760
|
+
className: node.attrs?.class || void 0,
|
|
761
|
+
style: {
|
|
762
|
+
...style,
|
|
763
|
+
...inlineCssToJs(node.attrs?.style)
|
|
764
|
+
},
|
|
765
|
+
children
|
|
766
|
+
}));
|
|
767
|
+
|
|
555
768
|
//#endregion
|
|
556
769
|
//#region src/extensions/button.tsx
|
|
557
770
|
const Button = EmailNode.create({
|
|
@@ -673,6 +886,16 @@ const ClassAttribute = _tiptap_core.Extension.create({
|
|
|
673
886
|
}
|
|
674
887
|
});
|
|
675
888
|
|
|
889
|
+
//#endregion
|
|
890
|
+
//#region src/extensions/code.tsx
|
|
891
|
+
const Code = EmailMark.from(_tiptap_extension_code.default, ({ children, node, style }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
892
|
+
style: {
|
|
893
|
+
...style,
|
|
894
|
+
...inlineCssToJs(node.attrs?.style)
|
|
895
|
+
},
|
|
896
|
+
children
|
|
897
|
+
}));
|
|
898
|
+
|
|
676
899
|
//#endregion
|
|
677
900
|
//#region src/utils/prism-utils.ts
|
|
678
901
|
const publicURL = "/styles/prism";
|
|
@@ -951,6 +1174,29 @@ const Div = EmailNode.create({
|
|
|
951
1174
|
}
|
|
952
1175
|
});
|
|
953
1176
|
|
|
1177
|
+
//#endregion
|
|
1178
|
+
//#region src/extensions/hard-break.tsx
|
|
1179
|
+
const HardBreak = EmailNode.from(_tiptap_extension_hard_break.default, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}));
|
|
1180
|
+
|
|
1181
|
+
//#endregion
|
|
1182
|
+
//#region src/extensions/italic.tsx
|
|
1183
|
+
const Italic = EmailMark.from(_tiptap_extension_italic.default, ({ children, style }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("em", {
|
|
1184
|
+
style,
|
|
1185
|
+
children
|
|
1186
|
+
}));
|
|
1187
|
+
|
|
1188
|
+
//#endregion
|
|
1189
|
+
//#region src/extensions/list-item.tsx
|
|
1190
|
+
const ListItem = EmailNode.from(_tiptap_extension_list_item.default, ({ children, node, style }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
1191
|
+
className: node.attrs?.class || void 0,
|
|
1192
|
+
style: {
|
|
1193
|
+
...style,
|
|
1194
|
+
...inlineCssToJs(node.attrs?.style),
|
|
1195
|
+
...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
|
|
1196
|
+
},
|
|
1197
|
+
children
|
|
1198
|
+
}));
|
|
1199
|
+
|
|
954
1200
|
//#endregion
|
|
955
1201
|
//#region src/extensions/max-nesting.ts
|
|
956
1202
|
const MaxNesting = _tiptap_core.Extension.create({
|
|
@@ -1026,6 +1272,33 @@ const MaxNesting = _tiptap_core.Extension.create({
|
|
|
1026
1272
|
}
|
|
1027
1273
|
});
|
|
1028
1274
|
|
|
1275
|
+
//#endregion
|
|
1276
|
+
//#region src/extensions/ordered-list.tsx
|
|
1277
|
+
const OrderedList = EmailNode.from(_tiptap_extension_ordered_list.default, ({ children, node, style }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
|
|
1278
|
+
className: node.attrs?.class || void 0,
|
|
1279
|
+
start: node.attrs?.start,
|
|
1280
|
+
style: {
|
|
1281
|
+
...style,
|
|
1282
|
+
...inlineCssToJs(node.attrs?.style)
|
|
1283
|
+
},
|
|
1284
|
+
children
|
|
1285
|
+
}));
|
|
1286
|
+
|
|
1287
|
+
//#endregion
|
|
1288
|
+
//#region src/extensions/paragraph.tsx
|
|
1289
|
+
const Paragraph = EmailNode.from(_tiptap_extension_paragraph.default, ({ children, node, style }) => {
|
|
1290
|
+
const isEmpty = !node.content || node.content.length === 0;
|
|
1291
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1292
|
+
className: node.attrs?.class || void 0,
|
|
1293
|
+
style: {
|
|
1294
|
+
...style,
|
|
1295
|
+
...inlineCssToJs(node.attrs?.style),
|
|
1296
|
+
...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
|
|
1297
|
+
},
|
|
1298
|
+
children: isEmpty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}) : children
|
|
1299
|
+
});
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1029
1302
|
//#endregion
|
|
1030
1303
|
//#region src/extensions/placeholder.ts
|
|
1031
1304
|
const Placeholder = _tiptap_extension_placeholder.default.configure({
|
|
@@ -1037,8 +1310,8 @@ const Placeholder = _tiptap_extension_placeholder.default.configure({
|
|
|
1037
1310
|
});
|
|
1038
1311
|
|
|
1039
1312
|
//#endregion
|
|
1040
|
-
//#region src/extensions/preserved-style.
|
|
1041
|
-
const PreservedStyle =
|
|
1313
|
+
//#region src/extensions/preserved-style.tsx
|
|
1314
|
+
const PreservedStyle = EmailMark.create({
|
|
1042
1315
|
name: "preservedStyle",
|
|
1043
1316
|
addAttributes() {
|
|
1044
1317
|
return { style: {
|
|
@@ -1067,6 +1340,12 @@ const PreservedStyle = _tiptap_core.Mark.create({
|
|
|
1067
1340
|
(0, _tiptap_core.mergeAttributes)(HTMLAttributes),
|
|
1068
1341
|
0
|
|
1069
1342
|
];
|
|
1343
|
+
},
|
|
1344
|
+
renderToReactEmail({ children, mark }) {
|
|
1345
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1346
|
+
style: mark.attrs?.style ? inlineCssToJs(mark.attrs.style) : void 0,
|
|
1347
|
+
children
|
|
1348
|
+
});
|
|
1070
1349
|
}
|
|
1071
1350
|
});
|
|
1072
1351
|
const LINK_INDICATOR_STYLES = [
|
|
@@ -1151,17 +1430,6 @@ const PreviewText = _tiptap_core.Node.create({
|
|
|
1151
1430
|
}
|
|
1152
1431
|
});
|
|
1153
1432
|
|
|
1154
|
-
//#endregion
|
|
1155
|
-
//#region src/utils/get-text-alignment.ts
|
|
1156
|
-
function getTextAlignment(alignment) {
|
|
1157
|
-
switch (alignment) {
|
|
1158
|
-
case "left": return { textAlign: "left" };
|
|
1159
|
-
case "center": return { textAlign: "center" };
|
|
1160
|
-
case "right": return { textAlign: "right" };
|
|
1161
|
-
default: return {};
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
1433
|
//#endregion
|
|
1166
1434
|
//#region src/extensions/section.tsx
|
|
1167
1435
|
const Section = EmailNode.create({
|
|
@@ -1210,6 +1478,13 @@ const Section = EmailNode.create({
|
|
|
1210
1478
|
}
|
|
1211
1479
|
});
|
|
1212
1480
|
|
|
1481
|
+
//#endregion
|
|
1482
|
+
//#region src/extensions/strike.tsx
|
|
1483
|
+
const Strike = EmailMark.from(_tiptap_extension_strike.default, ({ children, style }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("s", {
|
|
1484
|
+
style,
|
|
1485
|
+
children
|
|
1486
|
+
}));
|
|
1487
|
+
|
|
1213
1488
|
//#endregion
|
|
1214
1489
|
//#region src/extensions/style-attribute.tsx
|
|
1215
1490
|
const StyleAttribute = _tiptap_core.Extension.create({
|
|
@@ -1259,12 +1534,12 @@ const StyleAttribute = _tiptap_core.Extension.create({
|
|
|
1259
1534
|
});
|
|
1260
1535
|
|
|
1261
1536
|
//#endregion
|
|
1262
|
-
//#region src/extensions/sup.
|
|
1537
|
+
//#region src/extensions/sup.tsx
|
|
1263
1538
|
/**
|
|
1264
1539
|
* This extension allows you to mark text as superscript.
|
|
1265
1540
|
* @see https://tiptap.dev/api/marks/superscript
|
|
1266
1541
|
*/
|
|
1267
|
-
const Sup =
|
|
1542
|
+
const Sup = EmailMark.create({
|
|
1268
1543
|
name: "sup",
|
|
1269
1544
|
addOptions() {
|
|
1270
1545
|
return { HTMLAttributes: {} };
|
|
@@ -1279,6 +1554,12 @@ const Sup = _tiptap_core.Mark.create({
|
|
|
1279
1554
|
0
|
|
1280
1555
|
];
|
|
1281
1556
|
},
|
|
1557
|
+
renderToReactEmail({ children, style }) {
|
|
1558
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("sup", {
|
|
1559
|
+
style,
|
|
1560
|
+
children
|
|
1561
|
+
});
|
|
1562
|
+
},
|
|
1282
1563
|
addCommands() {
|
|
1283
1564
|
return {
|
|
1284
1565
|
setSup: () => ({ commands }) => {
|
|
@@ -1481,8 +1762,8 @@ const TableHeader = _tiptap_core.Node.create({
|
|
|
1481
1762
|
});
|
|
1482
1763
|
|
|
1483
1764
|
//#endregion
|
|
1484
|
-
//#region src/extensions/uppercase.
|
|
1485
|
-
const Uppercase =
|
|
1765
|
+
//#region src/extensions/uppercase.tsx
|
|
1766
|
+
const Uppercase = EmailMark.create({
|
|
1486
1767
|
name: "uppercase",
|
|
1487
1768
|
addOptions() {
|
|
1488
1769
|
return { HTMLAttributes: {} };
|
|
@@ -1503,6 +1784,15 @@ const Uppercase = _tiptap_core.Mark.create({
|
|
|
1503
1784
|
0
|
|
1504
1785
|
];
|
|
1505
1786
|
},
|
|
1787
|
+
renderToReactEmail({ children, style }) {
|
|
1788
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1789
|
+
style: {
|
|
1790
|
+
...style,
|
|
1791
|
+
textTransform: "uppercase"
|
|
1792
|
+
},
|
|
1793
|
+
children
|
|
1794
|
+
});
|
|
1795
|
+
},
|
|
1506
1796
|
addCommands() {
|
|
1507
1797
|
return {
|
|
1508
1798
|
setUppercase: () => ({ commands }) => {
|
|
@@ -1698,17 +1988,17 @@ const coreExtensions = [
|
|
|
1698
1988
|
underline: false,
|
|
1699
1989
|
trailingNode: false,
|
|
1700
1990
|
bold: false,
|
|
1991
|
+
italic: false,
|
|
1992
|
+
strike: false,
|
|
1993
|
+
code: false,
|
|
1994
|
+
paragraph: false,
|
|
1995
|
+
bulletList: false,
|
|
1996
|
+
orderedList: false,
|
|
1997
|
+
listItem: false,
|
|
1998
|
+
blockquote: false,
|
|
1999
|
+
hardBreak: false,
|
|
1701
2000
|
gapcursor: false,
|
|
1702
|
-
listItem: {},
|
|
1703
|
-
bulletList: { HTMLAttributes: { class: "node-bulletList" } },
|
|
1704
|
-
paragraph: { HTMLAttributes: { class: "node-paragraph" } },
|
|
1705
|
-
orderedList: { HTMLAttributes: { class: "node-orderedList" } },
|
|
1706
|
-
blockquote: { HTMLAttributes: { class: "node-blockquote" } },
|
|
1707
2001
|
codeBlock: false,
|
|
1708
|
-
code: { HTMLAttributes: {
|
|
1709
|
-
class: "node-inlineCode",
|
|
1710
|
-
spellcheck: "false"
|
|
1711
|
-
} },
|
|
1712
2002
|
horizontalRule: false,
|
|
1713
2003
|
dropcursor: {
|
|
1714
2004
|
color: "#61a8f8",
|
|
@@ -1720,9 +2010,21 @@ const coreExtensions = [
|
|
|
1720
2010
|
defaultLanguage: "javascript",
|
|
1721
2011
|
HTMLAttributes: { class: "prism node-codeBlock" }
|
|
1722
2012
|
}),
|
|
2013
|
+
Code.configure({ HTMLAttributes: {
|
|
2014
|
+
class: "node-inlineCode",
|
|
2015
|
+
spellcheck: "false"
|
|
2016
|
+
} }),
|
|
2017
|
+
Paragraph.configure({ HTMLAttributes: { class: "node-paragraph" } }),
|
|
2018
|
+
BulletList.configure({ HTMLAttributes: { class: "node-bulletList" } }),
|
|
2019
|
+
OrderedList.configure({ HTMLAttributes: { class: "node-orderedList" } }),
|
|
2020
|
+
Blockquote.configure({ HTMLAttributes: { class: "node-blockquote" } }),
|
|
2021
|
+
ListItem,
|
|
2022
|
+
HardBreak,
|
|
2023
|
+
Italic,
|
|
1723
2024
|
Placeholder,
|
|
1724
2025
|
PreviewText,
|
|
1725
2026
|
Bold,
|
|
2027
|
+
Strike,
|
|
1726
2028
|
Sup,
|
|
1727
2029
|
Uppercase,
|
|
1728
2030
|
PreservedStyle,
|
|
@@ -1809,6 +2111,221 @@ const coreExtensions = [
|
|
|
1809
2111
|
})
|
|
1810
2112
|
];
|
|
1811
2113
|
|
|
2114
|
+
//#endregion
|
|
2115
|
+
//#region src/core/create-drop-handler.ts
|
|
2116
|
+
function createDropHandler({ onPaste, onUploadImage }) {
|
|
2117
|
+
return (view, event, _slice, moved) => {
|
|
2118
|
+
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
|
|
2119
|
+
event.preventDefault();
|
|
2120
|
+
const file = event.dataTransfer.files[0];
|
|
2121
|
+
if (onPaste?.(file, view)) return true;
|
|
2122
|
+
if (file.type.includes("image/") && onUploadImage) {
|
|
2123
|
+
onUploadImage(file, view, (view.posAtCoords({
|
|
2124
|
+
left: event.clientX,
|
|
2125
|
+
top: event.clientY
|
|
2126
|
+
})?.pos || 0) - 1);
|
|
2127
|
+
return true;
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
return false;
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
//#endregion
|
|
2135
|
+
//#region src/utils/paste-sanitizer.ts
|
|
2136
|
+
/**
|
|
2137
|
+
* Sanitizes pasted HTML.
|
|
2138
|
+
* - From editor (has node-* classes): pass through as-is
|
|
2139
|
+
* - From external: strip all styles/classes, keep only semantic HTML
|
|
2140
|
+
*/
|
|
2141
|
+
/**
|
|
2142
|
+
* Detects content from the Resend editor by checking for node-* class names.
|
|
2143
|
+
*/
|
|
2144
|
+
const EDITOR_CLASS_PATTERN = /class="[^"]*node-/;
|
|
2145
|
+
/**
|
|
2146
|
+
* Attributes to preserve on specific elements for EXTERNAL content.
|
|
2147
|
+
* Only functional attributes - NO style or class.
|
|
2148
|
+
*/
|
|
2149
|
+
const PRESERVED_ATTRIBUTES = {
|
|
2150
|
+
a: [
|
|
2151
|
+
"href",
|
|
2152
|
+
"target",
|
|
2153
|
+
"rel"
|
|
2154
|
+
],
|
|
2155
|
+
img: [
|
|
2156
|
+
"src",
|
|
2157
|
+
"alt",
|
|
2158
|
+
"width",
|
|
2159
|
+
"height"
|
|
2160
|
+
],
|
|
2161
|
+
td: ["colspan", "rowspan"],
|
|
2162
|
+
th: [
|
|
2163
|
+
"colspan",
|
|
2164
|
+
"rowspan",
|
|
2165
|
+
"scope"
|
|
2166
|
+
],
|
|
2167
|
+
table: [
|
|
2168
|
+
"border",
|
|
2169
|
+
"cellpadding",
|
|
2170
|
+
"cellspacing"
|
|
2171
|
+
],
|
|
2172
|
+
"*": ["id"]
|
|
2173
|
+
};
|
|
2174
|
+
function isFromEditor(html) {
|
|
2175
|
+
return EDITOR_CLASS_PATTERN.test(html);
|
|
2176
|
+
}
|
|
2177
|
+
function sanitizePastedHtml(html) {
|
|
2178
|
+
if (isFromEditor(html)) return html;
|
|
2179
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
2180
|
+
sanitizeNode(doc.body);
|
|
2181
|
+
return doc.body.innerHTML;
|
|
2182
|
+
}
|
|
2183
|
+
function sanitizeNode(node) {
|
|
2184
|
+
if (node.nodeType === Node.ELEMENT_NODE) sanitizeElement(node);
|
|
2185
|
+
for (const child of Array.from(node.childNodes)) sanitizeNode(child);
|
|
2186
|
+
}
|
|
2187
|
+
function sanitizeElement(el) {
|
|
2188
|
+
const allowedForTag = PRESERVED_ATTRIBUTES[el.tagName.toLowerCase()] || [];
|
|
2189
|
+
const allowedGlobal = PRESERVED_ATTRIBUTES["*"] || [];
|
|
2190
|
+
const allowed = new Set([...allowedForTag, ...allowedGlobal]);
|
|
2191
|
+
const attributesToRemove = [];
|
|
2192
|
+
for (const attr of Array.from(el.attributes)) {
|
|
2193
|
+
if (attr.name.startsWith("data-")) {
|
|
2194
|
+
attributesToRemove.push(attr.name);
|
|
2195
|
+
continue;
|
|
2196
|
+
}
|
|
2197
|
+
if (!allowed.has(attr.name)) attributesToRemove.push(attr.name);
|
|
2198
|
+
}
|
|
2199
|
+
for (const attr of attributesToRemove) el.removeAttribute(attr);
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
//#endregion
|
|
2203
|
+
//#region src/core/create-paste-handler.ts
|
|
2204
|
+
function createPasteHandler({ onPaste, onUploadImage, extensions }) {
|
|
2205
|
+
return (view, event, slice) => {
|
|
2206
|
+
const text = event.clipboardData?.getData("text/plain");
|
|
2207
|
+
if (text && onPaste?.(text, view)) {
|
|
2208
|
+
event.preventDefault();
|
|
2209
|
+
return true;
|
|
2210
|
+
}
|
|
2211
|
+
if (event.clipboardData?.files?.[0]) {
|
|
2212
|
+
const file = event.clipboardData.files[0];
|
|
2213
|
+
if (onPaste?.(file, view)) {
|
|
2214
|
+
event.preventDefault();
|
|
2215
|
+
return true;
|
|
2216
|
+
}
|
|
2217
|
+
if (file.type.includes("image/") && onUploadImage) {
|
|
2218
|
+
const pos = view.state.selection.from;
|
|
2219
|
+
onUploadImage(file, view, pos);
|
|
2220
|
+
return true;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
/**
|
|
2224
|
+
* If the coming content has a single child, we can assume
|
|
2225
|
+
* it's a plain text and doesn't need to be parsed and
|
|
2226
|
+
* be introduced in a new line
|
|
2227
|
+
*/
|
|
2228
|
+
if (slice.content.childCount === 1) return false;
|
|
2229
|
+
if (event.clipboardData?.getData?.("text/html")) {
|
|
2230
|
+
event.preventDefault();
|
|
2231
|
+
const jsonContent = (0, _tiptap_html.generateJSON)(sanitizePastedHtml(event.clipboardData.getData("text/html")), extensions);
|
|
2232
|
+
const node = view.state.schema.nodeFromJSON(jsonContent);
|
|
2233
|
+
const transaction = view.state.tr.replaceSelectionWith(node, false);
|
|
2234
|
+
view.dispatch(transaction);
|
|
2235
|
+
return true;
|
|
2236
|
+
}
|
|
2237
|
+
return false;
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
//#endregion
|
|
2242
|
+
//#region src/core/is-document-visually-empty.ts
|
|
2243
|
+
function isDocumentVisuallyEmpty(doc) {
|
|
2244
|
+
let nonGlobalNodeCount = 0;
|
|
2245
|
+
let firstNonGlobalNode = null;
|
|
2246
|
+
for (let index = 0; index < doc.childCount; index += 1) {
|
|
2247
|
+
const node = doc.child(index);
|
|
2248
|
+
if (node.type.name === "globalContent") continue;
|
|
2249
|
+
nonGlobalNodeCount += 1;
|
|
2250
|
+
if (firstNonGlobalNode === null) firstNonGlobalNode = {
|
|
2251
|
+
type: node.type,
|
|
2252
|
+
textContent: node.textContent,
|
|
2253
|
+
childCount: node.content.childCount
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
if (nonGlobalNodeCount === 0) return true;
|
|
2257
|
+
if (nonGlobalNodeCount !== 1) return false;
|
|
2258
|
+
return firstNonGlobalNode?.type.name === "paragraph" && firstNonGlobalNode.textContent.trim().length === 0 && firstNonGlobalNode.childCount === 0;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
//#endregion
|
|
2262
|
+
//#region src/core/use-editor.ts
|
|
2263
|
+
const COLLABORATION_EXTENSION_NAMES = new Set(["liveblocksExtension", "collaboration"]);
|
|
2264
|
+
function hasCollaborationExtension(exts) {
|
|
2265
|
+
return exts.some((ext) => COLLABORATION_EXTENSION_NAMES.has(ext.name));
|
|
2266
|
+
}
|
|
2267
|
+
function useEditor({ content, extensions = [], onUpdate, onPaste, onUploadImage, onReady, editable = true, ...rest }) {
|
|
2268
|
+
const [contentError, setContentError] = react.useState(null);
|
|
2269
|
+
const isCollaborative = hasCollaborationExtension(extensions);
|
|
2270
|
+
const effectiveExtensions = react.useMemo(() => [
|
|
2271
|
+
...coreExtensions,
|
|
2272
|
+
...isCollaborative ? [] : [_tiptap_extensions.UndoRedo],
|
|
2273
|
+
...extensions
|
|
2274
|
+
], [extensions, isCollaborative]);
|
|
2275
|
+
const editor = (0, _tiptap_react.useEditor)({
|
|
2276
|
+
content: isCollaborative ? void 0 : content,
|
|
2277
|
+
extensions: effectiveExtensions,
|
|
2278
|
+
immediatelyRender: false,
|
|
2279
|
+
enableContentCheck: true,
|
|
2280
|
+
onContentError({ editor: editor$1, error, disableCollaboration }) {
|
|
2281
|
+
disableCollaboration();
|
|
2282
|
+
setContentError(error);
|
|
2283
|
+
console.error(error);
|
|
2284
|
+
editor$1.setEditable(false);
|
|
2285
|
+
},
|
|
2286
|
+
onCreate({ editor: editor$1 }) {
|
|
2287
|
+
onReady?.(editor$1);
|
|
2288
|
+
},
|
|
2289
|
+
onUpdate({ editor: editor$1, transaction }) {
|
|
2290
|
+
onUpdate?.(editor$1, transaction);
|
|
2291
|
+
},
|
|
2292
|
+
editorProps: {
|
|
2293
|
+
handleDOMEvents: { click: (view, event) => {
|
|
2294
|
+
if (!view.editable) {
|
|
2295
|
+
if (event.target.closest("a")) {
|
|
2296
|
+
event.preventDefault();
|
|
2297
|
+
return true;
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
return false;
|
|
2301
|
+
} },
|
|
2302
|
+
handlePaste: createPasteHandler({
|
|
2303
|
+
onPaste,
|
|
2304
|
+
onUploadImage,
|
|
2305
|
+
extensions: effectiveExtensions
|
|
2306
|
+
}),
|
|
2307
|
+
handleDrop: createDropHandler({
|
|
2308
|
+
onPaste,
|
|
2309
|
+
onUploadImage
|
|
2310
|
+
})
|
|
2311
|
+
},
|
|
2312
|
+
...rest
|
|
2313
|
+
});
|
|
2314
|
+
return {
|
|
2315
|
+
editor,
|
|
2316
|
+
isEditorEmpty: (0, _tiptap_react.useEditorState)({
|
|
2317
|
+
editor,
|
|
2318
|
+
selector: (context) => {
|
|
2319
|
+
if (!context.editor) return true;
|
|
2320
|
+
return isDocumentVisuallyEmpty(context.editor.state.doc);
|
|
2321
|
+
}
|
|
2322
|
+
}) ?? true,
|
|
2323
|
+
extensions: effectiveExtensions,
|
|
2324
|
+
contentError,
|
|
2325
|
+
isCollaborative
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
|
|
1812
2329
|
//#endregion
|
|
1813
2330
|
//#region src/utils/set-text-alignment.ts
|
|
1814
2331
|
function setTextAlignment(editor, alignment) {
|
|
@@ -2912,8 +3429,469 @@ const LinkBubbleMenu = {
|
|
|
2912
3429
|
Default: LinkBubbleMenuDefault
|
|
2913
3430
|
};
|
|
2914
3431
|
|
|
3432
|
+
//#endregion
|
|
3433
|
+
//#region src/ui/slash-command/utils.ts
|
|
3434
|
+
function isInsideNode(editor, type) {
|
|
3435
|
+
const { $from } = editor.state.selection;
|
|
3436
|
+
for (let d = $from.depth; d > 0; d--) if ($from.node(d).type.name === type) return true;
|
|
3437
|
+
return false;
|
|
3438
|
+
}
|
|
3439
|
+
function isAtMaxColumnsDepth(editor) {
|
|
3440
|
+
const { from } = editor.state.selection;
|
|
3441
|
+
return getColumnsDepth(editor.state.doc, from) >= MAX_COLUMNS_DEPTH;
|
|
3442
|
+
}
|
|
3443
|
+
function updateScrollView(container, item) {
|
|
3444
|
+
const containerRect = container.getBoundingClientRect();
|
|
3445
|
+
const itemRect = item.getBoundingClientRect();
|
|
3446
|
+
if (itemRect.top < containerRect.top) container.scrollTop -= containerRect.top - itemRect.top;
|
|
3447
|
+
else if (itemRect.bottom > containerRect.bottom) container.scrollTop += itemRect.bottom - containerRect.bottom;
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
//#endregion
|
|
3451
|
+
//#region src/ui/slash-command/command-list.tsx
|
|
3452
|
+
const CATEGORY_ORDER = [
|
|
3453
|
+
"Text",
|
|
3454
|
+
"Media",
|
|
3455
|
+
"Layout",
|
|
3456
|
+
"Utility"
|
|
3457
|
+
];
|
|
3458
|
+
function groupByCategory(items) {
|
|
3459
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3460
|
+
for (const item of items) {
|
|
3461
|
+
const existing = seen.get(item.category);
|
|
3462
|
+
if (existing) existing.push(item);
|
|
3463
|
+
else seen.set(item.category, [item]);
|
|
3464
|
+
}
|
|
3465
|
+
const ordered = [];
|
|
3466
|
+
for (const cat of CATEGORY_ORDER) {
|
|
3467
|
+
const group = seen.get(cat);
|
|
3468
|
+
if (group) {
|
|
3469
|
+
ordered.push({
|
|
3470
|
+
category: cat,
|
|
3471
|
+
items: group
|
|
3472
|
+
});
|
|
3473
|
+
seen.delete(cat);
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
for (const [category, group] of seen) ordered.push({
|
|
3477
|
+
category,
|
|
3478
|
+
items: group
|
|
3479
|
+
});
|
|
3480
|
+
return ordered;
|
|
3481
|
+
}
|
|
3482
|
+
function CommandItem({ item, selected, onSelect }) {
|
|
3483
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3484
|
+
"data-re-slash-command-item": "",
|
|
3485
|
+
"data-selected": selected || void 0,
|
|
3486
|
+
onClick: onSelect,
|
|
3487
|
+
type: "button",
|
|
3488
|
+
children: [item.icon, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: item.title })]
|
|
3489
|
+
});
|
|
3490
|
+
}
|
|
3491
|
+
function CommandList({ items, command, query, ref }) {
|
|
3492
|
+
const [selectedIndex, setSelectedIndex] = (0, react.useState)(0);
|
|
3493
|
+
const containerRef = (0, react.useRef)(null);
|
|
3494
|
+
(0, react.useEffect)(() => {
|
|
3495
|
+
setSelectedIndex(0);
|
|
3496
|
+
}, [items]);
|
|
3497
|
+
(0, react.useLayoutEffect)(() => {
|
|
3498
|
+
const container = containerRef.current;
|
|
3499
|
+
if (!container) return;
|
|
3500
|
+
const selected = container.querySelector("[data-selected]");
|
|
3501
|
+
if (selected) updateScrollView(container, selected);
|
|
3502
|
+
}, [selectedIndex]);
|
|
3503
|
+
const selectItem = (0, react.useCallback)((index) => {
|
|
3504
|
+
const item = items[index];
|
|
3505
|
+
if (item) command(item);
|
|
3506
|
+
}, [items, command]);
|
|
3507
|
+
(0, react.useImperativeHandle)(ref, () => ({ onKeyDown: ({ event }) => {
|
|
3508
|
+
if (items.length === 0) return false;
|
|
3509
|
+
if (event.key === "ArrowUp") {
|
|
3510
|
+
setSelectedIndex((i) => (i + items.length - 1) % items.length);
|
|
3511
|
+
return true;
|
|
3512
|
+
}
|
|
3513
|
+
if (event.key === "ArrowDown") {
|
|
3514
|
+
setSelectedIndex((i) => (i + 1) % items.length);
|
|
3515
|
+
return true;
|
|
3516
|
+
}
|
|
3517
|
+
if (event.key === "Enter") {
|
|
3518
|
+
selectItem(selectedIndex);
|
|
3519
|
+
return true;
|
|
3520
|
+
}
|
|
3521
|
+
return false;
|
|
3522
|
+
} }), [
|
|
3523
|
+
items.length,
|
|
3524
|
+
selectItem,
|
|
3525
|
+
selectedIndex
|
|
3526
|
+
]);
|
|
3527
|
+
if (items.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3528
|
+
"data-re-slash-command": "",
|
|
3529
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3530
|
+
"data-re-slash-command-empty": "",
|
|
3531
|
+
children: "No results"
|
|
3532
|
+
})
|
|
3533
|
+
});
|
|
3534
|
+
if (query.trim().length > 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3535
|
+
"data-re-slash-command": "",
|
|
3536
|
+
ref: containerRef,
|
|
3537
|
+
children: items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommandItem, {
|
|
3538
|
+
item,
|
|
3539
|
+
onSelect: () => selectItem(index),
|
|
3540
|
+
selected: index === selectedIndex
|
|
3541
|
+
}, item.title))
|
|
3542
|
+
});
|
|
3543
|
+
const groups = groupByCategory(items);
|
|
3544
|
+
let flatIndex = 0;
|
|
3545
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3546
|
+
"data-re-slash-command": "",
|
|
3547
|
+
ref: containerRef,
|
|
3548
|
+
children: groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3549
|
+
"data-re-slash-command-category": "",
|
|
3550
|
+
children: group.category
|
|
3551
|
+
}), group.items.map((item) => {
|
|
3552
|
+
const currentIndex = flatIndex++;
|
|
3553
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommandItem, {
|
|
3554
|
+
item,
|
|
3555
|
+
onSelect: () => selectItem(currentIndex),
|
|
3556
|
+
selected: currentIndex === selectedIndex
|
|
3557
|
+
}, item.title);
|
|
3558
|
+
})] }, group.category))
|
|
3559
|
+
});
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
//#endregion
|
|
3563
|
+
//#region src/ui/slash-command/commands.tsx
|
|
3564
|
+
const TEXT = {
|
|
3565
|
+
title: "Text",
|
|
3566
|
+
description: "Plain text block",
|
|
3567
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Text, { size: 20 }),
|
|
3568
|
+
category: "Text",
|
|
3569
|
+
searchTerms: ["p", "paragraph"],
|
|
3570
|
+
command: ({ editor, range }) => {
|
|
3571
|
+
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
|
|
3572
|
+
}
|
|
3573
|
+
};
|
|
3574
|
+
const H1 = {
|
|
3575
|
+
title: "Title",
|
|
3576
|
+
description: "Large heading",
|
|
3577
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Heading1, { size: 20 }),
|
|
3578
|
+
category: "Text",
|
|
3579
|
+
searchTerms: [
|
|
3580
|
+
"title",
|
|
3581
|
+
"big",
|
|
3582
|
+
"large",
|
|
3583
|
+
"h1"
|
|
3584
|
+
],
|
|
3585
|
+
command: ({ editor, range }) => {
|
|
3586
|
+
editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
|
|
3587
|
+
}
|
|
3588
|
+
};
|
|
3589
|
+
const H2 = {
|
|
3590
|
+
title: "Subtitle",
|
|
3591
|
+
description: "Medium heading",
|
|
3592
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Heading2, { size: 20 }),
|
|
3593
|
+
category: "Text",
|
|
3594
|
+
searchTerms: [
|
|
3595
|
+
"subtitle",
|
|
3596
|
+
"medium",
|
|
3597
|
+
"h2"
|
|
3598
|
+
],
|
|
3599
|
+
command: ({ editor, range }) => {
|
|
3600
|
+
editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
|
|
3601
|
+
}
|
|
3602
|
+
};
|
|
3603
|
+
const H3 = {
|
|
3604
|
+
title: "Heading",
|
|
3605
|
+
description: "Small heading",
|
|
3606
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Heading3, { size: 20 }),
|
|
3607
|
+
category: "Text",
|
|
3608
|
+
searchTerms: [
|
|
3609
|
+
"subtitle",
|
|
3610
|
+
"small",
|
|
3611
|
+
"h3"
|
|
3612
|
+
],
|
|
3613
|
+
command: ({ editor, range }) => {
|
|
3614
|
+
editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
|
|
3615
|
+
}
|
|
3616
|
+
};
|
|
3617
|
+
const BULLET_LIST = {
|
|
3618
|
+
title: "Bullet list",
|
|
3619
|
+
description: "Unordered list",
|
|
3620
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.List, { size: 20 }),
|
|
3621
|
+
category: "Text",
|
|
3622
|
+
searchTerms: ["unordered", "point"],
|
|
3623
|
+
command: ({ editor, range }) => {
|
|
3624
|
+
editor.chain().focus().deleteRange(range).toggleBulletList().run();
|
|
3625
|
+
}
|
|
3626
|
+
};
|
|
3627
|
+
const NUMBERED_LIST = {
|
|
3628
|
+
title: "Numbered list",
|
|
3629
|
+
description: "Ordered list",
|
|
3630
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ListOrdered, { size: 20 }),
|
|
3631
|
+
category: "Text",
|
|
3632
|
+
searchTerms: ["ordered"],
|
|
3633
|
+
command: ({ editor, range }) => {
|
|
3634
|
+
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
|
3635
|
+
}
|
|
3636
|
+
};
|
|
3637
|
+
const QUOTE = {
|
|
3638
|
+
title: "Quote",
|
|
3639
|
+
description: "Block quote",
|
|
3640
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.TextQuote, { size: 20 }),
|
|
3641
|
+
category: "Text",
|
|
3642
|
+
searchTerms: ["blockquote"],
|
|
3643
|
+
command: ({ editor, range }) => {
|
|
3644
|
+
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run();
|
|
3645
|
+
}
|
|
3646
|
+
};
|
|
3647
|
+
const CODE = {
|
|
3648
|
+
title: "Code block",
|
|
3649
|
+
description: "Code snippet",
|
|
3650
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.SquareCode, { size: 20 }),
|
|
3651
|
+
category: "Text",
|
|
3652
|
+
searchTerms: ["codeblock"],
|
|
3653
|
+
command: ({ editor, range }) => {
|
|
3654
|
+
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
|
|
3655
|
+
}
|
|
3656
|
+
};
|
|
3657
|
+
const BUTTON = {
|
|
3658
|
+
title: "Button",
|
|
3659
|
+
description: "Clickable button",
|
|
3660
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.MousePointer, { size: 20 }),
|
|
3661
|
+
category: "Layout",
|
|
3662
|
+
searchTerms: ["button"],
|
|
3663
|
+
command: ({ editor, range }) => {
|
|
3664
|
+
editor.chain().focus().deleteRange(range).setButton().run();
|
|
3665
|
+
}
|
|
3666
|
+
};
|
|
3667
|
+
const DIVIDER = {
|
|
3668
|
+
title: "Divider",
|
|
3669
|
+
description: "Horizontal separator",
|
|
3670
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.SplitSquareVertical, { size: 20 }),
|
|
3671
|
+
category: "Layout",
|
|
3672
|
+
searchTerms: [
|
|
3673
|
+
"hr",
|
|
3674
|
+
"divider",
|
|
3675
|
+
"separator"
|
|
3676
|
+
],
|
|
3677
|
+
command: ({ editor, range }) => {
|
|
3678
|
+
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
|
3679
|
+
}
|
|
3680
|
+
};
|
|
3681
|
+
const SECTION = {
|
|
3682
|
+
title: "Section",
|
|
3683
|
+
description: "Content section",
|
|
3684
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Rows2, { size: 20 }),
|
|
3685
|
+
category: "Layout",
|
|
3686
|
+
searchTerms: [
|
|
3687
|
+
"section",
|
|
3688
|
+
"row",
|
|
3689
|
+
"container"
|
|
3690
|
+
],
|
|
3691
|
+
command: ({ editor, range }) => {
|
|
3692
|
+
editor.chain().focus().deleteRange(range).insertSection().run();
|
|
3693
|
+
}
|
|
3694
|
+
};
|
|
3695
|
+
const TWO_COLUMNS = {
|
|
3696
|
+
title: "2 columns",
|
|
3697
|
+
description: "Two column layout",
|
|
3698
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Columns2, { size: 20 }),
|
|
3699
|
+
category: "Layout",
|
|
3700
|
+
searchTerms: [
|
|
3701
|
+
"columns",
|
|
3702
|
+
"column",
|
|
3703
|
+
"layout",
|
|
3704
|
+
"grid",
|
|
3705
|
+
"split",
|
|
3706
|
+
"side-by-side",
|
|
3707
|
+
"multi-column",
|
|
3708
|
+
"row",
|
|
3709
|
+
"two",
|
|
3710
|
+
"2"
|
|
3711
|
+
],
|
|
3712
|
+
command: ({ editor, range }) => {
|
|
3713
|
+
editor.chain().focus().deleteRange(range).insertColumns(2).run();
|
|
3714
|
+
}
|
|
3715
|
+
};
|
|
3716
|
+
const THREE_COLUMNS = {
|
|
3717
|
+
title: "3 columns",
|
|
3718
|
+
description: "Three column layout",
|
|
3719
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Columns3, { size: 20 }),
|
|
3720
|
+
category: "Layout",
|
|
3721
|
+
searchTerms: [
|
|
3722
|
+
"columns",
|
|
3723
|
+
"column",
|
|
3724
|
+
"layout",
|
|
3725
|
+
"grid",
|
|
3726
|
+
"split",
|
|
3727
|
+
"multi-column",
|
|
3728
|
+
"row",
|
|
3729
|
+
"three",
|
|
3730
|
+
"3"
|
|
3731
|
+
],
|
|
3732
|
+
command: ({ editor, range }) => {
|
|
3733
|
+
editor.chain().focus().deleteRange(range).insertColumns(3).run();
|
|
3734
|
+
}
|
|
3735
|
+
};
|
|
3736
|
+
const FOUR_COLUMNS = {
|
|
3737
|
+
title: "4 columns",
|
|
3738
|
+
description: "Four column layout",
|
|
3739
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Columns4, { size: 20 }),
|
|
3740
|
+
category: "Layout",
|
|
3741
|
+
searchTerms: [
|
|
3742
|
+
"columns",
|
|
3743
|
+
"column",
|
|
3744
|
+
"layout",
|
|
3745
|
+
"grid",
|
|
3746
|
+
"split",
|
|
3747
|
+
"multi-column",
|
|
3748
|
+
"row",
|
|
3749
|
+
"four",
|
|
3750
|
+
"4"
|
|
3751
|
+
],
|
|
3752
|
+
command: ({ editor, range }) => {
|
|
3753
|
+
editor.chain().focus().deleteRange(range).insertColumns(4).run();
|
|
3754
|
+
}
|
|
3755
|
+
};
|
|
3756
|
+
const defaultSlashCommands = [
|
|
3757
|
+
TEXT,
|
|
3758
|
+
H1,
|
|
3759
|
+
H2,
|
|
3760
|
+
H3,
|
|
3761
|
+
BULLET_LIST,
|
|
3762
|
+
NUMBERED_LIST,
|
|
3763
|
+
QUOTE,
|
|
3764
|
+
CODE,
|
|
3765
|
+
BUTTON,
|
|
3766
|
+
DIVIDER,
|
|
3767
|
+
SECTION,
|
|
3768
|
+
TWO_COLUMNS,
|
|
3769
|
+
THREE_COLUMNS,
|
|
3770
|
+
FOUR_COLUMNS
|
|
3771
|
+
];
|
|
3772
|
+
|
|
3773
|
+
//#endregion
|
|
3774
|
+
//#region src/ui/slash-command/extension.ts
|
|
3775
|
+
const SlashCommandExtension = _tiptap_core.Extension.create({
|
|
3776
|
+
name: "slash-command",
|
|
3777
|
+
addOptions() {
|
|
3778
|
+
return { suggestion: {
|
|
3779
|
+
char: "/",
|
|
3780
|
+
allow: ({ editor }) => !editor.isActive("codeBlock"),
|
|
3781
|
+
command: ({ editor, range, props }) => {
|
|
3782
|
+
props.command({
|
|
3783
|
+
editor,
|
|
3784
|
+
range
|
|
3785
|
+
});
|
|
3786
|
+
}
|
|
3787
|
+
} };
|
|
3788
|
+
},
|
|
3789
|
+
addProseMirrorPlugins() {
|
|
3790
|
+
return [(0, _tiptap_suggestion.default)({
|
|
3791
|
+
pluginKey: new _tiptap_pm_state.PluginKey("slash-command"),
|
|
3792
|
+
editor: this.editor,
|
|
3793
|
+
...this.options.suggestion
|
|
3794
|
+
})];
|
|
3795
|
+
}
|
|
3796
|
+
});
|
|
3797
|
+
|
|
3798
|
+
//#endregion
|
|
3799
|
+
//#region src/ui/slash-command/render.tsx
|
|
3800
|
+
function createRenderItems(component = CommandList) {
|
|
3801
|
+
return () => {
|
|
3802
|
+
let renderer = null;
|
|
3803
|
+
let popup = null;
|
|
3804
|
+
return {
|
|
3805
|
+
onStart: (props) => {
|
|
3806
|
+
renderer = new _tiptap_react.ReactRenderer(component, {
|
|
3807
|
+
props,
|
|
3808
|
+
editor: props.editor
|
|
3809
|
+
});
|
|
3810
|
+
if (!props.clientRect) return;
|
|
3811
|
+
popup = (0, tippy_js.default)("body", {
|
|
3812
|
+
getReferenceClientRect: props.clientRect,
|
|
3813
|
+
appendTo: () => document.body,
|
|
3814
|
+
content: renderer.element,
|
|
3815
|
+
showOnCreate: true,
|
|
3816
|
+
interactive: true,
|
|
3817
|
+
trigger: "manual",
|
|
3818
|
+
placement: "bottom-start"
|
|
3819
|
+
});
|
|
3820
|
+
},
|
|
3821
|
+
onUpdate: (props) => {
|
|
3822
|
+
if (!renderer) return;
|
|
3823
|
+
renderer.updateProps(props);
|
|
3824
|
+
if (popup?.[0] && props.clientRect) popup[0].setProps({ getReferenceClientRect: props.clientRect });
|
|
3825
|
+
},
|
|
3826
|
+
onKeyDown: (props) => {
|
|
3827
|
+
if (props.event.key === "Escape") {
|
|
3828
|
+
popup?.[0]?.hide();
|
|
3829
|
+
return true;
|
|
3830
|
+
}
|
|
3831
|
+
return renderer?.ref?.onKeyDown(props) ?? false;
|
|
3832
|
+
},
|
|
3833
|
+
onExit: () => {
|
|
3834
|
+
popup?.[0]?.destroy();
|
|
3835
|
+
renderer?.destroy();
|
|
3836
|
+
popup = null;
|
|
3837
|
+
renderer = null;
|
|
3838
|
+
}
|
|
3839
|
+
};
|
|
3840
|
+
};
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3843
|
+
//#endregion
|
|
3844
|
+
//#region src/ui/slash-command/search.ts
|
|
3845
|
+
function scoreItem(item, query) {
|
|
3846
|
+
if (!query) return 100;
|
|
3847
|
+
const q = query.toLowerCase();
|
|
3848
|
+
const title = item.title.toLowerCase();
|
|
3849
|
+
const description = item.description.toLowerCase();
|
|
3850
|
+
const terms = item.searchTerms?.map((t) => t.toLowerCase()) ?? [];
|
|
3851
|
+
if (title === q) return 100;
|
|
3852
|
+
if (title.startsWith(q)) return 90;
|
|
3853
|
+
if (title.split(/\s+/).some((w) => w.startsWith(q))) return 80;
|
|
3854
|
+
if (terms.some((t) => t === q)) return 70;
|
|
3855
|
+
if (terms.some((t) => t.startsWith(q))) return 60;
|
|
3856
|
+
if (title.includes(q)) return 40;
|
|
3857
|
+
if (terms.some((t) => t.includes(q))) return 30;
|
|
3858
|
+
if (description.includes(q)) return 20;
|
|
3859
|
+
return 0;
|
|
3860
|
+
}
|
|
3861
|
+
function filterAndRankItems(items, query) {
|
|
3862
|
+
const trimmed = query.trim();
|
|
3863
|
+
if (!trimmed) return items;
|
|
3864
|
+
const scored = items.map((item) => ({
|
|
3865
|
+
item,
|
|
3866
|
+
score: scoreItem(item, trimmed)
|
|
3867
|
+
})).filter(({ score }) => score > 0);
|
|
3868
|
+
scored.sort((a, b) => b.score - a.score);
|
|
3869
|
+
return scored.map(({ item }) => item);
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3872
|
+
//#endregion
|
|
3873
|
+
//#region src/ui/slash-command/create-slash-command.ts
|
|
3874
|
+
function defaultFilterItems(items, query, editor) {
|
|
3875
|
+
return filterAndRankItems(isAtMaxColumnsDepth(editor) ? items.filter((item) => item.category !== "Layout" || !item.title.includes("column")) : items, query);
|
|
3876
|
+
}
|
|
3877
|
+
function createSlashCommand(options) {
|
|
3878
|
+
const items = options?.items ?? defaultSlashCommands;
|
|
3879
|
+
const filterFn = options?.filterItems ?? defaultFilterItems;
|
|
3880
|
+
return SlashCommandExtension.configure({ suggestion: {
|
|
3881
|
+
items: ({ query, editor }) => filterFn(items, query, editor),
|
|
3882
|
+
render: createRenderItems(options?.component)
|
|
3883
|
+
} });
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
//#endregion
|
|
3887
|
+
//#region src/ui/slash-command/index.ts
|
|
3888
|
+
const SlashCommand = createSlashCommand();
|
|
3889
|
+
|
|
2915
3890
|
//#endregion
|
|
2916
3891
|
exports.AlignmentAttribute = AlignmentAttribute;
|
|
3892
|
+
exports.BULLET_LIST = BULLET_LIST;
|
|
3893
|
+
exports.BUTTON = BUTTON;
|
|
3894
|
+
exports.Blockquote = Blockquote;
|
|
2917
3895
|
exports.Body = Body;
|
|
2918
3896
|
exports.Bold = Bold;
|
|
2919
3897
|
exports.BubbleMenu = BubbleMenu;
|
|
@@ -2933,24 +3911,35 @@ exports.BubbleMenuSeparator = BubbleMenuSeparator;
|
|
|
2933
3911
|
exports.BubbleMenuStrike = BubbleMenuStrike;
|
|
2934
3912
|
exports.BubbleMenuUnderline = BubbleMenuUnderline;
|
|
2935
3913
|
exports.BubbleMenuUppercase = BubbleMenuUppercase;
|
|
3914
|
+
exports.BulletList = BulletList;
|
|
2936
3915
|
exports.Button = Button;
|
|
2937
3916
|
exports.ButtonBubbleMenu = ButtonBubbleMenu;
|
|
2938
3917
|
exports.ButtonBubbleMenuDefault = ButtonBubbleMenuDefault;
|
|
2939
3918
|
exports.ButtonBubbleMenuEditLink = ButtonBubbleMenuEditLink;
|
|
2940
3919
|
exports.ButtonBubbleMenuRoot = ButtonBubbleMenuRoot;
|
|
2941
3920
|
exports.ButtonBubbleMenuToolbar = ButtonBubbleMenuToolbar;
|
|
3921
|
+
exports.CODE = CODE;
|
|
2942
3922
|
exports.COLUMN_PARENT_TYPES = COLUMN_PARENT_TYPES;
|
|
2943
3923
|
exports.ClassAttribute = ClassAttribute;
|
|
3924
|
+
exports.Code = Code;
|
|
2944
3925
|
exports.CodeBlockPrism = CodeBlockPrism;
|
|
2945
3926
|
exports.ColumnsColumn = ColumnsColumn;
|
|
3927
|
+
exports.CommandList = CommandList;
|
|
3928
|
+
exports.DIVIDER = DIVIDER;
|
|
2946
3929
|
exports.Div = Div;
|
|
2947
3930
|
exports.EmailNode = EmailNode;
|
|
3931
|
+
exports.FOUR_COLUMNS = FOUR_COLUMNS;
|
|
2948
3932
|
exports.FourColumns = FourColumns;
|
|
3933
|
+
exports.H1 = H1;
|
|
3934
|
+
exports.H2 = H2;
|
|
3935
|
+
exports.H3 = H3;
|
|
3936
|
+
exports.HardBreak = HardBreak;
|
|
2949
3937
|
exports.ImageBubbleMenu = ImageBubbleMenu;
|
|
2950
3938
|
exports.ImageBubbleMenuDefault = ImageBubbleMenuDefault;
|
|
2951
3939
|
exports.ImageBubbleMenuEditLink = ImageBubbleMenuEditLink;
|
|
2952
3940
|
exports.ImageBubbleMenuRoot = ImageBubbleMenuRoot;
|
|
2953
3941
|
exports.ImageBubbleMenuToolbar = ImageBubbleMenuToolbar;
|
|
3942
|
+
exports.Italic = Italic;
|
|
2954
3943
|
exports.LinkBubbleMenu = LinkBubbleMenu;
|
|
2955
3944
|
exports.LinkBubbleMenuDefault = LinkBubbleMenuDefault;
|
|
2956
3945
|
exports.LinkBubbleMenuEditLink = LinkBubbleMenuEditLink;
|
|
@@ -2959,17 +3948,28 @@ exports.LinkBubbleMenuOpenLink = LinkBubbleMenuOpenLink;
|
|
|
2959
3948
|
exports.LinkBubbleMenuRoot = LinkBubbleMenuRoot;
|
|
2960
3949
|
exports.LinkBubbleMenuToolbar = LinkBubbleMenuToolbar;
|
|
2961
3950
|
exports.LinkBubbleMenuUnlink = LinkBubbleMenuUnlink;
|
|
3951
|
+
exports.ListItem = ListItem;
|
|
2962
3952
|
exports.MAX_COLUMNS_DEPTH = MAX_COLUMNS_DEPTH;
|
|
2963
3953
|
exports.MaxNesting = MaxNesting;
|
|
3954
|
+
exports.NUMBERED_LIST = NUMBERED_LIST;
|
|
2964
3955
|
exports.NodeSelectorContent = NodeSelectorContent;
|
|
2965
3956
|
exports.NodeSelectorRoot = NodeSelectorRoot;
|
|
2966
3957
|
exports.NodeSelectorTrigger = NodeSelectorTrigger;
|
|
3958
|
+
exports.OrderedList = OrderedList;
|
|
3959
|
+
exports.Paragraph = Paragraph;
|
|
2967
3960
|
exports.Placeholder = Placeholder;
|
|
2968
3961
|
exports.PreservedStyle = PreservedStyle;
|
|
2969
3962
|
exports.PreviewText = PreviewText;
|
|
3963
|
+
exports.QUOTE = QUOTE;
|
|
3964
|
+
exports.SECTION = SECTION;
|
|
2970
3965
|
exports.Section = Section;
|
|
3966
|
+
exports.SlashCommand = SlashCommand;
|
|
3967
|
+
exports.Strike = Strike;
|
|
2971
3968
|
exports.StyleAttribute = StyleAttribute;
|
|
2972
3969
|
exports.Sup = Sup;
|
|
3970
|
+
exports.TEXT = TEXT;
|
|
3971
|
+
exports.THREE_COLUMNS = THREE_COLUMNS;
|
|
3972
|
+
exports.TWO_COLUMNS = TWO_COLUMNS;
|
|
2973
3973
|
exports.Table = Table;
|
|
2974
3974
|
exports.TableCell = TableCell;
|
|
2975
3975
|
exports.TableHeader = TableHeader;
|
|
@@ -2977,11 +3977,19 @@ exports.TableRow = TableRow;
|
|
|
2977
3977
|
exports.ThreeColumns = ThreeColumns;
|
|
2978
3978
|
exports.TwoColumns = TwoColumns;
|
|
2979
3979
|
exports.Uppercase = Uppercase;
|
|
3980
|
+
exports.composeReactEmail = composeReactEmail;
|
|
2980
3981
|
exports.coreExtensions = coreExtensions;
|
|
3982
|
+
exports.createSlashCommand = createSlashCommand;
|
|
3983
|
+
exports.defaultSlashCommands = defaultSlashCommands;
|
|
2981
3984
|
exports.editorEventBus = editorEventBus;
|
|
3985
|
+
exports.filterAndRankItems = filterAndRankItems;
|
|
2982
3986
|
exports.getColumnsDepth = getColumnsDepth;
|
|
3987
|
+
exports.isAtMaxColumnsDepth = isAtMaxColumnsDepth;
|
|
3988
|
+
exports.isInsideNode = isInsideNode;
|
|
2983
3989
|
exports.processStylesForUnlink = processStylesForUnlink;
|
|
3990
|
+
exports.scoreItem = scoreItem;
|
|
2984
3991
|
exports.setTextAlignment = setTextAlignment;
|
|
2985
3992
|
exports.useButtonBubbleMenuContext = useButtonBubbleMenuContext;
|
|
3993
|
+
exports.useEditor = useEditor;
|
|
2986
3994
|
exports.useImageBubbleMenuContext = useImageBubbleMenuContext;
|
|
2987
3995
|
exports.useLinkBubbleMenuContext = useLinkBubbleMenuContext;
|