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