✅ Complete Step 16 by extracting and testing the useJsonFilter composable with comprehensive functionality and test coverage
This commit is contained in:
350
code/websites/pokedex.online/src/composables/useJsonFilter.js
Normal file
350
code/websites/pokedex.online/src/composables/useJsonFilter.js
Normal file
@@ -0,0 +1,350 @@
|
||||
/**
|
||||
* useJsonFilter Composable
|
||||
*
|
||||
* Manages JSON path filtering and property-based data filtering.
|
||||
* Handles path extraction from JSON objects and filtering by property values.
|
||||
*/
|
||||
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { getValueByPath } from '../utilities/json-utils.js';
|
||||
|
||||
/**
|
||||
* Get value from object using dot notation path
|
||||
* @param {Object} obj - Object to get value from
|
||||
* @param {string} path - Dot notation path
|
||||
* @returns {any} Value at path
|
||||
*/
|
||||
function extractValueByPath(obj, path) {
|
||||
return getValueByPath(obj, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all unique paths from an object recursively
|
||||
* @param {Object} obj - Object to extract from
|
||||
* @param {string} prefix - Current path prefix
|
||||
* @param {Set} paths - Set to accumulate paths
|
||||
* @param {number} maxDepth - Maximum recursion depth
|
||||
* @param {number} currentDepth - Current depth
|
||||
*/
|
||||
function extractPathsRecursive(
|
||||
obj,
|
||||
prefix = '',
|
||||
paths = new Set(),
|
||||
maxDepth = 5,
|
||||
currentDepth = 0
|
||||
) {
|
||||
if (currentDepth >= maxDepth || obj === null || typeof obj !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(obj).forEach(key => {
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
paths.add(path);
|
||||
|
||||
if (
|
||||
typeof obj[key] === 'object' &&
|
||||
obj[key] !== null &&
|
||||
!Array.isArray(obj[key])
|
||||
) {
|
||||
extractPathsRecursive(obj[key], path, paths, maxDepth, currentDepth + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default function useJsonFilter() {
|
||||
// State
|
||||
const filterProperty = ref('');
|
||||
const filterValue = ref('');
|
||||
const filterMode = ref('equals'); // equals, contains, regex
|
||||
const availablePaths = ref([]);
|
||||
const isFiltering = ref(false);
|
||||
const filterError = ref(null);
|
||||
|
||||
// Data to filter (referenced, not copied)
|
||||
let rawData = null;
|
||||
|
||||
/**
|
||||
* Initialize filter with raw data
|
||||
* @param {Array} data - Array of objects to filter
|
||||
* @param {number} sampleSize - Number of items to sample for path extraction
|
||||
*/
|
||||
function initializeFilter(data, sampleSize = 100) {
|
||||
rawData = data;
|
||||
extractPathsFromData(data, sampleSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract paths from data array
|
||||
* @param {Array} data - Data to extract from
|
||||
* @param {number} sampleSize - Sample size for extraction
|
||||
*/
|
||||
function extractPathsFromData(data, sampleSize = 100) {
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
availablePaths.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const paths = new Set();
|
||||
const sample = data.slice(0, Math.min(sampleSize, data.length));
|
||||
|
||||
sample.forEach(item => {
|
||||
if (typeof item === 'object' && item !== null) {
|
||||
extractPathsRecursive(item, '', paths);
|
||||
}
|
||||
});
|
||||
|
||||
availablePaths.value = Array.from(paths)
|
||||
.sort()
|
||||
.map(path => ({
|
||||
path,
|
||||
breadcrumb: path.replace(/\./g, ' › ')
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue extracting paths from remaining items (lazy)
|
||||
* @param {Array} data - Full data array
|
||||
* @param {number} startIndex - Where to start
|
||||
* @param {Function} callback - Callback with new paths
|
||||
* @param {number} chunkSize - Items per chunk
|
||||
*/
|
||||
function extractPathsLazy(
|
||||
data,
|
||||
startIndex = 100,
|
||||
callback,
|
||||
chunkSize = 100
|
||||
) {
|
||||
if (!Array.isArray(data) || startIndex >= data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingPaths = new Set(availablePaths.value.map(p => p.path));
|
||||
|
||||
const processChunk = index => {
|
||||
const end = Math.min(index + chunkSize, data.length);
|
||||
const chunk = data.slice(index, end);
|
||||
const newPaths = new Set(existingPaths);
|
||||
|
||||
chunk.forEach(item => {
|
||||
if (typeof item === 'object' && item !== null) {
|
||||
extractPathsRecursive(item, '', newPaths);
|
||||
}
|
||||
});
|
||||
|
||||
const addedPaths = Array.from(newPaths).filter(p => !existingPaths.has(p));
|
||||
|
||||
if (addedPaths.length > 0) {
|
||||
addedPaths.forEach(p => existingPaths.add(p));
|
||||
availablePaths.value = Array.from(existingPaths)
|
||||
.sort()
|
||||
.map(path => ({
|
||||
path,
|
||||
breadcrumb: path.replace(/\./g, ' › ')
|
||||
}));
|
||||
|
||||
if (callback) {
|
||||
callback(
|
||||
addedPaths.map(path => ({
|
||||
path,
|
||||
breadcrumb: path.replace(/\./g, ' › ')
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (end < data.length) {
|
||||
if (typeof requestIdleCallback !== 'undefined') {
|
||||
requestIdleCallback(() => processChunk(end));
|
||||
} else {
|
||||
setTimeout(() => processChunk(end), 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof requestIdleCallback !== 'undefined') {
|
||||
requestIdleCallback(() => processChunk(startIndex));
|
||||
} else {
|
||||
setTimeout(() => processChunk(startIndex), 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter data by property value
|
||||
* @returns {Array} Filtered items
|
||||
*/
|
||||
const filteredData = computed(() => {
|
||||
if (!rawData || !Array.isArray(rawData)) {
|
||||
return rawData || [];
|
||||
}
|
||||
|
||||
// Return all data if no filter is set or if there's a filter error
|
||||
if (!filterProperty.value || filterError.value) {
|
||||
return rawData;
|
||||
}
|
||||
|
||||
return rawData.filter(item => {
|
||||
try {
|
||||
const value = extractValueByPath(item, filterProperty.value);
|
||||
return matchesFilter(value, filterValue.value, filterMode.value);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Check if value matches filter criteria
|
||||
* @param {any} value - Value to check
|
||||
* @param {string} filterVal - Filter value
|
||||
* @param {string} mode - Filter mode (equals, contains, regex)
|
||||
* @returns {boolean} Whether value matches
|
||||
*/
|
||||
function matchesFilter(value, filterVal, mode) {
|
||||
if (!filterVal) return true;
|
||||
|
||||
const strValue = String(value).toLowerCase();
|
||||
const strFilter = String(filterVal).toLowerCase();
|
||||
|
||||
switch (mode) {
|
||||
case 'equals':
|
||||
return strValue === strFilter;
|
||||
case 'contains':
|
||||
return strValue.includes(strFilter);
|
||||
case 'regex':
|
||||
try {
|
||||
const regex = new RegExp(filterVal, 'i');
|
||||
return regex.test(strValue);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unique values for a property
|
||||
* @param {string} property - Property path
|
||||
* @returns {Array} Sorted unique values
|
||||
*/
|
||||
function getUniqueValues(property) {
|
||||
if (!rawData || !Array.isArray(rawData) || !property) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const values = new Set();
|
||||
rawData.forEach(item => {
|
||||
try {
|
||||
const value = extractValueByPath(item, property);
|
||||
if (value !== undefined && value !== null) {
|
||||
values.add(String(value));
|
||||
}
|
||||
} catch {
|
||||
// Silently skip items where path doesn't exist
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(values).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
function clearFilters() {
|
||||
filterProperty.value = '';
|
||||
filterValue.value = '';
|
||||
filterMode.value = 'equals';
|
||||
filterError.value = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set filter from user input
|
||||
* @param {string} property - Property to filter by
|
||||
* @param {string} value - Value to filter
|
||||
* @param {string} mode - Filter mode
|
||||
*/
|
||||
function setFilter(property, value, mode = 'equals') {
|
||||
try {
|
||||
filterProperty.value = property;
|
||||
filterValue.value = value;
|
||||
filterMode.value = mode;
|
||||
filterError.value = null;
|
||||
|
||||
// Validate regex if mode is regex
|
||||
if (mode === 'regex') {
|
||||
new RegExp(value);
|
||||
}
|
||||
} catch (err) {
|
||||
filterError.value = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if filter is active
|
||||
*/
|
||||
const hasActiveFilter = computed(() => {
|
||||
return filterProperty.value && (filterValue.value || filterMode.value);
|
||||
});
|
||||
|
||||
/**
|
||||
* Get filter description for display
|
||||
*/
|
||||
const filterDescription = computed(() => {
|
||||
if (!hasActiveFilter.value) {
|
||||
return 'No filter applied';
|
||||
}
|
||||
|
||||
const property = filterProperty.value;
|
||||
const value = filterValue.value || '*';
|
||||
const mode = filterMode.value;
|
||||
|
||||
const modeLabel = {
|
||||
equals: '=',
|
||||
contains: '∋',
|
||||
regex: '~'
|
||||
}[mode] || mode;
|
||||
|
||||
return `${property} ${modeLabel} ${value}`;
|
||||
});
|
||||
|
||||
/**
|
||||
* Get filter statistics
|
||||
*/
|
||||
const filterStats = computed(() => {
|
||||
const total = rawData?.length || 0;
|
||||
const filtered = filteredData.value.length;
|
||||
|
||||
return {
|
||||
total,
|
||||
filtered,
|
||||
matched: filtered,
|
||||
filtered: total - filtered,
|
||||
percentage: total > 0 ? Math.round((filtered / total) * 100) : 0
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
// State
|
||||
filterProperty,
|
||||
filterValue,
|
||||
filterMode,
|
||||
availablePaths,
|
||||
isFiltering,
|
||||
filterError,
|
||||
|
||||
// Computed
|
||||
filteredData,
|
||||
hasActiveFilter,
|
||||
filterDescription,
|
||||
filterStats,
|
||||
|
||||
// Methods
|
||||
initializeFilter,
|
||||
extractPathsFromData,
|
||||
extractPathsLazy,
|
||||
matchesFilter,
|
||||
getUniqueValues,
|
||||
clearFilters,
|
||||
setFilter
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user