UNPKG

rune-form

Version:

Type-safe reactive form builder for Svelte 5

219 lines (218 loc) 8.6 kB
// Generic helper utilities shared across the library export function escapeRegex(source) { return source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } export function evictOldestFromMap(map) { const firstKey = map.keys().next().value; if (firstKey !== undefined) { map.delete(firstKey); } } // Array utility functions function isArrayIndex(str) { return /^\d+$/.test(str); } export function parsePath(path) { return path .split('.') .map((segment) => (isArrayIndex(segment) ? parseInt(segment, 10) : segment)); } function getTouchedKeysForArray(touched, arrayPath) { return Object.keys(touched).filter((key) => key.startsWith(arrayPath + '.') && key !== arrayPath); } // Array touched state management utilities export function syncTouchedStateForArraySwap(touched, arrayPath, i, j) { // Get all touched keys for the array const touchedKeys = Object.keys(touched).filter((key) => key.startsWith(`${arrayPath}.${i}.`) || key.startsWith(`${arrayPath}.${j}.`)); // Create a temporary map to store the touched states const tempTouched = new Map(); // Store current touched states for (const key of touchedKeys) { tempTouched.set(key, touched[key]); } // First, delete all existing keys to avoid conflicts for (const key of touchedKeys) { delete touched[key]; } // Then create the new swapped keys for (const key of touchedKeys) { if (key.startsWith(`${arrayPath}.${i}.`)) { const newKey = key.replace(`${arrayPath}.${i}.`, `${arrayPath}.${j}.`); touched[newKey] = tempTouched.get(key) ?? false; } else if (key.startsWith(`${arrayPath}.${j}.`)) { const newKey = key.replace(`${arrayPath}.${j}.`, `${arrayPath}.${i}.`); touched[newKey] = tempTouched.get(key) ?? false; } } } export function syncTouchedStateForArrayRemoval(touched, arrayPath, startIndex, deleteCount) { // Get all touched keys for the array (including primitive element keys like arrayPath.0) const touchedKeys = getTouchedKeysForArray(touched, arrayPath); const escapedArrayPath = escapeRegex(arrayPath); // Store keys that need to be shifted (those after the deleted range) const keysToShift = []; // Process all touched keys for (const key of touchedKeys) { // Extract the index from the key const match = key.match(new RegExp(`^${escapedArrayPath}\\.(\\d+)`)); if (match) { const index = parseInt(match[1], 10); // If the key is in the deletion range, remove it if (index >= startIndex && index < startIndex + deleteCount) { delete touched[key]; } // If the key is after the deletion range, it needs to be shifted else if (index >= startIndex + deleteCount) { keysToShift.push(key); } } } // Shift remaining indices down shiftArrayIndicesInTouchedState(touched, keysToShift, arrayPath, startIndex + deleteCount, -deleteCount); } export function syncTouchedStateForArrayInsertion(touched, arrayPath, startIndex, insertCount) { // Get all touched keys for the array const touchedKeys = getTouchedKeysForArray(touched, arrayPath); // Shift existing indices up to make room for new items shiftArrayIndicesInTouchedState(touched, touchedKeys, arrayPath, startIndex, insertCount); } // Helper function to shift array indices in touched state function shiftArrayIndicesInTouchedState(touched, touchedKeys, arrayPath, startIndex, shiftAmount) { const escapedArrayPath = escapeRegex(arrayPath); // Match indices either followed by a dot or end of string const indexPattern = new RegExp(`^${escapedArrayPath}\\.(\\d+)(?:\\.|$)`); const fullPattern = new RegExp(`^${escapedArrayPath}\\.(\\d+)(?:\\.(.*))?$`); // Filter keys that need to be shifted const keysToShift = touchedKeys.filter((key) => { const match = key.match(indexPattern); if (!match) return false; const index = parseInt(match[1], 10); return index >= startIndex; }); // Sort by index in descending order to avoid conflicts (for insertion) if (shiftAmount > 0) { keysToShift.sort((a, b) => { const matchA = a.match(indexPattern); const matchB = b.match(indexPattern); if (!matchA || !matchB) return 0; return parseInt(matchB[1], 10) - parseInt(matchA[1], 10); }); } else { // Sort by index in ascending order for removal keysToShift.sort((a, b) => { const matchA = a.match(indexPattern); const matchB = b.match(indexPattern); if (!matchA || !matchB) return 0; return parseInt(matchA[1], 10) - parseInt(matchB[1], 10); }); } // Process each key that needs to be shifted for (const key of keysToShift) { const match = key.match(fullPattern); if (!match) continue; const currentIndex = parseInt(match[1], 10); const newIndex = currentIndex + shiftAmount; const restOfPath = match[2] || ''; // Skip if the new index would be negative if (newIndex < 0) continue; // Construct the new key const newKey = restOfPath ? `${arrayPath}.${newIndex}.${restOfPath}` : `${arrayPath}.${newIndex}`; // Move the touched state to the new key if (touched[key] !== undefined) { touched[newKey] = touched[key]; delete touched[key]; } } } // Cache cleanup utilities export function clearStaleFieldCacheEntries(fieldCache, arrayPath) { const keysToRemove = []; // Find all field cache entries that start with the array path for (const key of fieldCache.keys()) { if (key.startsWith(arrayPath)) { keysToRemove.push(key); } } // Remove the stale entries for (const key of keysToRemove) { fieldCache.delete(key); } } export function clearStalePathCacheEntries(pathCache, arrayPath) { const keysToRemove = []; // Find all path cache entries that start with the array path // We need to clear: // 1. Individual array element paths (e.g., address.parkingLots.0, address.parkingLots.1) // 2. Field paths within array elements (e.g., address.parkingLots.0.name) // BUT preserve the array path itself (e.g., address.parkingLots) for (const key of pathCache.keys()) { if (key.startsWith(arrayPath + '.') && key !== arrayPath) { keysToRemove.push(key); } } // Remove the stale entries for (const key of keysToRemove) { pathCache.delete(key); } } // Array method utilities export const MUTATING_ARRAY_METHODS = new Set([ 'splice', 'push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'fill' ]); // Array method touched state handling utilities export function handleArrayMethodTouchedState(methodName, args, target, previousLength) { switch (methodName) { case 'splice': { const [start, deleteCount = 0, ...insertItems] = args; const actualDeleteCount = deleteCount ?? target.length - start; const insertCount = insertItems.length; return { start, deleteCount: actualDeleteCount, insertCount }; } case 'pop': { const lastIndexBeforePop = (previousLength ?? target.length + 1) - 1; return { start: 0, deleteCount: 0, insertCount: 0, lastIndexBeforePop }; } case 'shift': { return { start: 0, deleteCount: 1, insertCount: 0 }; } case 'unshift': { const insertCount = args.length; return { start: 0, deleteCount: 0, insertCount }; } default: return { start: 0, deleteCount: 0, insertCount: 0 }; } } // Zod utility functions (used by zodAdapter.ts) export function flattenZodIssues(issues) { const flattened = {}; for (const issue of issues) { if (issue && typeof issue === 'object' && 'path' in issue && 'message' in issue) { const path = issue.path; const message = issue.message; if (Array.isArray(path)) { const key = path.map((p) => String(p)).join('.'); if (!flattened[key]) flattened[key] = []; flattened[key].push(message); } } } return flattened; }