neumorphic-peripheral
Version:
A lightweight, framework-agnostic JavaScript/TypeScript library for beautiful neumorphic styling
311 lines (310 loc) • 12.1 kB
JavaScript
"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);
}