@duskmoon-dev/el-markdown-input 0.11.2 → 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.
@@ -252,6 +252,7 @@ var elementStyles = import_el_base.css`
252
252
  color: var(--md-text);
253
253
  overflow: hidden;
254
254
  height: inherit;
255
+ min-height: 12rem;
255
256
  }
256
257
 
257
258
  .editor:focus-within {
@@ -303,13 +304,18 @@ var elementStyles = import_el_base.css`
303
304
 
304
305
  /* ── Write area (render-layer + textarea overlay) ──────────────────── */
305
306
  /*
306
- * CodeMirror-style render model: .render-layer sits in normal flow and
307
- * drives the container height; the textarea is absolutely positioned on
308
- * top. No scroll sync required both layers always share the same size.
307
+ * CSS grid overlay model: both .render-layer and textarea occupy the same
308
+ * grid cell (grid-area: 1/1), making them normal-flow siblings. The
309
+ * render-layer drives the cell's height; the textarea stretches to match.
310
+ * The write-area is the scroll container — both layers scroll together
311
+ * with no JS sync required. This fixes overflow when the editor has a
312
+ * fixed height set by the consumer.
309
313
  */
310
314
  .write-area {
311
315
  position: relative;
312
- min-height: 12rem;
316
+ display: grid;
317
+ overflow-y: auto;
318
+ min-height: 0;
313
319
  flex: 1 1 auto;
314
320
  }
315
321
 
@@ -318,15 +324,13 @@ var elementStyles = import_el_base.css`
318
324
  }
319
325
 
320
326
  /*
321
- * Render layer: highlighted HTML in normal flow. Drives container height.
322
- * pointer-events: none lets clicks pass through to the textarea underneath.
327
+ * Render layer: highlighted HTML that drives the grid cell height.
328
+ * pointer-events: none lets clicks pass through to the textarea on top.
323
329
  * Font metrics MUST match the textarea exactly for pixel-aligned overlay.
324
330
  */
325
331
  .render-layer {
326
- position: relative;
327
- z-index: 1;
332
+ grid-area: 1 / 1;
328
333
  pointer-events: none;
329
- min-height: 12rem;
330
334
  font-family: ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
331
335
  font-size: 0.875rem;
332
336
  line-height: 1.6;
@@ -338,14 +342,13 @@ var elementStyles = import_el_base.css`
338
342
  }
339
343
 
340
344
  /*
341
- * Textarea: absolute overlay on top of the render layer. Transparent text
345
+ * Textarea: grid overlay on top of the render layer. Transparent text
342
346
  * lets highlighted content show through; caret-color keeps cursor visible.
343
- * overflow: hidden — the render layer drives height, not the textarea.
347
+ * overflow: hidden — the write-area is the scroll container, not textarea.
344
348
  */
