@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.
- package/README.md +1 -2
- package/dist/custom-elements.json +2620 -5
- package/dist/html-custom-data.json +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/index.min.js.map +1 -1
- package/package.json +4 -4
- package/cem.config.mjs +0 -16
- package/demo/index.html +0 -49
- package/dist/src/TextareaAutosizeElement.d.ts +0 -82
- package/dist/src/TextareaAutosizeElement.d.ts.map +0 -1
- package/dist/src/index.d.ts +0 -2
- package/dist/src/index.d.ts.map +0 -1
- package/eslint.config.mjs +0 -13
- package/rollup.config.js +0 -32
- package/src/TextareaAutosizeElement.ts +0 -291
- package/src/index.ts +0 -1
- package/tsconfig.json +0 -9
|
@@ -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";
|