@marianmeres/stuic 3.111.0 → 3.112.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/components/MarkdownEditor/MarkdownEditor.svelte +99 -0
- package/dist/components/MarkdownEditor/MarkdownEditor.svelte.d.ts +17 -0
- package/dist/components/MarkdownEditor/README.md +30 -1
- package/dist/components/MarkdownEditor/_internal/max-height.d.ts +56 -0
- package/dist/components/MarkdownEditor/_internal/max-height.js +60 -0
- package/dist/components/MarkdownEditor/index.css +6 -1
- package/package.json +1 -1
|
@@ -112,6 +112,23 @@
|
|
|
112
112
|
* (any `window.prompt`-compatible sync/async function works).
|
|
113
113
|
*/
|
|
114
114
|
prompt?: PromptFn;
|
|
115
|
+
/**
|
|
116
|
+
* Caps the editing surface height so a long document scrolls *inside* the
|
|
117
|
+
* surface instead of growing the editor (which would push the toolbar out
|
|
118
|
+
* of view). `number` → pixels; `string` → any CSS length (`"40rem"`,
|
|
119
|
+
* `"60vh"`, `"min(40rem,50vh)"`). Defaults to `32rem` (themeable via
|
|
120
|
+
* `--stuic-markdown-editor-max-height`). The surface is additionally capped
|
|
121
|
+
* to the parent's available height when {@link capToParent} is on — the
|
|
122
|
+
* smaller limit wins.
|
|
123
|
+
*/
|
|
124
|
+
maxHeight?: number | string;
|
|
125
|
+
/**
|
|
126
|
+
* Also cap the editing surface to the height available in the parent
|
|
127
|
+
* container (measured at runtime), so the editor never overflows a
|
|
128
|
+
* height-constrained parent. Default `true`. Set `false` to rely solely on
|
|
129
|
+
* {@link maxHeight}.
|
|
130
|
+
*/
|
|
131
|
+
capToParent?: boolean;
|
|
115
132
|
/** Show the WYSIWYG/Source mode toggle. Default `true`. */
|
|
116
133
|
showModeToggle?: boolean;
|
|
117
134
|
/** Label for the toggle when in WYSIWYG mode (switches to source). */
|
|
@@ -165,6 +182,12 @@
|
|
|
165
182
|
} from "../../icons/index.js";
|
|
166
183
|
import InputWrap from "../Input/_internal/InputWrap.svelte";
|
|
167
184
|
import type { EditorCommands, EditorHandle, PromptFn } from "./_internal/types.js";
|
|
185
|
+
import {
|
|
186
|
+
computeParentAvailable,
|
|
187
|
+
DEFAULT_MAX_HEIGHT_VAR,
|
|
188
|
+
maxHeightToCss,
|
|
189
|
+
surfaceMaxHeight,
|
|
190
|
+
} from "./_internal/max-height.js";
|
|
168
191
|
import "./index.css";
|
|
169
192
|
|
|
170
193
|
// Default URL prompt for link/image — the native `window.prompt`. Opt into
|
|
@@ -227,6 +250,8 @@
|
|
|
227
250
|
autoSourceOnMobile = true,
|
|
228
251
|
mobileQuery = "(pointer: coarse) and (max-width: 640px)",
|
|
229
252
|
prompt = defaultPrompt,
|
|
253
|
+
maxHeight,
|
|
254
|
+
capToParent = true,
|
|
230
255
|
showModeToggle = true,
|
|
231
256
|
sourceLabel = "Source",
|
|
232
257
|
previewLabel = "Preview",
|
|
@@ -285,6 +310,78 @@
|
|
|
285
310
|
input = host;
|
|
286
311
|
});
|
|
287
312
|
|
|
313
|
+
// --- Surface max-height ---------------------------------------------------
|
|
314
|
+
// Cap the editing surface so a long document scrolls internally instead of
|
|
315
|
+
// growing the editor (which scrolls the toolbar out of view). The base cap is
|
|
316
|
+
// the `maxHeight` prop (funneled into the CSS var so the surface rule + the
|
|
317
|
+
// min() below both see it) or `32rem`; `capToParent` further caps it to the
|
|
318
|
+
// parent's measured available height. `min()` picks the smaller limit.
|
|
319
|
+
|
|
320
|
+
// Funnel an explicit `maxHeight` into the themeable CSS var on the root.
|
|
321
|
+
const maxHeightVar = $derived.by(() => {
|
|
322
|
+
const css = maxHeightToCss(maxHeight);
|
|
323
|
+
return css ? `--stuic-markdown-editor-max-height: ${css};` : undefined;
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Measured parent-available height (px), or null when capping is off / not
|
|
327
|
+
// yet measured / the parent is unconstrained — then the CSS default applies.
|
|
328
|
+
let parentAvailable = $state<number | null>(null);
|
|
329
|
+
|
|
330
|
+
// Inline surface `max-height`; undefined falls back to the CSS rule default.
|
|
331
|
+
const surfaceMaxHeightCss = $derived(
|
|
332
|
+
capToParent && parentAvailable != null
|
|
333
|
+
? surfaceMaxHeight(DEFAULT_MAX_HEIGHT_VAR, parentAvailable)
|
|
334
|
+
: undefined
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
$effect(() => {
|
|
338
|
+
if (!capToParent || !host || typeof ResizeObserver === "undefined") {
|
|
339
|
+
parentAvailable = null;
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const surface = host;
|
|
343
|
+
// Measure against the parent of the whole field (InputWrap's root), which
|
|
344
|
+
// is the consumer's container — that is the "parent" whose height we cap to.
|
|
345
|
+
const root = (surface.closest(".stuic-input") as HTMLElement | null) ?? surface;
|
|
346
|
+
const parent = root.parentElement;
|
|
347
|
+
if (!parent) {
|
|
348
|
+
parentAvailable = null;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const measure = () => {
|
|
353
|
+
const cs = getComputedStyle(parent);
|
|
354
|
+
const next = computeParentAvailable({
|
|
355
|
+
fromTop: surface.getBoundingClientRect().top,
|
|
356
|
+
parentBottom: parent.getBoundingClientRect().bottom,
|
|
357
|
+
parentBorderBottom: parseFloat(cs.borderBottomWidth) || 0,
|
|
358
|
+
parentPaddingBottom: parseFloat(cs.paddingBottom) || 0,
|
|
359
|
+
});
|
|
360
|
+
// `untrack` the read of `parentAvailable`: the initial measure() runs
|
|
361
|
+
// inside this effect, and tracking its own output would tear down and
|
|
362
|
+
// rebuild the observer on every measurement. The threshold also guards
|
|
363
|
+
// against sub-pixel ResizeObserver feedback loops.
|
|
364
|
+
const prev = untrack(() => parentAvailable);
|
|
365
|
+
if (next == null) {
|
|
366
|
+
if (prev !== null) parentAvailable = null;
|
|
367
|
+
} else if (prev == null || Math.abs(next - prev) > 1) {
|
|
368
|
+
parentAvailable = next;
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
measure();
|
|
373
|
+
const ro = new ResizeObserver(measure);
|
|
374
|
+
ro.observe(parent);
|
|
375
|
+
// Observe the field root too, so the surface re-measures when content above
|
|
376
|
+
// it reflows (label wraps, description expands, validation message appears).
|
|
377
|
+
ro.observe(root);
|
|
378
|
+
window.addEventListener("resize", measure);
|
|
379
|
+
return () => {
|
|
380
|
+
ro.disconnect();
|
|
381
|
+
window.removeEventListener("resize", measure);
|
|
382
|
+
};
|
|
383
|
+
});
|
|
384
|
+
|
|
288
385
|
// Handle to whichever backend is currently mounted (null while (re)loading).
|
|
289
386
|
let activeHandle = $state<EditorHandle | undefined>();
|
|
290
387
|
|
|
@@ -464,6 +561,7 @@
|
|
|
464
561
|
class:disabled
|
|
465
562
|
data-size={renderSize}
|
|
466
563
|
data-mode={mode}
|
|
564
|
+
style={maxHeightVar}
|
|
467
565
|
>
|
|
468
566
|
{#if toolbarItems.length || showModeToggle}
|
|
469
567
|
<div class={twMerge("stuic-markdown-editor-bar", classToolbar)}>
|
|
@@ -511,6 +609,7 @@
|
|
|
511
609
|
<div
|
|
512
610
|
bind:this={host}
|
|
513
611
|
class="stuic-markdown-editor-surface"
|
|
612
|
+
style:max-height={surfaceMaxHeightCss}
|
|
514
613
|
onfocusout={handleFocusOut}
|
|
515
614
|
role="textbox"
|
|
516
615
|
aria-multiline="true"
|
|
@@ -54,6 +54,23 @@ export interface Props extends InputWrapClassProps {
|
|
|
54
54
|
* (any `window.prompt`-compatible sync/async function works).
|
|
55
55
|
*/
|
|
56
56
|
prompt?: PromptFn;
|
|
57
|
+
/**
|
|
58
|
+
* Caps the editing surface height so a long document scrolls *inside* the
|
|
59
|
+
* surface instead of growing the editor (which would push the toolbar out
|
|
60
|
+
* of view). `number` → pixels; `string` → any CSS length (`"40rem"`,
|
|
61
|
+
* `"60vh"`, `"min(40rem,50vh)"`). Defaults to `32rem` (themeable via
|
|
62
|
+
* `--stuic-markdown-editor-max-height`). The surface is additionally capped
|
|
63
|
+
* to the parent's available height when {@link capToParent} is on — the
|
|
64
|
+
* smaller limit wins.
|
|
65
|
+
*/
|
|
66
|
+
maxHeight?: number | string;
|
|
67
|
+
/**
|
|
68
|
+
* Also cap the editing surface to the height available in the parent
|
|
69
|
+
* container (measured at runtime), so the editor never overflows a
|
|
70
|
+
* height-constrained parent. Default `true`. Set `false` to rely solely on
|
|
71
|
+
* {@link maxHeight}.
|
|
72
|
+
*/
|
|
73
|
+
capToParent?: boolean;
|
|
57
74
|
/** Show the WYSIWYG/Source mode toggle. Default `true`. */
|
|
58
75
|
showModeToggle?: boolean;
|
|
59
76
|
/** Label for the toggle when in WYSIWYG mode (switches to source). */
|
|
@@ -60,6 +60,8 @@ Bind the component instance to access:
|
|
|
60
60
|
| `validate` | `boolean \| ValidateOptions` | – | Pass a `customValidator` to validate markdown |
|
|
61
61
|
| `renderSize` | `"sm" \| "md" \| "lg"` | `"md"` | |
|
|
62
62
|
| `toolbar` | `boolean \| ToolbarItem[]` | `true` | `true` = default toolbar, `false` = none, or an ordered button list |
|
|
63
|
+
| `maxHeight` | `number \| string` | `32rem` | Surface height cap (`number` = px). Long docs scroll internally |
|
|
64
|
+
| `capToParent` | `boolean` | `true` | Also cap the surface to the parent's available height |
|
|
63
65
|
| `showModeToggle` | `boolean` | `true` | Show the WYSIWYG/source toggle |
|
|
64
66
|
| `name` | `string` | – | Hidden input name for form submission |
|
|
65
67
|
|
|
@@ -95,6 +97,33 @@ Available items: `bold`, `italic`, `heading1`, `heading2`, `heading3`, `link`,
|
|
|
95
97
|
`hardBreak`, `undo`, `redo`, and `"|"` (separator). The default layout is
|
|
96
98
|
exported as `DEFAULT_TOOLBAR`.
|
|
97
99
|
|
|
100
|
+
### Height & scrolling
|
|
101
|
+
|
|
102
|
+
A long document never grows the editor unbounded (which would scroll the toolbar
|
|
103
|
+
out of view). The editing **surface** has a finite max height and scrolls
|
|
104
|
+
internally instead:
|
|
105
|
+
|
|
106
|
+
- **Fixed cap** — `maxHeight` (`number` → px, or any CSS length string like
|
|
107
|
+
`"40rem"` / `"60vh"`), defaulting to `32rem` (themeable via
|
|
108
|
+
`--stuic-markdown-editor-max-height`).
|
|
109
|
+
- **Parent cap** — with `capToParent` on (default), the surface is _also_ capped
|
|
110
|
+
to the height available in the parent container, measured at runtime. The
|
|
111
|
+
**smaller** of the two limits wins, so the editor fills — but never overflows —
|
|
112
|
+
a height-constrained parent (e.g. a flex column, modal body, or split pane).
|
|
113
|
+
|
|
114
|
+
```svelte
|
|
115
|
+
<!-- explicit fixed cap; ignore the parent -->
|
|
116
|
+
<MarkdownEditor bind:value maxHeight="20rem" capToParent={false} />
|
|
117
|
+
|
|
118
|
+
<!-- fill a height-constrained pane, scrolling internally past its available height -->
|
|
119
|
+
<div style="height: 60vh; display: flex; flex-direction: column;">
|
|
120
|
+
<MarkdownEditor bind:value maxHeight="100vh" />
|
|
121
|
+
</div>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The minimum height (`--stuic-markdown-editor-min-height`, `12rem` for `md`) still
|
|
125
|
+
applies, so the surface never collapses below a usable editing area.
|
|
126
|
+
|
|
98
127
|
### Link / image URL prompt
|
|
99
128
|
|
|
100
129
|
By default the link and image buttons ask for a URL via the native
|
|
@@ -146,7 +175,7 @@ input/global tokens:
|
|
|
146
175
|
--stuic-markdown-editor-border-focus
|
|
147
176
|
--stuic-markdown-editor-bg
|
|
148
177
|
--stuic-markdown-editor-min-height /* default 12rem (9/16rem for sm/lg) */
|
|
149
|
-
--stuic-markdown-editor-max-height
|
|
178
|
+
--stuic-markdown-editor-max-height /* default 32rem; see "Height & scrolling" */
|
|
150
179
|
--stuic-markdown-editor-font-size
|
|
151
180
|
--stuic-markdown-editor-font-mono
|
|
152
181
|
--stuic-markdown-editor-code-bg
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the MarkdownEditor's editing-surface max-height resolution.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately framework-free (no DOM, no Svelte) so the height-capping logic is
|
|
5
|
+
* unit-testable without a browser/layout engine. `MarkdownEditor.svelte` does the
|
|
6
|
+
* actual DOM measurement and feeds the resulting numbers through these helpers.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Base cap applied to the editing surface, as a CSS expression. Reads the
|
|
10
|
+
* themeable `--stuic-markdown-editor-max-height` custom property, falling back
|
|
11
|
+
* to `32rem`. The `32rem` fallback is kept in sync with the same value in
|
|
12
|
+
* `index.css` (`.stuic-markdown-editor-surface`).
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_MAX_HEIGHT_VAR = "var(--stuic-markdown-editor-max-height, 32rem)";
|
|
15
|
+
/**
|
|
16
|
+
* Normalize a `maxHeight` prop value into a CSS length.
|
|
17
|
+
* - `number` → pixels (`240` → `"240px"`)
|
|
18
|
+
* - `string` → used verbatim (any CSS length: `"40rem"`, `"60vh"`, `"min(40rem,50vh)"`)
|
|
19
|
+
* - `null` / `undefined` / empty / non-finite → `null` (caller uses the default)
|
|
20
|
+
*/
|
|
21
|
+
export declare function maxHeightToCss(value: number | string | null | undefined): string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Combine the configured cap (`base`, a CSS length/expression) with the measured
|
|
24
|
+
* parent-available height. When a positive `parentAvailablePx` is given the
|
|
25
|
+
* surface is capped to the smaller of the two via CSS `min()`, so it never
|
|
26
|
+
* overflows a height-constrained parent. Otherwise the base cap is returned
|
|
27
|
+
* unchanged.
|
|
28
|
+
*/
|
|
29
|
+
export declare function surfaceMaxHeight(base: string, parentAvailablePx: number | null): string;
|
|
30
|
+
/** Inputs for {@link computeParentAvailable}, in CSS px (viewport coords for tops/bottoms). */
|
|
31
|
+
export interface ParentAvailableInput {
|
|
32
|
+
/** Viewport-relative top of the element we measure from (the editing surface). */
|
|
33
|
+
fromTop: number;
|
|
34
|
+
/** Viewport-relative bottom of the parent's border box (`getBoundingClientRect().bottom`). */
|
|
35
|
+
parentBottom: number;
|
|
36
|
+
/** Parent computed `border-bottom-width` in px. Default `0`. */
|
|
37
|
+
parentBorderBottom?: number;
|
|
38
|
+
/** Parent computed `padding-bottom` in px. Default `0`. */
|
|
39
|
+
parentPaddingBottom?: number;
|
|
40
|
+
/** Extra breathing room to leave below the editor, in px. Default `0`. */
|
|
41
|
+
bottomGap?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Compute the vertical space available to the editor inside its parent: the
|
|
45
|
+
* distance from `fromTop` down to the bottom of the parent's content box, less
|
|
46
|
+
* an optional `bottomGap`.
|
|
47
|
+
*
|
|
48
|
+
* Returns `null` when the result is not positive — the parent is smaller than
|
|
49
|
+
* the editor's offset (e.g. not laid out yet), so no meaningful cap can be
|
|
50
|
+
* derived and the caller should fall back to the configured base.
|
|
51
|
+
*
|
|
52
|
+
* Note: in an auto-height (content-sized) parent this naturally returns roughly
|
|
53
|
+
* the editor's own height, so `surfaceMaxHeight(base, …)` resolves to the base
|
|
54
|
+
* cap and the parent constraint only bites when the parent is genuinely smaller.
|
|
55
|
+
*/
|
|
56
|
+
export declare function computeParentAvailable(input: ParentAvailableInput): number | null;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the MarkdownEditor's editing-surface max-height resolution.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately framework-free (no DOM, no Svelte) so the height-capping logic is
|
|
5
|
+
* unit-testable without a browser/layout engine. `MarkdownEditor.svelte` does the
|
|
6
|
+
* actual DOM measurement and feeds the resulting numbers through these helpers.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Base cap applied to the editing surface, as a CSS expression. Reads the
|
|
10
|
+
* themeable `--stuic-markdown-editor-max-height` custom property, falling back
|
|
11
|
+
* to `32rem`. The `32rem` fallback is kept in sync with the same value in
|
|
12
|
+
* `index.css` (`.stuic-markdown-editor-surface`).
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_MAX_HEIGHT_VAR = "var(--stuic-markdown-editor-max-height, 32rem)";
|
|
15
|
+
/**
|
|
16
|
+
* Normalize a `maxHeight` prop value into a CSS length.
|
|
17
|
+
* - `number` → pixels (`240` → `"240px"`)
|
|
18
|
+
* - `string` → used verbatim (any CSS length: `"40rem"`, `"60vh"`, `"min(40rem,50vh)"`)
|
|
19
|
+
* - `null` / `undefined` / empty / non-finite → `null` (caller uses the default)
|
|
20
|
+
*/
|
|
21
|
+
export function maxHeightToCss(value) {
|
|
22
|
+
if (value == null)
|
|
23
|
+
return null;
|
|
24
|
+
if (typeof value === "number")
|
|
25
|
+
return Number.isFinite(value) ? `${value}px` : null;
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
return trimmed.length ? trimmed : null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Combine the configured cap (`base`, a CSS length/expression) with the measured
|
|
31
|
+
* parent-available height. When a positive `parentAvailablePx` is given the
|
|
32
|
+
* surface is capped to the smaller of the two via CSS `min()`, so it never
|
|
33
|
+
* overflows a height-constrained parent. Otherwise the base cap is returned
|
|
34
|
+
* unchanged.
|
|
35
|
+
*/
|
|
36
|
+
export function surfaceMaxHeight(base, parentAvailablePx) {
|
|
37
|
+
if (parentAvailablePx != null && parentAvailablePx > 0) {
|
|
38
|
+
return `min(${base}, ${Math.round(parentAvailablePx)}px)`;
|
|
39
|
+
}
|
|
40
|
+
return base;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Compute the vertical space available to the editor inside its parent: the
|
|
44
|
+
* distance from `fromTop` down to the bottom of the parent's content box, less
|
|
45
|
+
* an optional `bottomGap`.
|
|
46
|
+
*
|
|
47
|
+
* Returns `null` when the result is not positive — the parent is smaller than
|
|
48
|
+
* the editor's offset (e.g. not laid out yet), so no meaningful cap can be
|
|
49
|
+
* derived and the caller should fall back to the configured base.
|
|
50
|
+
*
|
|
51
|
+
* Note: in an auto-height (content-sized) parent this naturally returns roughly
|
|
52
|
+
* the editor's own height, so `surfaceMaxHeight(base, …)` resolves to the base
|
|
53
|
+
* cap and the parent constraint only bites when the parent is genuinely smaller.
|
|
54
|
+
*/
|
|
55
|
+
export function computeParentAvailable(input) {
|
|
56
|
+
const { fromTop, parentBottom, parentBorderBottom = 0, parentPaddingBottom = 0, bottomGap = 0, } = input;
|
|
57
|
+
const contentBottom = parentBottom - parentBorderBottom - parentPaddingBottom;
|
|
58
|
+
const available = contentBottom - fromTop - bottomGap;
|
|
59
|
+
return available > 0 ? Math.floor(available) : null;
|
|
60
|
+
}
|
|
@@ -157,7 +157,12 @@
|
|
|
157
157
|
flex: 1;
|
|
158
158
|
min-width: 0;
|
|
159
159
|
min-height: var(--_min-height);
|
|
160
|
-
|
|
160
|
+
/* A finite default cap so a long document scrolls INSIDE the surface instead
|
|
161
|
+
of growing the editor unbounded (which scrolls the toolbar out of view).
|
|
162
|
+
`32rem` is kept in sync with DEFAULT_MAX_HEIGHT_VAR in _internal/max-height.ts.
|
|
163
|
+
The component may narrow this further (inline `max-height`) to the parent's
|
|
164
|
+
available height — see the `maxHeight` / `capToParent` props. */
|
|
165
|
+
max-height: var(--stuic-markdown-editor-max-height, 32rem);
|
|
161
166
|
overflow: auto;
|
|
162
167
|
padding: var(--_pad-y) var(--_pad-x);
|
|
163
168
|
}
|