UNPKG

20.6 kBJavaScriptView Raw
1import * as i1 from '@angular/cdk/platform';
2import { normalizePassiveListenerOptions } from '@angular/cdk/platform';
3import * as i0 from '@angular/core';
4import { Injectable, EventEmitter, Directive, Output, Optional, Inject, Input, NgModule } from '@angular/core';
5import { coerceElement, coerceNumberProperty, coerceBooleanProperty } from '@angular/cdk/coercion';
6import { EMPTY, Subject, fromEvent } from 'rxjs';
7import { auditTime, takeUntil } from 'rxjs/operators';
8import { DOCUMENT } from '@angular/common';
9
10/** Options to pass to the animationstart listener. */
11const listenerOptions = normalizePassiveListenerOptions({ passive: true });
12/**
13 * An injectable service that can be used to monitor the autofill state of an input.
14 * Based on the following blog post:
15 * https://medium.com/@brunn/detecting-autofilled-fields-in-javascript-aed598d25da7
16 */
17class AutofillMonitor {
18 constructor(_platform, _ngZone) {
19 this._platform = _platform;
20 this._ngZone = _ngZone;
21 this._monitoredElements = new Map();
22 }
23 monitor(elementOrRef) {
24 if (!this._platform.isBrowser) {
25 return EMPTY;
26 }
27 const element = coerceElement(elementOrRef);
28 const info = this._monitoredElements.get(element);
29 if (info) {
30 return info.subject;
31 }
32 const result = new Subject();
33 const cssClass = 'cdk-text-field-autofilled';
34 const listener = ((event) => {
35 // Animation events fire on initial element render, we check for the presence of the autofill
36 // CSS class to make sure this is a real change in state, not just the initial render before
37 // we fire off events.
38 if (event.animationName === 'cdk-text-field-autofill-start' &&
39 !element.classList.contains(cssClass)) {
40 element.classList.add(cssClass);
41 this._ngZone.run(() => result.next({ target: event.target, isAutofilled: true }));
42 }
43 else if (event.animationName === 'cdk-text-field-autofill-end' &&
44 element.classList.contains(cssClass)) {
45 element.classList.remove(cssClass);
46 this._ngZone.run(() => result.next({ target: event.target, isAutofilled: false }));
47 }
48 });
49 this._ngZone.runOutsideAngular(() => {
50 element.addEventListener('animationstart', listener, listenerOptions);
51 element.classList.add('cdk-text-field-autofill-monitored');
52 });
53 this._monitoredElements.set(element, {
54 subject: result,
55 unlisten: () => {
56 element.removeEventListener('animationstart', listener, listenerOptions);
57 },
58 });
59 return result;
60 }
61 stopMonitoring(elementOrRef) {
62 const element = coerceElement(elementOrRef);
63 const info = this._monitoredElements.get(element);
64 if (info) {
65 info.unlisten();
66 info.subject.complete();
67 element.classList.remove('cdk-text-field-autofill-monitored');
68 element.classList.remove('cdk-text-field-autofilled');
69 this._monitoredElements.delete(element);
70 }
71 }
72 ngOnDestroy() {
73 this._monitoredElements.forEach((_info, element) => this.stopMonitoring(element));
74 }
75 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: AutofillMonitor, deps: [{ token: i1.Platform }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); }
76 static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: AutofillMonitor, providedIn: 'root' }); }
77}
78i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: AutofillMonitor, decorators: [{
79 type: Injectable,
80 args: [{ providedIn: 'root' }]
81 }], ctorParameters: function () { return [{ type: i1.Platform }, { type: i0.NgZone }]; } });
82/** A directive that can be used to monitor the autofill state of an input. */
83class CdkAutofill {
84 constructor(_elementRef, _autofillMonitor) {
85 this._elementRef = _elementRef;
86 this._autofillMonitor = _autofillMonitor;
87 /** Emits when the autofill state of the element changes. */
88 this.cdkAutofill = new EventEmitter();
89 }
90 ngOnInit() {
91 this._autofillMonitor
92 .monitor(this._elementRef)
93 .subscribe(event => this.cdkAutofill.emit(event));
94 }
95 ngOnDestroy() {
96 this._autofillMonitor.stopMonitoring(this._elementRef);
97 }
98 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkAutofill, deps: [{ token: i0.ElementRef }, { token: AutofillMonitor }], target: i0.ɵɵFactoryTarget.Directive }); }
99 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkAutofill, selector: "[cdkAutofill]", outputs: { cdkAutofill: "cdkAutofill" }, ngImport: i0 }); }
100}
101i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkAutofill, decorators: [{
102 type: Directive,
103 args: [{
104 selector: '[cdkAutofill]',
105 }]
106 }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: AutofillMonitor }]; }, propDecorators: { cdkAutofill: [{
107 type: Output
108 }] } });
109
110/** Directive to automatically resize a textarea to fit its content. */
111class CdkTextareaAutosize {
112 /** Minimum amount of rows in the textarea. */
113 get minRows() {
114 return this._minRows;
115 }
116 set minRows(value) {
117 this._minRows = coerceNumberProperty(value);
118 this._setMinHeight();
119 }
120 /** Maximum amount of rows in the textarea. */
121 get maxRows() {
122 return this._maxRows;
123 }
124 set maxRows(value) {
125 this._maxRows = coerceNumberProperty(value);
126 this._setMaxHeight();
127 }
128 /** Whether autosizing is enabled or not */
129 get enabled() {
130 return this._enabled;
131 }
132 set enabled(value) {
133 value = coerceBooleanProperty(value);
134 // Only act if the actual value changed. This specifically helps to not run
135 // resizeToFitContent too early (i.e. before ngAfterViewInit)
136 if (this._enabled !== value) {
137 (this._enabled = value) ? this.resizeToFitContent(true) : this.reset();
138 }
139 }
140 get placeholder() {
141 return this._textareaElement.placeholder;
142 }
143 set placeholder(value) {
144 this._cachedPlaceholderHeight = undefined;
145 if (value) {
146 this._textareaElement.setAttribute('placeholder', value);
147 }
148 else {
149 this._textareaElement.removeAttribute('placeholder');
150 }
151 this._cacheTextareaPlaceholderHeight();
152 }
153 constructor(_elementRef, _platform, _ngZone,
154 /** @breaking-change 11.0.0 make document required */
155 document) {
156 this._elementRef = _elementRef;
157 this._platform = _platform;
158 this._ngZone = _ngZone;
159 this._destroyed = new Subject();
160 this._enabled = true;
161 /**
162 * Value of minRows as of last resize. If the minRows has decreased, the
163 * height of the textarea needs to be recomputed to reflect the new minimum. The maxHeight
164 * does not have the same problem because it does not affect the textarea's scrollHeight.
165 */
166 this._previousMinRows = -1;
167 this._isViewInited = false;
168 /** Handles `focus` and `blur` events. */
169 this._handleFocusEvent = (event) => {
170 this._hasFocus = event.type === 'focus';
171 };
172 this._document = document;
173 this._textareaElement = this._elementRef.nativeElement;
174 }
175 /** Sets the minimum height of the textarea as determined by minRows. */
176 _setMinHeight() {
177 const minHeight = this.minRows && this._cachedLineHeight ? `${this.minRows * this._cachedLineHeight}px` : null;
178 if (minHeight) {
179 this._textareaElement.style.minHeight = minHeight;
180 }
181 }
182 /** Sets the maximum height of the textarea as determined by maxRows. */
183 _setMaxHeight() {
184 const maxHeight = this.maxRows && this._cachedLineHeight ? `${this.maxRows * this._cachedLineHeight}px` : null;
185 if (maxHeight) {
186 this._textareaElement.style.maxHeight = maxHeight;
187 }
188 }
189 ngAfterViewInit() {
190 if (this._platform.isBrowser) {
191 // Remember the height which we started with in case autosizing is disabled
192 this._initialHeight = this._textareaElement.style.height;
193 this.resizeToFitContent();
194 this._ngZone.runOutsideAngular(() => {
195 const window = this._getWindow();
196 fromEvent(window, 'resize')
197 .pipe(auditTime(16), takeUntil(this._destroyed))
198 .subscribe(() => this.resizeToFitContent(true));
199 this._textareaElement.addEventListener('focus', this._handleFocusEvent);
200 this._textareaElement.addEventListener('blur', this._handleFocusEvent);
201 });
202 this._isViewInited = true;
203 this.resizeToFitContent(true);
204 }
205 }
206 ngOnDestroy() {
207 this._textareaElement.removeEventListener('focus', this._handleFocusEvent);
208 this._textareaElement.removeEventListener('blur', this._handleFocusEvent);
209 this._destroyed.next();
210 this._destroyed.complete();
211 }
212 /**
213 * Cache the height of a single-row textarea if it has not already been cached.
214 *
215 * We need to know how large a single "row" of a textarea is in order to apply minRows and
216 * maxRows. For the initial version, we will assume that the height of a single line in the
217 * textarea does not ever change.
218 */
219 _cacheTextareaLineHeight() {
220 if (this._cachedLineHeight) {
221 return;
222 }
223 // Use a clone element because we have to override some styles.
224 let textareaClone = this._textareaElement.cloneNode(false);
225 textareaClone.rows = 1;
226 // Use `position: absolute` so that this doesn't cause a browser layout and use
227 // `visibility: hidden` so that nothing is rendered. Clear any other styles that
228 // would affect the height.
229 textareaClone.style.position = 'absolute';
230 textareaClone.style.visibility = 'hidden';
231 textareaClone.style.border = 'none';
232 textareaClone.style.padding = '0';
233 textareaClone.style.height = '';
234 textareaClone.style.minHeight = '';
235 textareaClone.style.maxHeight = '';
236 // In Firefox it happens that textarea elements are always bigger than the specified amount
237 // of rows. This is because Firefox tries to add extra space for the horizontal scrollbar.
238 // As a workaround that removes the extra space for the scrollbar, we can just set overflow
239 // to hidden. This ensures that there is no invalid calculation of the line height.
240 // See Firefox bug report: https://bugzilla.mozilla.org/show_bug.cgi?id=33654
241 textareaClone.style.overflow = 'hidden';
242 this._textareaElement.parentNode.appendChild(textareaClone);
243 this._cachedLineHeight = textareaClone.clientHeight;
244 textareaClone.remove();
245 // Min and max heights have to be re-calculated if the cached line height changes
246 this._setMinHeight();
247 this._setMaxHeight();
248 }
249 _measureScrollHeight() {
250 const element = this._textareaElement;
251 const previousMargin = element.style.marginBottom || '';
252 const isFirefox = this._platform.FIREFOX;
253 const needsMarginFiller = isFirefox && this._hasFocus;
254 const measuringClass = isFirefox
255 ? 'cdk-textarea-autosize-measuring-firefox'
256 : 'cdk-textarea-autosize-measuring';
257 // In some cases the page might move around while we're measuring the `textarea` on Firefox. We
258 // work around it by assigning a temporary margin with the same height as the `textarea` so that
259 // it occupies the same amount of space. See #23233.
260 if (needsMarginFiller) {
261 element.style.marginBottom = `${element.clientHeight}px`;
262 }
263 // Reset the textarea height to auto in order to shrink back to its default size.
264 // Also temporarily force overflow:hidden, so scroll bars do not interfere with calculations.
265 element.classList.add(measuringClass);
266 // The measuring class includes a 2px padding to workaround an issue with Chrome,
267 // so we account for that extra space here by subtracting 4 (2px top + 2px bottom).
268 const scrollHeight = element.scrollHeight - 4;
269 element.classList.remove(measuringClass);
270 if (needsMarginFiller) {
271 element.style.marginBottom = previousMargin;
272 }
273 return scrollHeight;
274 }
275 _cacheTextareaPlaceholderHeight() {
276 if (!this._isViewInited || this._cachedPlaceholderHeight != undefined) {
277 return;
278 }
279 if (!this.placeholder) {
280 this._cachedPlaceholderHeight = 0;
281 return;
282 }
283 const value = this._textareaElement.value;
284 this._textareaElement.value = this._textareaElement.placeholder;
285 this._cachedPlaceholderHeight = this._measureScrollHeight();
286 this._textareaElement.value = value;
287 }
288 ngDoCheck() {
289 if (this._platform.isBrowser) {
290 this.resizeToFitContent();
291 }
292 }
293 /**
294 * Resize the textarea to fit its content.
295 * @param force Whether to force a height recalculation. By default the height will be
296 * recalculated only if the value changed since the last call.
297 */
298 resizeToFitContent(force = false) {
299 // If autosizing is disabled, just skip everything else
300 if (!this._enabled) {
301 return;
302 }
303 this._cacheTextareaLineHeight();
304 this._cacheTextareaPlaceholderHeight();
305 // If we haven't determined the line-height yet, we know we're still hidden and there's no point
306 // in checking the height of the textarea.
307 if (!this._cachedLineHeight) {
308 return;
309 }
310 const textarea = this._elementRef.nativeElement;
311 const value = textarea.value;
312 // Only resize if the value or minRows have changed since these calculations can be expensive.
313 if (!force && this._minRows === this._previousMinRows && value === this._previousValue) {
314 return;
315 }
316 const scrollHeight = this._measureScrollHeight();
317 const height = Math.max(scrollHeight, this._cachedPlaceholderHeight || 0);
318 // Use the scrollHeight to know how large the textarea *would* be if fit its entire value.
319 textarea.style.height = `${height}px`;
320 this._ngZone.runOutsideAngular(() => {
321 if (typeof requestAnimationFrame !== 'undefined') {
322 requestAnimationFrame(() => this._scrollToCaretPosition(textarea));
323 }
324 else {
325 setTimeout(() => this._scrollToCaretPosition(textarea));
326 }
327 });
328 this._previousValue = value;
329 this._previousMinRows = this._minRows;
330 }
331 /**
332 * Resets the textarea to its original size
333 */
334 reset() {
335 // Do not try to change the textarea, if the initialHeight has not been determined yet
336 // This might potentially remove styles when reset() is called before ngAfterViewInit
337 if (this._initialHeight !== undefined) {
338 this._textareaElement.style.height = this._initialHeight;
339 }
340 }
341 _noopInputHandler() {
342 // no-op handler that ensures we're running change detection on input events.
343 }
344 /** Access injected document if available or fallback to global document reference */
345 _getDocument() {
346 return this._document || document;
347 }
348 /** Use defaultView of injected document if available or fallback to global window reference */
349 _getWindow() {
350 const doc = this._getDocument();
351 return doc.defaultView || window;
352 }
353 /**
354 * Scrolls a textarea to the caret position. On Firefox resizing the textarea will
355 * prevent it from scrolling to the caret position. We need to re-set the selection
356 * in order for it to scroll to the proper position.
357 */
358 _scrollToCaretPosition(textarea) {
359 const { selectionStart, selectionEnd } = textarea;
360 // IE will throw an "Unspecified error" if we try to set the selection range after the
361 // element has been removed from the DOM. Assert that the directive hasn't been destroyed
362 // between the time we requested the animation frame and when it was executed.
363 // Also note that we have to assert that the textarea is focused before we set the
364 // selection range. Setting the selection range on a non-focused textarea will cause
365 // it to receive focus on IE and Edge.
366 if (!this._destroyed.isStopped && this._hasFocus) {
367 textarea.setSelectionRange(selectionStart, selectionEnd);
368 }
369 }
370 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkTextareaAutosize, deps: [{ token: i0.ElementRef }, { token: i1.Platform }, { token: i0.NgZone }, { token: DOCUMENT, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); }
371 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkTextareaAutosize, selector: "textarea[cdkTextareaAutosize]", inputs: { minRows: ["cdkAutosizeMinRows", "minRows"], maxRows: ["cdkAutosizeMaxRows", "maxRows"], enabled: ["cdkTextareaAutosize", "enabled"], placeholder: "placeholder" }, host: { attributes: { "rows": "1" }, listeners: { "input": "_noopInputHandler()" }, classAttribute: "cdk-textarea-autosize" }, exportAs: ["cdkTextareaAutosize"], ngImport: i0 }); }
372}
373i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkTextareaAutosize, decorators: [{
374 type: Directive,
375 args: [{
376 selector: 'textarea[cdkTextareaAutosize]',
377 exportAs: 'cdkTextareaAutosize',
378 host: {
379 'class': 'cdk-textarea-autosize',
380 // Textarea elements that have the directive applied should have a single row by default.
381 // Browsers normally show two rows by default and therefore this limits the minRows binding.
382 'rows': '1',
383 '(input)': '_noopInputHandler()',
384 },
385 }]
386 }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i1.Platform }, { type: i0.NgZone }, { type: undefined, decorators: [{
387 type: Optional
388 }, {
389 type: Inject,
390 args: [DOCUMENT]
391 }] }]; }, propDecorators: { minRows: [{
392 type: Input,
393 args: ['cdkAutosizeMinRows']
394 }], maxRows: [{
395 type: Input,
396 args: ['cdkAutosizeMaxRows']
397 }], enabled: [{
398 type: Input,
399 args: ['cdkTextareaAutosize']
400 }], placeholder: [{
401 type: Input
402 }] } });
403
404class TextFieldModule {
405 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: TextFieldModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
406 static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.0.0", ngImport: i0, type: TextFieldModule, declarations: [CdkAutofill, CdkTextareaAutosize], exports: [CdkAutofill, CdkTextareaAutosize] }); }
407 static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: TextFieldModule }); }
408}
409i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: TextFieldModule, decorators: [{
410 type: NgModule,
411 args: [{
412 declarations: [CdkAutofill, CdkTextareaAutosize],
413 exports: [CdkAutofill, CdkTextareaAutosize],
414 }]
415 }] });
416
417/**
418 * Generated bundle index. Do not edit.
419 */
420
421export { AutofillMonitor, CdkAutofill, CdkTextareaAutosize, TextFieldModule };
422//# sourceMappingURL=text-field.mjs.map