File size: 4,329 Bytes
f48efbb 2dd779d f48efbb 2dd779d f48efbb 2dd779d f48efbb 2dd779d f48efbb 2dd779d f48efbb 2dd779d f48efbb 2dd779d f48efbb 2dd779d f48efbb 9b4caaa 2dd779d f48efbb 2dd779d f48efbb 2dd779d f48efbb 2dd779d 9b4caaa f48efbb |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
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`;
}
};
}
|