@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/esm/index.js CHANGED
@@ -236,6 +236,7 @@ var elementStyles = css`
236
236
  color: var(--md-text);
237
237
  overflow: hidden;
238
238
  height: inherit;
239
+ min-height: 12rem;
239
240
  }
240
241
 
241
242
  .editor:focus-within {
@@ -287,13 +288,18 @@ var elementStyles = css`
287
288
 
288
289
  /* ── Write area (render-layer + textarea overlay) ──────────────────── */
289
290
  /*
290
- * CodeMirror-style render model: .render-layer sits in normal flow and
291
- * drives the container height; the textarea is absolutely positioned on
292
- * top. No scroll sync required both layers always share the same size.
291
+ * CSS grid overlay model: both .render-layer and textarea occupy the same
292
+ * grid cell (grid-area: 1/1), making them normal-flow siblings. The
293
+ * render-layer drives the cell's height; the textarea stretches to match.
294
+ * The write-area is the scroll container — both layers scroll together
295
+ * with no JS sync required. This fixes overflow when the editor has a
296
+ * fixed height set by the consumer.
293
297
  */
294
298
  .write-area {
295
299
  position: relative;
296
- min-height: 12rem;
300
+ display: grid;
301
+ overflow-y: auto;
302
+ min-height: 0;
297
303
  flex: 1 1 auto;
298
304
  }
299
305
 
@@ -302,15 +308,13 @@ var elementStyles = css`
302
308
  }
303
309
 
304
310
  /*
305
- * Render layer: highlighted HTML in normal flow. Drives container height.
306
- * pointer-events: none lets clicks pass through to the textarea underneath.
311
+ * Render layer: highlighted HTML that drives the grid cell height.
312
+ * pointer-events: none lets clicks pass through to the textarea on top.
307
313
  * Font metrics MUST match the textarea exactly for pixel-aligned overlay.
308
314
  */
309
315
  .render-layer {
310
- position: relative;
311
- z-index: 1;
316
+ grid-area: 1 / 1;
312
317
  pointer-events: none;
313
- min-height: 12rem;
314
318
  font-family: ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
315
319
  font-size: 0.875rem;
316
320
  line-height: 1.6;
@@ -322,14 +326,13 @@ var elementStyles = css`
322
326
  }
323
327
 
324
328
  /*
325
- * Textarea: absolute overlay on top of the render layer. Transparent text
329
+ * Textarea: grid overlay on top of the render layer. Transparent text
326
330
  * lets highlighted content show through; caret-color keeps cursor visible.
327
- * overflow: hidden — the render layer drives height, not the textarea.
331
+ * overflow: hidden — the write-area is the scroll container, not textarea.
328
332
  */
