Initial Commit
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import Component from "../core/component";
|
||||
import SaladUI from "..";
|
||||
|
||||
/**
|
||||
* CommandComponent for SaladUI
|
||||
* Implements filtering, keyboard navigation, and selection for a command palette/list.
|
||||
*/
|
||||
class CommandComponent extends Component {
|
||||
constructor(el, hookContext) {
|
||||
super(el, { hookContext, ignoreItems: false });
|
||||
|
||||
// Set default field state
|
||||
this.currentItemIdx = 0;
|
||||
|
||||
// Core elements
|
||||
this.input = this.getPart("input");
|
||||
this.list = this.getPart("list");
|
||||
this.empty = this.getPart("empty");
|
||||
this.groups = this.getAllParts("group");
|
||||
this.items = this.getAllParts("item");
|
||||
|
||||
// Bind event handlers
|
||||
this.input.addEventListener("input", this.handleSearch);
|
||||
|
||||
// Initial search/filter
|
||||
this.handleSearch();
|
||||
|
||||
// Prevent default for navigation keys
|
||||
this.config.preventDefaultKeys = ["Escape", "ArrowDown", "ArrowUp"];
|
||||
}
|
||||
|
||||
getComponentConfig() {
|
||||
return {
|
||||
stateMachine: {
|
||||
idle: { transitions: {} },
|
||||
},
|
||||
events: {
|
||||
idle: {
|
||||
keyMap: {
|
||||
Enter: "selectItem",
|
||||
ArrowDown: "focusNextItem",
|
||||
ArrowUp: "focusPrevItem",
|
||||
Escape: "blurInput",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Focus item by index, wrap around if needed
|
||||
focusItem(index) {
|
||||
if (!this.selectableItems?.length) return;
|
||||
|
||||
// Wrap index
|
||||
if (index < 0) index = this.selectableItems.length - 1;
|
||||
if (index >= this.selectableItems.length) index = 0;
|
||||
|
||||
this.currentItemIdx = index;
|
||||
|
||||
// Deselect all
|
||||
this.items.forEach((item) => {
|
||||
item.setAttribute("data-selected", "false");
|
||||
item.setAttribute("aria-selected", "false");
|
||||
});
|
||||
|
||||
// Select current
|
||||
const selectedItem = this.selectableItems[index];
|
||||
selectedItem.setAttribute("data-selected", "true");
|
||||
selectedItem.setAttribute("aria-selected", "true");
|
||||
}
|
||||
|
||||
focusNextItem = () => this.focusItem(this.currentItemIdx + 1);
|
||||
focusPrevItem = () => this.focusItem(this.currentItemIdx - 1);
|
||||
|
||||
blurInput = () => this.input?.blur();
|
||||
|
||||
selectItem = () => {
|
||||
if (this.currentItemIdx === -1) return;
|
||||
const item = this.selectableItems[this.currentItemIdx];
|
||||
item.click();
|
||||
};
|
||||
|
||||
// Handle search/filtering
|
||||
handleSearch = () => {
|
||||
const query = this.input.value.trim().toLowerCase();
|
||||
|
||||
// Filter items
|
||||
this.items.forEach((item) => {
|
||||
const text = item.textContent.trim().toLowerCase();
|
||||
const visible = query === "" || text.includes(query);
|
||||
item.setAttribute("data-visible", visible ? "true" : "false");
|
||||
});
|
||||
|
||||
this.visibleItems = this.items.filter(
|
||||
(el) => el.getAttribute("data-visible") === "true",
|
||||
);
|
||||
|
||||
this.selectableItems = this.visibleItems.filter(
|
||||
(el) => !el.hasAttribute("disabled"),
|
||||
);
|
||||
|
||||
this.groups.forEach((group) => {
|
||||
const visibleOptions = group.querySelectorAll("[data-visible='true']");
|
||||
group.setAttribute(
|
||||
"data-visible",
|
||||
visibleOptions.length > 0 ? "true" : "false",
|
||||
);
|
||||
});
|
||||
|
||||
this.focusItem(0);
|
||||
|
||||
const noItems = this.visibleItems.length === 0;
|
||||
|
||||
if (noItems) {
|
||||
this.empty.setAttribute("data-visible", "true");
|
||||
} else {
|
||||
this.empty.setAttribute("data-visible", "false");
|
||||
}
|
||||
};
|
||||
|
||||
beforeDestroy() {
|
||||
this.input.removeEventListener("input", this.handleSearch);
|
||||
}
|
||||
}
|
||||
|
||||
// Register the component
|
||||
SaladUI.register("command", CommandComponent);
|
||||
|
||||
export default CommandComponent;
|
||||
Reference in New Issue
Block a user