UNPKG

neumorphic-peripheral

Version:

A lightweight, framework-agnostic JavaScript/TypeScript library for beautiful neumorphic styling

311 lines (310 loc) 12.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TextareaComponent = void 0; exports.textarea = textarea; const base_1 = require("./base"); const utils_1 = require("../utils"); const validators_1 = require("../validators"); class TextareaComponent extends base_1.BaseComponent { constructor(element, config = {}) { if (!(element instanceof HTMLTextAreaElement)) { throw new Error('Textarea component requires an HTMLTextAreaElement'); } super(element, config); this._validationResult = null; this._textareaConfig = { autoResize: true, maxHeight: '200px', minHeight: '80px', validateOn: 'blur', showValidation: true, ...config }; this._debouncedValidate = (0, utils_1.debounce)(() => this.performValidation(), 300); } get textareaElement() { return this._element; } init() { this.applyBaseStyles(); this.applyTextareaStyles(); this.setupAutoResize(); this.setupValidation(); } bindEvents() { super.bindEvents(); // Focus events this.addEventListener(this._element, 'focus', this.handleFocus.bind(this)); this.addEventListener(this._element, 'blur', this.handleBlur.bind(this)); // Input validation events if (this._textareaConfig.validateOn === 'change') { this.addEventListener(this._element, 'input', this._debouncedValidate); } else if (this._textareaConfig.validateOn === 'blur') { this.addEventListener(this._element, 'blur', () => this.performValidation()); } // Auto-resize on input if (this._textareaConfig.autoResize) { this.addEventListener(this._element, 'input', () => this.autoResize()); } // Custom input event this.addEventListener(this._element, 'input', () => { this.emit('input', { value: this.getValue() }); }); } applyTextareaStyles() { (0, utils_1.addClassName)(this._element, 'textarea'); // Core textarea styles this._element.style.boxShadow = this.createShadowStyle('inset'); this._element.style.padding = '12px 16px'; this._element.style.fontSize = '16px'; this._element.style.lineHeight = '1.5'; this._element.style.width = this._element.style.width || '100%'; this._element.style.boxSizing = 'border-box'; this._element.style.resize = this._textareaConfig.autoResize ? 'none' : 'vertical'; this._element.style.minHeight = this._textareaConfig.minHeight; this._element.style.maxHeight = this._textareaConfig.maxHeight; this._element.style.fontFamily = 'inherit'; // Placeholder styling this.applyPlaceholderStyles(); // Set up hover and focus effects this.setupInteractionEffects(); } applyPlaceholderStyles() { if (this._textareaConfig.placeholder) { this.textareaElement.placeholder = this._textareaConfig.placeholder; } // Reuse placeholder styles from input component const placeholderColor = `color: ${this._theme.colors.textSecondary}; opacity: 1;`; const styleId = 'np-textarea-placeholder-styles'; if (!document.getElementById(styleId)) { const style = document.createElement('style'); style.id = styleId; style.textContent = ` .np-textarea::placeholder { ${placeholderColor} } .np-textarea::-webkit-input-placeholder { ${placeholderColor} } .np-textarea::-moz-placeholder { ${placeholderColor} } .np-textarea:-ms-input-placeholder { ${placeholderColor} } `; document.head.appendChild(style); } } setupInteractionEffects() { const originalShadow = this.createShadowStyle('inset'); const focusShadow = `${this.createShadowStyle('inset')}, 0 0 0 2px ${this._theme.colors.accent}40`; this.addEventListener(this._element, 'mouseenter', () => { if (!this._config.disabled && !this.textareaElement.matches(':focus')) { this._element.style.boxShadow = this.createHoverShadowStyle('inset'); } }); this.addEventListener(this._element, 'mouseleave', () => { if (!this.textareaElement.matches(':focus')) { this._element.style.boxShadow = originalShadow; } }); } setupAutoResize() { if (!this._textareaConfig.autoResize) return; // Set initial height this.autoResize(); // Use ResizeObserver if available for better performance if (typeof ResizeObserver !== 'undefined') { this._resizeObserver = new ResizeObserver(() => { this.autoResize(); }); this._resizeObserver.observe(this._element); } } autoResize() { if (!this._textareaConfig.autoResize) return; const element = this.textareaElement; const minHeight = parseInt(this._textareaConfig.minHeight) || 80; const maxHeight = parseInt(this._textareaConfig.maxHeight) || 200; // Reset height to auto to get the scroll height element.style.height = 'auto'; // Calculate new height const scrollHeight = element.scrollHeight; const newHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight); // Apply new height element.style.height = `${newHeight}px`; // Show/hide scrollbar based on content element.style.overflowY = scrollHeight > maxHeight ? 'auto' : 'hidden'; // Emit resize event this.emit('resize', { height: newHeight, scrollHeight, isScrollable: scrollHeight > maxHeight }); } handleFocus() { (0, utils_1.addClassName)(this._element, 'focused'); this._element.style.boxShadow = `${this.createShadowStyle('inset')}, 0 0 0 2px ${this._theme.colors.accent}40`; this.emit('focus'); } handleBlur() { this._element.classList.remove('np-focused'); this._element.style.boxShadow = this.createShadowStyle('inset'); this.emit('blur'); } setupValidation() { if (!this._textareaConfig.validate) return; // Set up ARIA attributes for accessibility this._element.setAttribute('aria-invalid', 'false'); if (this._textareaConfig.errorMessage) { const errorId = `error-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; this._element.setAttribute('aria-describedby', errorId); } } performValidation() { if (!this._textareaConfig.validate || !this._textareaConfig.showValidation) return; const value = this.getValue(); this._validationResult = (0, validators_1.validateValue)(value, this._textareaConfig.validate); // Update global validation manager validators_1.globalValidationManager.validate(this._element, this._textareaConfig.validate); // Update visual state this.updateValidationState(); // Emit validation event this.emit('validation', { result: this._validationResult, value }); return this._validationResult; } updateValidationState() { if (!this._validationResult) return; // Update ARIA attributes this._element.setAttribute('aria-invalid', this._validationResult.isValid ? 'false' : 'true'); if (this._validationResult.isValid) { this._element.classList.remove('np-error'); this._element.style.boxShadow = this.createShadowStyle('inset'); } else { (0, utils_1.addClassName)(this._element, 'error'); this._element.style.boxShadow = `${this.createShadowStyle('inset')}, 0 0 0 2px ${this._theme.colors.error}40`; } } // Public API methods validate() { return this.performValidation() || { isValid: true, errors: [] }; } getValue() { return (0, utils_1.getElementValue)(this._element); } setValue(value) { (0, utils_1.setElementValue)(this._element, value); this.emit('input', { value }); // Trigger auto-resize and validation if (this._textareaConfig.autoResize) { this.autoResize(); } if (this._textareaConfig.validateOn === 'change') { this._debouncedValidate(); } } clearErrors() { this._validationResult = null; validators_1.globalValidationManager.clearValidation(this._element); this._element.classList.remove('np-error'); this._element.setAttribute('aria-invalid', 'false'); this._element.style.boxShadow = this.createShadowStyle('inset'); } focus() { this.textareaElement.focus(); } blur() { this.textareaElement.blur(); } select() { this.textareaElement.select(); } isValid() { return this._validationResult ? this._validationResult.isValid : true; } getValidationResult() { return this._validationResult; } // Textarea-specific methods insertAtCursor(text) { const element = this.textareaElement; const startPos = element.selectionStart || 0; const endPos = element.selectionEnd || 0; const value = element.value; element.value = value.substring(0, startPos) + text + value.substring(endPos); element.selectionStart = element.selectionEnd = startPos + text.length; if (this._textareaConfig.autoResize) { this.autoResize(); } this.emit('input', { value: element.value }); } getSelection() { const element = this.textareaElement; return { start: element.selectionStart || 0, end: element.selectionEnd || 0, text: element.value.substring(element.selectionStart || 0, element.selectionEnd || 0) }; } setSelection(start, end) { this.textareaElement.setSelectionRange(start, end); } onUpdate(newConfig) { const oldConfig = { ...this._textareaConfig }; this._textareaConfig = { ...this._textareaConfig, ...newConfig }; // Update placeholder if (newConfig.placeholder !== oldConfig.placeholder) { this.applyPlaceholderStyles(); } // Update auto-resize settings if (newConfig.autoResize !== oldConfig.autoResize) { if (newConfig.autoResize) { this.setupAutoResize(); } else { this._element.style.resize = 'vertical'; if (this._resizeObserver) { this._resizeObserver.disconnect(); this._resizeObserver = undefined; } } } // Update height constraints if (newConfig.minHeight !== oldConfig.minHeight) { this._element.style.minHeight = newConfig.minHeight; } if (newConfig.maxHeight !== oldConfig.maxHeight) { this._element.style.maxHeight = newConfig.maxHeight; } // Update validation configuration if (newConfig.validate !== oldConfig.validate) { this.clearErrors(); if (newConfig.validate) { this.setupValidation(); } } } onDestroy() { validators_1.globalValidationManager.clearValidation(this._element); if (this._resizeObserver) { this._resizeObserver.disconnect(); this._resizeObserver = undefined; } } } exports.TextareaComponent = TextareaComponent; // Factory function for easy usage function textarea(element, config = {}) { return new TextareaComponent(element, config); }