This commit is contained in:
eewing
2026-02-17 14:10:16 -06:00
parent 2bca5834c5
commit cf73cd3b4c
11246 changed files with 1690552 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
export * from "./textarea-autosize.svelte.js";
+1
View File
@@ -0,0 +1 @@
export * from "./textarea-autosize.svelte.js";
@@ -0,0 +1,51 @@
import type { Getter, MaybeGetter } from "../../internal/types.js";
export interface TextareaAutosizeOptions {
/**
* The textarea element to autosize.
*/
element: MaybeGetter<HTMLElement | undefined>;
/**
* The current text value of the textarea.
* Should be reactive.
*/
input: Getter<string>;
/**
* Callback triggered whenever the textarea resizes.
*/
onResize?: () => void;
/**
* Style property to update during resize. Defaults to `"height"`.
* Use `"minHeight"` to allow natural expansion without shrinking.
*
* @default "height"
*/
styleProp?: "height" | "minHeight";
/**
* Optional maximum height in pixels. If exceeded, scrolling is enabled.
*
* @default undefined (no limit)
*/
maxHeight?: number;
}
/**
* Automatically resizes a textarea element based on its content and width.
*
* Uses a hidden clone for accurate measurements without layout shift.
* Reactively updates when the input or element changes, or when the
* element is resized.
*/
export declare class TextareaAutosize {
#private;
element: HTMLElement | undefined;
input: string;
styleProp: "height" | "minHeight";
maxHeight: number | undefined;
textareaHeight: number;
textareaOldWidth: number;
constructor(options: TextareaAutosizeOptions);
/**
* Recomputes the required height based on current content and applies it
* to the textarea. If `maxHeight` is exceeded, vertical scrolling is enabled.
*/
triggerResize: () => void;
}
@@ -0,0 +1,131 @@
import { useResizeObserver } from "../use-resize-observer/index.js";
import { watch } from "../watch/index.js";
import { tick } from "svelte";
import { extract } from "../extract/index.js";
const stylesToCopy = [
"box-sizing",
"width",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"border-top-width",
"border-right-width",
"border-bottom-width",
"border-left-width",
"font-family",
"font-size",
"font-weight",
"font-style",
"letter-spacing",
"text-indent",
"text-transform",
"line-height",
"word-spacing",
"word-wrap",
"word-break",
"white-space",
];
/**
* Automatically resizes a textarea element based on its content and width.
*
* Uses a hidden clone for accurate measurements without layout shift.
* Reactively updates when the input or element changes, or when the
* element is resized.
*/
export class TextareaAutosize {
#options;
#resizeTimeout = null;
#hiddenTextarea = null;
element = $derived.by(() => extract(this.#options.element));
input = $derived.by(() => extract(this.#options.input));
styleProp = $derived.by(() => extract(this.#options.styleProp, "height"));
maxHeight = $derived.by(() => extract(this.#options.maxHeight, undefined));
textareaHeight = $state(0);
textareaOldWidth = $state(0);
constructor(options) {
this.#options = options;
// Create hidden textarea for measurements
this.#createHiddenTextarea();
watch([() => this.input, () => this.element], () => {
tick().then(() => this.triggerResize());
});
watch(() => this.textareaHeight, () => options?.onResize?.());
useResizeObserver(() => this.element, ([entry]) => {
if (!entry)
return;
const { contentRect } = entry;
if (this.textareaOldWidth === contentRect.width)
return;
this.textareaOldWidth = contentRect.width;
this.triggerResize();
});
$effect(() => {
return () => {
// Clean up
if (this.#hiddenTextarea) {
this.#hiddenTextarea.remove();
this.#hiddenTextarea = null;
}
if (this.#resizeTimeout) {
window.cancelAnimationFrame(this.#resizeTimeout);
this.#resizeTimeout = null;
}
};
});
}
// Copy all the styles that affect text layout
#createHiddenTextarea() {
// Create a hidden textarea that will be used for measurements
// This avoids layout shifts caused by manipulating the actual textarea
if (typeof window === "undefined")
return;
this.#hiddenTextarea = document.createElement("textarea");
const style = this.#hiddenTextarea.style;
// Make it invisible but keep same text layout properties
style.visibility = "hidden";
style.position = "absolute";
style.overflow = "hidden";
style.height = "0";
style.top = "0";
style.left = "-9999px";
document.body.appendChild(this.#hiddenTextarea);
}
#copyStyles() {
if (!this.element || !this.#hiddenTextarea)
return;
const computed = window.getComputedStyle(this.element);
for (const style of stylesToCopy) {
this.#hiddenTextarea.style.setProperty(style, computed.getPropertyValue(style));
}
// Ensure the width matches exactly
this.#hiddenTextarea.style.width = `${this.element.clientWidth}px`;
}
/**
* Recomputes the required height based on current content and applies it
* to the textarea. If `maxHeight` is exceeded, vertical scrolling is enabled.
*/
triggerResize = () => {
if (!this.element || !this.#hiddenTextarea)
return;
// Copy current styles and content to hidden textarea
this.#copyStyles();
this.#hiddenTextarea.value = this.input || "";
// Measure the hidden textarea
const scrollHeight = this.#hiddenTextarea.scrollHeight;
// Apply the height, respecting maxHeight if set
let newHeight = scrollHeight;
if (this.maxHeight && newHeight > this.maxHeight) {
newHeight = this.maxHeight;
this.element.style.overflowY = "auto";
}
else {
this.element.style.overflowY = "hidden";
}
// Only update if height actually changed
if (this.textareaHeight !== newHeight) {
this.textareaHeight = newHeight;
this.element.style[this.styleProp] = `${newHeight}px`;
}
};
}