338 lines
9.0 KiB
JavaScript
338 lines
9.0 KiB
JavaScript
// 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;
|