UNPKG

neumorphic-peripheral

Version:

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

720 lines (706 loc) 26.9 kB
"use strict"; /** * Joi Validation Adapter for Neumorphic Peripheral * * This adapter allows you to use Joi schemas for validation * Install: npm install joi * * Usage: * import Joi from 'joi' * import { joiAdapter } from 'neumorphic-peripheral/adapters/joi' * * const schema = Joi.string().email().min(5) * np.input(element, { validate: joiAdapter(schema) }) */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.examples = exports.joiUtils = exports.joiIntegration = exports.JoiFormValidator = exports.joiSchemas = void 0; exports.joiAdapter = joiAdapter; exports.joiAdapterDetailed = joiAdapterDetailed; exports.joiAdapterAsync = joiAdapterAsync; exports.createJoiFormValidator = createJoiFormValidator; /** * Creates a validation function from a Joi schema */ function joiAdapter(schema) { return (value) => { const result = schema.validate(value); if (result.error) { // Return first error message from Joi return result.error.details[0]?.message || 'Invalid value'; } return null; // Valid }; } /** * Creates a comprehensive validation function that returns all errors */ function joiAdapterDetailed(schema) { return (value) => { const result = schema.validate(value, { abortEarly: false }); if (result.error) { const errors = result.error.details.map((detail) => detail.message); return { isValid: false, errors }; } return { isValid: true, errors: [] }; }; } /** * Async validation adapter for Joi schemas */ function joiAdapterAsync(schema) { return async (value) => { try { await schema.validateAsync(value, { abortEarly: false }); return { isValid: true, errors: [] }; } catch (error) { const errors = []; if (error.details && Array.isArray(error.details)) { errors.push(...error.details.map((detail) => detail.message)); } else if (error.message) { errors.push(error.message); } else { errors.push('Invalid value'); } return { isValid: false, errors }; } }; } /** * Schema builder utilities for common patterns */ exports.joiSchemas = { email: () => { return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => Joi.default.string().email({ tlds: { allow: false } }).required().messages({ 'string.email': 'Please enter a valid email address', 'any.required': 'Email is required' })); }, password: (minLength = 8) => { return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => Joi.default.string() .min(minLength) .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/) .required() .messages({ 'string.min': `Password must be at least ${minLength} characters`, 'string.pattern.base': 'Password must contain uppercase, lowercase, number and special character', 'any.required': 'Password is required' })); }, phone: () => { return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => Joi.default.string() .pattern(/^\+?[1-9]\d{1,14}$/) .required() .messages({ 'string.pattern.base': 'Please enter a valid phone number', 'any.required': 'Phone number is required' })); }, url: () => { return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => Joi.default.string().uri().required().messages({ 'string.uri': 'Please enter a valid URL', 'any.required': 'URL is required' })); }, required: (message = 'This field is required') => { return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => Joi.default.string().required().messages({ 'any.required': message })); }, number: (min, max) => { return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => { let schema = Joi.default.number(); if (min !== undefined) { schema = schema.min(min); } if (max !== undefined) { schema = schema.max(max); } return schema.required().messages({ 'number.base': 'Must be a number', 'number.min': `Must be at least ${min}`, 'number.max': `Must be no more than ${max}`, 'any.required': 'This field is required' }); }); }, /** * Date validation schemas */ date: () => { return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => ({ birthDate: Joi.default.date() .max('now') .min('1900-01-01') .messages({ 'date.max': 'Birth date cannot be in the future', 'date.min': 'Birth date must be after 1900' }), futureDate: Joi.default.date() .min('now') .messages({ 'date.min': 'Date must be in the future' }), pastDate: Joi.default.date() .max('now') .messages({ 'date.max': 'Date must be in the past' }), ageRange: (minAge, maxAge) => Joi.default.date() .max(new Date(Date.now() - minAge * 365.25 * 24 * 60 * 60 * 1000)) .min(new Date(Date.now() - maxAge * 365.25 * 24 * 60 * 60 * 1000)) .messages({ 'date.max': `Must be at least ${minAge} years old`, 'date.min': `Must be no more than ${maxAge} years old` }) })); } }; /** * Form-level validation using Joi */ class JoiFormValidator { constructor(schema) { this.fields = new Map(); this.schema = schema; } /** * Register a field with its schema key */ registerField(element, key) { this.fields.set(element, key); } /** * Validate a specific field */ validateField(element) { const key = this.fields.get(element); if (!key) { return { isValid: true, errors: [] }; } const value = this.getElementValue(element); try { // Try to extract field schema from object schema let fieldSchema; if (this.schema.extract) { fieldSchema = this.schema.extract(key); } else { // If extract is not available, create a simple test const testData = { [key]: value }; const result = this.schema.validate(testData, { abortEarly: false, allowUnknown: true }); if (result.error) { const fieldErrors = result.error.details .filter((detail) => detail.path[0] === key) .map((detail) => detail.message); return { isValid: fieldErrors.length === 0, errors: fieldErrors }; } return { isValid: true, errors: [] }; } const result = fieldSchema.validate(value, { abortEarly: false }); if (result.error) { const errors = result.error.details.map((detail) => detail.message); return { isValid: false, errors }; } return { isValid: true, errors: [] }; } catch (error) { // Fallback: validate the whole object with just this field const testData = { [key]: value }; const result = this.schema.validate(testData, { abortEarly: false, allowUnknown: true }); if (result.error) { const fieldErrors = result.error.details .filter((detail) => detail.path[0] === key) .map((detail) => detail.message); return { isValid: fieldErrors.length === 0, errors: fieldErrors }; } return { isValid: true, errors: [] }; } } /** * Async field validation */ async validateFieldAsync(element) { const key = this.fields.get(element); if (!key) { return { isValid: true, errors: [] }; } const value = this.getElementValue(element); try { // Try async validation let fieldSchema; if (this.schema.extract) { fieldSchema = this.schema.extract(key); await fieldSchema.validateAsync(value, { abortEarly: false }); } else { // Fallback to full object validation const testData = { [key]: value }; const result = await this.schema.validateAsync(testData, { abortEarly: false, allowUnknown: true }); return { isValid: true, errors: [] }; } return { isValid: true, errors: [] }; } catch (error) { const errors = []; if (error.details && Array.isArray(error.details)) { errors.push(...error.details.map((detail) => detail.message)); } else if (error.message) { errors.push(error.message); } else { errors.push('Invalid value'); } return { isValid: false, errors }; } } /** * Validate entire form */ validateForm() { const formData = {}; const errors = {}; // Collect all field values this.fields.forEach((key, element) => { formData[key] = this.getElementValue(element); }); const result = this.schema.validate(formData, { abortEarly: false }); if (result.error) { result.error.details.forEach((detail) => { const path = detail.path.join('.'); if (!errors[path]) { errors[path] = []; } errors[path].push(detail.message); }); return { isValid: false, errors }; } return { isValid: true, errors: {}, data: result.value }; } /** * Async form validation */ async validateFormAsync() { const formData = {}; const errors = {}; // Collect all field values this.fields.forEach((key, element) => { formData[key] = this.getElementValue(element); }); try { const result = await this.schema.validateAsync(formData, { abortEarly: false }); return { isValid: true, errors: {}, data: result }; } catch (error) { if (error.details && Array.isArray(error.details)) { error.details.forEach((detail) => { const path = detail.path.join('.'); if (!errors[path]) { errors[path] = []; } errors[path].push(detail.message); }); } return { isValid: false, errors }; } } getElementValue(element) { if (element instanceof HTMLInputElement) { if (element.type === 'checkbox') { return element.checked; } if (element.type === 'number') { return element.value ? Number(element.value) : undefined; } if (element.type === 'date') { return element.value ? new Date(element.value) : undefined; } return element.value; } if (element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) { return element.value; } return element.textContent || ''; } } exports.JoiFormValidator = JoiFormValidator; /** * Helper function to create form validator */ function createJoiFormValidator(schema) { return new JoiFormValidator(schema); } /** * Integration with neumorphic components */ exports.joiIntegration = { /** * Setup form validation with Joi schema */ setupForm(formElement, schema, components) { const validator = new JoiFormValidator(schema); // Register all components with their field names Object.entries(components).forEach(([key, component]) => { if (component && component.element) { validator.registerField(component.element, key); // Setup real-time validation component.element.addEventListener('blur', () => { const result = validator.validateField(component.element); if (!result.isValid && component.clearErrors && component.validate) { component.clearErrors(); // Create a validation function that returns the Joi result component.validate = () => result; component.updateValidationState?.(); } }); } }); // Setup form submission validation formElement.addEventListener('submit', (e) => { e.preventDefault(); const result = validator.validateForm(); if (result.isValid) { // Form is valid, can submit const event = new CustomEvent('np:form-valid', { detail: { data: result.data } }); formElement.dispatchEvent(event); } else { // Show field-specific errors Object.entries(result.errors).forEach(([fieldName, fieldErrors]) => { const component = components[fieldName]; if (component && component.clearErrors) { component.clearErrors(); // Set custom validation result component._validationResult = { isValid: false, errors: fieldErrors }; component.updateValidationState?.(); } }); const event = new CustomEvent('np:form-invalid', { detail: { errors: result.errors } }); formElement.dispatchEvent(event); } }); return validator; }, /** * Setup async form validation */ setupAsyncForm(formElement, schema, components) { const validator = new JoiFormValidator(schema); // Register all components with their field names Object.entries(components).forEach(([key, component]) => { if (component && component.element) { validator.registerField(component.element, key); // Setup real-time async validation component.element.addEventListener('blur', async () => { const result = await validator.validateFieldAsync(component.element); if (!result.isValid && component.clearErrors && component.validate) { component.clearErrors(); component.validate = () => result; component.updateValidationState?.(); } }); } }); // Setup async form submission validation formElement.addEventListener('submit', async (e) => { e.preventDefault(); const result = await validator.validateFormAsync(); if (result.isValid) { const event = new CustomEvent('np:form-valid', { detail: { data: result.data } }); formElement.dispatchEvent(event); } else { // Show field-specific errors Object.entries(result.errors).forEach(([fieldName, fieldErrors]) => { const component = components[fieldName]; if (component && component.clearErrors) { component.clearErrors(); component._validationResult = { isValid: false, errors: fieldErrors }; component.updateValidationState?.(); } }); const event = new CustomEvent('np:form-invalid', { detail: { errors: result.errors } }); formElement.dispatchEvent(event); } }); return validator; }, /** * Setup conditional validation based on other fields */ setupConditionalValidation(component, schemaBuilder, getFormData) { if (!component || !component.element) return; component.element.addEventListener('blur', () => { try { const formData = getFormData(); const schema = schemaBuilder(formData); const fieldName = component.element.name || component.element.id; const value = component.getValue ? component.getValue() : component.element.value; let fieldSchema; if (schema.extract) { fieldSchema = schema.extract(fieldName); } else { // Use the full schema fieldSchema = schema; } const result = fieldSchema.validate(value); if (result.error) { if (component.clearErrors && component.validate) { component.clearErrors(); component._validationResult = { isValid: false, errors: [result.error.details[0]?.message || 'Invalid value'] }; component.updateValidationState?.(); } } else { if (component.clearErrors) { component.clearErrors(); } } } catch (error) { console.warn('Conditional validation error:', error.message); } }); } }; /** * Advanced Joi utilities for neumorphic components */ exports.joiUtils = { /** * Create a cross-field validation schema */ createCrossFieldValidation: async (dependencies) => { const Joi = await Promise.resolve().then(() => __importStar(require('joi'))); return Joi.default.object().custom((value, helpers) => { // Example: password confirmation if (dependencies.includes('password') && dependencies.includes('confirmPassword')) { if (value.password !== value.confirmPassword) { return helpers.error('any.invalid', { message: 'Passwords must match' }); } } return value; }); }, /** * Create async validation (e.g., for username availability) */ createAsyncValidation: (checkFunction, errorMessage = 'Value is not available') => { return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => Joi.default.string().external(async (value) => { if (!value) return value; // Let required handle empty values try { const isValid = await checkFunction(value); if (!isValid) { throw new Error(errorMessage); } return value; } catch (error) { throw new Error(errorMessage); } })); }, /** * Create conditional validation based on other fields */ createConditionalValidation: (condition, trueSchema, falseSchema) => { return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => Joi.default.alternatives().conditional(Joi.default.ref('$context'), { is: condition, then: trueSchema, otherwise: falseSchema || Joi.default.any() })); }, /** * Create date validation with relative constraints */ createDateValidation: () => { return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => ({ birthDate: Joi.default.date() .max('now') .min('1900-01-01') .messages({ 'date.max': 'Birth date cannot be in the future', 'date.min': 'Birth date must be after 1900' }), futureDate: Joi.default.date() .min('now') .messages({ 'date.min': 'Date must be in the future' }), pastDate: Joi.default.date() .max('now') .messages({ 'date.max': 'Date must be in the past' }), ageRange: (minAge, maxAge) => Joi.default.date() .max(new Date(Date.now() - minAge * 365.25 * 24 * 60 * 60 * 1000)) .min(new Date(Date.now() - maxAge * 365.25 * 24 * 60 * 60 * 1000)) .messages({ 'date.max': `Must be at least ${minAge} years old`, 'date.min': `Must be no more than ${maxAge} years old` }) })); }, /** * Create file validation schemas */ createFileValidation: () => { return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => ({ imageFile: Joi.default.object({ mimetype: Joi.default.string().valid('image/jpeg', 'image/png', 'image/gif', 'image/webp') .messages({ 'any.only': 'Only JPEG, PNG, GIF, and WebP images are allowed' }), size: Joi.default.number().max(5 * 1024 * 1024) // 5MB .messages({ 'number.max': 'File size must be less than 5MB' }) }), documentFile: Joi.default.object({ mimetype: Joi.default.string().valid('application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') .messages({ 'any.only': 'Only PDF and Word documents are allowed' }), size: Joi.default.number().max(10 * 1024 * 1024) // 10MB .messages({ 'number.max': 'File size must be less than 10MB' }) }) })); } }; // Example usage documentation exports.examples = { basicField: ` import Joi from 'joi' import { joiAdapter } from 'neumorphic-peripheral/adapters/joi' const emailSchema = Joi.string().email().required() np.input(emailElement, { validate: joiAdapter(emailSchema) }) `, asyncValidation: ` import { joiUtils } from 'neumorphic-peripheral/adapters/joi' const checkUsernameAvailability = async (username) => { const response = await fetch(\`/api/check-username/\${username}\`) return response.ok } const usernameSchema = await joiUtils.createAsyncValidation( checkUsernameAvailability, 'Username is already taken' ) np.input(usernameElement, { validate: joiAdapterAsync(usernameSchema) }) `, complexForm: ` import Joi from 'joi' import { joiIntegration } from 'neumorphic-peripheral/adapters/joi' const formSchema = Joi.object({ email: Joi.string().email().required(), password: Joi.string().min(8).required(), confirmPassword: Joi.string() .valid(Joi.ref('password')) .required() .messages({ 'any.only': 'Passwords must match' }), age: Joi.number().min(18).required().messages({ 'number.min': 'Must be at least 18 years old' }) }) const components = { email: np.input(emailEl), password: np.password(passwordEl), confirmPassword: np.password(confirmEl), age: np.input(ageEl) } const validator = joiIntegration.setupForm( formElement, formSchema, components ) `, dateValidation: ` import { joiUtils } from 'neumorphic-peripheral/adapters/joi' const dateSchemas = await joiUtils.createDateValidation() // Birth date validation np.input(birthDateEl, { validate: joiAdapter(dateSchemas.birthDate) }) // Age range validation (18-65 years old) np.input(dobEl, { validate: joiAdapter(dateSchemas.ageRange(18, 65)) }) `, fileValidation: ` import { joiUtils } from 'neumorphic-peripheral/adapters/joi' const fileSchemas = await joiUtils.createFileValidation() // Image file validation np.input(imageUploadEl, { validate: joiAdapter(fileSchemas.imageFile) }) // Document file validation np.input(documentUploadEl, { validate: joiAdapter(fileSchemas.documentFile) }) ` };