345
349
  textarea {
346
- position: absolute;
347
- inset: 0;
348
- z-index: 2;
350
+ grid-area: 1 / 1;
351
+ z-index: 1;
349
352
  display: block;
350
353
  width: 100%;
351
354
  height: 100%;
@@ -370,6 +373,17 @@ var elementStyles = import_el_base.css`
370
373
  color: var(--md-text-muted);
371
374
  }
372
375
 
376
+ /*
377
+ * Selection: keep text transparent so the render layer stays visible, but
378
+ * apply a semi-transparent highlight so selected regions are clearly marked.
379
+ * Without this rule the browser's default opaque selection background covers
380
+ * the render layer, making selected text appear invisible.
381
+ */
382
+ textarea::selection {
383
+ color: transparent;
384
+ background-color: var(--md-selection-bg, color-mix(in srgb, var(--md-accent) 35%, transparent));
385
+ }
386
+
373
387
  textarea:disabled {
374
388
  cursor: not-allowed;
375
389
  opacity: 0.6;
@@ -378,8 +392,7 @@ var elementStyles = import_el_base.css`
378
392
  /* ── Preview panel ──────────────────────────────────────────────────── */
379
393
  .preview-body {
380
394
  padding: 0.75rem;
381
- min-height: 12rem;
382
- height: stretch;
395
+ min-height: 0; /* allow flex item to shrink and scroll */
383
396
  flex: 1 1 auto;
384
397
  display: flex;
385
398
  flex-direction: column;
@@ -392,6 +405,15 @@ var elementStyles = import_el_base.css`
392
405
  display: none;
393
406
  }
394
407
 
408
+ /*
409
+ * Markdown content children must not shrink below their natural height.
410
+ * Without this, flex-shrink:1 (default) compresses all children to fit
411
+ * the 329px container instead of letting overflow-y:auto scroll them.
412
+ */
413
+ .preview-body > * {
414
+ flex-shrink: 0;
415
+ }
416
+
395
417
  /* ── Preview skeleton (shown while render pipeline loads) ──────────── */
396
418
  .preview-skeleton {
397
419
  display: flex;
@@ -545,9 +567,9 @@ var elementStyles = import_el_base.css`
545
567
  .ac-dropdown {
546
568
  position: absolute;
547
569
  z-index: 100;
548
- left: 0.75rem;
549
- /* Align to bottom of the editor chrome; the editor fills 100% of :host height */
550
- bottom: calc(var(--md-status-bar-height, 2rem) + 4px);
570
+ /* top and left are set dynamically by #updateDropdown() via #getCaretCoords() */
571
+ top: 0;
572
+ left: 0;
551
573
  min-width: 16rem;
552
574
  max-width: 28rem;
553
575
  max-height: 16rem;
@@ -649,6 +671,20 @@ var elementStyles = import_el_base.css`
649
671
  white-space: nowrap;
650
672
  }
651
673
 
674
+ /* ── Resizable editor ──────────────────────────────────────────────── */
675
+ /* resize attribute mirrors the CSS resize property: vertical | horizontal | both */
676
+ :host([resize='vertical']) .editor {
677
+ resize: vertical;
678
+ }
679
+
680
+ :host([resize='horizontal']) .editor {
681
+ resize: horizontal;
682
+ }
683
+
684
+ :host([resize='both']) .editor {
685
+ resize: both;
686
+ }
687
+
652
688
  /* ── Reduced motion: disable all transitions and animations ──────── */
653
689
  @media (prefers-reduced-motion: reduce) {
654
690
  .tab-btn,
@@ -864,6 +900,14 @@ function escapeHtml2(text) {
864
900
  }
865
901
 
866
902
  // src/pairs.ts
903
+ function replaceRange(ta, from, to, text, cursorStart, cursorEnd) {
904
+ ta.setSelectionRange(from, to);
905
+ const execOk = typeof document.execCommand === "function" && document.execCommand("insertText", false, text);
906
+ if (!execOk) {
907
+ ta.value = ta.value.slice(0, from) + text + ta.value.slice(to);
908
+ }
909
+ ta.setSelectionRange(cursorStart, cursorEnd ?? cursorStart);
910
+ }
867
911
  function handlePairKey(ta, key) {
868
912
  if (key !== "`")
869
913
  return false;
@@ -872,17 +916,38 @@ function handlePairKey(ta, key) {
872
916
  const value = ta.value;
873
917
  if (start !== end) {
874
918
  const selected = value.slice(start, end);
875
- ta.value = value.slice(0, start) + "`" + selected + "`" + value.slice(end);
876
- ta.setSelectionRange(start + 1, end + 1);
919
+ replaceRange(ta, start, end, "`" + selected + "`", start + 1, end + 1);
877
920
  return true;
878
921
  }
879
922
  if (start >= 2 && value.slice(start - 2, start) === "``") {
880
- ta.value = value.slice(0, start) + "`\n\n```" + value.slice(end);
881
- ta.setSelectionRange(start + 2, start + 2);
923
+ let consumeEnd = end;
924
+ while (consumeEnd < value.length && value[consumeEnd] === "`")
925
+ consumeEnd++;
926
+ replaceRange(ta, start, consumeEnd, "`\n\n```", start + 2);
882
927
  return true;
883
928
  }
884
- ta.value = value.slice(0, start) + "``" + value.slice(end);
885
- ta.setSelectionRange(start + 1, start + 1);
929
+ replaceRange(ta, start, end, "``", start + 1);
930
+ return true;
931
+ }
932
+ var INDENT = " ";
933
+ function handleTabKey(ta, e) {
934
+ if (e.key !== "Tab")
935
+ return false;
936
+ e.preventDefault();
937
+ const { selectionStart: start, selectionEnd: end, value } = ta;
938
+ if (start === end && !e.shiftKey) {
939
+ replaceRange(ta, start, end, INDENT, start + INDENT.length);
940
+ return true;
941
+ }
942
+ const lineStart = value.lastIndexOf(`
943
+ `, start - 1) + 1;
944
+ const block = value.slice(lineStart, end);
945
+ const transformed = e.shiftKey ? block.replace(/^ {1,2}/gm, "") : block.replace(/^/gm, INDENT);
946
+ const delta = transformed.length - block.length;
947
+ const firstLineLeading = block.split(`
948
+ `)[0].match(/^ */)?.[0].length ?? 0;
949
+ const firstLineDelta = e.shiftKey ? -Math.min(2, firstLineLeading) : INDENT.length;
950
+ replaceRange(ta, lineStart, end, transformed, Math.max(lineStart, start + firstLineDelta), end + delta);
886
951
  return true;
887
952
  }
888
953
  function handleEnterKey(ta, e) {
@@ -900,15 +965,11 @@ function handleEnterKey(ta, e) {
900
965
  return false;
901
966
  e.preventDefault();
902
967
  if (result.eraseCurrentLine) {
903
- const newValue = value.slice(0, lineStart) + value.slice(pos);
904
- ta.value = newValue;
905
- ta.setSelectionRange(lineStart, lineStart);
968
+ replaceRange(ta, lineStart, pos, "", lineStart);
906
969
  } else {
907
- const newValue = value.slice(0, pos) + `
908
- ` + result.prefix + value.slice(ta.selectionEnd);
909
- const newPos = pos + 1 + result.prefix.length;
910
- ta.value = newValue;
911
- ta.setSelectionRange(newPos, newPos);
970
+ const insert = `
971
+ ` + result.prefix;
972
+ replaceRange(ta, pos, pos, insert, pos + insert.length);
912
973
  }
913
974
  return true;
914
975
  }
@@ -970,7 +1031,8 @@ class ElDmMarkdownInput extends import_el_base2.BaseElement {
970
1031
  livePreview: { type: Boolean, reflect: true, attribute: "live-preview" },
971
1032
  debounce: { type: Number, reflect: true, default: 300 },
972
1033
  katexCssUrl: { type: String, reflect: true, attribute: "katex-css-url" },
973
- mermaidSrc: { type: String, reflect: true, attribute: "mermaid-src" }
1034
+ mermaidSrc: { type: String, reflect: true, attribute: "mermaid-src" },
1035
+ resize: { type: String, reflect: true, default: "none" }
974
1036
  };
975
1037
  #internals;
976
1038
  #initialized = false;
@@ -1186,6 +1248,14 @@ class ElDmMarkdownInput extends import_el_base2.BaseElement {
1186
1248
  this.#scheduleHighlight();
1187
1249
  return;
1188
1250
  }
1251
+ if (e.key === "Tab" && !e.ctrlKey && !e.metaKey) {
1252
+ if (handleTabKey(ta, e)) {
1253
+ this.#syncFormValue();
1254
+ this.emit("change", { value: ta.value });
1255
+ this.#scheduleHighlight();
1256
+ }
1257
+ return;
1258
+ }
1189
1259
  if (e.key === "Enter" && !e.ctrlKey && !e.metaKey && !e.altKey) {
1190
1260
  if (handleEnterKey(ta, e)) {
1191
1261
  this.#syncFormValue();
@@ -1556,6 +1626,57 @@ class ElDmMarkdownInput extends import_el_base2.BaseElement {
1556
1626
  } else {
1557
1627
  this.#textarea?.removeAttribute("aria-activedescendant");
1558
1628
  }
1629
+ const coords = this.#getCaretCoords();
1630
+ if (coords) {
1631
+ this.#acDropdown.style.top = `${coords.top}px`;
1632
+ this.#acDropdown.style.left = `${coords.left}px`;
1633
+ }
1634
+ }
1635
+ #getCaretCoords() {
1636
+ const ta = this.#textarea;
1637
+ if (!ta)
1638
+ return null;
1639
+ const pos = ta.selectionStart ?? 0;
1640
+ const cs = getComputedStyle(ta);
1641
+ const taRect = ta.getBoundingClientRect();
1642
+ const mirror = document.createElement("div");
1643
+ Object.assign(mirror.style, {
1644
+ position: "fixed",
1645
+ visibility: "hidden",
1646
+ pointerEvents: "none",
1647
+ top: `${taRect.top}px`,
1648
+ left: `${taRect.left}px`,
1649
+ width: `${taRect.width}px`,
1650
+ font: cs.font,
1651
+ letterSpacing: cs.letterSpacing,
1652
+ paddingTop: cs.paddingTop,
1653
+ paddingRight: cs.paddingRight,
1654
+ paddingBottom: cs.paddingBottom,
1655
+ paddingLeft: cs.paddingLeft,
1656
+ borderTopWidth: cs.borderTopWidth,
1657
+ borderRightWidth: cs.borderRightWidth,
1658
+ borderBottomWidth: cs.borderBottomWidth,
1659
+ borderLeftWidth: cs.borderLeftWidth,
1660
+ boxSizing: cs.boxSizing,
1661
+ whiteSpace: "pre-wrap",
1662
+ wordBreak: "break-word",
1663
+ overflowWrap: cs.overflowWrap,
1664
+ overflow: "hidden"
1665
+ });
1666
+ const before = document.createTextNode(ta.value.substring(0, pos));
1667
+ const marker = document.createElement("span");
1668
+ marker.textContent = "​";
1669
+ mirror.appendChild(before);
1670
+ mirror.appendChild(marker);
1671
+ document.body.appendChild(mirror);
1672
+ const markerRect = marker.getBoundingClientRect();
1673
+ document.body.removeChild(mirror);
1674
+ const hostRect = this.getBoundingClientRect();
1675
+ const lineHeight = parseFloat(cs.lineHeight) || 20;
1676
+ return {
1677
+ top: markerRect.top - hostRect.top - ta.scrollTop + lineHeight,
1678
+ left: Math.max(0, markerRect.left - hostRect.left)
1679
+ };
1559
1680
  }
1560
1681
  #scheduleStatusUpdate() {
1561
1682
  if (this.#statusTimer !== null)
@@ -1624,5 +1745,5 @@ if (!customElements.get("el-dm-markdown-input")) {
1624
1745
  customElements.define("el-dm-markdown-input", ElDmMarkdownInput);
1625
1746
  }
1626
1747
 
1627
- //# debugId=CBBAC14DC8E0779464756E2164756E21
1748
+ //# debugId=A41B9FAB725BC28A64756E2164756E21
1628
1749
  //# sourceMappingURL=register.js.map