@duskmoon-dev/el-markdown-input 0.11.1 → 0.12.0

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/cjs/index.js CHANGED
@@ -261,6 +261,7 @@ var elementStyles = import_el_base.css`
261
261
  color: var(--md-text);
262
262
  overflow: hidden;
263
263
  height: inherit;
264
+ min-height: 12rem;
264
265
  }
265
266
 
266
267
  .editor:focus-within {
@@ -312,13 +313,18 @@ var elementStyles = import_el_base.css`
312
313
 
313
314
  /* ── Write area (render-layer + textarea overlay) ──────────────────── */
314
315
  /*
315
- * CodeMirror-style render model: .render-layer sits in normal flow and
316
- * drives the container height; the textarea is absolutely positioned on
317
- * top. No scroll sync required both layers always share the same size.
316
+ * CSS grid overlay model: both .render-layer and textarea occupy the same
317
+ * grid cell (grid-area: 1/1), making them normal-flow siblings. The
318
+ * render-layer drives the cell's height; the textarea stretches to match.
319
+ * The write-area is the scroll container — both layers scroll together
320
+ * with no JS sync required. This fixes overflow when the editor has a
321
+ * fixed height set by the consumer.
318
322
  */
319
323
  .write-area {
320
324
  position: relative;
321
- min-height: 12rem;
325
+ display: grid;
326
+ overflow-y: auto;
327
+ min-height: 0;
322
328
  flex: 1 1 auto;
323
329
  }
324
330
 
@@ -327,15 +333,13 @@ var elementStyles = import_el_base.css`
327
333
  }
328
334
 
329
335
  /*
330
- * Render layer: highlighted HTML in normal flow. Drives container height.
331
- * pointer-events: none lets clicks pass through to the textarea underneath.
336
+ * Render layer: highlighted HTML that drives the grid cell height.
337
+ * pointer-events: none lets clicks pass through to the textarea on top.
332
338
  * Font metrics MUST match the textarea exactly for pixel-aligned overlay.
333
339
  */
334
340
  .render-layer {
335
- position: relative;
336
- z-index: 1;
341
+ grid-area: 1 / 1;
337
342
  pointer-events: none;
338
- min-height: 12rem;
339
343
  font-family: ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
340
344
  font-size: 0.875rem;
341
345
  line-height: 1.6;
@@ -347,14 +351,13 @@ var elementStyles = import_el_base.css`
347
351
  }
348
352
 
349
353
  /*
350
- * Textarea: absolute overlay on top of the render layer. Transparent text
354
+ * Textarea: grid overlay on top of the render layer. Transparent text
351
355
  * lets highlighted content show through; caret-color keeps cursor visible.
352
- * overflow: hidden — the render layer drives height, not the textarea.
356
+ * overflow: hidden — the write-area is the scroll container, not textarea.
353
357
  */
