@react-email/editor 1.3.10 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/core/index.cjs +3 -3
  2. package/dist/{core-CLyAVVUW.cjs → core-Dte9KXcz.cjs} +2 -2
  3. package/dist/{email-node-DvyWFrOM.cjs → email-node-BhbXb22u.cjs} +1 -1
  4. package/dist/{event-bus-C_wS2tD6.cjs → event-bus-C0haBiDw.cjs} +1 -1
  5. package/dist/{extension-D_sNGTbX.cjs → extension-bVrEjbFA.cjs} +2 -2
  6. package/dist/{extension-C1ilEKq-.cjs → extension-mBZiGbpM.cjs} +2 -2
  7. package/dist/extensions/index.cjs +3 -3
  8. package/dist/extensions/index.d.cts.map +1 -1
  9. package/dist/extensions/index.d.mts.map +1 -1
  10. package/dist/extensions/index.mjs +2 -2
  11. package/dist/{extensions-oOPUnW7d.cjs → extensions-DBFEW67d.cjs} +13 -5
  12. package/dist/{extensions-CU0ndoee.mjs → extensions-w71aFrgc.mjs} +11 -3
  13. package/dist/extensions-w71aFrgc.mjs.map +1 -0
  14. package/dist/{focus-scopes-B_0O7l7d.mjs → focus-scopes-DOsiXV7b.mjs} +88 -30
  15. package/dist/focus-scopes-DOsiXV7b.mjs.map +1 -0
  16. package/dist/{focus-scopes-l_Ki0562.cjs → focus-scopes-Ncj54H_M.cjs} +87 -29
  17. package/dist/{image-BpsmuXKq.cjs → image-D8OUIgwB.cjs} +1 -1
  18. package/dist/index.cjs +7 -7
  19. package/dist/index.mjs +3 -3
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/plugins/index.cjs +4 -4
  22. package/dist/{root-D8zhkKgc.cjs → root-BpooQKCu.cjs} +3 -2
  23. package/dist/{root-yYM1BF1C.mjs → root-DTTCkmjD.mjs} +4 -3
  24. package/dist/root-DTTCkmjD.mjs.map +1 -0
  25. package/dist/ui/index.cjs +5 -5
  26. package/dist/ui/index.d.cts.map +1 -1
  27. package/dist/ui/index.d.mts.map +1 -1
  28. package/dist/ui/index.mjs +2 -2
  29. package/package.json +2 -2
  30. package/dist/extensions-CU0ndoee.mjs.map +0 -1
  31. package/dist/focus-scopes-B_0O7l7d.mjs.map +0 -1
  32. package/dist/root-yYM1BF1C.mjs.map +0 -1
@@ -2,7 +2,7 @@ import { i as inlineCssToJs, t as EmailNode } from "./email-node-B-_g4X-S.mjs";
2
2
  import { Column, Row } from "react-email";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  import { Extension, extensions, mergeAttributes } from "@tiptap/core";
