130 lines
3.4 KiB
JavaScript
130 lines
3.4 KiB
JavaScript
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;
|