329
333
  textarea {
330
- position: absolute;
331
- inset: 0;
332
- z-index: 2;
334
+ grid-area: 1 / 1;
335
+ z-index: 1;
333
336
  display: block;
334
337
  width: 100%;
335
338
  height: 100%;
@@ -354,6 +357,17 @@ var elementStyles = css`
354
357
  color: var(--md-text-muted);
355
358
  }
356
359
 
360
+ /*
361
+ * Selection: keep text transparent so the render layer stays visible, but
362
+ * apply a semi-transparent highlight so selected regions are clearly marked.
363
+ * Without this rule the browser's default opaque selection background covers
364
+ * the render layer, making selected text appear invisible.
365
+ */
366
+ textarea::selection {
367
+ color: transparent;
368
+ background-color: var(--md-selection-bg, color-mix(in srgb, var(--md-accent) 35%, transparent));
369
+ }
370
+
357
371
  textarea:disabled {
358
372
  cursor: not-allowed;
359
373
  opacity: 0.6;
@@ -362,8 +376,7 @@ var elementStyles = css`
362
376
  /* ── Preview panel ──────────────────────────────────────────────────── */
363
377
  .preview-body {
364
378
  padding: 0.75rem;
365
- min-height: 12rem;
366
- height: stretch;
379
+ min-height: 0; /* allow flex item to shrink and scroll */
367
380
  flex: 1 1 auto;
368
381
  display: flex;
369
382
  flex-direction: column;
@@ -376,6 +389,15 @@ var elementStyles = css`
376
389
  display: none;
377
390
  }
378
391
 
392
+ /*
393
+ * Markdown content children must not shrink below their natural height.
394
+ * Without this, flex-shrink:1 (default) compresses all children to fit
395
+ * the 329px container instead of letting overflow-y:auto scroll them.
396
+ */
397
+ .preview-body > * {
398
+ flex-shrink: 0;
399
+ }
400
+
379
401
  /* ── Preview skeleton (shown while render pipeline loads) ──────────── */
380
402
  .preview-skeleton {
381
403
  display: flex;
@@ -529,9 +551,9 @@ var elementStyles = css`
529
551
  .ac-dropdown {
530
552
  position: absolute;
531
553
  z-index: 100;
532
- left: 0.75rem;
533
- /* Align to bottom of the editor chrome; the editor fills 100% of :host height */
534
- bottom: calc(var(--md-status-bar-height, 2rem) + 4px);
554
+ /* top and left are set dynamically by #updateDropdown() via #getCaretCoords() */
555
+ top: 0;
556
+ left: 0;
535
557
  min-width: 16rem;
536
558
  max-width: 28rem;
537
559
  max-height: 16rem;
@@ -633,6 +655,20 @@ var elementStyles = css`
633
655
  white-space: nowrap;
634
656
  }
635
657
 
658
+ /* ── Resizable editor ──────────────────────────────────────────────── */
659
+ /* resize attribute mirrors the CSS resize property: vertical | horizontal | both */
660
+ :host([resize='vertical']) .editor {
661
+ resize: vertical;
662
+ }
663
+
664
+ :host([resize='horizontal']) .editor {
665
+ resize: horizontal;
666
+ }
667
+
668
+ :host([resize='both']) .editor {
669
+ resize: both;
670
+ }
671
+
636
672
  /* ── Reduced motion: disable all transitions and animations ──────── */
637
673
  @media (prefers-reduced-motion: reduce) {
638
674
  .tab-btn,
@@ -848,6 +884,14 @@ function escapeHtml2(text) {
848
884
  }
849
885
 
850
886
  // src/pairs.ts
887
+ function replaceRange(ta, from, to, text, cursorStart, cursorEnd) {
888
+ ta.setSelectionRange(from, to);
889
+ const execOk = typeof document.execCommand === "function" && document.execCommand("insertText", false, text);
890
+ if (!execOk) {
891
+ ta.value = ta.value.slice(0, from) + text + ta.value.slice(to);
892
+ }
893
+ ta.setSelectionRange(cursorStart, cursorEnd ?? cursorStart);
894
+ }
851
895
  function handlePairKey(ta, key) {
852
896
  if (key !== "`")
853
897
  return false;
@@ -856,17 +900,38 @@ function handlePairKey(ta, key) {
856
900
  const value = ta.value;
857
901
  if (start !== end) {
858
902
  const selected = value.slice(start, end);
859
- ta.value = value.slice(0, start) + "`" + selected + "`" + value.slice(end);
860
- ta.setSelectionRange(start + 1, end + 1);
903
+ replaceRange(ta, start, end, "`" + selected + "`", start + 1, end + 1);
861
904
  return true;
862
905
  }
863
906
  if (start >= 2 && value.slice(start - 2, start) === "``") {
864
- ta.value = value.slice(0, start) + "`\n\n```" + value.slice(end);
865
- ta.setSelectionRange(start + 2, start + 2);
907
+ let consumeEnd = end;
908
+ while (consumeEnd < value.length && value[consumeEnd] === "`")
909
+ consumeEnd++;
910
+ replaceRange(ta, start, consumeEnd, "`\n\n```", start + 2);
866
911
  return true;
867
912
  }
868
- ta.value = value.slice(0, start) + "``" + value.slice(end);
869
- ta.setSelectionRange(start + 1, start + 1);
913
+ replaceRange(ta, start, end, "``", start + 1);
914
+ return true;
915
+ }
916
+ var INDENT = " ";
917
+ function handleTabKey(ta, e) {
918
+ if (e.key !== "Tab")
919
+ return false;
920
+ e.preventDefault();
921
+ const { selectionStart: start, selectionEnd: end, value } = ta;
922
+ if (start === end && !e.shiftKey) {
923
+ replaceRange(ta, start, end, INDENT, start + INDENT.length);
924
+ return true;
925
+ }
926
+ const lineStart = value.lastIndexOf(`
927
+ `, start - 1) + 1;
928
+ const block = value.slice(lineStart, end);
929
+ const transformed = e.shiftKey ? block.replace(/^ {1,2}/gm, "") : block.replace(/^/gm, INDENT);
930
+ const delta = transformed.length - block.length;
931
+ const firstLineLeading = block.split(`
932
+ `)[0].match(/^ */)?.[0].length ?? 0;
933
+ const firstLineDelta = e.shiftKey ? -Math.min(2, firstLineLeading) : INDENT.length;
934
+ replaceRange(ta, lineStart, end, transformed, Math.max(lineStart, start + firstLineDelta), end + delta);
870
935
  return true;
871
936
  }
872
937
  function handleEnterKey(ta, e) {
@@ -884,15 +949,11 @@ function handleEnterKey(ta, e) {
884
949
  return false;
885
950
  e.preventDefault();
886
951
  if (result.eraseCurrentLine) {
887
- const newValue = value.slice(0, lineStart) + value.slice(pos);
888
- ta.value = newValue;
889
- ta.setSelectionRange(lineStart, lineStart);
952
+ replaceRange(ta, lineStart, pos, "", lineStart);
890
953
  } else {
891
- const newValue = value.slice(0, pos) + `
892
- ` + result.prefix + value.slice(ta.selectionEnd);
893
- const newPos = pos + 1 + result.prefix.length;
894
- ta.value = newValue;
895
- ta.setSelectionRange(newPos, newPos);
954
+ const insert = `
955
+ ` + result.prefix;
956
+ replaceRange(ta, pos, pos, insert, pos + insert.length);
896
957
  }
897
958
  return true;
898
959
  }
@@ -954,7 +1015,8 @@ class ElDmMarkdownInput extends BaseElement {
954
1015
  livePreview: { type: Boolean, reflect: true, attribute: "live-preview" },
955
1016
  debounce: { type: Number, reflect: true, default: 300 },
956
1017
  katexCssUrl: { type: String, reflect: true, attribute: "katex-css-url" },
957
- mermaidSrc: { type: String, reflect: true, attribute: "mermaid-src" }
1018
+ mermaidSrc: { type: String, reflect: true, attribute: "mermaid-src" },
1019
+ resize: { type: String, reflect: true, default: "none" }
958
1020
  };
959
1021
  #internals;
960
1022
  #initialized = false;
@@ -1170,6 +1232,14 @@ class ElDmMarkdownInput extends BaseElement {
1170
1232
  this.#scheduleHighlight();
1171
1233
  return;
1172
1234
  }
1235
+ if (e.key === "Tab" && !e.ctrlKey && !e.metaKey) {
1236
+ if (handleTabKey(ta, e)) {
1237
+ this.#syncFormValue();
1238
+ this.emit("change", { value: ta.value });
1239
+ this.#scheduleHighlight();
1240
+ }
1241
+ return;
1242
+ }
1173
1243
  if (e.key === "Enter" && !e.ctrlKey && !e.metaKey && !e.altKey) {
1174
1244
  if (handleEnterKey(ta, e)) {
1175
1245
  this.#syncFormValue();
@@ -1540,6 +1610,57 @@ class ElDmMarkdownInput extends BaseElement {
1540
1610
  } else {
1541
1611
  this.#textarea?.removeAttribute("aria-activedescendant");
1542
1612
  }
1613
+ const coords = this.#getCaretCoords();
1614
+ if (coords) {
1615
+ this.#acDropdown.style.top = `${coords.top}px`;
1616
+ this.#acDropdown.style.left = `${coords.left}px`;
1617
+ }
1618
+ }
1619
+ #getCaretCoords() {
1620
+ const ta = this.#textarea;
1621
+ if (!ta)
1622
+ return null;
1623
+ const pos = ta.selectionStart ?? 0;
1624
+ const cs = getComputedStyle(ta);
1625
+ const taRect = ta.getBoundingClientRect();
1626
+ const mirror = document.createElement("div");
1627
+ Object.assign(mirror.style, {
1628
+ position: "fixed",
1629
+ visibility: "hidden",
1630
+ pointerEvents: "none",
1631
+ top: `${taRect.top}px`,
1632
+ left: `${taRect.left}px`,
1633
+ width: `${taRect.width}px`,
1634
+ font: cs.font,
1635
+ letterSpacing: cs.letterSpacing,
1636
+ paddingTop: cs.paddingTop,
1637
+ paddingRight: cs.paddingRight,
1638
+ paddingBottom: cs.paddingBottom,
1639
+ paddingLeft: cs.paddingLeft,
1640
+ borderTopWidth: cs.borderTopWidth,
1641
+ borderRightWidth: cs.borderRightWidth,
1642
+ borderBottomWidth: cs.borderBottomWidth,
1643
+ borderLeftWidth: cs.borderLeftWidth,
1644
+ boxSizing: cs.boxSizing,
1645
+ whiteSpace: "pre-wrap",
1646
+ wordBreak: "break-word",
1647
+ overflowWrap: cs.overflowWrap,
1648
+ overflow: "hidden"
1649
+ });
1650
+ const before = document.createTextNode(ta.value.substring(0, pos));
1651
+ const marker = document.createElement("span");
1652
+ marker.textContent = "​";
1653
+ mirror.appendChild(before);
1654
+ mirror.appendChild(marker);
1655
+ document.body.appendChild(mirror);
1656
+ const markerRect = marker.getBoundingClientRect();
1657
+ document.body.removeChild(mirror);
1658
+ const hostRect = this.getBoundingClientRect();
1659
+ const lineHeight = parseFloat(cs.lineHeight) || 20;
1660
+ return {
1661
+ top: markerRect.top - hostRect.top - ta.scrollTop + lineHeight,
1662
+ left: Math.max(0, markerRect.left - hostRect.left)
1663
+ };
1543
1664
  }
1544
1665
  #scheduleStatusUpdate() {
1545
1666
  if (this.#statusTimer !== null)
@@ -1633,5 +1754,5 @@ export {
1633
1754
  ElDmMarkdownInput
1634
1755
  };
1635
1756
 
1636
- //# debugId=5F508B6BDB08E0B664756E2164756E21
1757
+ //# debugId=97DFE6DBE61B280764756E2164756E21
1637
1758
  //# sourceMappingURL=index.js.map