@m3e/textarea-autosize 1.0.0-rc.1 → 1.0.0-rc.2

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.
@@ -1,291 +0,0 @@
1
- /**
2
- * Adapted from Angular Material CDK Text Field
3
- * Source: https://github.com/angular/components/blob/main/src/cdk/text-field/autosize.ts
4
- *
5
- * @license MIT
6
- * Copyright (c) 2025 Google LLC
7
- * See LICENSE file in the project root for full license text.
8
- */
9
-
10
- import { css, CSSResultGroup, LitElement, PropertyValues } from "lit";
11
- import { customElement, property } from "lit/decorators.js";
12
-
13
- import { debounce, HtmlFor, Role } from "@m3e/core";
14
-
15
- /**
16
- * @summary
17
- * A non-visual element used to automatically resize a `textarea` to fit its content.
18
- *
19
- * @description
20
- * The `m3e-textarea-autosize` component automatically adjusts the height of a linked `textarea` to fit its content,
21
- * preserving layout integrity and user experience. This non-visual element listens to input changes and applies
22
- * dynamic resizing, constrained by optional row limits. It supports declarative configuration via attributes and
23
- * can be disabled when manual control is preferred.
24
- *
25
- * @example
26
- * The following example illustrates the `m3e-textarea-autosize` in conjunction with the `m3e-form-field` to
27
- * automatically resize a field's `textarea` with a 5 row limit.
28
- * ```html
29
- * <m3e-form-field>
30
- * <label slot="label" for="fld">Textarea Autosize</label>
31
- * <textarea id="fld"></textarea>
32
- * <m3e-textarea-autosize for="fld" max-rows="5"></m3e-textarea-autosize>
33
- * </m3e-form-field>
34
- * ```
35
- *
36
- * @tag m3e-textarea-autosize
37
- *
38
- * @attr disabled - Whether auto-sizing is disabled.
39
- * @attr for - The query selector used to specify the element related to this element.
40
- * @attr max-rows - The maximum amount of rows in the `textarea`.
41
- * @attr min-rows - The minimum amount of rows in the `textarea`.
42
- */
43
- @customElement("m3e-textarea-autosize")
44
- export class M3eTextareaAutosizeElement extends HtmlFor(Role(LitElement, "none")) {
45
- /** The styles of the element. */
46
- static override styles: CSSResultGroup = css`
47
- :host {
48
- display: none;
49
- }
50
- `;
51
-
52
- /** @private */ #initialHeight?: string;
53
- /** @private */ #cachedLineHeight?: number;
54
- /** @private */ #cachedPlaceholderHeight?: number;
55
- /** @private */ #previousMinRows?: number;
56
- /** @private */ #previousValue?: string;
57
- /** @private */ #hasFocus = false;
58
-
59
- /** @private */ readonly #windowResizeHandler = () => this._handleWindowResize();
60
- /** @private */ readonly #focusHandler = (e: FocusEvent) => (this.#hasFocus = e.type === "focus");
61
- /** @private */ readonly #inputHandler = () => this.resizeToFitContent();
62
-
63
- /**
64
- * The maximum amount of rows in the `textarea`.
65
- * @default 0
66
- */
67
- @property({ attribute: "max-rows", type: Number }) maxRows = 0;
68
-
69
- /**
70
- * The minimum amount of rows in the `textarea`.
71
- * @default 0
72
- */
73
- @property({ attribute: "min-rows", type: Number }) minRows = 0;
74
-
75
- /**
76
- * Whether auto-sizing is disabled.
77
- * @default false
78
- */
79
- @property({ type: Boolean, reflect: true }) disabled = false;
80
-
81
- /** @inheritdoc */
82
- override attach(control: HTMLElement): void {
83
- super.attach(control);
84
-
85
- if (control instanceof HTMLTextAreaElement) {
86
- control.style.resize = "none";
87
-
88
- this.#initialHeight = control.style.height;
89
- control.addEventListener("focus", this.#focusHandler);
90
- control.addEventListener("blur", this.#focusHandler);
91
- control.addEventListener("input", this.#inputHandler);
92
- window.addEventListener("resize", this.#windowResizeHandler);
93
- }
94
- }
95
-
96
- /** @inheritdoc */
97
- override detach(): void {
98
- if (this.control instanceof HTMLTextAreaElement) {
99
- window.removeEventListener("resize", this.#windowResizeHandler);
100
- this.control.removeEventListener("focus", this.#focusHandler);
101
- this.control.removeEventListener("blur", this.#focusHandler);
102
- this.control.removeEventListener("input", this.#inputHandler);
103
- }
104
- super.detach();
105
- }
106
-
107
- /** @inheritdoc */
108
- override connectedCallback(): void {
109
- this.ariaHidden = "true";
110
- super.connectedCallback();
111
- }
112
-
113
- /** @inheritdoc */
114
- protected override updated(_changedProperties: PropertyValues<this>): void {
115
- super.updated(_changedProperties);
116
-
117
- if (_changedProperties.has("disabled")) {
118
- if (this.disabled) {
119
- this.reset();
120
- } else {
121
- this.resizeToFitContent(true);
122
- }
123
- }
124
- }
125
-
126
- /**
127
- * Resize the `textarea` to fit its content.
128
- * @param {boolean} [force=false] - Whether to force a height recalculation.
129
- */
130
- resizeToFitContent(force: boolean = false): void {
131
- if (this.disabled || !(this.control instanceof HTMLTextAreaElement)) {
132
- return;
133
- }
134
-
135
- this.#cacheTextareaLineHeight();
136
- this.#cacheTextareaPlaceholderHeight();
137
-
138
- if (!this.#cachedLineHeight) {
139
- return;
140
- }
141
-
142
- const value = this.control.value;
143
- if (!force && this.minRows === this.#previousMinRows && value === this.#previousValue) {
144
- return;
145
- }
146
-
147
- const scrollHeight = this.#measureScrollHeight();
148
- const height = Math.max(scrollHeight, this.#cachedPlaceholderHeight || 0);
149
- this.control.style.height = `${height}px`;
150
-
151
- setTimeout(() => this.#scrollToCaretPosition());
152
-
153
- this.#previousValue = value;
154
- this.#previousMinRows = this.minRows;
155
- }
156
-
157
- /** Resets the `textarea` to its original size. */
158
- reset() {
159
- if (this.#initialHeight !== undefined && this.control instanceof HTMLTextAreaElement) {
160
- this.control.style.height = this.#initialHeight;
161
- }
162
- }
163
-
164
- /** @private */
165
- #cacheTextareaLineHeight(): void {
166
- if (this.#cachedLineHeight || !(this.control instanceof HTMLTextAreaElement)) {
167
- return;
168
- }
169
-
170
- const clone = <HTMLTextAreaElement>this.control.cloneNode(false);
171
- clone.rows = 1;
172
- clone.style.position = "absolute";
173
- clone.style.visibility = "hidden";
174
- clone.style.border = "none";
175
- clone.style.padding = "0";
176
- clone.style.height = "";
177
- clone.style.minHeight = "";
178
- clone.style.maxHeight = "";
179
- clone.style.top = clone.style.bottom = clone.style.left = clone.style.right = "auto";
180
- clone.style.overflow = "hidden";
181
-
182
- this.control.parentElement?.appendChild(clone);
183
- this.#cachedLineHeight = clone.clientHeight;
184
- clone.remove();
185
-
186
- this.#setMinHeight();
187
- this.#setMaxHeight();
188
- }
189
-
190
- /** @private */
191
- #cacheTextareaPlaceholderHeight(): void {
192
- if (!(this.control instanceof HTMLTextAreaElement) || this.#cachedPlaceholderHeight != undefined) {
193
- return;
194
- }
195
-
196
- if (!this.control.placeholder) {
197
- this.#cachedPlaceholderHeight = 0;
198
- return;
199
- }
200
-
201
- const value = this.control.value;
202
- this.control.value = this.control.placeholder;
203
- this.#cachedPlaceholderHeight = this.#measureScrollHeight();
204
- this.control.value = value;
205
- }
206
-
207
- /** @private */
208
- #setMinHeight(): void {
209
- const minHeight = this.minRows && this.#cachedLineHeight ? `${this.minRows * this.#cachedLineHeight}px` : null;
210
- if (minHeight && this.control) {
211
- this.control.style.minHeight = minHeight;
212
- }
213
- }
214
-
215
- /** @private */
216
- #setMaxHeight(): void {
217
- const maxHeight = this.maxRows && this.#cachedLineHeight ? `${this.maxRows * this.#cachedLineHeight}px` : null;
218
- if (maxHeight && this.control) {
219
- this.control.style.maxHeight = maxHeight;
220
- }
221
- }
222
-
223
- /** @private */
224
- #measureScrollHeight(): number {
225
- if (!this.control) {
226
- return 0;
227
- }
228
-
229
- const element = this.control;
230
- const previousMargin = element.style.marginBottom || "";
231
- const isFirefox = navigator.userAgent.includes("Firefox");
232
- const needsMarginFiller = isFirefox && this.#hasFocus;
233
-
234
- if (needsMarginFiller) {
235
- element.style.marginBottom = `${element.clientHeight}px`;
236
- }
237
-
238
- const initialStyle: Pick<CSSStyleDeclaration, "padding" | "boxSizing" | "height" | "overflow"> = {
239
- padding: element.style.padding,
240
- boxSizing: element.style.boxSizing,
241
- height: element.style.height,
242
- overflow: element.style.overflow,
243
- };
244
-
245
- element.style.padding = "2px 0";
246
- element.style.boxSizing = "content-box";
247
-
248
- if (!isFirefox) {
249
- element.style.height = "auto";
250
- element.style.overflow = "hidden";
251
- } else {
252
- element.style.height = "0";
253
- }
254
-
255
- const scrollHeight = element.scrollHeight - 4;
256
-
257
- element.style.padding = initialStyle.padding;
258
- element.style.boxSizing = initialStyle.boxSizing;
259
- element.style.height = initialStyle.height;
260
- element.style.overflow = initialStyle.overflow;
261
-
262
- if (needsMarginFiller) {
263
- element.style.marginBottom = previousMargin;
264
- }
265
-
266
- return scrollHeight;
267
- }
268
-
269
- /** @private */
270
- #scrollToCaretPosition() {
271
- if (!(this.control instanceof HTMLTextAreaElement) || !this.#hasFocus) {
272
- return;
273
- }
274
-
275
- const { selectionStart, selectionEnd } = this.control;
276
- this.control.setSelectionRange(selectionStart, selectionEnd);
277
- }
278
-
279
- /** @private */
280
- @debounce(16)
281
- private _handleWindowResize(): void {
282
- this.#cachedLineHeight = this.#cachedPlaceholderHeight = undefined;
283
- this.resizeToFitContent(true);
284
- }
285
- }
286
-
287
- declare global {
288
- interface HTMLElementTagNameMap {
289
- "m3e-textarea-autosize": M3eTextareaAutosizeElement;
290
- }
291
- }
package/src/index.ts DELETED
@@ -1 +0,0 @@
1
- export * from "./TextareaAutosizeElement";
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "rootDir": "./src",
5
- "outDir": "./dist/src"
6
- },
7
- "include": ["src/**/*.ts", "**/*.mjs", "**/*.js"],
8
- "exclude": []
9
- }