Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
| import type { Getter } from "melt"; | |
| import { extract } from "./extract.svelte.js"; | |
| import { useResizeObserver, watch } from "runed"; | |
| import { onDestroy, tick } from "svelte"; | |
| export interface TextareaAutosizeOptions { | |
| /** Textarea element to autosize. */ | |
| element: Getter<HTMLElement | undefined>; | |
| /** Textarea content. */ | |
| input: Getter<string>; | |
| /** Function called when the textarea size changes. */ | |
| onResize?: () => void; | |
| /** | |
| * Specify the style property that will be used to manipulate height. Can be `height | minHeight`. | |
| * @default `height` | |
| **/ | |
| styleProp?: "height" | "minHeight"; | |
| /** | |
| * Maximum height of the textarea before enabling scrolling. | |
| * @default `undefined` (no maximum) | |
| */ | |
| maxHeight?: number; | |
| } | |
| export class TextareaAutosize { | |
| #options: TextareaAutosizeOptions; | |
| #resizeTimeout: number | null = null; | |
| #hiddenTextarea: HTMLTextAreaElement | null = 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: TextareaAutosizeOptions) { | |
| 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(); | |
| } | |
| ); | |
| onDestroy(() => { | |
| // Clean up | |
| if (this.#hiddenTextarea) { | |
| this.#hiddenTextarea.remove(); | |
| this.#hiddenTextarea = null; | |
| } | |
| if (this.#resizeTimeout) { | |
| window.cancelAnimationFrame(this.#resizeTimeout); | |
| this.#resizeTimeout = null; | |
| } | |
| }); | |
| } | |
| #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); | |
| // Copy all the styles that affect text layout | |
| 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", | |
| ]; | |
| stylesToCopy.forEach(style => { | |
| this.#hiddenTextarea!.style.setProperty(style, computed.getPropertyValue(style)); | |
| }); | |
| // Ensure the width matches exactly | |
| this.#hiddenTextarea.style.width = `${this.element.clientWidth}px`; | |
| } | |
| 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`; | |
| } | |
| }; | |
| } | |