|
import type { Getter } from "melt"; |
|
import { extract } from "./extract.svelte.js"; |
|
import { useResizeObserver, watch } from "runed"; |
|
import { onDestroy, tick } from "svelte"; |
|
|
|
export interface TextareaAutosizeOptions { |
|
|
|
element: Getter<HTMLElement | undefined>; |
|
|
|
input: Getter<string>; |
|
|
|
onResize?: () => void; |
|
|
|
|
|
|
|
|
|
styleProp?: "height" | "minHeight"; |
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
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(() => { |
|
|
|
if (this.#hiddenTextarea) { |
|
this.#hiddenTextarea.remove(); |
|
this.#hiddenTextarea = null; |
|
} |
|
|
|
if (this.#resizeTimeout) { |
|
window.cancelAnimationFrame(this.#resizeTimeout); |
|
this.#resizeTimeout = null; |
|
} |
|
}); |
|
} |
|
|
|
#createHiddenTextarea() { |
|
|
|
|
|
if (typeof window === "undefined") return; |
|
|
|
this.#hiddenTextarea = document.createElement("textarea"); |
|
const style = this.#hiddenTextarea.style; |
|
|
|
|
|
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); |
|
|
|
|
|
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)); |
|
}); |
|
|
|
|
|
this.#hiddenTextarea.style.width = `${this.element.clientWidth}px`; |
|
} |
|
|
|
triggerResize = () => { |
|
if (!this.element || !this.#hiddenTextarea) return; |
|
|
|
|
|
this.#copyStyles(); |
|
this.#hiddenTextarea.value = this.input || ""; |
|
|
|
|
|
const scrollHeight = this.#hiddenTextarea.scrollHeight; |
|
|
|
|
|
let newHeight = scrollHeight; |
|
if (this.maxHeight && newHeight > this.maxHeight) { |
|
newHeight = this.maxHeight; |
|
this.element.style.overflowY = "auto"; |
|
} else { |
|
this.element.style.overflowY = "hidden"; |
|
} |
|
|
|
|
|
if (this.textareaHeight !== newHeight) { |
|
this.textareaHeight = newHeight; |
|
this.element.style[this.styleProp] = `${newHeight}px`; |
|
} |
|
}; |
|
} |
|
|