@kontakto/email-template-editor 2.1.0 → 2.2.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.
package/dist/index.cjs CHANGED
@@ -166,6 +166,20 @@ function renderMarkdownString(str) {
166
166
  }
167
167
  return sanitizer(html2);
168
168
  }
169
+ function renderInlineMarkdownString(str) {
170
+ const html2 = marked.marked.parseInline(str, {
171
+ async: false,
172
+ breaks: true,
173
+ gfm: true,
174
+ pedantic: false,
175
+ silent: false,
176
+ renderer: new CustomRenderer()
177
+ });
178
+ if (typeof html2 !== "string") {
179
+ throw new Error("marked.parseInline did not return a string");
180
+ }
181
+ return sanitizer(html2);
182
+ }
169
183
  function EmailMarkdown(_a) {
170
184
  var _b = _a, { markdown } = _b, props = __objRest(_b, ["markdown"]);
171
185
  const data = React58.useMemo(() => renderMarkdownString(markdown), [markdown]);
@@ -691,7 +705,8 @@ function getFontFamily3(fontFamily) {
691
705
  var HeadingPropsSchema = zod.z.object({
692
706
  props: zod.z.object({
693
707
  text: zod.z.string().optional().nullable(),
694
- level: zod.z.enum(["h1", "h2", "h3"]).optional().nullable()
708
+ level: zod.z.enum(["h1", "h2", "h3"]).optional().nullable(),
709
+ markdown: zod.z.boolean().optional().nullable()
695
710
  }).optional().nullable(),
696
711
  style: zod.z.object({
697
712
  color: COLOR_SCHEMA6,
@@ -710,28 +725,31 @@ var HeadingPropsDefaults = {
710
725
  text: ""
711
726
  };
712
727
  function Heading({ props, style }) {
713
- var _a, _b, _c, _d, _e, _f, _g;
728
+ var _a, _b, _c, _d, _e, _f, _g, _h;
714
729
  const level = (_a = props == null ? void 0 : props.level) != null ? _a : HeadingPropsDefaults.level;
715
730
  const text = (_b = props == null ? void 0 : props.text) != null ? _b : HeadingPropsDefaults.text;
731
+ const isMarkdown = (_c = props == null ? void 0 : props.markdown) != null ? _c : false;
716
732
  const hStyle = {
717
- color: (_c = style == null ? void 0 : style.color) != null ? _c : void 0,
718
- backgroundColor: (_d = style == null ? void 0 : style.backgroundColor) != null ? _d : void 0,
719
- fontWeight: (_e = style == null ? void 0 : style.fontWeight) != null ? _e : "bold",
720
- lineHeight: (_f = style == null ? void 0 : style.lineHeight) != null ? _f : void 0,
733
+ color: (_d = style == null ? void 0 : style.color) != null ? _d : void 0,
734
+ backgroundColor: (_e = style == null ? void 0 : style.backgroundColor) != null ? _e : void 0,
735
+ fontWeight: (_f = style == null ? void 0 : style.fontWeight) != null ? _f : "bold",
736
+ lineHeight: (_g = style == null ? void 0 : style.lineHeight) != null ? _g : void 0,
721
737
  letterSpacing: (style == null ? void 0 : style.letterSpacing) != null ? `${style.letterSpacing}px` : void 0,
722
- textAlign: (_g = style == null ? void 0 : style.textAlign) != null ? _g : void 0,
738
+ textAlign: (_h = style == null ? void 0 : style.textAlign) != null ? _h : void 0,
723
739
  margin: 0,
724
740
  fontFamily: getFontFamily3(style == null ? void 0 : style.fontFamily),
725
741
  fontSize: getFontSize(level),
726
742
  padding: getPadding7(style == null ? void 0 : style.padding)
727
743
  };
744
+ const html2 = React58.useMemo(() => isMarkdown ? renderInlineMarkdownString(text) : null, [isMarkdown, text]);
745
+ const renderProps = isMarkdown ? { style: hStyle, dangerouslySetInnerHTML: { __html: html2 != null ? html2 : "" } } : { style: hStyle, children: text };
728
746
  switch (level) {
729
747
  case "h1":
730
- return /* @__PURE__ */ React58__default.default.createElement("h1", { style: hStyle }, text);
748
+ return /* @__PURE__ */ React58__default.default.createElement("h1", __spreadValues({}, renderProps));
731
749
  case "h2":
732
- return /* @__PURE__ */ React58__default.default.createElement("h2", { style: hStyle }, text);
750
+ return /* @__PURE__ */ React58__default.default.createElement("h2", __spreadValues({}, renderProps));
733
751
  case "h3":
734
- return /* @__PURE__ */ React58__default.default.createElement("h3", { style: hStyle }, text);
752
+ return /* @__PURE__ */ React58__default.default.createElement("h3", __spreadValues({}, renderProps));
735
753
  }
736
754
  }
737
755
  function getFontSize(level) {
@@ -2504,59 +2522,46 @@ function FontWeightInput({ label, defaultValue, onChange }) {
2504
2522
  );
2505
2523
  }
2506
2524
  function LetterSpacingInput({ label, defaultValue, onChange }) {
2507
- const handleChange = (ev) => {
2508
- const raw = ev.target.value.trim();
2509
- if (raw === "") {
2510
- onChange(null);
2511
- return;
2512
- }
2513
- const value = parseFloat(raw);
2514
- onChange(isNaN(value) ? null : value);
2525
+ const [value, setValue] = React58.useState(defaultValue != null ? defaultValue : 0);
2526
+ React58.useEffect(() => {
2527
+ setValue(defaultValue != null ? defaultValue : 0);
2528
+ }, [defaultValue]);
2529
+ const handleChange = (v) => {
2530
+ setValue(v);
2531
+ onChange(v === 0 ? null : v);
2515
2532
  };
2516
- return /* @__PURE__ */ React58__default.default.createElement(material.Stack, { spacing: 1, alignItems: "flex-start" }, /* @__PURE__ */ React58__default.default.createElement(
2517
- material.TextField,
2533
+ return /* @__PURE__ */ React58__default.default.createElement(material.Stack, { spacing: 1, alignItems: "flex-start" }, /* @__PURE__ */ React58__default.default.createElement(material.InputLabel, { shrink: true }, label), /* @__PURE__ */ React58__default.default.createElement(
2534
+ RawSliderInput,
2518
2535
  {
2519
- fullWidth: true,
2520
- onChange: handleChange,
2521
- defaultValue: defaultValue != null ? defaultValue : "",
2522
- label,
2523
- variant: "standard",
2524
- placeholder: "normal",
2525
- size: "small",
2526
- type: "number",
2527
- inputProps: { step: 0.5 },
2528
- InputProps: {
2529
- startAdornment: /* @__PURE__ */ React58__default.default.createElement(material.InputAdornment, { position: "start" }, /* @__PURE__ */ React58__default.default.createElement(iconsMaterial.SpaceBarOutlined, { sx: { fontSize: 16 } })),
2530
- endAdornment: /* @__PURE__ */ React58__default.default.createElement(material.Typography, { variant: "body2", color: "text.secondary" }, "px")
2531
- }
2536
+ iconLabel: /* @__PURE__ */ React58__default.default.createElement(iconsMaterial.SpaceBarOutlined, { sx: { fontSize: 16 } }),
2537
+ value,
2538
+ setValue: handleChange,
2539
+ units: "px",
2540
+ step: 0.1,
2541
+ min: 0,
2542
+ max: 2
2532
2543
  }
2533
2544
  ));
2534
2545
  }
2535
2546
  function LineHeightInput({ label, defaultValue, onChange }) {
2536
- const handleChange = (ev) => {
2537
- const raw = ev.target.value.trim();
2538
- if (raw === "") {
2539
- onChange(null);
2540
- return;
2541
- }
2542
- const value = parseFloat(raw);
2543
- onChange(isNaN(value) ? null : value);
2547
+ const [value, setValue] = React58.useState(defaultValue != null ? defaultValue : 0);
2548
+ React58.useEffect(() => {
2549
+ setValue(defaultValue != null ? defaultValue : 0);
2550
+ }, [defaultValue]);
2551
+ const handleChange = (v) => {
2552
+ setValue(v);
2553
+ onChange(v === 0 ? null : v);
2544
2554
  };
2545
- return /* @__PURE__ */ React58__default.default.createElement(material.Stack, { spacing: 1, alignItems: "flex-start" }, /* @__PURE__ */ React58__default.default.createElement(
2546
- material.TextField,
2555
+ return /* @__PURE__ */ React58__default.default.createElement(material.Stack, { spacing: 1, alignItems: "flex-start" }, /* @__PURE__ */ React58__default.default.createElement(material.InputLabel, { shrink: true }, label), /* @__PURE__ */ React58__default.default.createElement(
2556
+ RawSliderInput,
2547
2557
  {
2548
- fullWidth: true,
2549
- onChange: handleChange,
2550
- defaultValue: defaultValue != null ? defaultValue : "",
2551
- label,
2552
- variant: "standard",
2553
- placeholder: "default",
2554
- size: "small",
2555
- type: "number",
2556
- inputProps: { step: 0.1 },
2557
- InputProps: {
2558
- startAdornment: /* @__PURE__ */ React58__default.default.createElement(material.InputAdornment, { position: "start" }, /* @__PURE__ */ React58__default.default.createElement(iconsMaterial.FormatLineSpacingOutlined, { sx: { fontSize: 16 } }))
2559
- }
2558
+ iconLabel: /* @__PURE__ */ React58__default.default.createElement(iconsMaterial.FormatLineSpacingOutlined, { sx: { fontSize: 16 } }),
2559
+ value,
2560
+ setValue: handleChange,
2561
+ units: "",
2562
+ step: 0.1,
2563
+ min: 0,
2564
+ max: 2
2560
2565
  }
2561
2566
  ));
2562
2567
  }
@@ -6229,6 +6234,174 @@ function ButtonEditor({ style, props }) {
6229
6234
  }
6230
6235
  return /* @__PURE__ */ React58__default.default.createElement("div", { style: wrapperStyle }, /* @__PURE__ */ React58__default.default.createElement("span", { style: linkStyle }, /* @__PURE__ */ React58__default.default.createElement("span", null, text)));
6231
6236
  }
6237
+ function useMarkdownToolbar({ text, isSelected, commitText, trackSelection }) {
6238
+ const textareaRef = React58.useRef(null);
6239
+ const [selection, setSelection] = React58.useState({ start: 0, end: 0 });
6240
+ const [linkPrompt, setLinkPrompt] = React58.useState(false);
6241
+ const pendingSelectionRef = React58.useRef(null);
6242
+ const textRef = React58.useRef(text);
6243
+ React58.useEffect(() => {
6244
+ textRef.current = text;
6245
+ }, [text]);
6246
+ const syncSelection = React58.useCallback(
6247
+ (start, end) => {
6248
+ const next = { start, end };
6249
+ setSelection(next);
6250
+ trackSelection == null ? void 0 : trackSelection(next);
6251
+ },
6252
+ [trackSelection]
6253
+ );
6254
+ const trackFocus = React58.useCallback(
6255
+ (e) => {
6256
+ var _a, _b;
6257
+ const el = e.currentTarget;
6258
+ const start = (_a = el.selectionStart) != null ? _a : el.value.length;
6259
+ const end = (_b = el.selectionEnd) != null ? _b : el.value.length;
6260
+ syncSelection(start, end);
6261
+ },
6262
+ [syncSelection]
6263
+ );
6264
+ React58.useEffect(() => {
6265
+ const target = pendingSelectionRef.current;
6266
+ if (!target) return;
6267
+ const ta = textareaRef.current;
6268
+ if (!ta) return;
6269
+ ta.focus();
6270
+ ta.setSelectionRange(target.start, target.end);
6271
+ syncSelection(target.start, target.end);
6272
+ pendingSelectionRef.current = null;
6273
+ }, [text, syncSelection]);
6274
+ React58.useEffect(() => {
6275
+ if (!isSelected || selection.start === selection.end) {
6276
+ setLinkPrompt(false);
6277
+ }
6278
+ }, [isSelected, selection.start, selection.end]);
6279
+ const wrapSelection = (prefix, suffix) => {
6280
+ var _a, _b;
6281
+ const ta = textareaRef.current;
6282
+ if (!ta) return;
6283
+ const start = (_a = ta.selectionStart) != null ? _a : selection.start;
6284
+ const end = (_b = ta.selectionEnd) != null ? _b : selection.end;
6285
+ if (start === end) return;
6286
+ const current = textRef.current;
6287
+ const selected = current.slice(start, end);
6288
+ const before = current.slice(0, start);
6289
+ const after = current.slice(end);
6290
+ const wrapped = `${prefix}${selected}${suffix}`;
6291
+ const newText = `${before}${wrapped}${after}`;
6292
+ const newStart = start + prefix.length;
6293
+ const newEnd = newStart + selected.length;
6294
+ pendingSelectionRef.current = { start: newStart, end: newEnd };
6295
+ commitText(newText);
6296
+ };
6297
+ const handleBold = () => wrapSelection("**", "**");
6298
+ const handleItalic = () => wrapSelection("*", "*");
6299
+ const handleLinkRequest = () => {
6300
+ if (selection.start === selection.end) return;
6301
+ setLinkPrompt(true);
6302
+ };
6303
+ const handleLinkSubmit = (url) => {
6304
+ const start = selection.start;
6305
+ const end = selection.end;
6306
+ if (start === end) {
6307
+ setLinkPrompt(false);
6308
+ return;
6309
+ }
6310
+ const current = textRef.current;
6311
+ const selected = current.slice(start, end);
6312
+ const before = current.slice(0, start);
6313
+ const after = current.slice(end);
6314
+ const wrapped = `[${selected}](${url})`;
6315
+ const newText = `${before}${wrapped}${after}`;
6316
+ const newStart = start + wrapped.length;
6317
+ pendingSelectionRef.current = { start: newStart, end: newStart };
6318
+ commitText(newText);
6319
+ setLinkPrompt(false);
6320
+ };
6321
+ const handleLinkCancel = () => {
6322
+ var _a;
6323
+ setLinkPrompt(false);
6324
+ (_a = textareaRef.current) == null ? void 0 : _a.focus();
6325
+ };
6326
+ const handleKeyDown = (e) => {
6327
+ if (!(e.metaKey || e.ctrlKey)) return;
6328
+ const key = e.key.toLowerCase();
6329
+ if (key === "b") {
6330
+ e.preventDefault();
6331
+ handleBold();
6332
+ } else if (key === "i") {
6333
+ e.preventDefault();
6334
+ handleItalic();
6335
+ } else if (key === "k") {
6336
+ e.preventDefault();
6337
+ handleLinkRequest();
6338
+ }
6339
+ };
6340
+ const toolbarVisible = isSelected && (selection.start !== selection.end || linkPrompt);
6341
+ return {
6342
+ textareaRef,
6343
+ selection,
6344
+ trackFocus,
6345
+ handleKeyDown,
6346
+ toolbarProps: {
6347
+ visible: toolbarVisible,
6348
+ linkPrompt,
6349
+ onBold: handleBold,
6350
+ onItalic: handleItalic,
6351
+ onLinkRequest: handleLinkRequest,
6352
+ onLinkSubmit: handleLinkSubmit,
6353
+ onLinkCancel: handleLinkCancel
6354
+ }
6355
+ };
6356
+ }
6357
+ function InlineFormattingToolbar({
6358
+ anchorEl,
6359
+ visible,
6360
+ linkPrompt,
6361
+ onBold,
6362
+ onItalic,
6363
+ onLinkRequest,
6364
+ onLinkSubmit,
6365
+ onLinkCancel
6366
+ }) {
6367
+ const [url, setUrl] = React58.useState("");
6368
+ const inputRef = React58.useRef(null);
6369
+ React58.useEffect(() => {
6370
+ if (linkPrompt) {
6371
+ setUrl("");
6372
+ setTimeout(() => {
6373
+ var _a;
6374
+ return (_a = inputRef.current) == null ? void 0 : _a.focus();
6375
+ }, 0);
6376
+ }
6377
+ }, [linkPrompt]);
6378
+ const preventBlur = (e) => e.preventDefault();
6379
+ return /* @__PURE__ */ React58__default.default.createElement(material.Popper, { open: visible, anchorEl, placement: "top-start", style: { zIndex: 1300 } }, /* @__PURE__ */ React58__default.default.createElement(material.Paper, { elevation: 4, sx: { px: 0.5, py: 0.25, mb: 0.5 }, onMouseDown: preventBlur }, linkPrompt ? /* @__PURE__ */ React58__default.default.createElement(material.Stack, { direction: "row", alignItems: "center", spacing: 0.5, sx: { px: 0.5 } }, /* @__PURE__ */ React58__default.default.createElement(
6380
+ material.TextField,
6381
+ {
6382
+ inputRef,
6383
+ value: url,
6384
+ onChange: (e) => setUrl(e.target.value),
6385
+ placeholder: "https://example.com",
6386
+ variant: "standard",
6387
+ size: "small",
6388
+ onKeyDown: (e) => {
6389
+ if (e.key === "Enter") {
6390
+ e.preventDefault();
6391
+ const trimmed = url.trim();
6392
+ if (trimmed) onLinkSubmit(trimmed);
6393
+ else onLinkCancel();
6394
+ } else if (e.key === "Escape") {
6395
+ e.preventDefault();
6396
+ onLinkCancel();
6397
+ }
6398
+ },
6399
+ sx: { width: 220 }
6400
+ }
6401
+ )) : /* @__PURE__ */ React58__default.default.createElement(material.Stack, { direction: "row", spacing: 0.25 }, /* @__PURE__ */ React58__default.default.createElement(material.IconButton, { size: "small", onClick: onBold, title: "Bold (Cmd+B)", "aria-label": "Bold" }, /* @__PURE__ */ React58__default.default.createElement(iconsMaterial.FormatBoldOutlined, { fontSize: "small" })), /* @__PURE__ */ React58__default.default.createElement(material.IconButton, { size: "small", onClick: onItalic, title: "Italic (Cmd+I)", "aria-label": "Italic" }, /* @__PURE__ */ React58__default.default.createElement(iconsMaterial.FormatItalicOutlined, { fontSize: "small" })), /* @__PURE__ */ React58__default.default.createElement(material.IconButton, { size: "small", onClick: onLinkRequest, title: "Link (Cmd+K)", "aria-label": "Link" }, /* @__PURE__ */ React58__default.default.createElement(iconsMaterial.LinkOutlined, { fontSize: "small" })))));
6402
+ }
6403
+
6404
+ // src/editor/blocks/heading/heading-editor.tsx
6232
6405
  function getFontFamily9(fontFamily) {
6233
6406
  switch (fontFamily) {
6234
6407
  case "MODERN_SANS":
@@ -6291,7 +6464,7 @@ function getFontSize2(level) {
6291
6464
  }
6292
6465
  }
6293
6466
  function HeadingEditor({ style, props }) {
6294
- var _a, _b, _c, _d, _e, _f, _g, _h;
6467
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
6295
6468
  const blockId = useCurrentBlockId();
6296
6469
  const selectedBlockId = useSelectedBlockId();
6297
6470
  const document2 = useDocument();
@@ -6299,6 +6472,7 @@ function HeadingEditor({ style, props }) {
6299
6472
  const level = (_a = props == null ? void 0 : props.level) != null ? _a : HeadingPropsDefaults.level;
6300
6473
  const textContent = (_b = props == null ? void 0 : props.text) != null ? _b : HeadingPropsDefaults.text;
6301
6474
  const [localText, setLocalText] = React58.useState(textContent);
6475
+ const isMarkdown = (_c = props == null ? void 0 : props.markdown) != null ? _c : false;
6302
6476
  const rootBlock = document2.root;
6303
6477
  const rootFontFamily = rootBlock && rootBlock.type === "EmailLayout" ? getFontFamily9(rootBlock.data.fontFamily) : '"Helvetica Neue", Arial, sans-serif';
6304
6478
  React58.useEffect(() => {
@@ -6306,12 +6480,12 @@ function HeadingEditor({ style, props }) {
6306
6480
  }, [textContent]);
6307
6481
  const fontFamily = getFontFamily9(style == null ? void 0 : style.fontFamily) || rootFontFamily;
6308
6482
  const hStyle = {
6309
- color: (_c = style == null ? void 0 : style.color) != null ? _c : void 0,
6310
- backgroundColor: (_d = style == null ? void 0 : style.backgroundColor) != null ? _d : void 0,
6311
- fontWeight: (_e = style == null ? void 0 : style.fontWeight) != null ? _e : "bold",
6312
- lineHeight: (_f = style == null ? void 0 : style.lineHeight) != null ? _f : void 0,
6483
+ color: (_d = style == null ? void 0 : style.color) != null ? _d : void 0,
6484
+ backgroundColor: (_e = style == null ? void 0 : style.backgroundColor) != null ? _e : void 0,
6485
+ fontWeight: (_f = style == null ? void 0 : style.fontWeight) != null ? _f : "bold",
6486
+ lineHeight: (_g = style == null ? void 0 : style.lineHeight) != null ? _g : void 0,
6313
6487
  letterSpacing: (style == null ? void 0 : style.letterSpacing) != null ? `${style.letterSpacing}px` : void 0,
6314
- textAlign: (_g = style == null ? void 0 : style.textAlign) != null ? _g : void 0,
6488
+ textAlign: (_h = style == null ? void 0 : style.textAlign) != null ? _h : void 0,
6315
6489
  margin: 0,
6316
6490
  fontFamily,
6317
6491
  fontSize: getFontSize2(level),
@@ -6325,69 +6499,92 @@ function HeadingEditor({ style, props }) {
6325
6499
  resize: "none",
6326
6500
  backgroundColor: "transparent",
6327
6501
  overflow: "hidden",
6328
- lineHeight: (_h = hStyle.lineHeight) != null ? _h : "inherit",
6502
+ lineHeight: (_i = hStyle.lineHeight) != null ? _i : "inherit",
6329
6503
  margin: 0,
6330
6504
  display: "block",
6331
6505
  width: "100%"
6332
6506
  });
6333
- const handleTextChange = (e) => {
6334
- const newText = e.target.value;
6507
+ const commitText = (newText, opts) => {
6335
6508
  setLocalText(newText);
6336
6509
  setDocument({
6337
6510
  [blockId]: {
6338
6511
  type: "Heading",
6339
6512
  data: {
6340
6513
  style,
6341
- props: __spreadProps(__spreadValues({}, props), {
6514
+ props: __spreadValues(__spreadProps(__spreadValues({}, props), {
6342
6515
  text: newText
6343
- })
6516
+ }), (opts == null ? void 0 : opts.enableMarkdown) ? { markdown: true } : {})
6344
6517
  }
6345
6518
  }
6346
6519
  });
6347
6520
  };
6348
- const adjustTextareaHeight = (element) => {
6349
- if (element) {
6350
- element.style.height = "auto";
6351
- element.style.height = `${element.scrollHeight}px`;
6352
- }
6521
+ const handleTextChange = (e) => {
6522
+ commitText(e.target.value);
6353
6523
  };
6354
- const trackFocus = (e) => {
6355
- var _a2, _b2;
6356
- const el = e.currentTarget;
6357
- setLastFocusedEditable({
6358
- blockId,
6359
- field: "text",
6360
- selectionStart: (_a2 = el.selectionStart) != null ? _a2 : el.value.length,
6361
- selectionEnd: (_b2 = el.selectionEnd) != null ? _b2 : el.value.length
6362
- });
6524
+ const displayRef = React58.useRef(null);
6525
+ const lastDisplayHeightRef = React58.useRef(0);
6526
+ React58.useLayoutEffect(() => {
6527
+ if (!isSelected && displayRef.current) {
6528
+ const h = displayRef.current.offsetHeight;
6529
+ if (h > 0) lastDisplayHeightRef.current = h;
6530
+ }
6531
+ }, [isSelected, textContent, isMarkdown, level]);
6532
+ const adjustTextareaHeight = (element) => {
6533
+ if (!element) return;
6534
+ element.style.height = "auto";
6535
+ const scrollH = element.scrollHeight;
6536
+ const floor = lastDisplayHeightRef.current;
6537
+ element.style.height = `${Math.max(scrollH, floor)}px`;
6363
6538
  };
6539
+ const { textareaRef, trackFocus, handleKeyDown, toolbarProps } = useMarkdownToolbar({
6540
+ text: localText,
6541
+ isSelected,
6542
+ commitText: (newText) => commitText(newText, { enableMarkdown: true }),
6543
+ trackSelection: (sel) => {
6544
+ setLastFocusedEditable({
6545
+ blockId,
6546
+ field: "text",
6547
+ selectionStart: sel.start,
6548
+ selectionEnd: sel.end
6549
+ });
6550
+ }
6551
+ });
6552
+ React58.useLayoutEffect(() => {
6553
+ if (textareaRef.current) adjustTextareaHeight(textareaRef.current);
6554
+ }, [localText, isSelected]);
6364
6555
  if (isSelected) {
6365
- return /* @__PURE__ */ React58__default.default.createElement(
6556
+ return /* @__PURE__ */ React58__default.default.createElement(React58__default.default.Fragment, null, /* @__PURE__ */ React58__default.default.createElement(
6366
6557
  "textarea",
6367
6558
  {
6559
+ ref: textareaRef,
6368
6560
  value: localText,
6369
6561
  onChange: handleTextChange,
6370
6562
  onFocus: trackFocus,
6371
6563
  onSelect: trackFocus,
6372
6564
  onKeyUp: trackFocus,
6565
+ onKeyDown: handleKeyDown,
6373
6566
  onClick: (e) => {
6374
6567
  e.stopPropagation();
6375
6568
  trackFocus(e);
6376
6569
  },
6377
6570
  style: textareaStyle,
6378
6571
  rows: 1,
6379
- onInput: (e) => adjustTextareaHeight(e.target),
6380
- ref: (el) => el && adjustTextareaHeight(el)
6572
+ onInput: (e) => adjustTextareaHeight(e.target)
6381
6573
  }
6382
- );
6574
+ ), /* @__PURE__ */ React58__default.default.createElement(InlineFormattingToolbar, __spreadValues({ anchorEl: textareaRef.current }, toolbarProps)));
6383
6575
  }
6576
+ const headingProps = isMarkdown ? {
6577
+ ref: displayRef,
6578
+ style: hStyle,
6579
+ dangerouslySetInnerHTML: { __html: renderInlineMarkdownString(textContent) }
6580
+ } : { ref: displayRef, style: hStyle, children: textContent };
6384
6581
  switch (level) {
6385
6582
  case "h1":
6386
- return /* @__PURE__ */ React58__default.default.createElement("h1", { style: hStyle }, textContent);
6583
+ return /* @__PURE__ */ React58__default.default.createElement("h1", __spreadValues({}, headingProps));
6387
6584
  case "h2":
6388
- return /* @__PURE__ */ React58__default.default.createElement("h2", { style: hStyle }, textContent);
6585
+ return /* @__PURE__ */ React58__default.default.createElement("h2", __spreadValues({}, headingProps));
6389
6586
  case "h3":
6390
- return /* @__PURE__ */ React58__default.default.createElement("h3", { style: hStyle }, textContent);
6587
+ return /* @__PURE__ */ React58__default.default.createElement("h3", __spreadValues({}, headingProps));
6391
6588
  }
6392
6589
  }
6393
6590
  function HtmlEditor({ style, props }) {
@@ -6795,61 +6992,86 @@ function TextEditor({ style, props }) {
6795
6992
  fontWeight: wStyle.fontWeight,
6796
6993
  textAlign: wStyle.textAlign
6797
6994
  });
6798
- const handleTextChange = (e) => {
6799
- const newText = e.target.value;
6995
+ const commitText = (newText, opts) => {
6800
6996
  setLocalText(newText);
6801
6997
  setDocument({
6802
6998
  [blockId]: {
6803
6999
  type: "Text",
6804
7000
  data: {
6805
7001
  style,
6806
- props: __spreadProps(__spreadValues({}, props), {
7002
+ props: __spreadValues(__spreadProps(__spreadValues({}, props), {
6807
7003
  text: newText
6808
- })
7004
+ }), (opts == null ? void 0 : opts.enableMarkdown) ? { markdown: true } : {})
6809
7005
  }
6810
7006
  }
6811
7007
  });
6812
7008
  };
6813
- const adjustTextareaHeight = (element) => {
6814
- if (element) {
6815
- element.style.height = "auto";
6816
- element.style.height = `${element.scrollHeight}px`;
6817
- }
7009
+ const handleTextChange = (e) => {
7010
+ commitText(e.target.value);
6818
7011
  };
6819
- const trackFocus = (e) => {
6820
- var _a2, _b2;
6821
- const el = e.currentTarget;
6822
- setLastFocusedEditable({
6823
- blockId,
6824
- field: "text",
6825
- selectionStart: (_a2 = el.selectionStart) != null ? _a2 : el.value.length,
6826
- selectionEnd: (_b2 = el.selectionEnd) != null ? _b2 : el.value.length
6827
- });
7012
+ const displayRef = React58.useRef(null);
7013
+ const lastDisplayHeightRef = React58.useRef(0);
7014
+ React58.useLayoutEffect(() => {
7015
+ if (!isSelected && displayRef.current) {
7016
+ const h = displayRef.current.offsetHeight;
7017
+ if (h > 0) lastDisplayHeightRef.current = h;
7018
+ }
7019
+ }, [isSelected, textContent, isMarkdown]);
7020
+ const adjustTextareaHeight = (element) => {
7021
+ if (!element) return;
7022
+ element.style.height = "auto";
7023
+ const scrollH = element.scrollHeight;
7024
+ const floor = lastDisplayHeightRef.current;
7025
+ element.style.height = `${Math.max(scrollH, floor)}px`;
6828
7026
  };
7027
+ const { textareaRef, trackFocus, handleKeyDown, toolbarProps } = useMarkdownToolbar({
7028
+ text: localText,
7029
+ isSelected,
7030
+ commitText: (newText) => commitText(newText, { enableMarkdown: true }),
7031
+ trackSelection: (sel) => {
7032
+ setLastFocusedEditable({
7033
+ blockId,
7034
+ field: "text",
7035
+ selectionStart: sel.start,
7036
+ selectionEnd: sel.end
7037
+ });
7038
+ }
7039
+ });
7040
+ React58.useLayoutEffect(() => {
7041
+ if (textareaRef.current) adjustTextareaHeight(textareaRef.current);
7042
+ }, [localText, isSelected]);
6829
7043
  if (isSelected) {
6830
- return /* @__PURE__ */ React58__default.default.createElement(
7044
+ return /* @__PURE__ */ React58__default.default.createElement(React58__default.default.Fragment, null, /* @__PURE__ */ React58__default.default.createElement(
6831
7045
  "textarea",
6832
7046
  {
7047
+ ref: textareaRef,
6833
7048
  value: localText,
6834
7049
  onChange: handleTextChange,
6835
7050
  onFocus: trackFocus,
6836
7051
  onSelect: trackFocus,
6837
7052
  onKeyUp: trackFocus,
7053
+ onKeyDown: handleKeyDown,
6838
7054
  onClick: (e) => {
6839
7055
  e.stopPropagation();
6840
7056
  trackFocus(e);
6841
7057
  },
6842
7058
  style: textareaStyle,
6843
7059
  rows: 1,
6844
- onInput: (e) => adjustTextareaHeight(e.target),
6845
- ref: (el) => el && adjustTextareaHeight(el)
7060
+ onInput: (e) => adjustTextareaHeight(e.target)
6846
7061
  }
6847
- );
7062
+ ), /* @__PURE__ */ React58__default.default.createElement(InlineFormattingToolbar, __spreadValues({ anchorEl: textareaRef.current }, toolbarProps)));
6848
7063
  }
6849
7064
  if (isMarkdown) {
6850
- return /* @__PURE__ */ React58__default.default.createElement(EmailMarkdown, { style: wStyle, markdown: textContent });
7065
+ return /* @__PURE__ */ React58__default.default.createElement(
7066
+ "div",
7067
+ {
7068
+ ref: displayRef,
7069
+ style: wStyle,
7070
+ dangerouslySetInnerHTML: { __html: renderMarkdownString(textContent) }
7071
+ }
7072
+ );
6851
7073
  }
6852
- return /* @__PURE__ */ React58__default.default.createElement("div", { style: wStyle }, textContent);
7074
+ return /* @__PURE__ */ React58__default.default.createElement("div", { ref: displayRef, style: wStyle }, textContent);
6853
7075
  }
6854
7076
 
6855
7077
  // src/editor/core.tsx