Initial Commit
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user