import { markEventAsHandled, stopPropagation } from './eventHandling'; import { createUniqueId } from './utils'; /// A text input field providing the following features and more: /// /// - A floating label /// - prefix text /// - suffix text export class InputBox { public outerElement: HTMLElement; private prefixTextElement: HTMLElement; private suffixElementContainer: HTMLElement; private suffixTextElement: HTMLElement; private labelWidthReserverElement: HTMLElement; // Ensures enough width for the label private labelElement: HTMLElement; private _accessibilityLabel: string | null; // NOTE: The input element can also be a textarea, but for some reason the // typing gets really wonky if this is a union. I don't like it, but I think // lying about the type is our best option. private _inputElement: HTMLInputElement; constructor({ inputElement = undefined, labelIsAlwaysSmall = false, connectClickHandlers = true, }: { inputElement?: HTMLInputElement | HTMLTextAreaElement; labelIsAlwaysSmall?: boolean; connectClickHandlers?: boolean; } = {}) { this.outerElement = document.createElement('div'); this.outerElement.classList.add('rio-input-box'); this.outerElement.innerHTML = `
`; this.prefixTextElement = this.outerElement.querySelector( '.rio-input-box-prefix-text' ) as HTMLElement; this.suffixElementContainer = this.outerElement.querySelector( '.rio-input-box-suffix-element > *' ) as HTMLElement; this.suffixTextElement = this.outerElement.querySelector( '.rio-input-box-suffix-text' ) as HTMLElement; this.labelWidthReserverElement = this.outerElement.querySelector( '.rio-input-box-label-width-reserver' ) as HTMLElement; this.labelElement = this.outerElement.querySelector( '.rio-input-box-label' ) as HTMLElement; this._inputElement = this.outerElement.querySelector( 'input' ) as HTMLInputElement; if (inputElement !== undefined) { this._inputElement.parentElement!.insertBefore( inputElement, this._inputElement ); this._inputElement.remove(); this._inputElement = inputElement as HTMLInputElement; } if (labelIsAlwaysSmall) { this.outerElement.classList.add('label-is-always-small'); } if (connectClickHandlers) { this.connectClickHandlers(); } // When keyboard focus is lost, check if the input is empty so that the // floating label can position itself accordingly this._inputElement.addEventListener('blur', () => { if (this._inputElement.value) { this.outerElement.classList.add('has-value'); } else { this.outerElement.classList.remove('has-value'); } }); // Assign defaults this.prefixText = null; this.suffixText = null; this.label = null; } private connectClickHandlers(): void { // Detect clicks on any part of the component and focus the input // // The `mousedown` are needed to prevent any potential drag events from // starting. this.prefixTextElement.addEventListener( 'mousedown', markEventAsHandled ); this.suffixTextElement.addEventListener( 'mousedown', markEventAsHandled ); // The `click` events pass focus to the input and move the cursor. // This has to be done in `mouseup`, rather than `mousedown`, because // otherwise the browser removes the focus again on mouseup. let selectStart = (event: Event) => { this._inputElement.focus(); this._inputElement.setSelectionRange(0, 0); markEventAsHandled(event); }; this.prefixTextElement.addEventListener('click', selectStart); let selectEnd = (event: Event) => { this._inputElement.focus(); this._inputElement.setSelectionRange( this._inputElement.value.length, this._inputElement.value.length ); markEventAsHandled(event); }; this.suffixElementContainer.addEventListener('click', selectEnd); this.suffixTextElement.addEventListener('click', selectEnd); let [paddingLeft, paddingRight] = this.outerElement.querySelectorAll( '.rio-input-box-padding' ); paddingLeft.addEventListener('click', selectStart); paddingRight.addEventListener('click', selectEnd); // Mousedown selects the input element and/or text in it (via dragging), // so let it do its default behavior but then stop it from propagating // to other elements this._inputElement.addEventListener('mousedown', stopPropagation); } get inputElement(): HTMLInputElement { return this._inputElement; } get value(): string { return this._inputElement.value; } set value(value: string) { this._inputElement.value = value; if (value) { this.outerElement.classList.add('has-value'); } else { this.outerElement.classList.remove('has-value'); } } get label(): string | null { return this.labelElement.textContent; } set label(label: string | null) { this.labelElement.textContent = label; this.labelWidthReserverElement.textContent = label; if (label) { this.outerElement.classList.add('has-label'); } else { this.outerElement.classList.remove('has-label'); } this.updateAccessibilityLabel(); } set accessibilityLabel(accessibilityLabel: string | null) { this._accessibilityLabel = accessibilityLabel; this.updateAccessibilityLabel(); } private updateAccessibilityLabel(): void { this.inputElement.ariaLabel = this._accessibilityLabel ? this._accessibilityLabel : this.label; } get prefixText(): string | null { return this.prefixTextElement.textContent; } set prefixText(prefixText: string | null) { this.prefixTextElement.textContent = prefixText; } get suffixText(): string | null { return this.suffixTextElement.textContent; } set suffixText(suffixText: string | null) { this.suffixTextElement.textContent = suffixText; } get suffixElement(): HTMLElement | null { // @ts-ignore return this.suffixElementContainer.firstChild; } set suffixElement(suffixElement: HTMLElement | null) { this.suffixElementContainer.firstChild?.remove(); if (suffixElement !== null) { this.suffixElementContainer.appendChild(suffixElement); } } get isSensitive(): boolean { return !this._inputElement.disabled; } set isSensitive(isSensitive: boolean) { this._inputElement.disabled = !isSensitive; this.outerElement.classList.toggle('rio-disabled-input', !isSensitive); } set isValid(isValid: boolean) { if (isValid) { this.outerElement.style.removeProperty('--rio-local-text-color'); } else { this.outerElement.style.setProperty( '--rio-local-text-color', 'var(--rio-global-danger-bg)' ); } } public focus(): void { this._inputElement.focus(); } public unfocus(): void { this._inputElement.blur(); } }