text-fields
Version:
TextFields is designed to create and manage text fields with advanced visuals and functionality, including dynamic notched outlines, floating labels, and adaptive text areas.
169 lines (128 loc) • 5.66 kB
text/typescript
class TextFields {
private textFieldContainer: (HTMLInputElement | HTMLTextAreaElement)[];
private floatingLabel: HTMLElement[];
private resizeObserver: ResizeObserver;
private notches: { container: HTMLElement; notch: HTMLElement }[] = [];
private mutationObserver: MutationObserver;
constructor() {
this.textFieldContainer = [];
this.floatingLabel = [];
this.resizeObserver = new ResizeObserver(TextFields.handleResize.bind(this));
this.mutationObserver = new MutationObserver(this.initializeElements.bind(this));
this.observeDOMChanges();
}
private observeDOMChanges() {
this.mutationObserver.observe(document.body, { childList: true, subtree: true });
}
private static handleResize(entries: ResizeObserverEntry[]) {
entries.forEach((entry) => {
const notch = entry.target.closest('.notched-outline')?.querySelector<HTMLElement>('.notched-outline__notch');
if (notch) {
TextFields.setNotchWidth(notch, TextFields.getNotchWidth(notch));
}
});
}
private initializeElements() {
this.textFieldContainer = Array.from(document.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>('.text-field-container input, .text-field-container textarea'));
this.floatingLabel = Array.from(document.querySelectorAll<HTMLElement>('.floating-label'));
if (this.textFieldContainer.length > 0 && this.floatingLabel.length > 0) {
this.setupNotches();
this.handleEvents();
this.updateInitialNotchWidths();
}
}
private setupNotches() {
this.floatingLabel.forEach((label) => {
const notchedOutline = label.closest('.notched-outline') ?? TextFields.createNotchedOutline(label);
if (notchedOutline) {
const notch = notchedOutline.querySelector<HTMLElement>('.notched-outline__notch');
if (notch) {
this.notches.push({ container: notchedOutline.parentElement as HTMLElement, notch });
TextFields.setNotchWidth(notch, TextFields.getNotchWidth(notch));
this.resizeObserver.observe(label);
}
}
});
}
private static createNotchedOutline(label: HTMLElement): HTMLElement {
const notchedOutline = document.createElement('div');
notchedOutline.classList.add('notched-outline');
notchedOutline.innerHTML = `
<div class="notched-outline__leading"></div>
<div class="notched-outline__notch">${label.outerHTML}</div>
<div class="notched-outline__trailing"></div>
`;
label.replaceWith(notchedOutline);
return notchedOutline;
}
private static setNotchWidth(notch: HTMLElement, width: string) {
const newNotch = notch;
newNotch.style.width = width;
}
private static getNotchWidth(notch: HTMLElement): string {
const label = notch.querySelector<HTMLElement>('.floating-label');
return label ? `${(parseFloat(getComputedStyle(label).width) + 13) * 0.75}px` : 'auto';
}
private handleEvents() {
this.textFieldContainer.forEach((field) => {
const notchData = this.notches.find((data) => data.container.contains(field));
if (notchData) {
TextFields.setupFieldEvents(field, notchData.container, notchData.notch);
}
});
}
private updateInitialNotchWidths() {
this.notches.forEach(({ notch }) => {
TextFields.setNotchWidth(notch, TextFields.getNotchWidth(notch));
});
}
private static setupFieldEvents(field: HTMLInputElement | HTMLTextAreaElement, container: HTMLElement, notch: HTMLElement) {
const fieldType = field instanceof HTMLTextAreaElement;
const eventType = fieldType ? 'input' : 'change';
field.addEventListener('focus', () => {
container.classList.add(fieldType ? 'textarea--focused' : 'input--focused');
TextFields.setNotchWidth(notch, TextFields.getNotchWidth(notch));
});
field.addEventListener('blur', () => {
container.classList.remove(fieldType ? 'textarea--focused' : 'input--focused');
TextFields.setNotchWidth(notch, field.value.trim() ? TextFields.getNotchWidth(notch) : 'auto');
});
field.addEventListener(eventType, () => {
TextFields.updateStyles(field, container, fieldType);
if (fieldType) {
TextFields.resizeTextarea(field as HTMLTextAreaElement, container);
}
});
TextFields.updateStyles(field, container, fieldType);
}
private static updateStyles(field: HTMLInputElement | HTMLTextAreaElement, container: HTMLElement, fieldType: boolean) {
container.classList.toggle(fieldType ? 'textarea--filled' : 'input--filled', field.value.trim().length > 0);
container.classList.toggle(fieldType ? 'textarea--disabled' : 'input--disabled', field.disabled);
}
private static resizeTextarea(field: HTMLTextAreaElement, container: HTMLElement) {
if (container.classList.contains('textarea--auto-resizeable')) {
const newField = field;
newField.style.height = 'auto';
newField.style.height = `${field.scrollHeight}px`;
}
}
public updateField(field: HTMLInputElement | HTMLTextAreaElement) {
const container = field.closest('.text-field-container') as HTMLElement;
const notchData = this.notches.find((data) => data.container.contains(field));
if (container) {
TextFields.updateStyles(field, container, field instanceof HTMLTextAreaElement);
if (notchData) {
TextFields.setNotchWidth(notchData.notch, TextFields.getNotchWidth(notchData.notch));
}
}
}
public async init() {
await new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, 0);
});
this.initializeElements();
}
}
export default TextFields;