354
358
  textarea {
355
- position: absolute;
356
- inset: 0;
357
- z-index: 2;
359
+ grid-area: 1 / 1;
360
+ z-index: 1;
358
361
  display: block;
359
362
  width: 100%;
360
363
  height: 100%;
@@ -379,6 +382,17 @@ var elementStyles = import_el_base.css`
379
382
  color: var(--md-text-muted);
380
383
  }
381
384
 
385
+ /*
386
+ * Selection: keep text transparent so the render layer stays visible, but
387
+ * apply a semi-transparent highlight so selected regions are clearly marked.
388
+ * Without this rule the browser's default opaque selection background covers
389
+ * the render layer, making selected text appear invisible.
390
+ */
391
+ textarea::selection {
392
+ color: transparent;
393
+ background-color: var(--md-selection-bg, color-mix(in srgb, var(--md-accent) 35%, transparent));
394
+ }
395
+
382
396
  textarea:disabled {
383
397
  cursor: not-allowed;
384
398
  opacity: 0.6;
@@ -387,8 +401,7 @@ var elementStyles = import_el_base.css`
387
401
  /* ── Preview panel ──────────────────────────────────────────────────── */
388
402
  .preview-body {
389
403
  padding: 0.75rem;
390
- min-height: 12rem;
391
- height: stretch;
404
+ min-height: 0; /* allow flex item to shrink and scroll */
392
405
  flex: 1 1 auto;
393
406
  display: flex;
394
407
  flex-direction: column;
@@ -401,6 +414,15 @@ var elementStyles = import_el_base.css`
401
414
  display: none;
402
415
  }
403
416
 
417
+ /*
418
+ * Markdown content children must not shrink below their natural height.
419
+ * Without this, flex-shrink:1 (default) compresses all children to fit
420
+ * the 329px container instead of letting overflow-y:auto scroll them.
421
+ */
422
+ .preview-body > * {
423
+ flex-shrink: 0;
424
+ }
425
+
404
426
  /* ── Preview skeleton (shown while render pipeline loads) ──────────── */
405
427
  .preview-skeleton {
406
428
  display: flex;
@@ -554,9 +576,9 @@ var elementStyles = import_el_base.css`
554
576
  .ac-dropdown {
555
577
  position: absolute;
556
578
  z-index: 100;
557
- left: 0.75rem;
558
- /* Align to bottom of the editor chrome; the editor fills 100% of :host height */
559
- bottom: calc(var(--md-status-bar-height, 2rem) + 4px);
579
+ /* top and left are set dynamically by #updateDropdown() via #getCaretCoords() */
580
+ top: 0;
581
+ left: 0;
560
582
  min-width: 16rem;
561
583
  max-width: 28rem;
562
584
  max-height: 16rem;
@@ -658,6 +680,20 @@ var elementStyles = import_el_base.css`
658
680
  white-space: nowrap;
659
681
  }
660
682
 
683
+ /* ── Resizable editor ──────────────────────────────────────────────── */
684
+ /* resize attribute mirrors the CSS resize property: vertical | horizontal | both */
685
+ :host([resize='vertical']) .editor {
686
+ resize: vertical;
687
+ }
688
+
689
+ :host([resize='horizontal']) .editor {
690
+ resize: horizontal;
691
+ }
692
+
693
+ :host([resize='both']) .editor {
694
+ resize: both;
695
+ }
696
+
661
697
  /* ── Reduced motion: disable all transitions and animations ──────── */
662
698
  @media (prefers-reduced-motion: reduce) {
663
699
  .tab-btn,
@@ -873,6 +909,14 @@ function escapeHtml2(text) {
873
909
  }
874
910
 
875
911
  // src/pairs.ts
912
+ function replaceRange(ta, from, to, text, cursorStart, cursorEnd) {
913
+ ta.setSelectionRange(from, to);
914
+ const execOk = typeof document.execCommand === "function" && document.execCommand("insertText", false, text);
915
+ if (!execOk) {
916
+ ta.value = ta.value.slice(0, from) + text + ta.value.slice(to);
917
+ }
918
+ ta.setSelectionRange(cursorStart, cursorEnd ?? cursorStart);
919
+ }
876
920
  function handlePairKey(ta, key) {
877
921
  if (key !== "`")
878
922
  return false;
@@ -881,17 +925,38 @@ function handlePairKey(ta, key) {
881
925
  const value = ta.value;
882
926
  if (start !== end) {
883
927
  const selected = value.slice(start, end);
884
- ta.value = value.slice(0, start) + "`" + selected + "`" + value.slice(end);
885
- ta.setSelectionRange(start + 1, end + 1);
928
+ replaceRange(ta, start, end, "`" + selected + "`", start + 1, end + 1);
886
929
  return true;
887
930
  }
888
931
  if (start >= 2 && value.slice(start - 2, start) === "``") {
889
- ta.value = value.slice(0, start) + "`\n\n```" + value.slice(end);
890
- ta.setSelectionRange(start + 2, start + 2);
932
+ let consumeEnd = end;
933
+ while (consumeEnd < value.length && value[consumeEnd] === "`")
934
+ consumeEnd++;
935
+ replaceRange(ta, start, consumeEnd, "`\n\n```", start + 2);
891
936
  return true;
892
937
  }
893
- ta.value = value.slice(0, start) + "``" + value.slice(end);
894
- ta.setSelectionRange(start + 1, start + 1);
938
+ replaceRange(ta, start, end, "``", start + 1);
939
+ return true;
940
+ }
941
+ var INDENT = " ";
942
+ function handleTabKey(ta, e) {
943
+ if (e.key !== "Tab")
944
+ return false;
945
+ e.preventDefault();
946
+ const { selectionStart: start, selectionEnd: end, value } = ta;
947
+ if (start === end && !e.shiftKey) {
948
+ replaceRange(ta, start, end, INDENT, start + INDENT.length);
949
+ return true;
950
+ }
951
+ const lineStart = value.lastIndexOf(`
952
+ `, start - 1) + 1;
953
+ const block = value.slice(lineStart, end);
954
+ const transformed = e.shiftKey ? block.replace(/^ {1,2}/gm, "") : block.replace(/^/gm, INDENT);
955
+ const delta = transformed.length - block.length;
956
+ const firstLineLeading = block.split(`
957
+ `)[0].match(/^ */)?.[0].length ?? 0;
958
+ const firstLineDelta = e.shiftKey ? -Math.min(2, firstLineLeading) : INDENT.length;
959
+ replaceRange(ta, lineStart, end, transformed, Math.max(lineStart, start + firstLineDelta), end + delta);
895
960
  return true;
896
961
  }
897
962
  function handleEnterKey(ta, e) {
@@ -909,15 +974,11 @@ function handleEnterKey(ta, e) {
909
974
  return false;
910
975
  e.preventDefault();
911
976
  if (result.eraseCurrentLine) {
912
- const newValue = value.slice(0, lineStart) + value.slice(pos);
913
- ta.value = newValue;
914
- ta.setSelectionRange(lineStart, lineStart);
977
+ replaceRange(ta, lineStart, pos, "", lineStart);
915
978
  } else {
916
- const newValue = value.slice(0, pos) + `
917
- ` + result.prefix + value.slice(ta.selectionEnd);
918
- const newPos = pos + 1 + result.prefix.length;
919
- ta.value = newValue;
920
- ta.setSelectionRange(newPos, newPos);
979
+ const insert = `
980
+ ` + result.prefix;
981
+ replaceRange(ta, pos, pos, insert, pos + insert.length);
921
982
  }
922
983
  return true;
923
984
  }
@@ -979,7 +1040,8 @@ class ElDmMarkdownInput extends import_el_base2.BaseElement {
979
1040
  livePreview: { type: Boolean, reflect: true, attribute: "live-preview" },
980
1041
  debounce: { type: Number, reflect: true, default: 300 },
981
1042
  katexCssUrl: { type: String, reflect: true, attribute: "katex-css-url" },
982
- mermaidSrc: { type: String, reflect: true, attribute: "mermaid-src" }
1043
+ mermaidSrc: { type: String, reflect: true, attribute: "mermaid-src" },
1044
+ resize: { type: String, reflect: true, default: "none" }
983
1045
  };
984
1046
  #internals;
985
1047
  #initialized = false;
@@ -1195,6 +1257,14 @@ class ElDmMarkdownInput extends import_el_base2.BaseElement {
1195
1257
  this.#scheduleHighlight();
1196
1258
  return;
1197
1259
  }
1260
+ if (e.key === "Tab" && !e.ctrlKey && !e.metaKey) {
1261
+ if (handleTabKey(ta, e)) {
1262
+ this.#syncFormValue();
1263
+ this.emit("change", { value: ta.value });
1264
+ this.#scheduleHighlight();
1265
+ }
1266
+ return;
1267
+ }
1198
1268
  if (e.key === "Enter" && !e.ctrlKey && !e.metaKey && !e.altKey) {
1199
1269
  if (handleEnterKey(ta, e)) {
1200
1270
  this.#syncFormValue();
@@ -1565,6 +1635,57 @@ class ElDmMarkdownInput extends import_el_base2.BaseElement {
1565
1635
  } else {
1566
1636
  this.#textarea?.removeAttribute("aria-activedescendant");
1567
1637
  }
1638
+ const coords = this.#getCaretCoords();
1639
+ if (coords) {
1640
+ this.#acDropdown.style.top = `${coords.top}px`;
1641
+ this.#acDropdown.style.left = `${coords.left}px`;
1642
+ }
1643
+ }
1644
+ #getCaretCoords() {
1645
+ const ta = this.#textarea;
1646
+ if (!ta)
1647
+ return null;
1648
+ const pos = ta.selectionStart ?? 0;
1649
+ const cs = getComputedStyle(ta);
1650
+ const taRect = ta.getBoundingClientRect();
1651
+ const mirror = document.createElement("div");
1652
+ Object.assign(mirror.style, {
1653
+ position: "fixed",
1654
+ visibility: "hidden",
1655
+ pointerEvents: "none",
1656
+ top: `${taRect.top}px`,
1657
+ left: `${taRect.left}px`,
1658
+ width: `${taRect.width}px`,
1659
+ font: cs.font,
1660
+ letterSpacing: cs.letterSpacing,
1661
+ paddingTop: cs.paddingTop,
1662
+ paddingRight: cs.paddingRight,
1663
+ paddingBottom: cs.paddingBottom,
1664
+ paddingLeft: cs.paddingLeft,
1665
+ borderTopWidth: cs.borderTopWidth,
1666
+ borderRightWidth: cs.borderRightWidth,
1667
+ borderBottomWidth: cs.borderBottomWidth,
1668
+ borderLeftWidth: cs.borderLeftWidth,
1669
+ boxSizing: cs.boxSizing,
1670
+ whiteSpace: "pre-wrap",
1671
+ wordBreak: "break-word",
1672
+ overflowWrap: cs.overflowWrap,
1673
+ overflow: "hidden"
1674
+ });
1675
+ const before = document.createTextNode(ta.value.substring(0, pos));
1676
+ const marker = document.createElement("span");
1677
+ marker.textContent = "​";
1678
+ mirror.appendChild(before);
1679
+ mirror.appendChild(marker);
1680
+ document.body.appendChild(mirror);
1681
+ const markerRect = marker.getBoundingClientRect();
1682
+ document.body.removeChild(mirror);
1683
+ const hostRect = this.getBoundingClientRect();
1684
+ const lineHeight = parseFloat(cs.lineHeight) || 20;
1685
+ return {
1686
+ top: markerRect.top - hostRect.top - ta.scrollTop + lineHeight,
1687
+ left: Math.max(0, markerRect.left - hostRect.left)
1688
+ };
1568
1689
  }
1569
1690
  #scheduleStatusUpdate() {
1570
1691
  if (this.#statusTimer !== null)
@@ -1653,5 +1774,5 @@ var MarkdownInputHook = {
1653
1774
  }
1654
1775
  };
1655
1776
 
1656
- //# debugId=6EA563DA00732E0C64756E2164756E21
1777
+ //# debugId=5C9887BADB0DFDB864756E2164756E21
1657
1778
  //# sourceMappingURL=index.js.map