5
- import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
5
+ import { NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
6
6
  //#region src/utils/attribute-helpers.ts
7
7
  /**
8
8
  * Creates TipTap attribute definitions for a list of HTML attributes.
@@ -159,6 +159,76 @@ const NODE_TYPE_MAP = {
159
159
  3: "threeColumns",
160
160
  4: "fourColumns"
161
161
  };
162
+ function isColumnEmpty(col) {
163
+ return col.childCount === 1 && col.firstChild?.type.name === "paragraph" && col.firstChild?.childCount === 0;
164
+ }
165
+ function deleteColumnParent(editor, state, from, to, parentChildCount) {
166
+ const tr = state.tr;
167
+ if (parentChildCount === 1) tr.replaceWith(from, to, state.schema.nodes.paragraph.create());
168
+ else tr.delete(from, to);
169
+ tr.setSelection(TextSelection.near(tr.doc.resolve(from)));
170
+ editor.view.dispatch(tr);
171
+ return true;
172
+ }
173
+ function handleNodeSelectionBackspace(editor, selection, $from, state) {
174
+ if (!(selection instanceof NodeSelection)) return false;
175
+ if (!COLUMN_PARENT_SET.has(selection.node.type.name)) return false;
176
+ if (!Array.from({ length: selection.node.childCount }, (_, i) => selection.node.child(i)).every(isColumnEmpty)) return false;
177
+ const parent = $from.node($from.depth);
178
+ return deleteColumnParent(editor, state, selection.from, selection.to, parent.childCount);
179
+ }
180
+ function handleCursorBackspace(editor, $from, state) {
181
+ for (let depth = $from.depth; depth >= 1; depth--) {
182
+ if ($from.node(depth).type.name === "columnsColumn") return handleBackspaceInsideColumn(editor, $from, state, depth);
183
+ const indexInParent = $from.index(depth - 1);
184
+ if (indexInParent === 0) continue;
185
+ const prevNode = $from.node(depth - 1).child(indexInParent - 1);
186
+ if (COLUMN_PARENT_SET.has(prevNode.type.name)) {
187
+ const from = $from.before(depth) - prevNode.nodeSize;
188
+ const to = $from.before(depth);
189
+ editor.view.dispatch(state.tr.delete(from, to));
190
+ return true;
191
+ }
192
+ break;
193
+ }
194
+ return false;
195
+ }
196
+ function handleBackspaceInsideColumn(editor, $from, state, depth) {
197
+ const column = $from.node(depth);
198
+ const columnParent = $from.node(depth - 1);
199
+ if (!COLUMN_PARENT_SET.has(columnParent.type.name)) return false;
200
+ if (!isColumnEmpty(column)) return false;
201
+ const deletedIndex = $from.index(depth - 1);
202
+ const remainingColumns = Array.from({ length: columnParent.childCount }, (_, i) => columnParent.child(i)).filter((_, i) => i !== deletedIndex);
203
+ const columnParentFrom = $from.before(depth - 1);
204
+ const columnParentTo = $from.after(depth - 1);
205
+ const tr = state.tr;
206
+ const deletingFirstColumn = deletedIndex === 0;
207
+ const focusIndex = deletingFirstColumn ? 0 : deletedIndex - 1;
208
+ const direction = deletingFirstColumn ? "right" : "left";
209
+ if (remainingColumns.length === 1) {
210
+ const survivingContent = Array.from({ length: remainingColumns[0].childCount }, (_, i) => remainingColumns[0].child(i));
211
+ const replacement = survivingContent.length > 0 ? survivingContent : [state.schema.nodes.paragraph.create()];
212
+ tr.replaceWith(columnParentFrom, columnParentTo, replacement);
213
+ const totalReplacementSize = replacement.reduce((sum, n) => sum + n.nodeSize, 0);
214
+ const cursorPos = deletingFirstColumn ? columnParentFrom + 1 : columnParentFrom + totalReplacementSize;
215
+ tr.setSelection(TextSelection.near(tr.doc.resolve(cursorPos), direction === "right" ? 1 : -1));
216
+ } else {
217
+ const smallerLayoutType = state.schema.nodes[NODE_TYPE_MAP[remainingColumns.length]];
218
+ tr.replaceWith(columnParentFrom, columnParentTo, smallerLayoutType.create(columnParent.attrs, remainingColumns));
219
+ focusColumn(tr, columnParentFrom, remainingColumns, focusIndex, direction);
220
+ }
221
+ editor.view.dispatch(tr);
222
+ return true;
223
+ }
224
+ function focusColumn(tr, columnParentPos, columns, index, direction = "right") {
225
+ let pos = columnParentPos + 1;
226
+ for (let i = 0; i < index; i++) pos += columns[i].nodeSize;
227
+ pos += 1;
228
+ if (direction === "left") pos += columns[index].content.size;
229
+ const bias = direction === "right" ? 1 : -1;
230
+ tr.setSelection(TextSelection.near(tr.doc.resolve(pos), bias));
231
+ }
162
232
  function createColumnsNode(config, includeCommands) {
163
233
  return EmailNode.create({
164
234
  name: config.name,
@@ -190,20 +260,20 @@ function createColumnsNode(config, includeCommands) {
190
260
  ];
191
261
  },
192
262
  ...includeCommands && { addCommands() {
193
- return { insertColumns: (count) => ({ commands, state }) => {
263
+ return { insertColumns: (count) => ({ state, dispatch }) => {
194
264
  if (getColumnsDepth(state.doc, state.selection.from) >= 3) return false;
195
- const nodeType = NODE_TYPE_MAP[count];
196
- const children = Array.from({ length: count }, () => ({
197
- type: "columnsColumn",
198
- content: [{
199
- type: "paragraph",
200
- content: []
201
- }]
202
- }));
203
- return commands.insertContent({
204
- type: nodeType,
205
- content: children
206
- });
265
+ const emptyColumn = () => state.schema.nodes.columnsColumn.create(null, state.schema.nodes.paragraph.create());
266
+ const columnBlock = state.schema.nodes[NODE_TYPE_MAP[count]].create(null, Array.from({ length: count }, emptyColumn));
267
+ const tr = state.tr.replaceSelectionWith(columnBlock);
268
+ const $head = tr.selection.$head;
269
+ for (let d = $head.depth; d >= 0; d--) {
270
+ if (!COLUMN_PARENT_SET.has($head.node(d).type.name)) continue;
271
+ const insertedColumns = Array.from({ length: $head.node(d).childCount }, (_, i) => $head.node(d).child(i));
272
+ focusColumn(tr, $head.before(d), insertedColumns, 0);
273
+ break;
274
+ }
275
+ if (dispatch) dispatch(tr);
276
+ return true;
207
277
  } };
208
278
  } },
209
279
  renderToReactEmail({ children, node, style }) {
@@ -251,21 +321,9 @@ const ColumnsColumn = EmailNode.create({
251
321
  const { state } = editor;
252
322
  const { selection } = state;
253
323
  const { empty, $from } = selection;
254
- if (!empty) return false;
255
- for (let depth = $from.depth; depth >= 1; depth--) {
256
- if ($from.pos !== $from.start(depth)) break;
257
- const indexInParent = $from.index(depth - 1);
258
- if (indexInParent === 0) continue;
259
- const prevNode = $from.node(depth - 1).child(indexInParent - 1);
260
- if (COLUMN_PARENT_SET.has(prevNode.type.name)) {
261
- const deleteFrom = $from.before(depth) - prevNode.nodeSize;
262
- const deleteTo = $from.before(depth);
263
- editor.view.dispatch(state.tr.delete(deleteFrom, deleteTo));
264
- return true;
265
- }
266
- break;
267
- }
268
- return false;
324
+ if (!empty) return handleNodeSelectionBackspace(editor, selection, $from, state);
325
+ if ($from.parentOffset !== 0) return false;
326
+ return handleCursorBackspace(editor, $from, state);
269
327
  },
270
328
  "Mod-a": ({ editor }) => {
271
329
  const { state } = editor;
@@ -406,4 +464,4 @@ const FocusScopes = Extension.create({
406
464
  //#endregion
407
465
  export { TABLE_ATTRIBUTES as _, COLUMN_PARENT_TYPES as a, createStandardAttributes as b, MAX_COLUMNS_DEPTH as c, getColumnsDepth as d, hasPrismThemeLoaded as f, LAYOUT_ATTRIBUTES as g, COMMON_HTML_ATTRIBUTES as h, focusScopePluginKey as i, ThreeColumns as l, removePrismTheme as m, createFocusScopePlugin as n, ColumnsColumn as o, loadPrismTheme as p, createFocusScopesStorage as r, FourColumns as s, FocusScopes as t, TwoColumns as u, TABLE_CELL_ATTRIBUTES as v, TABLE_HEADER_ATTRIBUTES as y };
408
466
 
409
- //# sourceMappingURL=focus-scopes-B_0O7l7d.mjs.map
467
+ //# sourceMappingURL=focus-scopes-DOsiXV7b.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus-scopes-DOsiXV7b.mjs","names":["nativeTiptapExtensions"],"sources":["../src/utils/attribute-helpers.ts","../src/utils/prism-utils.ts","../src/extensions/columns.tsx","../src/extensions/focus-scopes.ts"],"sourcesContent":["/**\n * Creates TipTap attribute definitions for a list of HTML attributes.\n * Each attribute will have the same pattern:\n * - default: null\n * - parseHTML: extracts the attribute from the element\n * - renderHTML: conditionally renders the attribute if it has a value\n *\n * @param attributeNames - Array of HTML attribute names to create definitions for\n * @returns Object with TipTap attribute definitions\n *\n * @example\n * const attrs = createStandardAttributes(['class', 'id', 'title']);\n * // Returns:\n * // {\n * // class: {\n * // default: null,\n * // parseHTML: (element) => element.getAttribute('class'),\n * // renderHTML: (attributes) => attributes.class ? { class: attributes.class } : {}\n * // },\n * // ...\n * // }\n */\nexport function createStandardAttributes(attributeNames: readonly string[]) {\n return Object.fromEntries(\n attributeNames.map((attr) => [\n attr,\n {\n default: null,\n parseHTML: (element: HTMLElement) => element.getAttribute(attr),\n renderHTML: (attributes: Record<string, unknown>) => {\n if (!attributes[attr]) {\n return {};\n }\n\n return {\n [attr]: attributes[attr],\n };\n },\n },\n ]),\n );\n}\n\n/**\n * Common HTML attributes used across multiple extensions.\n * These preserve attributes during HTML import and editing for better\n * fidelity when importing existing email templates.\n */\nexport const COMMON_HTML_ATTRIBUTES = [\n 'id',\n 'class',\n 'title',\n 'lang',\n 'dir',\n 'data-id',\n] as const;\n\n/**\n * Layout-specific HTML attributes used for positioning and sizing.\n */\nexport const LAYOUT_ATTRIBUTES = ['align', 'width', 'height'] as const;\n\n/**\n * Table-specific HTML attributes used for table layout and styling.\n */\nexport const TABLE_ATTRIBUTES = [\n 'border',\n 'cellpadding',\n 'cellspacing',\n] as const;\n\n/**\n * Table cell-specific HTML attributes.\n */\nexport const TABLE_CELL_ATTRIBUTES = [\n 'valign',\n 'bgcolor',\n 'colspan',\n 'rowspan',\n] as const;\n\n/**\n * Table header cell-specific HTML attributes.\n * These are additional attributes that only apply to <th> elements.\n */\nexport const TABLE_HEADER_ATTRIBUTES = [\n ...TABLE_CELL_ATTRIBUTES,\n 'scope',\n] as const;\n","const publicURL = '/styles/prism';\n\nexport function loadPrismTheme(theme: string) {\n // Create new link element for the new theme\n const link = document.createElement('link');\n link.rel = 'stylesheet';\n link.href = `${publicURL}/prism-${theme}.css`;\n link.setAttribute('data-prism-theme', ''); // Mark this link as the Prism theme\n\n // Append the new link element to the head\n document.head.appendChild(link);\n}\n\nexport function removePrismTheme() {\n const existingTheme = document.querySelectorAll(\n 'link[rel=\"stylesheet\"][data-prism-theme]',\n );\n if (existingTheme.length > 0) {\n existingTheme.forEach((cssLinkTag) => {\n cssLinkTag.remove();\n });\n }\n}\n\nexport function hasPrismThemeLoaded(theme: string) {\n const existingTheme = document.querySelector(\n `link[rel=\"stylesheet\"][data-prism-theme][href=\"${publicURL}/prism-${theme}.css\"]`,\n );\n return !!existingTheme;\n}\n","import { type CommandProps, type Editor, mergeAttributes } from '@tiptap/core';\nimport type { Node as ProseMirrorNode, ResolvedPos } from '@tiptap/pm/model';\nimport {\n type EditorState,\n NodeSelection,\n TextSelection,\n type Transaction,\n} from '@tiptap/pm/state';\nimport { Column, Row } from 'react-email';\nimport { EmailNode } from '../core/serializer/email-node';\nimport {\n COMMON_HTML_ATTRIBUTES,\n createStandardAttributes,\n LAYOUT_ATTRIBUTES,\n} from '../utils/attribute-helpers';\nimport { inlineCssToJs } from '../utils/styles';\n\nconst COLUMN_PARENT_ATTRIBUTES = ['cellspacing'] as const;\n\nfunction getColumnGapCss(cellspacing: unknown): string | undefined {\n if (cellspacing === undefined || cellspacing === null || cellspacing === '') {\n return undefined;\n }\n\n const value = String(cellspacing).trim();\n if (value === '') {\n return undefined;\n }\n\n if (/^\\d+(\\.\\d+)?$/.test(value)) {\n return `${value}px`;\n }\n\n return value;\n}\n\nfunction getRowCellSpacing(cellspacing: unknown): string | undefined {\n if (cellspacing === undefined || cellspacing === null || cellspacing === '') {\n return undefined;\n }\n\n const value = String(cellspacing).trim();\n if (value === '' || value === '0') {\n return undefined;\n }\n\n return value;\n}\n\nfunction mergeColumnsEditorStyle(\n spacing: unknown,\n existingStyle: unknown,\n): string | undefined {\n const gap = getColumnGapCss(spacing);\n const currentStyle =\n typeof existingStyle === 'string' && existingStyle.trim() !== ''\n ? existingStyle.trim()\n : '';\n\n if (!gap) {\n return currentStyle || undefined;\n }\n\n const separator = currentStyle.endsWith(';') ? '' : ';';\n return currentStyle\n ? `${currentStyle}${separator}gap:${gap};`\n : `gap:${gap};`;\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n columns: {\n insertColumns: (count: 2 | 3 | 4) => ReturnType;\n };\n }\n}\n\nexport const COLUMN_PARENT_TYPES = [\n 'twoColumns',\n 'threeColumns',\n 'fourColumns',\n] as const;\n\nconst COLUMN_PARENT_SET = new Set<string>(COLUMN_PARENT_TYPES);\n\nexport const MAX_COLUMNS_DEPTH = 3;\n\nexport function getColumnsDepth(doc: ProseMirrorNode, from: number): number {\n const $from = doc.resolve(from);\n let depth = 0;\n for (let d = $from.depth; d > 0; d--) {\n if (COLUMN_PARENT_SET.has($from.node(d).type.name)) {\n depth++;\n }\n }\n return depth;\n}\n\ninterface ColumnsVariantConfig {\n name: (typeof COLUMN_PARENT_TYPES)[number];\n columnCount: number;\n content: string;\n dataType: string;\n}\n\nconst VARIANTS: ColumnsVariantConfig[] = [\n {\n name: 'twoColumns',\n columnCount: 2,\n content: 'columnsColumn columnsColumn',\n dataType: 'two-columns',\n },\n {\n name: 'threeColumns',\n columnCount: 3,\n content: 'columnsColumn columnsColumn columnsColumn',\n dataType: 'three-columns',\n },\n {\n name: 'fourColumns',\n columnCount: 4,\n content: 'columnsColumn{4}',\n dataType: 'four-columns',\n },\n];\n\nconst NODE_TYPE_MAP: Record<number, (typeof COLUMN_PARENT_TYPES)[number]> = {\n 2: 'twoColumns',\n 3: 'threeColumns',\n 4: 'fourColumns',\n};\n\nfunction isColumnEmpty(col: ProseMirrorNode): boolean {\n return (\n col.childCount === 1 &&\n col.firstChild?.type.name === 'paragraph' &&\n col.firstChild?.childCount === 0\n );\n}\n\nfunction deleteColumnParent(\n editor: Editor,\n state: EditorState,\n from: number,\n to: number,\n parentChildCount: number,\n): boolean {\n const tr = state.tr;\n if (parentChildCount === 1) {\n tr.replaceWith(from, to, state.schema.nodes.paragraph.create());\n } else {\n tr.delete(from, to);\n }\n tr.setSelection(TextSelection.near(tr.doc.resolve(from)));\n editor.view.dispatch(tr);\n return true;\n}\n\nfunction handleNodeSelectionBackspace(\n editor: Editor,\n selection: EditorState['selection'],\n $from: ResolvedPos,\n state: EditorState,\n): boolean {\n if (!(selection instanceof NodeSelection)) return false;\n if (!COLUMN_PARENT_SET.has(selection.node.type.name)) return false;\n\n const allColumnsEmpty = Array.from(\n { length: selection.node.childCount },\n (_, i) => selection.node.child(i),\n ).every(isColumnEmpty);\n\n if (!allColumnsEmpty) return false;\n\n const parent = $from.node($from.depth);\n return deleteColumnParent(\n editor,\n state,\n selection.from,\n selection.to,\n parent.childCount,\n );\n}\n\nfunction handleCursorBackspace(\n editor: Editor,\n $from: ResolvedPos,\n state: EditorState,\n): boolean {\n for (let depth = $from.depth; depth >= 1; depth--) {\n const currentNode = $from.node(depth);\n\n if (currentNode.type.name === 'columnsColumn') {\n return handleBackspaceInsideColumn(editor, $from, state, depth);\n }\n\n const indexInParent = $from.index(depth - 1);\n\n if (indexInParent === 0) continue;\n\n const parent = $from.node(depth - 1);\n const prevNode = parent.child(indexInParent - 1);\n\n if (COLUMN_PARENT_SET.has(prevNode.type.name)) {\n const from = $from.before(depth) - prevNode.nodeSize;\n const to = $from.before(depth);\n editor.view.dispatch(state.tr.delete(from, to));\n return true;\n }\n\n break;\n }\n\n return false;\n}\n\nfunction handleBackspaceInsideColumn(\n editor: Editor,\n $from: ResolvedPos,\n state: EditorState,\n depth: number,\n): boolean {\n const column = $from.node(depth);\n const columnParent = $from.node(depth - 1);\n\n if (!COLUMN_PARENT_SET.has(columnParent.type.name)) return false;\n if (!isColumnEmpty(column)) return false;\n\n const deletedIndex = $from.index(depth - 1);\n const remainingColumns = Array.from(\n { length: columnParent.childCount },\n (_, i) => columnParent.child(i),\n ).filter((_, i) => i !== deletedIndex);\n\n const columnParentFrom = $from.before(depth - 1);\n const columnParentTo = $from.after(depth - 1);\n const tr = state.tr;\n const deletingFirstColumn = deletedIndex === 0;\n const focusIndex = deletingFirstColumn ? 0 : deletedIndex - 1;\n const direction = deletingFirstColumn ? 'right' : 'left';\n\n if (remainingColumns.length === 1) {\n // 2 → 1: replace the column layout with the surviving column's content\n const survivingContent = Array.from(\n { length: remainingColumns[0].childCount },\n (_, i) => remainingColumns[0].child(i),\n );\n const replacement =\n survivingContent.length > 0\n ? survivingContent\n : [state.schema.nodes.paragraph.create()];\n tr.replaceWith(columnParentFrom, columnParentTo, replacement);\n const totalReplacementSize = replacement.reduce(\n (sum, n) => sum + n.nodeSize,\n 0,\n );\n const cursorPos = deletingFirstColumn\n ? columnParentFrom + 1\n : columnParentFrom + totalReplacementSize;\n tr.setSelection(\n TextSelection.near(\n tr.doc.resolve(cursorPos),\n direction === 'right' ? 1 : -1,\n ),\n );\n } else {\n // 3 → 2 or 4 → 3: convert to the smaller column layout\n const smallerLayoutType =\n state.schema.nodes[NODE_TYPE_MAP[remainingColumns.length]];\n tr.replaceWith(\n columnParentFrom,\n columnParentTo,\n smallerLayoutType.create(columnParent.attrs, remainingColumns),\n );\n focusColumn(tr, columnParentFrom, remainingColumns, focusIndex, direction);\n }\n\n editor.view.dispatch(tr);\n return true;\n}\n\nfunction focusColumn(\n tr: Transaction,\n columnParentPos: number,\n columns: ProseMirrorNode[],\n index: number,\n direction: 'left' | 'right' = 'right',\n): void {\n let pos = columnParentPos + 1;\n for (let i = 0; i < index; i++) {\n pos += columns[i].nodeSize;\n }\n pos += 1;\n\n if (direction === 'left') {\n pos += columns[index].content.size;\n }\n\n const bias = direction === 'right' ? 1 : -1;\n tr.setSelection(TextSelection.near(tr.doc.resolve(pos), bias));\n}\n\nfunction createColumnsNode(\n config: ColumnsVariantConfig,\n includeCommands: boolean,\n) {\n return EmailNode.create({\n name: config.name,\n group: 'block',\n content: config.content,\n isolating: true,\n defining: true,\n\n addAttributes() {\n return createStandardAttributes([\n ...LAYOUT_ATTRIBUTES,\n ...COMMON_HTML_ATTRIBUTES,\n ...COLUMN_PARENT_ATTRIBUTES,\n ]);\n },\n\n parseHTML() {\n return [{ tag: `div[data-type=\"${config.dataType}\"]` }];\n },\n\n renderHTML({ HTMLAttributes }) {\n const { style, cellspacing, ...attributes } = HTMLAttributes as Record<\n string,\n unknown\n >;\n const mergedStyle = mergeColumnsEditorStyle(cellspacing, style);\n\n return [\n 'div',\n mergeAttributes(\n {\n 'data-type': config.dataType,\n class: 'node-columns',\n ...(mergedStyle ? { style: mergedStyle } : {}),\n },\n attributes,\n ),\n 0,\n ];\n },\n\n ...(includeCommands && {\n addCommands() {\n return {\n insertColumns:\n (count: 2 | 3 | 4) =>\n ({ state, dispatch }: CommandProps) => {\n const tooDeep =\n getColumnsDepth(state.doc, state.selection.from) >=\n MAX_COLUMNS_DEPTH;\n if (tooDeep) return false;\n\n const emptyColumn = () =>\n state.schema.nodes.columnsColumn.create(\n null,\n state.schema.nodes.paragraph.create(),\n );\n\n const columnBlock = state.schema.nodes[\n NODE_TYPE_MAP[count]\n ].create(null, Array.from({ length: count }, emptyColumn));\n\n const tr = state.tr.replaceSelectionWith(columnBlock);\n\n // Find the inserted column block and focus its first column\n const $head = tr.selection.$head;\n for (let d = $head.depth; d >= 0; d--) {\n if (!COLUMN_PARENT_SET.has($head.node(d).type.name)) continue;\n const insertedColumns = Array.from(\n { length: $head.node(d).childCount },\n (_, i) => $head.node(d).child(i),\n );\n focusColumn(tr, $head.before(d), insertedColumns, 0);\n break;\n }\n\n if (dispatch) dispatch(tr);\n return true;\n },\n };\n },\n }),\n\n renderToReactEmail({ children, node, style }) {\n const inlineStyles = inlineCssToJs(node.attrs?.style);\n const cellSpacing = getRowCellSpacing(node.attrs?.cellspacing);\n\n return (\n <Row\n className={node.attrs?.class || undefined}\n style={{ ...style, ...inlineStyles }}\n {...(cellSpacing ? { cellSpacing } : {})}\n >\n {children}\n </Row>\n );\n },\n });\n}\n\nexport const TwoColumns = createColumnsNode(VARIANTS[0], true);\nexport const ThreeColumns = createColumnsNode(VARIANTS[1], false);\nexport const FourColumns = createColumnsNode(VARIANTS[2], false);\n\nexport const ColumnsColumn = EmailNode.create({\n name: 'columnsColumn',\n group: 'columnsColumn',\n content: 'block+',\n isolating: true,\n\n addAttributes() {\n return {\n ...createStandardAttributes([\n ...LAYOUT_ATTRIBUTES,\n ...COMMON_HTML_ATTRIBUTES,\n ]),\n };\n },\n\n parseHTML() {\n return [{ tag: 'div[data-type=\"column\"]' }];\n },\n\n renderHTML({ HTMLAttributes }) {\n return [\n 'div',\n mergeAttributes(\n { 'data-type': 'column', class: 'node-column' },\n HTMLAttributes,\n ),\n 0,\n ];\n },\n\n addKeyboardShortcuts() {\n return {\n Backspace: ({ editor }) => {\n const { state } = editor;\n const { selection } = state;\n const { empty, $from } = selection;\n\n if (!empty) {\n return handleNodeSelectionBackspace(editor, selection, $from, state);\n }\n\n if ($from.parentOffset !== 0) return false;\n\n return handleCursorBackspace(editor, $from, state);\n },\n 'Mod-a': ({ editor }) => {\n const { state } = editor;\n const { $from } = state.selection;\n\n for (let d = $from.depth; d > 0; d--) {\n if ($from.node(d).type.name !== 'columnsColumn') {\n continue;\n }\n\n const columnStart = $from.start(d);\n const columnEnd = $from.end(d);\n const { from, to } = state.selection;\n\n if (from === columnStart && to === columnEnd) {\n return false;\n }\n\n editor.view.dispatch(\n state.tr.setSelection(\n TextSelection.create(state.doc, columnStart, columnEnd),\n ),\n );\n return true;\n }\n\n return false;\n },\n };\n },\n\n renderToReactEmail({ children, node, style }) {\n const inlineStyles = inlineCssToJs(node.attrs?.style);\n const width = node.attrs?.width;\n return (\n <Column\n className={node.attrs?.class || undefined}\n style={{\n ...style,\n ...inlineStyles,\n ...(width ? { width } : {}),\n }}\n >\n {children}\n </Column>\n );\n },\n});\n","import {\n type Editor,\n Extension,\n extensions as nativeTiptapExtensions,\n} from '@tiptap/core';\nimport { Plugin, PluginKey, TextSelection } from '@tiptap/pm/state';\n\nexport interface FocusScopesOptions {\n clearSelectionOnBlur: boolean;\n}\n\nexport interface FocusScopesStorage {\n registerScope: (el: HTMLElement | null) => void;\n unregisterScope: (el: HTMLElement | null) => void;\n}\n\ndeclare module '@tiptap/core' {\n interface Storage {\n focusScope: FocusScopesStorage;\n }\n}\n\nexport const focusScopePluginKey = new PluginKey('reactEmailFocusScopes');\n\nconst noop = () => {};\n\nexport function createFocusScopesStorage(): FocusScopesStorage {\n return {\n registerScope: noop,\n unregisterScope: noop,\n };\n}\n\nexport function createFocusScopePlugin({\n editor,\n storage,\n clearSelectionOnBlur,\n}: {\n editor: Editor;\n storage: FocusScopesStorage;\n clearSelectionOnBlur: boolean;\n}) {\n const scopeRefs = new Set<HTMLElement>();\n\n const isInsideScope = (node: Node | null) =>\n Boolean(node && [...scopeRefs].some((el) => el.contains(node)));\n\n const getClosestScope = (node: Node | null) => {\n if (!node) return null;\n\n let closest: HTMLElement | null = null;\n for (const scope of scopeRefs) {\n if (!scope.contains(node)) continue;\n if (!closest || closest.contains(scope)) {\n closest = scope;\n }\n }\n return closest;\n };\n\n const handleFocusIn = (event: FocusEvent) => {\n const target = event.target;\n if (!(target instanceof Node) || !editor.view.dom.contains(target)) {\n return;\n }\n\n const previous = editor.isFocused;\n editor.isFocused = true;\n\n if (previous !== editor.isFocused) {\n const transaction = editor.state.tr\n .setMeta('focus', { event })\n .setMeta('addToHistory', false);\n\n editor.view.dispatch(transaction);\n }\n };\n\n const handleFocusOut = (event: FocusEvent) => {\n const blur = () => {\n const previous = editor.isFocused;\n editor.isFocused = false;\n\n if (previous !== editor.isFocused) {\n const transaction = editor.state.tr\n .setMeta('blur', { event })\n .setMeta('addToHistory', false);\n\n if (clearSelectionOnBlur) {\n transaction.setSelection(TextSelection.create(transaction.doc, 0));\n }\n\n editor.view.dispatch(transaction);\n }\n };\n\n const nextFocus = event.relatedTarget as Node | null;\n if (!nextFocus) {\n const previousFocus = event.target as Node | null;\n const fallbackScope = getClosestScope(previousFocus);\n\n // queueMicrotask is needed so that we can determine reliably\n // whether or not previousFocus is inside of the DOM\n queueMicrotask(() => {\n if (isInsideScope(event.view?.document.activeElement ?? null)) {\n return;\n }\n\n if (!previousFocus?.isConnected && fallbackScope?.isConnected) {\n fallbackScope.focus({ preventScroll: true });\n return;\n }\n\n blur();\n });\n return;\n }\n\n if (isInsideScope(nextFocus)) {\n return;\n }\n\n blur();\n };\n\n const registerScope = (el: HTMLElement | null) => {\n if (!el || scopeRefs.has(el)) return;\n\n scopeRefs.add(el);\n el.addEventListener('focusin', handleFocusIn);\n el.addEventListener('focusout', handleFocusOut);\n };\n\n const unregisterScope = (el: HTMLElement | null) => {\n if (!el || !scopeRefs.has(el)) return;\n\n scopeRefs.delete(el);\n el.removeEventListener('focusin', handleFocusIn);\n el.removeEventListener('focusout', handleFocusOut);\n };\n\n storage.registerScope = registerScope;\n storage.unregisterScope = unregisterScope;\n\n return new Plugin({\n key: focusScopePluginKey,\n view(view) {\n storage.registerScope = registerScope;\n storage.unregisterScope = unregisterScope;\n registerScope(view.dom);\n\n return {\n destroy() {\n for (const scope of [...scopeRefs]) {\n unregisterScope(scope);\n }\n storage.registerScope = noop;\n storage.unregisterScope = noop;\n },\n };\n },\n });\n}\n\nexport const FocusScopes = Extension.create<\n FocusScopesOptions,\n FocusScopesStorage\n>({\n name: 'focusScope',\n\n addOptions() {\n return {\n clearSelectionOnBlur: true,\n };\n },\n\n addStorage() {\n return createFocusScopesStorage();\n },\n\n onCreate() {\n this.editor.unregisterPlugin(nativeTiptapExtensions.focusEventsPluginKey);\n },\n\n addProseMirrorPlugins() {\n return [\n createFocusScopePlugin({\n editor: this.editor,\n storage: this.storage,\n clearSelectionOnBlur: this.options.clearSelectionOnBlur,\n }),\n ];\n },\n});\n"],"mappings":"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"}
@@ -20,7 +20,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
20
20
  enumerable: true
21
21
  }) : target, mod));
