Initial Commit

This commit is contained in:
2026-02-12 17:10:03 -06:00
commit 6992a06105
135 changed files with 18014 additions and 0 deletions
+337
View File
@@ -0,0 +1,337 @@
// saladui/core/collection-manager.js
/**
* Collection utility for SaladUI components
* Manages collections of items with focus, highlight, and selection states
*/
class Collection {
/**
* Create a collection manager
*
* @param {Object} options - Configuration options
* @param {string} options.type - Collection type: 'single' or 'multiple'
* @param {Array|*} options.defaultValue - Default value(s) if none provided
* @param {Array|*} options.value - Initial value(s) for the collection
* @param {function} options.getItemValue - Function to get value from an item instance
* @param {function} options.isItemDisabled - Function to check if an item is disabled
*/
constructor(options = {}) {
this.options = {
type: "single",
defaultValue: null,
value: null,
getItemValue: (item) => item.value,
isItemDisabled: (item) => item.disabled,
...options,
};
// Initialize collection
this.items = [];
this.focusedItem = null;
// Initialize values
this.values = [];
if (this.options.value !== null && this.options.value !== undefined) {
this.setValues(this.options.value);
} else if (
this.options.defaultValue !== null &&
this.options.defaultValue !== undefined
) {
this.setValues(this.options.defaultValue);
}
}
/**
* Reset the collection
*/
reset() {
this.items = [];
this.values = Array.isArray(this.options.defaultValue)
? [...this.options.defaultValue]
: this.options.defaultValue
? [this.options.defaultValue]
: [];
this.focusedItem = null;
}
/**
* Set collection values
*
* @param {Array|*} values - Value or array of values
*/
setValues(values) {
if (values === undefined || values === null) {
this.values = Array.isArray(this.options.defaultValue)
? [...this.options.defaultValue]
: this.options.defaultValue
? [this.options.defaultValue]
: [];
return;
}
if (this.options.type === "single") {
this.values = Array.isArray(values) ? [values[0]] : [values];
} else {
this.values = Array.isArray(values) ? [...values] : [values];
}
// Update selected state for all items
this.updateSelectedStates();
}
/**
* Get the selected value(s)
* For 'multiple' type collections, returns an array of all selected values
* For 'single' type collections, returns just the first selected value (or null if none)
*
* @param {boolean} asArray - Force return value to be an array even for single selection
* @returns {*|Array} Selected value(s)
*/
getValue(asArray = false) {
if (this.options.type === "multiple" || asArray) {
return [...this.values];
}
return this.values.length > 0 ? this.values[0] : null;
}
/**
* Add an item to the collection
*
* @param {Object} item - Item instance to add
* @param {*} value - Item value
* @returns {Object} The collection item wrapper
*/
add(item) {
const itemValue = this.options.getItemValue(item);
const isSelected = this.values.includes(itemValue);
const collectionItem = {
instance: item,
value: itemValue,
focused: false,
selected: isSelected,
};
this.items.push(collectionItem);
// Initialize item state
if (isSelected && typeof item.handleEvent === "function") {
item.handleEvent("select");
}
return collectionItem;
}
/**
* Remove an item from the collection
*
* @param {Object} itemInstance - Item instance to remove
*/
remove(itemInstance) {
const index = this.items.findIndex(
(item) => item.instance === itemInstance,
);
if (index >= 0) {
const [removedItem] = this.items.splice(index, 1);
if (this.focusedItem === removedItem) {
this.focusedItem = null;
}
// Remove from values if selected
if (removedItem.selected) {
this.values = this.values.filter(
(value) => value !== removedItem.value,
);
}
}
}
/**
* Clear all items from the collection
*/
clear() {
this.items = [];
this.focusedItem = null;
}
/**
* Get item by its instance
*
* @param {Object} itemInstance - Item instance to find
* @returns {Object} Collection item or null if not found
*/
getItemByInstance(itemInstance) {
return this.items.find((item) => item.instance === itemInstance) || null;
}
/**
* Get item by its value
*
* @param {*} value - Value to find
* @returns {Object} Collection item or null if not found
*/
getItemByValue(value) {
return this.items.find((item) => item.value === value) || null;
}
/**
* Get an item from the collection based on navigation direction
*
* @param {string} direction - Navigation direction: 'first', 'last', 'next', or 'prev'/'previous'
* @param {Object} referenceItem - Reference item for 'next' and 'prev' directions (optional)
* @param {boolean} loop - Whether to loop when reaching boundaries (optional, default: true)
* @returns {Object} The requested item or null if not found
*/
getItem(direction, referenceItem = null, loop = true) {
const enabledItems = this.items.filter(
(item) => !this.options.isItemDisabled(item.instance),
);
if (enabledItems.length === 0) return null;
switch (direction) {
case "first":
return enabledItems[0];
case "last":
return enabledItems[enabledItems.length - 1];
case "next":
if (!referenceItem) return this.getItem("first");
const nextIndex = enabledItems.indexOf(referenceItem) + 1;
if (nextIndex >= enabledItems.length) {
return loop ? enabledItems[0] : null;
}
return enabledItems[nextIndex];
case "prev":
case "previous":
if (!referenceItem) return this.getItem("last");
const currentIndex = enabledItems.indexOf(referenceItem);
if (currentIndex === -1) return enabledItems[enabledItems.length - 1];
const prevIndex = currentIndex - 1;
if (prevIndex < 0) {
return loop ? enabledItems[enabledItems.length - 1] : null;
}
return enabledItems[prevIndex];
default:
return null;
}
}
/**
* Focus an item
*
* @param {Object} item - Item to focus
* @returns {boolean} Whether the operation was successful
*/
focus(item) {
if (!item || this.options.isItemDisabled(item.instance)) return false;
// Clear previous focus
if (this.focusedItem) {
this.focusedItem.focused = false;
if (typeof this.focusedItem.instance.handleEvent === "function") {
this.focusedItem.instance.handleEvent("blur");
}
}
// Set new focus
this.focusedItem = item;
item.focused = true;
if (typeof item.instance.handleEvent === "function") {
return item.instance.handleEvent("focus") !== false;
}
return true;
}
/**
* Select an item
*
* @param {Object} item - Item to select
* @returns {boolean} Whether the operation was successful
*/
select(item) {
if (!item || this.options.isItemDisabled(item.instance)) return false;
const isMultiple = this.options.type === "multiple";
// If it's already selected in single mode, do nothing
if (!isMultiple && item.selected && this.values.length === 1) {
return true;
}
// For single selection, clear all other selections
if (!isMultiple) {
this.items.forEach((existingItem) => {
if (existingItem !== item && existingItem.selected) {
existingItem.selected = false;
if (typeof existingItem.instance.handleEvent === "function") {
existingItem.instance.handleEvent("unselect");
}
}
});
this.values = [];
}
// Toggle selection for the item
if (item.selected) {
// Unselect the item
item.selected = false;
this.values = this.values.filter((value) => value !== item.value);
if (typeof item.instance.handleEvent === "function") {
return item.instance.handleEvent("unselect") !== false;
}
} else {
// Select the item
item.selected = true;
this.values.push(item.value);
if (typeof item.instance.handleEvent === "function") {
return item.instance.handleEvent("select") !== false;
}
}
return true;
}
/**
* Update all item selected states based on values
*/
updateSelectedStates() {
this.items.forEach((item) => {
const shouldBeSelected = this.values.includes(item.value);
if (item.selected !== shouldBeSelected) {
item.selected = shouldBeSelected;
if (typeof item.instance.handleEvent === "function") {
item.instance.handleEvent(shouldBeSelected ? "select" : "unselect");
}
}
});
}
/**
* Check if a value is selected
*
* @param {*} value - Value to check
* @returns {boolean} Whether the value is selected
*/
isValueSelected(value) {
return this.values.includes(value);
}
each(callback) {
this.items.forEach((item) => callback(item.instance));
}
}
export default Collection;