22
22
  //#endregion
23
- const require_email_node = require("./email-node-DvyWFrOM.cjs");
23
+ const require_email_node = require("./email-node-BhbXb22u.cjs");
24
24
  let react_email = require("react-email");
25
25
  let react_jsx_runtime = require("react/jsx-runtime");
26
26
  let _tiptap_core = require("@tiptap/core");
@@ -181,6 +181,76 @@ const NODE_TYPE_MAP = {
181
181
  3: "threeColumns",
182
182
  4: "fourColumns"
183
183
  };
184
+ function isColumnEmpty(col) {
185
+ return col.childCount === 1 && col.firstChild?.type.name === "paragraph" && col.firstChild?.childCount === 0;
186
+ }
187
+ function deleteColumnParent(editor, state, from, to, parentChildCount) {
188
+ const tr = state.tr;
189
+ if (parentChildCount === 1) tr.replaceWith(from, to, state.schema.nodes.paragraph.create());
190
+ else tr.delete(from, to);
191
+ tr.setSelection(_tiptap_pm_state.TextSelection.near(tr.doc.resolve(from)));
192
+ editor.view.dispatch(tr);
193
+ return true;
194
+ }
195
+ function handleNodeSelectionBackspace(editor, selection, $from, state) {
196
+ if (!(selection instanceof _tiptap_pm_state.NodeSelection)) return false;
197
+ if (!COLUMN_PARENT_SET.has(selection.node.type.name)) return false;
198
+ if (!Array.from({ length: selection.node.childCount }, (_, i) => selection.node.child(i)).every(isColumnEmpty)) return false;
199
+ const parent = $from.node($from.depth);
200
+ return deleteColumnParent(editor, state, selection.from, selection.to, parent.childCount);
201
+ }
202
+ function handleCursorBackspace(editor, $from, state) {
203
+ for (let depth = $from.depth; depth >= 1; depth--) {
204
+ if ($from.node(depth).type.name === "columnsColumn") return handleBackspaceInsideColumn(editor, $from, state, depth);
205
+ const indexInParent = $from.index(depth - 1);
206
+ if (indexInParent === 0) continue;
207
+ const prevNode = $from.node(depth - 1).child(indexInParent - 1);
208
+ if (COLUMN_PARENT_SET.has(prevNode.type.name)) {
209
+ const from = $from.before(depth) - prevNode.nodeSize;
210
+ const to = $from.before(depth);
211
+ editor.view.dispatch(state.tr.delete(from, to));
212
+ return true;
213
+ }
214
+ break;
215
+ }
216
+ return false;
217
+ }
218
+ function handleBackspaceInsideColumn(editor, $from, state, depth) {
219
+ const column = $from.node(depth);
220
+ const columnParent = $from.node(depth - 1);
221
+ if (!COLUMN_PARENT_SET.has(columnParent.type.name)) return false;
222
+ if (!isColumnEmpty(column)) return false;
223
+ const deletedIndex = $from.index(depth - 1);
224
+ const remainingColumns = Array.from({ length: columnParent.childCount }, (_, i) => columnParent.child(i)).filter((_, i) => i !== deletedIndex);
225
+ const columnParentFrom = $from.before(depth - 1);
226
+ const columnParentTo = $from.after(depth - 1);
227
+ const tr = state.tr;
228
+ const deletingFirstColumn = deletedIndex === 0;
229
+ const focusIndex = deletingFirstColumn ? 0 : deletedIndex - 1;
230
+ const direction = deletingFirstColumn ? "right" : "left";
231
+ if (remainingColumns.length === 1) {
232
+ const survivingContent = Array.from({ length: remainingColumns[0].childCount }, (_, i) => remainingColumns[0].child(i));
233
+ const replacement = survivingContent.length > 0 ? survivingContent : [state.schema.nodes.paragraph.create()];
234
+ tr.replaceWith(columnParentFrom, columnParentTo, replacement);
235
+ const totalReplacementSize = replacement.reduce((sum, n) => sum + n.nodeSize, 0);
236
+ const cursorPos = deletingFirstColumn ? columnParentFrom + 1 : columnParentFrom + totalReplacementSize;
237
+ tr.setSelection(_tiptap_pm_state.TextSelection.near(tr.doc.resolve(cursorPos), direction === "right" ? 1 : -1));
238
+ } else {
239
+ const smallerLayoutType = state.schema.nodes[NODE_TYPE_MAP[remainingColumns.length]];
240
+ tr.replaceWith(columnParentFrom, columnParentTo, smallerLayoutType.create(columnParent.attrs, remainingColumns));
241
+ focusColumn(tr, columnParentFrom, remainingColumns, focusIndex, direction);
242
+ }
243
+ editor.view.dispatch(tr);
244
+ return true;
245
+ }
246
+ function focusColumn(tr, columnParentPos, columns, index, direction = "right") {
247
+ let pos = columnParentPos + 1;
248
+ for (let i = 0; i < index; i++) pos += columns[i].nodeSize;
249
+ pos += 1;
250
+ if (direction === "left") pos += columns[index].content.size;
251
+ const bias = direction === "right" ? 1 : -1;
252
+ tr.setSelection(_tiptap_pm_state.TextSelection.near(tr.doc.resolve(pos), bias));
253
+ }
184
254
  function createColumnsNode(config, includeCommands) {
185
255
  return require_email_node.EmailNode.create({
186
256
  name: config.name,
@@ -212,20 +282,20 @@ function createColumnsNode(config, includeCommands) {
212
282
  ];
213
283
  },
214
284
  ...includeCommands && { addCommands() {
215
- return { insertColumns: (count) => ({ commands, state }) => {
285
+ return { insertColumns: (count) => ({ state, dispatch }) => {
216
286
  if (getColumnsDepth(state.doc, state.selection.from) >= 3) return false;
217
- const nodeType = NODE_TYPE_MAP[count];
218
- const children = Array.from({ length: count }, () => ({
219
- type: "columnsColumn",
220
- content: [{
221
- type: "paragraph",
222
- content: []
223
- }]
224
- }));
225
- return commands.insertContent({
226
- type: nodeType,
227
- content: children
228
- });
287
+ const emptyColumn = () => state.schema.nodes.columnsColumn.create(null, state.schema.nodes.paragraph.create());
288
+ const columnBlock = state.schema.nodes[NODE_TYPE_MAP[count]].create(null, Array.from({ length: count }, emptyColumn));
289
+ const tr = state.tr.replaceSelectionWith(columnBlock);
290
+ const $head = tr.selection.$head;
291
+ for (let d = $head.depth; d >= 0; d--) {
292
+ if (!COLUMN_PARENT_SET.has($head.node(d).type.name)) continue;
293
+ const insertedColumns = Array.from({ length: $head.node(d).childCount }, (_, i) => $head.node(d).child(i));
294
+ focusColumn(tr, $head.before(d), insertedColumns, 0);
295
+ break;
296
+ }
297
+ if (dispatch) dispatch(tr);
298
+ return true;
229
299
  } };
230
300
  } },
231
301
  renderToReactEmail({ children, node, style }) {
@@ -273,21 +343,9 @@ const ColumnsColumn = require_email_node.EmailNode.create({
273
343
  const { state } = editor;
274
344
  const { selection } = state;
275
345
  const { empty, $from } = selection;
276
- if (!empty) return false;
277
- for (let depth = $from.depth; depth >= 1; depth--) {
278
- if ($from.pos !== $from.start(depth)) break;
279
- const indexInParent = $from.index(depth - 1);
280
- if (indexInParent === 0) continue;
281
- const prevNode = $from.node(depth - 1).child(indexInParent - 1);
282
- if (COLUMN_PARENT_SET.has(prevNode.type.name)) {
283
- const deleteFrom = $from.before(depth) - prevNode.nodeSize;
284
- const deleteTo = $from.before(depth);
285
- editor.view.dispatch(state.tr.delete(deleteFrom, deleteTo));
286
- return true;
287
- }
288
- break;
289
- }
290
- return false;
346
+ if (!empty) return handleNodeSelectionBackspace(editor, selection, $from, state);
347
+ if ($from.parentOffset !== 0) return false;
348
+ return handleCursorBackspace(editor, $from, state);
291
349
  },
292
350
  "Mod-a": ({ editor }) => {
293
351
  const { state } = editor;
@@ -1,4 +1,4 @@
1
- require("./focus-scopes-l_Ki0562.cjs");
1
+ require("./focus-scopes-Ncj54H_M.cjs");
2
2
  let react_jsx_runtime = require("react/jsx-runtime");
3
3
  //#region src/ui/icons/image.tsx
4
4
  function ImageIcon({ size, width, height, ...props }) {
package/dist/index.cjs CHANGED
@@ -1,10 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- require("./focus-scopes-l_Ki0562.cjs");
3
- const require_core = require("./core-CLyAVVUW.cjs");
4
- const require_extensions = require("./extensions-oOPUnW7d.cjs");
5
- const require_extension = require("./extension-C1ilEKq-.cjs");
6
- const require_extension$1 = require("./extension-D_sNGTbX.cjs");
7
- const require_root = require("./root-D8zhkKgc.cjs");
2
+ require("./focus-scopes-Ncj54H_M.cjs");
3
+ const require_core = require("./core-Dte9KXcz.cjs");
4
+ const require_extensions = require("./extensions-DBFEW67d.cjs");
5
+ const require_extension = require("./extension-mBZiGbpM.cjs");
6
+ const require_extension$1 = require("./extension-bVrEjbFA.cjs");
7
+ const require_root = require("./root-BpooQKCu.cjs");
8
8
  let _tiptap_react = require("@tiptap/react");
9
9
  let react = require("react");
10
10
  let _tiptap_html = require("@tiptap/html");
@@ -200,7 +200,7 @@ const EmailEditor = (0, react.forwardRef)(({ content, onUpdate, onReady, theme =
200
200
  }),
201
201
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmailEditorReadyBridge, { onReadyRef }),
202
202
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_root.BubbleMenu, {
203
- hideWhenActiveNodes: bubbleMenu?.hideWhenActiveNodes ?? ["button"],
203
+ hideWhenActiveNodes: bubbleMenu?.hideWhenActiveNodes ?? ["button", "horizontalRule"],
204
204
  hideWhenActiveMarks: bubbleMenu?.hideWhenActiveMarks ?? ["link"]
205
205
  }),
206
206
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_root.BubbleMenu.LinkDefault, {}),
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import { n as composeReactEmail } from "./core-CbSTyrV4.mjs";
2
- import { t as StarterKit } from "./extensions-CU0ndoee.mjs";
2
+ import { t as StarterKit } from "./extensions-w71aFrgc.mjs";
3
3
  import { t as EmailTheming } from "./extension-B_qJnv3m.mjs";
4
4
  import { t as createImageExtension } from "./extension-CDrL5i44.mjs";
5
- import { t as SlashCommandRoot, x as BubbleMenu } from "./root-yYM1BF1C.mjs";
5
+ import { t as SlashCommandRoot, x as BubbleMenu } from "./root-DTTCkmjD.mjs";
6
6
  import { EditorProvider, useCurrentEditor } from "@tiptap/react";
7
7
  import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef } from "react";
8
8
  import { generateJSON } from "@tiptap/html";
@@ -198,7 +198,7 @@ const EmailEditor = forwardRef(({ content, onUpdate, onReady, theme = "basic", e
198
198
  }),
199
199
  /* @__PURE__ */ jsx(EmailEditorReadyBridge, { onReadyRef }),
200
200
  /* @__PURE__ */ jsx(BubbleMenu, {
201
- hideWhenActiveNodes: bubbleMenu?.hideWhenActiveNodes ?? ["button"],
201
+ hideWhenActiveNodes: bubbleMenu?.hideWhenActiveNodes ?? ["button", "horizontalRule"],
202
202
  hideWhenActiveMarks: bubbleMenu?.hideWhenActiveMarks ?? ["link"]
203
203
  }),
204
204
  /* @__PURE__ */ jsx(BubbleMenu.LinkDefault, {}),
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/utils/paste-sanitizer.ts","../src/core/create-paste-handler.ts","../src/email-editor/email-editor.tsx"],"sourcesContent":["/**\n * Sanitizes pasted HTML.\n * - From editor (has node-* classes): pass through as-is\n * - From external: strip all styles/classes, keep only semantic HTML\n */\n\n/**\n * Detects content from the Resend editor by checking for node-* class names.\n */\nconst EDITOR_CLASS_PATTERN = /class=\"[^\"]*node-/;\n\n/**\n * Attributes to preserve on specific elements for EXTERNAL content.\n * Only functional attributes - NO style or class.\n */\nconst PRESERVED_ATTRIBUTES: Record<string, string[]> = {\n a: ['href', 'target', 'rel'],\n img: ['src', 'alt', 'width', 'height'],\n td: ['colspan', 'rowspan'],\n th: ['colspan', 'rowspan', 'scope'],\n table: ['border', 'cellpadding', 'cellspacing'],\n '*': ['id'],\n};\n\nfunction isFromEditor(html: string): boolean {\n return EDITOR_CLASS_PATTERN.test(html);\n}\n\nexport function sanitizePastedHtml(html: string): string {\n if (isFromEditor(html)) {\n return html;\n }\n\n const parser = new DOMParser();\n const doc = parser.parseFromString(html, 'text/html');\n\n sanitizeNode(doc.body);\n\n return doc.body.innerHTML;\n}\n\nfunction sanitizeNode(node: Node): void {\n if (node.nodeType === Node.ELEMENT_NODE) {\n const el = node as HTMLElement;\n sanitizeElement(el);\n }\n\n for (const child of Array.from(node.childNodes)) {\n sanitizeNode(child);\n }\n}\n\nfunction sanitizeElement(el: HTMLElement): void {\n const tagName = el.tagName.toLowerCase();\n\n const allowedForTag = PRESERVED_ATTRIBUTES[tagName] || [];\n const allowedGlobal = PRESERVED_ATTRIBUTES['*'] || [];\n const allowed = new Set([...allowedForTag, ...allowedGlobal]);\n\n const attributesToRemove: string[] = [];\n\n for (const attr of Array.from(el.attributes)) {\n if (attr.name.startsWith('data-')) {\n attributesToRemove.push(attr.name);\n continue;\n }\n\n if (!allowed.has(attr.name)) {\n attributesToRemove.push(attr.name);\n }\n }\n\n for (const attr of attributesToRemove) {\n el.removeAttribute(attr);\n }\n}\n","import type { Extensions } from '@tiptap/core';\nimport { generateJSON } from '@tiptap/html';\nimport type { Slice } from '@tiptap/pm/model';\nimport type { EditorView } from '@tiptap/pm/view';\nimport { sanitizePastedHtml } from '../utils/paste-sanitizer';\n\nexport type PasteHandler = (\n payload: string | File,\n view: EditorView,\n) => boolean;\n\nexport function createPasteHandler({\n onPaste,\n extensions,\n}: {\n onPaste?: PasteHandler;\n extensions: Extensions;\n}) {\n return (view: EditorView, event: ClipboardEvent, slice: Slice): boolean => {\n const text = event.clipboardData?.getData('text/plain');\n\n if (text && onPaste?.(text, view)) {\n event.preventDefault();\n\n return true;\n }\n\n if (event.clipboardData?.files?.[0]) {\n const file = event.clipboardData.files[0];\n if (onPaste?.(file, view)) {\n event.preventDefault();\n\n return true;\n }\n }\n\n if (slice.content.childCount === 1) {\n return false;\n }\n\n if (event.clipboardData?.getData?.('text/html')) {\n event.preventDefault();\n const html = event.clipboardData.getData('text/html');\n\n const sanitizedHtml = sanitizePastedHtml(html);\n\n const jsonContent = generateJSON(sanitizedHtml, extensions);\n const node = view.state.schema.nodeFromJSON(jsonContent);\n\n const transaction = view.state.tr.replaceSelectionWith(node, false);\n view.dispatch(transaction);\n\n return true;\n }\n return false;\n };\n}\n","import type { Content, Editor, Extensions, JSONContent } from '@tiptap/core';\nimport {\n EditorProvider,\n type UseEditorOptions,\n useCurrentEditor,\n} from '@tiptap/react';\nimport {\n forwardRef,\n type ReactNode,\n type Ref,\n useEffect,\n useImperativeHandle,\n useLayoutEffect,\n useMemo,\n useRef,\n} from 'react';\nimport { createPasteHandler } from '../core/create-paste-handler';\nimport { composeReactEmail } from '../core/serializer/compose-react-email';\nimport { StarterKit } from '../extensions';\nimport { EmailTheming } from '../plugins/email-theming/extension';\nimport type { EditorThemeInput } from '../plugins/email-theming/types';\nimport { createImageExtension } from '../plugins/image/extension';\nimport { BubbleMenu } from '../ui/bubble-menu';\nimport { SlashCommandRoot } from '../ui/slash-command/root';\nimport '../ui/themes/default.css';\nimport { Placeholder } from '@tiptap/extension-placeholder';\n\nexport interface EmailEditorRef {\n getEmail: () => Promise<{ html: string; text: string }>;\n getEmailHTML: () => Promise<string>;\n getEmailText: () => Promise<string>;\n getJSON: () => JSONContent;\n editor: Editor | null;\n}\n\nexport interface EmailEditorProps {\n content?: Content;\n onUpdate?: (ref: EmailEditorRef) => void;\n onReady?: (ref: EmailEditorRef) => void;\n theme?: EditorThemeInput;\n editable?: boolean;\n placeholder?: string;\n bubbleMenu?: {\n hideWhenActiveNodes?: string[];\n hideWhenActiveMarks?: string[];\n };\n extensions?: Extensions;\n onUploadImage?: (file: File) => Promise<{ url: string }>;\n className?: string;\n children?: ReactNode;\n}\n\nfunction buildRef(editor: Editor | null): EmailEditorRef {\n return {\n getEmail: async () => {\n if (!editor) return { html: '', text: '' };\n return composeReactEmail({ editor });\n },\n getEmailHTML: async () => {\n if (!editor) return '';\n const result = await composeReactEmail({ editor });\n return result.html;\n },\n getEmailText: async () => {\n if (!editor) return '';\n const result = await composeReactEmail({ editor });\n return result.text;\n },\n getJSON: () => editor?.getJSON() ?? { type: 'doc', content: [] },\n editor,\n };\n}\n\nfunction RefBridge({\n editorRef,\n onUpdateRef,\n}: {\n editorRef: Ref<EmailEditorRef>;\n onUpdateRef: React.RefObject<((ref: EmailEditorRef) => void) | undefined>;\n}) {\n const { editor } = useCurrentEditor();\n\n const emailEditorRef = useMemo(() => buildRef(editor), [editor]);\n\n useImperativeHandle(editorRef, () => emailEditorRef, [emailEditorRef]);\n\n useEffect(() => {\n if (!editor) return;\n\n const handler = () => {\n onUpdateRef.current?.(emailEditorRef);\n };\n\n editor.on('update', handler);\n return () => {\n editor.off('update', handler);\n };\n }, [editor, emailEditorRef, onUpdateRef]);\n\n return null;\n}\n\nfunction EmailEditorReadyBridge({\n onReadyRef,\n}: {\n onReadyRef: React.RefObject<((ref: EmailEditorRef) => void) | undefined>;\n}) {\n const { editor } = useCurrentEditor();\n\n useLayoutEffect(() => {\n if (!editor) return;\n onReadyRef.current?.(buildRef(editor));\n }, [editor, onReadyRef]);\n\n return null;\n}\n\nexport const EmailEditor = forwardRef<EmailEditorRef, EmailEditorProps>(\n (\n {\n content,\n onUpdate,\n onReady,\n theme = 'basic',\n editable = true,\n placeholder,\n bubbleMenu,\n extensions: extensionsProp,\n onUploadImage,\n className,\n children,\n },\n ref,\n ) => {\n const onUpdateRef = useRef(onUpdate);\n onUpdateRef.current = onUpdate;\n\n const onReadyRef = useRef(onReady);\n onReadyRef.current = onReady;\n\n const imageExtension = useMemo(() => {\n if (!onUploadImage) return null;\n return createImageExtension({ uploadImage: onUploadImage });\n }, [onUploadImage]);\n\n const extensions = useMemo(() => {\n const base = extensionsProp ?? [\n StarterKit.configure(),\n Placeholder.configure({\n placeholder:\n placeholder ??\n (({ node }) => {\n // TODO: this heading placeholder is not working,\n // in part because styles are only targetting paragraphs,\n // but in part because of the way the content is rendered\n if (node.type.name === 'heading') {\n return `Heading ${node.attrs.level}`;\n }\n return \"Press '/' for commands\";\n }),\n includeChildren: true,\n }),\n EmailTheming.configure({ theme }),\n ];\n\n return imageExtension ? [...base, imageExtension] : base;\n }, [extensionsProp, theme, placeholder, imageExtension]);\n\n const editorProps: UseEditorOptions['editorProps'] = useMemo(\n () => ({\n handlePaste: createPasteHandler({\n extensions,\n }),\n }),\n [extensions],\n );\n\n return (\n <EditorProvider\n key={typeof theme === 'string' ? theme : JSON.stringify(theme)}\n extensions={extensions}\n content={content}\n editable={editable}\n immediatelyRender={false}\n editorProps={editorProps}\n editorContainerProps={{ className }}\n >\n <RefBridge editorRef={ref} onUpdateRef={onUpdateRef} />\n <EmailEditorReadyBridge onReadyRef={onReadyRef} />\n <BubbleMenu\n hideWhenActiveNodes={bubbleMenu?.hideWhenActiveNodes ?? ['button']}\n hideWhenActiveMarks={bubbleMenu?.hideWhenActiveMarks ?? ['link']}\n />\n <BubbleMenu.LinkDefault />\n <BubbleMenu.ButtonDefault />\n <BubbleMenu.ImageDefault />\n <SlashCommandRoot />\n {children}\n </EditorProvider>\n );\n },\n);\n\nEmailEditor.displayName = 'EmailEditor';\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/utils/paste-sanitizer.ts","../src/core/create-paste-handler.ts","../src/email-editor/email-editor.tsx"],"sourcesContent":["/**\n * Sanitizes pasted HTML.\n * - From editor (has node-* classes): pass through as-is\n * - From external: strip all styles/classes, keep only semantic HTML\n */\n\n/**\n * Detects content from the Resend editor by checking for node-* class names.\n */\nconst EDITOR_CLASS_PATTERN = /class=\"[^\"]*node-/;\n\n/**\n * Attributes to preserve on specific elements for EXTERNAL content.\n * Only functional attributes - NO style or class.\n */\nconst PRESERVED_ATTRIBUTES: Record<string, string[]> = {\n a: ['href', 'target', 'rel'],\n img: ['src', 'alt', 'width', 'height'],\n td: ['colspan', 'rowspan'],\n th: ['colspan', 'rowspan', 'scope'],\n table: ['border', 'cellpadding', 'cellspacing'],\n '*': ['id'],\n};\n\nfunction isFromEditor(html: string): boolean {\n return EDITOR_CLASS_PATTERN.test(html);\n}\n\nexport function sanitizePastedHtml(html: string): string {\n if (isFromEditor(html)) {\n return html;\n }\n\n const parser = new DOMParser();\n const doc = parser.parseFromString(html, 'text/html');\n\n sanitizeNode(doc.body);\n\n return doc.body.innerHTML;\n}\n\nfunction sanitizeNode(node: Node): void {\n if (node.nodeType === Node.ELEMENT_NODE) {\n const el = node as HTMLElement;\n sanitizeElement(el);\n }\n\n for (const child of Array.from(node.childNodes)) {\n sanitizeNode(child);\n }\n}\n\nfunction sanitizeElement(el: HTMLElement): void {\n const tagName = el.tagName.toLowerCase();\n\n const allowedForTag = PRESERVED_ATTRIBUTES[tagName] || [];\n const allowedGlobal = PRESERVED_ATTRIBUTES['*'] || [];\n const allowed = new Set([...allowedForTag, ...allowedGlobal]);\n\n const attributesToRemove: string[] = [];\n\n for (const attr of Array.from(el.attributes)) {\n if (attr.name.startsWith('data-')) {\n attributesToRemove.push(attr.name);\n continue;\n }\n\n if (!allowed.has(attr.name)) {\n attributesToRemove.push(attr.name);\n }\n }\n\n for (const attr of attributesToRemove) {\n el.removeAttribute(attr);\n }\n}\n","import type { Extensions } from '@tiptap/core';\nimport { generateJSON } from '@tiptap/html';\nimport type { Slice } from '@tiptap/pm/model';\nimport type { EditorView } from '@tiptap/pm/view';\nimport { sanitizePastedHtml } from '../utils/paste-sanitizer';\n\nexport type PasteHandler = (\n payload: string | File,\n view: EditorView,\n) => boolean;\n\nexport function createPasteHandler({\n onPaste,\n extensions,\n}: {\n onPaste?: PasteHandler;\n extensions: Extensions;\n}) {\n return (view: EditorView, event: ClipboardEvent, slice: Slice): boolean => {\n const text = event.clipboardData?.getData('text/plain');\n\n if (text && onPaste?.(text, view)) {\n event.preventDefault();\n\n return true;\n }\n\n if (event.clipboardData?.files?.[0]) {\n const file = event.clipboardData.files[0];\n if (onPaste?.(file, view)) {\n event.preventDefault();\n\n return true;\n }\n }\n\n if (slice.content.childCount === 1) {\n return false;\n }\n\n if (event.clipboardData?.getData?.('text/html')) {\n event.preventDefault();\n const html = event.clipboardData.getData('text/html');\n\n const sanitizedHtml = sanitizePastedHtml(html);\n\n const jsonContent = generateJSON(sanitizedHtml, extensions);\n const node = view.state.schema.nodeFromJSON(jsonContent);\n\n const transaction = view.state.tr.replaceSelectionWith(node, false);\n view.dispatch(transaction);\n\n return true;\n }\n return false;\n };\n}\n","import type { Content, Editor, Extensions, JSONContent } from '@tiptap/core';\nimport {\n EditorProvider,\n type UseEditorOptions,\n useCurrentEditor,\n} from '@tiptap/react';\nimport {\n forwardRef,\n type ReactNode,\n type Ref,\n useEffect,\n useImperativeHandle,\n useLayoutEffect,\n useMemo,\n useRef,\n} from 'react';\nimport { createPasteHandler } from '../core/create-paste-handler';\nimport { composeReactEmail } from '../core/serializer/compose-react-email';\nimport { StarterKit } from '../extensions';\nimport { EmailTheming } from '../plugins/email-theming/extension';\nimport type { EditorThemeInput } from '../plugins/email-theming/types';\nimport { createImageExtension } from '../plugins/image/extension';\nimport { BubbleMenu } from '../ui/bubble-menu';\nimport { SlashCommandRoot } from '../ui/slash-command/root';\nimport '../ui/themes/default.css';\nimport { Placeholder } from '@tiptap/extension-placeholder';\n\nexport interface EmailEditorRef {\n getEmail: () => Promise<{ html: string; text: string }>;\n getEmailHTML: () => Promise<string>;\n getEmailText: () => Promise<string>;\n getJSON: () => JSONContent;\n editor: Editor | null;\n}\n\nexport interface EmailEditorProps {\n content?: Content;\n onUpdate?: (ref: EmailEditorRef) => void;\n onReady?: (ref: EmailEditorRef) => void;\n theme?: EditorThemeInput;\n editable?: boolean;\n placeholder?: string;\n bubbleMenu?: {\n hideWhenActiveNodes?: string[];\n hideWhenActiveMarks?: string[];\n };\n extensions?: Extensions;\n onUploadImage?: (file: File) => Promise<{ url: string }>;\n className?: string;\n children?: ReactNode;\n}\n\nfunction buildRef(editor: Editor | null): EmailEditorRef {\n return {\n getEmail: async () => {\n if (!editor) return { html: '', text: '' };\n return composeReactEmail({ editor });\n },\n getEmailHTML: async () => {\n if (!editor) return '';\n const result = await composeReactEmail({ editor });\n return result.html;\n },\n getEmailText: async () => {\n if (!editor) return '';\n const result = await composeReactEmail({ editor });\n return result.text;\n },\n getJSON: () => editor?.getJSON() ?? { type: 'doc', content: [] },\n editor,\n };\n}\n\nfunction RefBridge({\n editorRef,\n onUpdateRef,\n}: {\n editorRef: Ref<EmailEditorRef>;\n onUpdateRef: React.RefObject<((ref: EmailEditorRef) => void) | undefined>;\n}) {\n const { editor } = useCurrentEditor();\n\n const emailEditorRef = useMemo(() => buildRef(editor), [editor]);\n\n useImperativeHandle(editorRef, () => emailEditorRef, [emailEditorRef]);\n\n useEffect(() => {\n if (!editor) return;\n\n const handler = () => {\n onUpdateRef.current?.(emailEditorRef);\n };\n\n editor.on('update', handler);\n return () => {\n editor.off('update', handler);\n };\n }, [editor, emailEditorRef, onUpdateRef]);\n\n return null;\n}\n\nfunction EmailEditorReadyBridge({\n onReadyRef,\n}: {\n onReadyRef: React.RefObject<((ref: EmailEditorRef) => void) | undefined>;\n}) {\n const { editor } = useCurrentEditor();\n\n useLayoutEffect(() => {\n if (!editor) return;\n onReadyRef.current?.(buildRef(editor));\n }, [editor, onReadyRef]);\n\n return null;\n}\n\nexport const EmailEditor = forwardRef<EmailEditorRef, EmailEditorProps>(\n (\n {\n content,\n onUpdate,\n onReady,\n theme = 'basic',\n editable = true,\n placeholder,\n bubbleMenu,\n extensions: extensionsProp,\n onUploadImage,\n className,\n children,\n },\n ref,\n ) => {\n const onUpdateRef = useRef(onUpdate);\n onUpdateRef.current = onUpdate;\n\n const onReadyRef = useRef(onReady);\n onReadyRef.current = onReady;\n\n const imageExtension = useMemo(() => {\n if (!onUploadImage) return null;\n return createImageExtension({ uploadImage: onUploadImage });\n }, [onUploadImage]);\n\n const extensions = useMemo(() => {\n const base = extensionsProp ?? [\n StarterKit.configure(),\n Placeholder.configure({\n placeholder:\n placeholder ??\n (({ node }) => {\n // TODO: this heading placeholder is not working,\n // in part because styles are only targetting paragraphs,\n // but in part because of the way the content is rendered\n if (node.type.name === 'heading') {\n return `Heading ${node.attrs.level}`;\n }\n return \"Press '/' for commands\";\n }),\n includeChildren: true,\n }),\n EmailTheming.configure({ theme }),\n ];\n\n return imageExtension ? [...base, imageExtension] : base;\n }, [extensionsProp, theme, placeholder, imageExtension]);\n\n const editorProps: UseEditorOptions['editorProps'] = useMemo(\n () => ({\n handlePaste: createPasteHandler({\n extensions,\n }),\n }),\n [extensions],\n );\n\n return (\n <EditorProvider\n key={typeof theme === 'string' ? theme : JSON.stringify(theme)}\n extensions={extensions}\n content={content}\n editable={editable}\n immediatelyRender={false}\n editorProps={editorProps}\n editorContainerProps={{ className }}\n >\n <RefBridge editorRef={ref} onUpdateRef={onUpdateRef} />\n <EmailEditorReadyBridge onReadyRef={onReadyRef} />\n <BubbleMenu\n hideWhenActiveNodes={\n bubbleMenu?.hideWhenActiveNodes ?? ['button', 'horizontalRule']\n }\n hideWhenActiveMarks={bubbleMenu?.hideWhenActiveMarks ?? ['link']}\n />\n <BubbleMenu.LinkDefault />\n <BubbleMenu.ButtonDefault />\n <BubbleMenu.ImageDefault />\n <SlashCommandRoot />\n {children}\n </EditorProvider>\n );\n },\n);\n\nEmailEditor.displayName = 'EmailEditor';\n"],"mappings":";;;;;;;;;;;;;;;;;;;AASA,MAAM,uBAAuB;;;;;AAM7B,MAAM,uBAAiD;CACrD,GAAG;EAAC;EAAQ;EAAU;EAAM;CAC5B,KAAK;EAAC;EAAO;EAAO;EAAS;EAAS;CACtC,IAAI,CAAC,WAAW,UAAU;CAC1B,IAAI;EAAC;EAAW;EAAW;EAAQ;CACnC,OAAO;EAAC;EAAU;EAAe;EAAc;CAC/C,KAAK,CAAC,KAAK;CACZ;AAED,SAAS,aAAa,MAAuB;AAC3C,QAAO,qBAAqB,KAAK,KAAK;;AAGxC,SAAgB,mBAAmB,MAAsB;AACvD,KAAI,aAAa,KAAK,CACpB,QAAO;CAIT,MAAM,MADS,IAAI,WAAW,CACX,gBAAgB,MAAM,YAAY;AAErD,cAAa,IAAI,KAAK;AAEtB,QAAO,IAAI,KAAK;;AAGlB,SAAS,aAAa,MAAkB;AACtC,KAAI,KAAK,aAAa,KAAK,aAEzB,iBADW,KACQ;AAGrB,MAAK,MAAM,SAAS,MAAM,KAAK,KAAK,WAAW,CAC7C,cAAa,MAAM;;AAIvB,SAAS,gBAAgB,IAAuB;CAG9C,MAAM,gBAAgB,qBAFN,GAAG,QAAQ,aAAa,KAEe,EAAE;CACzD,MAAM,gBAAgB,qBAAqB,QAAQ,EAAE;CACrD,MAAM,UAAU,IAAI,IAAI,CAAC,GAAG,eAAe,GAAG,cAAc,CAAC;CAE7D,MAAM,qBAA+B,EAAE;AAEvC,MAAK,MAAM,QAAQ,MAAM,KAAK,GAAG,WAAW,EAAE;AAC5C,MAAI,KAAK,KAAK,WAAW,QAAQ,EAAE;AACjC,sBAAmB,KAAK,KAAK,KAAK;AAClC;;AAGF,MAAI,CAAC,QAAQ,IAAI,KAAK,KAAK,CACzB,oBAAmB,KAAK,KAAK,KAAK;;AAItC,MAAK,MAAM,QAAQ,mBACjB,IAAG,gBAAgB,KAAK;;;;AC9D5B,SAAgB,mBAAmB,EACjC,SACA,cAIC;AACD,SAAQ,MAAkB,OAAuB,UAA0B;EACzE,MAAM,OAAO,MAAM,eAAe,QAAQ,aAAa;AAEvD,MAAI,QAAQ,UAAU,MAAM,KAAK,EAAE;AACjC,SAAM,gBAAgB;AAEtB,UAAO;;AAGT,MAAI,MAAM,eAAe,QAAQ,IAAI;GACnC,MAAM,OAAO,MAAM,cAAc,MAAM;AACvC,OAAI,UAAU,MAAM,KAAK,EAAE;AACzB,UAAM,gBAAgB;AAEtB,WAAO;;;AAIX,MAAI,MAAM,QAAQ,eAAe,EAC/B,QAAO;AAGT,MAAI,MAAM,eAAe,UAAU,YAAY,EAAE;AAC/C,SAAM,gBAAgB;GAKtB,MAAM,cAAc,aAFE,mBAFT,MAAM,cAAc,QAAQ,YAAY,CAEP,EAEE,WAAW;GAC3D,MAAM,OAAO,KAAK,MAAM,OAAO,aAAa,YAAY;GAExD,MAAM,cAAc,KAAK,MAAM,GAAG,qBAAqB,MAAM,MAAM;AACnE,QAAK,SAAS,YAAY;AAE1B,UAAO;;AAET,SAAO;;;;;ACFX,SAAS,SAAS,QAAuC;AACvD,QAAO;EACL,UAAU,YAAY;AACpB,OAAI,CAAC,OAAQ,QAAO;IAAE,MAAM;IAAI,MAAM;IAAI;AAC1C,UAAO,kBAAkB,EAAE,QAAQ,CAAC;;EAEtC,cAAc,YAAY;AACxB,OAAI,CAAC,OAAQ,QAAO;AAEpB,WADe,MAAM,kBAAkB,EAAE,QAAQ,CAAC,EACpC;;EAEhB,cAAc,YAAY;AACxB,OAAI,CAAC,OAAQ,QAAO;AAEpB,WADe,MAAM,kBAAkB,EAAE,QAAQ,CAAC,EACpC;;EAEhB,eAAe,QAAQ,SAAS,IAAI;GAAE,MAAM;GAAO,SAAS,EAAE;GAAE;EAChE;EACD;;AAGH,SAAS,UAAU,EACjB,WACA,eAIC;CACD,MAAM,EAAE,WAAW,kBAAkB;CAErC,MAAM,iBAAiB,cAAc,SAAS,OAAO,EAAE,CAAC,OAAO,CAAC;AAEhE,qBAAoB,iBAAiB,gBAAgB,CAAC,eAAe,CAAC;AAEtE,iBAAgB;AACd,MAAI,CAAC,OAAQ;EAEb,MAAM,gBAAgB;AACpB,eAAY,UAAU,eAAe;;AAGvC,SAAO,GAAG,UAAU,QAAQ;AAC5B,eAAa;AACX,UAAO,IAAI,UAAU,QAAQ;;IAE9B;EAAC;EAAQ;EAAgB;EAAY,CAAC;AAEzC,QAAO;;AAGT,SAAS,uBAAuB,EAC9B,cAGC;CACD,MAAM,EAAE,WAAW,kBAAkB;AAErC,uBAAsB;AACpB,MAAI,CAAC,OAAQ;AACb,aAAW,UAAU,SAAS,OAAO,CAAC;IACrC,CAAC,QAAQ,WAAW,CAAC;AAExB,QAAO;;AAGT,MAAa,cAAc,YAEvB,EACE,SACA,UACA,SACA,QAAQ,SACR,WAAW,MACX,aACA,YACA,YAAY,gBACZ,eACA,WACA,YAEF,QACG;CACH,MAAM,cAAc,OAAO,SAAS;AACpC,aAAY,UAAU;CAEtB,MAAM,aAAa,OAAO,QAAQ;AAClC,YAAW,UAAU;CAErB,MAAM,iBAAiB,cAAc;AACnC,MAAI,CAAC,cAAe,QAAO;AAC3B,SAAO,qBAAqB,EAAE,aAAa,eAAe,CAAC;IAC1D,CAAC,cAAc,CAAC;CAEnB,MAAM,aAAa,cAAc;EAC/B,MAAM,OAAO,kBAAkB;GAC7B,WAAW,WAAW;GACtB,YAAY,UAAU;IACpB,aACE,iBACE,EAAE,WAAW;AAIb,SAAI,KAAK,KAAK,SAAS,UACrB,QAAO,WAAW,KAAK,MAAM;AAE/B,YAAO;;IAEX,iBAAiB;IAClB,CAAC;GACF,aAAa,UAAU,EAAE,OAAO,CAAC;GAClC;AAED,SAAO,iBAAiB,CAAC,GAAG,MAAM,eAAe,GAAG;IACnD;EAAC;EAAgB;EAAO;EAAa;EAAe,CAAC;AAWxD,QACE,qBAAC,gBAAD;EAEc;EACH;EACC;EACV,mBAAmB;EACnB,aAhBiD,eAC5C,EACL,aAAa,mBAAmB,EAC9B,YACD,CAAC,EACH,GACD,CAAC,WAAW,CACb;EAUG,sBAAsB,EAAE,WAAW;YAPrC;GASE,oBAAC,WAAD;IAAW,WAAW;IAAkB;IAAe,CAAA;GACvD,oBAAC,wBAAD,EAAoC,YAAc,CAAA;GAClD,oBAAC,YAAD;IACE,qBACE,YAAY,uBAAuB,CAAC,UAAU,iBAAiB;IAEjE,qBAAqB,YAAY,uBAAuB,CAAC,OAAO;IAChE,CAAA;GACF,oBAAC,WAAW,aAAZ,EAA0B,CAAA;GAC1B,oBAAC,WAAW,eAAZ,EAA4B,CAAA;GAC5B,oBAAC,WAAW,cAAZ,EAA2B,CAAA;GAC3B,oBAAC,kBAAD,EAAoB,CAAA;GACnB;GACc;IArBV,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,MAAM,CAqB/C;EAGtB;AAED,YAAY,cAAc"}
@@ -1,8 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- require("../focus-scopes-l_Ki0562.cjs");
3
- const require_extension = require("../extension-C1ilEKq-.cjs");
4
- const require_extension$1 = require("../extension-D_sNGTbX.cjs");
5
- const require_image = require("../image-BpsmuXKq.cjs");
2
+ require("../focus-scopes-Ncj54H_M.cjs");
3
+ const require_extension = require("../extension-mBZiGbpM.cjs");
4
+ const require_extension$1 = require("../extension-bVrEjbFA.cjs");
5
+ const require_image = require("../image-D8OUIgwB.cjs");
6
6
  let react = require("react");
7
7
  //#region src/plugins/image/slash-command.tsx
8
8
  const imageSlashCommand = {
@@ -1,5 +1,5 @@
1
- const require_focus_scopes = require("./focus-scopes-l_Ki0562.cjs");
2
- const require_event_bus = require("./event-bus-C_wS2tD6.cjs");
1
+ const require_focus_scopes = require("./focus-scopes-Ncj54H_M.cjs");
2
+ const require_event_bus = require("./event-bus-C0haBiDw.cjs");
3
3
  const require_set_text_alignment = require("./set-text-alignment-CpGakuZ2.cjs");
4
4
  let _tiptap_react = require("@tiptap/react");
5
5
  let react = require("react");
@@ -1431,6 +1431,7 @@ const BubbleMenuStrike = createMarkBubbleItem({
1431
1431
  const bubbleMenuTriggers = {
1432
1432
  textSelection(hideWhenActiveNodes = [], hideWhenActiveMarks = []) {
1433
1433
  return ({ editor, state }) => {
1434
+ if (state.selection instanceof _tiptap_pm_state.NodeSelection && hideWhenActiveNodes.includes(state.selection.node.type.name)) return false;
1434
1435
  for (const node of hideWhenActiveNodes) {
1435
1436
  if (editor.isActive(node)) return false;
1436
1437
  const { $from } = state.selection;
@@ -1,4 +1,4 @@
1
- import { d as getColumnsDepth, i as focusScopePluginKey, n as createFocusScopePlugin, r as createFocusScopesStorage } from "./focus-scopes-B_0O7l7d.mjs";
1
+ import { d as getColumnsDepth, i as focusScopePluginKey, n as createFocusScopePlugin, r as createFocusScopesStorage } from "./focus-scopes-DOsiXV7b.mjs";
2
2
  import { t as editorEventBus } from "./event-bus-CqrIUE24.mjs";
3
3
  import { t as setTextAlignment } from "./set-text-alignment-Bgdg7-5y.mjs";
4
4
  import { useCurrentEditor, useEditorState } from "@tiptap/react";
@@ -6,7 +6,7 @@ import * as React$1 from "react";
6
6
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
7
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
8
  import { extensions } from "@tiptap/core";
9
- import { PluginKey } from "@tiptap/pm/state";
9
+ import { NodeSelection, PluginKey } from "@tiptap/pm/state";
10
10
  import { BubbleMenu } from "@tiptap/react/menus";
11
11
  import { Slot } from "@radix-ui/react-slot";
12
12
  import * as Popover from "@radix-ui/react-popover";
@@ -1429,6 +1429,7 @@ const BubbleMenuStrike = createMarkBubbleItem({
1429
1429
  const bubbleMenuTriggers = {
1430
1430
  textSelection(hideWhenActiveNodes = [], hideWhenActiveMarks = []) {
1431
1431
  return ({ editor, state }) => {
1432
+ if (state.selection instanceof NodeSelection && hideWhenActiveNodes.includes(state.selection.node.type.name)) return false;
1432
1433
  for (const node of hideWhenActiveNodes) {
1433
1434
  if (editor.isActive(node)) return false;
1434
1435
  const { $from } = state.selection;
@@ -2515,4 +2516,4 @@ function SlashCommandRoot({ items: itemsProp, filterItems: filterItemsProp, char
2515
2516
  //#endregion
2516
2517
  export { BubbleMenuButtonToolbar as $, BubbleMenuImageUnlink as A, ChevronDownIcon as At, BubbleMenuNodeSelector as B, BubbleMenuLinkDefault as C, Heading2Icon as Ct, BubbleMenuLinkForm as D, Columns3Icon as Dt, BubbleMenuLinkOpenLink as E, Columns4Icon as Et, RootWithDefault as F, AlignLeftIcon as Ft, BubbleMenuItalic as G, NodeSelectorRoot as H, BubbleMenuUppercase as I, AlignCenterIcon as It, EditorFocusScope as J, BubbleMenuItemGroup as K, BubbleMenuUnderline as L, BubbleMenuImageForm as M, CaseUpperIcon as Mt, BubbleMenuImageEditLink as N, BoldIcon as Nt, BubbleMenuLinkEditLink as O, Columns2Icon as Ot, BubbleMenuButtonDefault as P, AlignRightIcon as Pt, BubbleMenuButtonUnlink as Q, bubbleMenuTriggers as R, BubbleMenuSeparator as S, Heading3Icon as St, BubbleMenuLinkToolbar as T, ExternalLinkIcon as Tt, NodeSelectorTrigger as U, NodeSelectorContent as V, BubbleMenuLinkSelector as W, FocusScopeContext as X, EditorFocusScopeProvider as Y, useEditorFocusScope as Z, TWO_COLUMNS as _, MousePointerIcon as _t, BUTTON as a, BubbleMenuAlignCenter as at, isInsideNode as b, LinkIcon as bt, FOUR_COLUMNS as c, UnlinkIcon as ct, H3 as d, TextIcon as dt, BubbleMenuButtonForm as et, NUMBERED_LIST as f, StrikethroughIcon as ft, THREE_COLUMNS as g, PencilIcon as gt, TEXT as h, Rows2Icon as ht, BULLET_LIST as i, BubbleMenuAlignLeft as it, BubbleMenuImageToolbar as j, CheckIcon as jt, BubbleMenuImageDefault as k, CodeIcon as kt, H1 as l, UnderlineIcon as lt, SECTION as m, SplitSquareVerticalIcon as mt, filterAndRankItems as n, BubbleMenuBold as nt, CODE as o, BubbleMenuItem as ot, QUOTE as p, SquareCodeIcon as pt, BubbleMenuCode as q, scoreItem as r, BubbleMenuAlignRight as rt, DIVIDER as s, useBubbleMenuContext as st, SlashCommandRoot as t, BubbleMenuButtonEditLink as tt, H2 as u, TextQuoteIcon as ut, defaultSlashCommands as v, ListOrderedIcon as vt, BubbleMenuLinkUnlink as w, Heading1Icon as wt, BubbleMenu$1 as x, ItalicIcon as xt, isAtMaxColumnsDepth as y, ListIcon as yt, BubbleMenuStrike as z };
2517
2518
 
2518
- //# sourceMappingURL=root-yYM1BF1C.mjs.map
2519
+ //# sourceMappingURL=root-DTTCkmjD.mjs.map