| import { afterSleep, afterTick, srOnlyStyles, attachRef, } from "svelte-toolbelt"; |
| import { Context, watch } from "runed"; |
| import { findNextSibling, findPreviousSibling } from "./utils.js"; |
| import { kbd } from "../../internal/kbd.js"; |
| import { createBitsAttrs, boolToStr, boolToEmptyStrOrUndef } from "../../internal/attrs.js"; |
| import { getFirstNonCommentChild } from "../../internal/dom.js"; |
| import { computeCommandScore } from "./index.js"; |
| import { cssEscape } from "../../internal/css-escape.js"; |
| const COMMAND_VALUE_ATTR = "data-value"; |
| const commandAttrs = createBitsAttrs({ |
| component: "command", |
| parts: [ |
| "root", |
| "list", |
| "input", |
| "separator", |
| "loading", |
| "empty", |
| "group", |
| "group-items", |
| "group-heading", |
| "item", |
| "viewport", |
| "input-label", |
| ], |
| }); |
| |
| const COMMAND_GROUP_SELECTOR = commandAttrs.selector("group"); |
| const COMMAND_GROUP_ITEMS_SELECTOR = commandAttrs.selector("group-items"); |
| const COMMAND_GROUP_HEADING_SELECTOR = commandAttrs.selector("group-heading"); |
| const COMMAND_ITEM_SELECTOR = commandAttrs.selector("item"); |
| const COMMAND_VALID_ITEM_SELECTOR = `${commandAttrs.selector("item")}:not([aria-disabled="true"])`; |
| const CommandRootContext = new Context("Command.Root"); |
| const CommandListContext = new Context("Command.List"); |
| const CommandGroupContainerContext = new Context("Command.Group"); |
| const defaultState = { |
| |
| search: "", |
| |
| value: "", |
| filtered: { |
| |
| count: 0, |
| |
| items: new Map(), |
| |
| groups: new Set(), |
| }, |
| }; |
| export class CommandRootState { |
| static create(opts) { |
| return CommandRootContext.set(new CommandRootState(opts)); |
| } |
| opts; |
| attachment; |
| #updateScheduled = false; |
| #isInitialMount = true; |
| sortAfterTick = false; |
| sortAndFilterAfterTick = false; |
| allItems = new Set(); |
| allGroups = new Map(); |
| allIds = new Map(); |
| |
| key = $state(0); |
| viewportNode = $state(null); |
| inputNode = $state(null); |
| labelNode = $state(null); |
| |
| commandState = $state.raw(defaultState); |
| |
| _commandState = $state(defaultState); |
| #snapshot() { |
| return $state.snapshot(this._commandState); |
| } |
| #scheduleUpdate() { |
| if (this.#updateScheduled) |
| return; |
| this.#updateScheduled = true; |
| afterTick(() => { |
| this.#updateScheduled = false; |
| const currentState = this.#snapshot(); |
| const hasStateChanged = !Object.is(this.commandState, currentState); |
| if (hasStateChanged) { |
| this.commandState = currentState; |
| this.opts.onStateChange?.current?.(currentState); |
| } |
| }); |
| } |
| setState(key, value, preventScroll) { |
| if (Object.is(this._commandState[key], value)) |
| return; |
| this._commandState[key] = value; |
| if (key === "search") { |
| |
| this.#filterItems(); |
| this.#sort(); |
| } |
| else if (key === "value") { |
| if (!preventScroll) |
| this.#scrollSelectedIntoView(); |
| } |
| this.#scheduleUpdate(); |
| } |
| constructor(opts) { |
| this.opts = opts; |
| this.attachment = attachRef(this.opts.ref); |
| const defaults = { ...this._commandState, value: this.opts.value.current ?? "" }; |
| this._commandState = defaults; |
| this.commandState = defaults; |
| this.onkeydown = this.onkeydown.bind(this); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| #score(value, keywords) { |
| const filter = this.opts.filter.current ?? computeCommandScore; |
| const score = value ? filter(value, this._commandState.search, keywords) : 0; |
| return score; |
| } |
| |
| |
| |
| |
| |
| #sort() { |
| if (!this._commandState.search || this.opts.shouldFilter.current === false) { |
| |
| |
| if (!this._commandState.value || !this.#isInitialMount) { |
| this.#selectFirstItem(); |
| } |
| else if (this.#isInitialMount && this._commandState.value) { |
| |
| this.#scrollInitialValue(); |
| } |
| return; |
| } |
| const scores = this._commandState.filtered.items; |
| |
| const groups = []; |
| for (const value of this._commandState.filtered.groups) { |
| const items = this.allGroups.get(value); |
| let max = 0; |
| if (!items) { |
| groups.push([value, max]); |
| continue; |
| } |
| |
| for (const item of items) { |
| const score = scores.get(item); |
| max = Math.max(score ?? 0, max); |
| } |
| groups.push([value, max]); |
| } |
| |
| |
| |
| const listInsertionElement = this.viewportNode; |
| const sorted = this.getValidItems().sort((a, b) => { |
| const valueA = a.getAttribute("data-value"); |
| const valueB = b.getAttribute("data-value"); |
| const scoresA = scores.get(valueA) ?? 0; |
| const scoresB = scores.get(valueB) ?? 0; |
| return scoresB - scoresA; |
| }); |
| for (const item of sorted) { |
| const group = item.closest(COMMAND_GROUP_ITEMS_SELECTOR); |
| if (group) { |
| const itemToAppend = item.parentElement === group |
| ? item |
| : item.closest(`${COMMAND_GROUP_ITEMS_SELECTOR} > *`); |
| if (itemToAppend) { |
| group.appendChild(itemToAppend); |
| } |
| } |
| else { |
| const itemToAppend = item.parentElement === listInsertionElement |
| ? item |
| : item.closest(`${COMMAND_GROUP_ITEMS_SELECTOR} > *`); |
| if (itemToAppend) { |
| listInsertionElement?.appendChild(itemToAppend); |
| } |
| } |
| } |
| const sortedGroups = groups.sort((a, b) => b[1] - a[1]); |
| for (const group of sortedGroups) { |
| const element = listInsertionElement?.querySelector(`${COMMAND_GROUP_SELECTOR}[${COMMAND_VALUE_ATTR}="${cssEscape(group[0])}"]`); |
| element?.parentElement?.appendChild(element); |
| } |
| this.#selectFirstItem(); |
| } |
| |
| |
| |
| |
| |
| setValue(value, opts) { |
| if (value !== this.opts.value.current && value === "") { |
| afterTick(() => { |
| this.key++; |
| }); |
| } |
| this.setState("value", value, opts); |
| this.opts.value.current = value; |
| } |
| |
| |
| |
| #selectFirstItem() { |
| afterTick(() => { |
| const item = this.getValidItems().find((item) => item.getAttribute("aria-disabled") !== "true"); |
| const value = item?.getAttribute(COMMAND_VALUE_ATTR); |
| const shouldPreventScroll = this.#isInitialMount && this.opts.disableInitialScroll.current; |
| this.setValue(value ?? "", shouldPreventScroll); |
| this.#isInitialMount = false; |
| }); |
| } |
| |
| |
| |
| |
| #scrollInitialValue() { |
| afterTick(() => { |
| const shouldPreventScroll = this.opts.disableInitialScroll.current; |
| if (!shouldPreventScroll) { |
| this.#scrollSelectedIntoView(); |
| } |
| this.#isInitialMount = false; |
| }); |
| } |
| |
| |
| |
| |
| #filterItems() { |
| if (!this._commandState.search || this.opts.shouldFilter.current === false) { |
| this._commandState.filtered.count = this.allItems.size; |
| return; |
| } |
| |
| this._commandState.filtered.groups = new Set(); |
| let itemCount = 0; |
| |
| for (const id of this.allItems) { |
| const value = this.allIds.get(id)?.value ?? ""; |
| const keywords = this.allIds.get(id)?.keywords ?? []; |
| const rank = this.#score(value, keywords); |
| this._commandState.filtered.items.set(id, rank); |
| if (rank > 0) |
| itemCount++; |
| } |
| |
| for (const [groupId, group] of this.allGroups) { |
| for (const itemId of group) { |
| const currItem = this._commandState.filtered.items.get(itemId); |
| if (currItem && currItem > 0) { |
| this._commandState.filtered.groups.add(groupId); |
| break; |
| } |
| } |
| } |
| this._commandState.filtered.count = itemCount; |
| } |
| |
| |
| |
| |
| |
| |
| getValidItems() { |
| const node = this.opts.ref.current; |
| if (!node) |
| return []; |
| const validItems = Array.from(node.querySelectorAll(COMMAND_VALID_ITEM_SELECTOR)).filter((el) => !!el); |
| return validItems; |
| } |
| |
| |
| |
| |
| |
| |
| getVisibleItems() { |
| const node = this.opts.ref.current; |
| if (!node) |
| return []; |
| const visibleItems = Array.from(node.querySelectorAll(COMMAND_ITEM_SELECTOR)).filter((el) => !!el); |
| return visibleItems; |
| } |
| |
| |
| |
| |
| |
| |
| get itemsGrid() { |
| if (!this.isGrid) |
| return []; |
| const columns = this.opts.columns.current ?? 1; |
| const items = this.getVisibleItems(); |
| const grid = [[]]; |
| let currentGroup = items[0]?.getAttribute("data-group"); |
| let column = 0; |
| let row = 0; |
| for (let i = 0; i < items.length; i++) { |
| const item = items[i]; |
| const itemGroup = item?.getAttribute("data-group"); |
| if (currentGroup !== itemGroup) { |
| currentGroup = itemGroup; |
| column = 1; |
| row++; |
| grid.push([{ index: i, firstRowOfGroup: true, ref: item }]); |
| } |
| else { |
| column++; |
| if (column > columns) { |
| row++; |
| column = 1; |
| grid.push([]); |
| } |
| grid[row]?.push({ |
| index: i, |
| firstRowOfGroup: grid[row]?.[0]?.firstRowOfGroup ?? i === 0, |
| ref: item, |
| }); |
| } |
| } |
| return grid; |
| } |
| |
| |
| |
| |
| |
| #getSelectedItem() { |
| const node = this.opts.ref.current; |
| if (!node) |
| return; |
| const selectedNode = node.querySelector(`${COMMAND_VALID_ITEM_SELECTOR}[data-selected]`); |
| if (!selectedNode) |
| return; |
| return selectedNode; |
| } |
| |
| |
| |
| |
| #scrollSelectedIntoView() { |
| afterTick(() => { |
| const item = this.#getSelectedItem(); |
| if (!item) |
| return; |
| const grandparent = item.parentElement?.parentElement; |
| if (!grandparent) |
| return; |
| if (this.isGrid) { |
| const isFirstRowOfGroup = this.#itemIsFirstRowOfGroup(item); |
| |
| item.scrollIntoView({ block: "nearest" }); |
| if (isFirstRowOfGroup) { |
| const closestGroupHeader = item |
| ?.closest(COMMAND_GROUP_SELECTOR) |
| ?.querySelector(COMMAND_GROUP_HEADING_SELECTOR); |
| closestGroupHeader?.scrollIntoView({ block: "nearest" }); |
| return; |
| } |
| } |
| else { |
| const firstChildOfParent = getFirstNonCommentChild(grandparent); |
| if (firstChildOfParent && |
| firstChildOfParent.dataset?.value === item.dataset?.value) { |
| const closestGroupHeader = item |
| ?.closest(COMMAND_GROUP_SELECTOR) |
| ?.querySelector(COMMAND_GROUP_HEADING_SELECTOR); |
| closestGroupHeader?.scrollIntoView({ block: "nearest" }); |
| return; |
| } |
| } |
| item.scrollIntoView({ block: "nearest" }); |
| }); |
| } |
| #itemIsFirstRowOfGroup(item) { |
| const grid = this.itemsGrid; |
| if (grid.length === 0) |
| return false; |
| for (let r = 0; r < grid.length; r++) { |
| const row = grid[r]; |
| if (row === undefined) |
| continue; |
| for (let c = 0; c < row.length; c++) { |
| const column = row[c]; |
| if (column === undefined || column.ref !== item) |
| continue; |
| return column.firstRowOfGroup; |
| } |
| } |
| return false; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| updateSelectedToIndex(index) { |
| const item = this.getValidItems()[index]; |
| if (!item) |
| return; |
| this.setValue(item.getAttribute(COMMAND_VALUE_ATTR) ?? ""); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| updateSelectedByItem(change) { |
| const selected = this.#getSelectedItem(); |
| const items = this.getValidItems(); |
| const index = items.findIndex((item) => item === selected); |
| |
| let newSelected = items[index + change]; |
| if (this.opts.loop.current) { |
| newSelected = |
| index + change < 0 |
| ? items[items.length - 1] |
| : index + change === items.length |
| ? items[0] |
| : items[index + change]; |
| } |
| if (newSelected) { |
| this.setValue(newSelected.getAttribute(COMMAND_VALUE_ATTR) ?? ""); |
| } |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| updateSelectedByGroup(change) { |
| const selected = this.#getSelectedItem(); |
| let group = selected?.closest(COMMAND_GROUP_SELECTOR); |
| let item; |
| while (group && !item) { |
| group = |
| change > 0 |
| ? findNextSibling(group, COMMAND_GROUP_SELECTOR) |
| : findPreviousSibling(group, COMMAND_GROUP_SELECTOR); |
| item = group?.querySelector(COMMAND_VALID_ITEM_SELECTOR); |
| } |
| if (item) { |
| this.setValue(item.getAttribute(COMMAND_VALUE_ATTR) ?? ""); |
| } |
| else { |
| this.updateSelectedByItem(change); |
| } |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| registerValue(value, keywords) { |
| if (!(value && value === this.allIds.get(value)?.value)) { |
| this.allIds.set(value, { value, keywords }); |
| } |
| this._commandState.filtered.items.set(value, this.#score(value, keywords)); |
| |
| if (!this.sortAfterTick) { |
| this.sortAfterTick = true; |
| afterTick(() => { |
| this.#sort(); |
| this.sortAfterTick = false; |
| }); |
| } |
| return () => { |
| this.allIds.delete(value); |
| }; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| registerItem(id, groupId) { |
| this.allItems.add(id); |
| |
| if (groupId) { |
| if (!this.allGroups.has(groupId)) { |
| this.allGroups.set(groupId, new Set([id])); |
| } |
| else { |
| this.allGroups.get(groupId).add(id); |
| } |
| } |
| |
| if (!this.sortAndFilterAfterTick) { |
| this.sortAndFilterAfterTick = true; |
| afterTick(() => { |
| this.#filterItems(); |
| this.#sort(); |
| this.sortAndFilterAfterTick = false; |
| }); |
| } |
| this.#scheduleUpdate(); |
| return () => { |
| const selectedItem = this.#getSelectedItem(); |
| this.allItems.delete(id); |
| this.commandState.filtered.items.delete(id); |
| this.#filterItems(); |
| |
| |
| if (selectedItem?.getAttribute("id") === id) { |
| this.#selectFirstItem(); |
| } |
| this.#scheduleUpdate(); |
| }; |
| } |
| |
| |
| |
| |
| |
| |
| registerGroup(id) { |
| if (!this.allGroups.has(id)) { |
| this.allGroups.set(id, new Set()); |
| } |
| return () => { |
| this.allIds.delete(id); |
| this.allGroups.delete(id); |
| }; |
| } |
| get isGrid() { |
| return this.opts.columns.current !== null; |
| } |
| |
| |
| |
| #last() { |
| return this.updateSelectedToIndex(this.getValidItems().length - 1); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| #next(e) { |
| e.preventDefault(); |
| if (e.metaKey) { |
| this.#last(); |
| } |
| else if (e.altKey) { |
| this.updateSelectedByGroup(1); |
| } |
| else { |
| this.updateSelectedByItem(1); |
| } |
| } |
| #down(e) { |
| if (this.opts.columns.current === null) |
| return; |
| e.preventDefault(); |
| if (e.metaKey) { |
| this.updateSelectedByGroup(1); |
| } |
| else { |
| this.updateSelectedByItem(this.#nextRowColumnOffset(e)); |
| } |
| } |
| #getColumn(item, grid) { |
| if (grid.length === 0) |
| return null; |
| for (let r = 0; r < grid.length; r++) { |
| const row = grid[r]; |
| if (row === undefined) |
| continue; |
| for (let c = 0; c < row.length; c++) { |
| const column = row[c]; |
| if (column === undefined || column.ref !== item) |
| continue; |
| return { columnIndex: c, rowIndex: r }; |
| } |
| } |
| return null; |
| } |
| #nextRowColumnOffset(e) { |
| const grid = this.itemsGrid; |
| const selected = this.#getSelectedItem(); |
| if (!selected) |
| return 0; |
| const column = this.#getColumn(selected, grid); |
| if (!column) |
| return 0; |
| let newItem = null; |
| const skipRows = e.altKey ? 1 : 0; |
| |
| if (e.altKey && column.rowIndex === grid.length - 2 && !this.opts.loop.current) { |
| newItem = this.#findNextNonDisabledItem({ |
| start: grid.length - 1, |
| end: grid.length, |
| expectedColumnIndex: column.columnIndex, |
| grid, |
| }); |
| } |
| else if (column.rowIndex === grid.length - 1) { |
| |
| if (!this.opts.loop.current) |
| return 0; |
| newItem = this.#findNextNonDisabledItem({ |
| start: 0 + skipRows, |
| end: column.rowIndex, |
| expectedColumnIndex: column.columnIndex, |
| grid, |
| }); |
| } |
| else { |
| newItem = this.#findNextNonDisabledItem({ |
| start: column.rowIndex + 1 + skipRows, |
| end: grid.length, |
| expectedColumnIndex: column.columnIndex, |
| grid, |
| }); |
| |
| |
| if (newItem === null && this.opts.loop.current) { |
| newItem = this.#findNextNonDisabledItem({ |
| start: 0, |
| end: column.rowIndex, |
| expectedColumnIndex: column.columnIndex, |
| grid, |
| }); |
| } |
| } |
| return this.#calculateOffset(selected, newItem); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| #findNextNonDisabledItem({ start, end, grid, expectedColumnIndex, }) { |
| let newItem = null; |
| for (let r = start; r < end; r++) { |
| const row = grid[r]; |
| |
| newItem = row[expectedColumnIndex]?.ref ?? null; |
| |
| if (newItem !== null && itemIsDisabled(newItem)) { |
| newItem = null; |
| continue; |
| } |
| |
| if (newItem === null) { |
| |
| |
| for (let i = row.length - 1; i >= 0; i--) { |
| const item = row[row.length - 1]; |
| if (item === undefined || itemIsDisabled(item.ref)) |
| continue; |
| newItem = item.ref; |
| break; |
| } |
| } |
| break; |
| } |
| return newItem; |
| } |
| #calculateOffset(selected, newSelected) { |
| if (newSelected === null) |
| return 0; |
| const items = this.getValidItems(); |
| const ogIndex = items.findIndex((item) => item === selected); |
| const newIndex = items.findIndex((item) => item === newSelected); |
| return newIndex - ogIndex; |
| } |
| #up(e) { |
| if (this.opts.columns.current === null) |
| return; |
| e.preventDefault(); |
| if (e.metaKey) { |
| this.updateSelectedByGroup(-1); |
| } |
| else { |
| this.updateSelectedByItem(this.#previousRowColumnOffset(e)); |
| } |
| } |
| #previousRowColumnOffset(e) { |
| const grid = this.itemsGrid; |
| const selected = this.#getSelectedItem(); |
| if (selected === undefined) |
| return 0; |
| const column = this.#getColumn(selected, grid); |
| if (column === null) |
| return 0; |
| let newItem = null; |
| const skipRows = e.altKey ? 1 : 0; |
| |
| if (e.altKey && column.rowIndex === 1 && this.opts.loop.current === false) { |
| newItem = this.#findNextNonDisabledItemDesc({ |
| start: 0, |
| end: 0, |
| expectedColumnIndex: column.columnIndex, |
| grid, |
| }); |
| } |
| else if (column.rowIndex === 0) { |
| |
| if (this.opts.loop.current === false) |
| return 0; |
| newItem = this.#findNextNonDisabledItemDesc({ |
| start: grid.length - 1 - skipRows, |
| end: column.rowIndex + 1, |
| expectedColumnIndex: column.columnIndex, |
| grid, |
| }); |
| } |
| else { |
| newItem = this.#findNextNonDisabledItemDesc({ |
| start: column.rowIndex - 1 - skipRows, |
| end: 0, |
| expectedColumnIndex: column.columnIndex, |
| grid, |
| }); |
| |
| |
| if (newItem === null && this.opts.loop.current) { |
| newItem = this.#findNextNonDisabledItemDesc({ |
| start: grid.length - 1, |
| end: column.rowIndex + 1, |
| expectedColumnIndex: column.columnIndex, |
| grid, |
| }); |
| } |
| } |
| return this.#calculateOffset(selected, newItem); |
| } |
| |
| |
| |
| |
| |
| |
| |
| #findNextNonDisabledItemDesc({ start, end, grid, expectedColumnIndex, }) { |
| let newItem = null; |
| for (let r = start; r >= end; r--) { |
| const row = grid[r]; |
| if (row === undefined) |
| continue; |
| |
| newItem = row[expectedColumnIndex]?.ref ?? null; |
| |
| if (newItem !== null && itemIsDisabled(newItem)) { |
| newItem = null; |
| continue; |
| } |
| |
| if (newItem === null) { |
| |
| |
| for (let i = row.length - 1; i >= 0; i--) { |
| const item = row[row.length - 1]; |
| if (item === undefined || itemIsDisabled(item.ref)) |
| continue; |
| newItem = item.ref; |
| break; |
| } |
| } |
| break; |
| } |
| return newItem; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| #prev(e) { |
| e.preventDefault(); |
| if (e.metaKey) { |
| |
| this.updateSelectedToIndex(0); |
| } |
| else if (e.altKey) { |
| |
| this.updateSelectedByGroup(-1); |
| } |
| else { |
| |
| this.updateSelectedByItem(-1); |
| } |
| } |
| onkeydown(e) { |
| const isVim = this.opts.vimBindings.current && e.ctrlKey; |
| switch (e.key) { |
| case kbd.n: |
| case kbd.j: { |
| |
| if (isVim) { |
| if (this.isGrid) { |
| this.#down(e); |
| } |
| else { |
| this.#next(e); |
| } |
| } |
| break; |
| } |
| case kbd.l: { |
| |
| if (isVim) { |
| if (this.isGrid) { |
| this.#next(e); |
| } |
| } |
| break; |
| } |
| case kbd.ARROW_DOWN: |
| if (this.isGrid) { |
| this.#down(e); |
| } |
| else { |
| this.#next(e); |
| } |
| break; |
| case kbd.ARROW_RIGHT: |
| if (!this.isGrid) |
| break; |
| this.#next(e); |
| break; |
| case kbd.p: |
| case kbd.k: { |
| |
| if (isVim) { |
| if (this.isGrid) { |
| this.#up(e); |
| } |
| else { |
| this.#prev(e); |
| } |
| } |
| break; |
| } |
| case kbd.h: { |
| |
| if (isVim && this.isGrid) { |
| this.#prev(e); |
| } |
| break; |
| } |
| case kbd.ARROW_UP: |
| if (this.isGrid) { |
| this.#up(e); |
| } |
| else { |
| this.#prev(e); |
| } |
| break; |
| case kbd.ARROW_LEFT: |
| if (!this.isGrid) |
| break; |
| this.#prev(e); |
| break; |
| case kbd.HOME: |
| |
| e.preventDefault(); |
| this.updateSelectedToIndex(0); |
| break; |
| case kbd.END: |
| |
| e.preventDefault(); |
| this.#last(); |
| break; |
| case kbd.ENTER: { |
| |
| |
| |
| |
| |
| |
| if (!e.isComposing && e.keyCode !== 229) { |
| e.preventDefault(); |
| const item = this.#getSelectedItem(); |
| if (item) { |
| item?.click(); |
| } |
| } |
| } |
| } |
| } |
| props = $derived.by(() => ({ |
| id: this.opts.id.current, |
| role: "application", |
| [commandAttrs.root]: "", |
| tabindex: -1, |
| onkeydown: this.onkeydown, |
| ...this.attachment, |
| })); |
| } |
| function itemIsDisabled(item) { |
| return item.getAttribute("aria-disabled") === "true"; |
| } |
| export class CommandEmptyState { |
| static create(opts) { |
| return new CommandEmptyState(opts, CommandRootContext.get()); |
| } |
| opts; |
| root; |
| attachment; |
| shouldRender = $derived.by(() => { |
| return ((this.root._commandState.filtered.count === 0 && this.#isInitialRender === false) || |
| this.opts.forceMount.current); |
| }); |
| #isInitialRender = true; |
| constructor(opts, root) { |
| this.opts = opts; |
| this.root = root; |
| this.attachment = attachRef(this.opts.ref); |
| $effect.pre(() => { |
| this.#isInitialRender = false; |
| }); |
| } |
| props = $derived.by(() => ({ |
| id: this.opts.id.current, |
| role: "presentation", |
| [commandAttrs.empty]: "", |
| ...this.attachment, |
| })); |
| } |
| export class CommandGroupContainerState { |
| static create(opts) { |
| return CommandGroupContainerContext.set(new CommandGroupContainerState(opts, CommandRootContext.get())); |
| } |
| opts; |
| root; |
| attachment; |
| shouldRender = $derived.by(() => { |
| if (this.opts.forceMount.current) |
| return true; |
| if (this.root.opts.shouldFilter.current === false) |
| return true; |
| if (!this.root.commandState.search) |
| return true; |
| return this.root._commandState.filtered.groups.has(this.trueValue); |
| }); |
| headingNode = $state(null); |
| trueValue = $state(""); |
| constructor(opts, root) { |
| this.opts = opts; |
| this.root = root; |
| this.attachment = attachRef(this.opts.ref); |
| this.trueValue = opts.value.current ?? opts.id.current; |
| watch(() => this.trueValue, () => { |
| return this.root.registerGroup(this.trueValue); |
| }); |
| $effect(() => { |
| if (this.opts.value.current) { |
| this.trueValue = this.opts.value.current; |
| return this.root.registerValue(this.opts.value.current); |
| } |
| else if (this.headingNode && this.headingNode.textContent) { |
| this.trueValue = this.headingNode.textContent.trim().toLowerCase(); |
| return this.root.registerValue(this.trueValue); |
| } |
| else { |
| this.trueValue = `-----${this.opts.id.current}`; |
| return this.root.registerValue(this.trueValue); |
| } |
| }); |
| } |
| props = $derived.by(() => ({ |
| id: this.opts.id.current, |
| role: "presentation", |
| hidden: this.shouldRender ? undefined : true, |
| "data-value": this.trueValue, |
| [commandAttrs.group]: "", |
| ...this.attachment, |
| })); |
| } |
| export class CommandGroupHeadingState { |
| static create(opts) { |
| return new CommandGroupHeadingState(opts, CommandGroupContainerContext.get()); |
| } |
| opts; |
| group; |
| attachment; |
| constructor(opts, group) { |
| this.opts = opts; |
| this.group = group; |
| this.attachment = attachRef(this.opts.ref, (v) => (this.group.headingNode = v)); |
| } |
| props = $derived.by(() => ({ |
| id: this.opts.id.current, |
| [commandAttrs["group-heading"]]: "", |
| ...this.attachment, |
| })); |
| } |
| export class CommandGroupItemsState { |
| static create(opts) { |
| return new CommandGroupItemsState(opts, CommandGroupContainerContext.get()); |
| } |
| opts; |
| group; |
| attachment; |
| constructor(opts, group) { |
| this.opts = opts; |
| this.group = group; |
| this.attachment = attachRef(this.opts.ref); |
| } |
| props = $derived.by(() => ({ |
| id: this.opts.id.current, |
| role: "group", |
| [commandAttrs["group-items"]]: "", |
| "aria-labelledby": this.group.headingNode?.id ?? undefined, |
| ...this.attachment, |
| })); |
| } |
| export class CommandInputState { |
| static create(opts) { |
| return new CommandInputState(opts, CommandRootContext.get()); |
| } |
| opts; |
| root; |
| attachment; |
| #selectedItemId = $derived.by(() => { |
| const item = this.root.viewportNode?.querySelector(`${COMMAND_ITEM_SELECTOR}[${COMMAND_VALUE_ATTR}="${cssEscape(this.root.opts.value.current)}"]`); |
| if (item === undefined || item === null) |
| return; |
| return item.getAttribute("id") ?? undefined; |
| }); |
| constructor(opts, root) { |
| this.opts = opts; |
| this.root = root; |
| this.attachment = attachRef(this.opts.ref, (v) => (this.root.inputNode = v)); |
| watch(() => this.opts.ref.current, () => { |
| const node = this.opts.ref.current; |
| if (node && this.opts.autofocus.current) { |
| afterSleep(10, () => node.focus()); |
| } |
| }); |
| watch(() => this.opts.value.current, () => { |
| if (this.root.commandState.search !== this.opts.value.current) { |
| this.root.setState("search", this.opts.value.current); |
| } |
| }); |
| } |
| props = $derived.by(() => ({ |
| id: this.opts.id.current, |
| type: "text", |
| [commandAttrs.input]: "", |
| autocomplete: "off", |
| autocorrect: "off", |
| spellcheck: false, |
| "aria-autocomplete": "list", |
| role: "combobox", |
| "aria-expanded": boolToStr(true), |
| "aria-controls": this.root.viewportNode?.id ?? undefined, |
| "aria-labelledby": this.root.labelNode?.id ?? undefined, |
| "aria-activedescendant": this.#selectedItemId, |
| ...this.attachment, |
| })); |
| } |
| export class CommandItemState { |
| static create(opts) { |
| const group = CommandGroupContainerContext.getOr(null); |
| return new CommandItemState({ ...opts, group }, CommandRootContext.get()); |
| } |
| opts; |
| root; |
| attachment; |
| #group = null; |
| #trueForceMount = $derived.by(() => { |
| return this.opts.forceMount.current || this.#group?.opts.forceMount.current === true; |
| }); |
| shouldRender = $derived.by(() => { |
| this.opts.ref.current; |
| if (this.#trueForceMount || |
| this.root.opts.shouldFilter.current === false || |
| !this.root.commandState.search) { |
| return true; |
| } |
| const currentScore = this.root.commandState.filtered.items.get(this.trueValue); |
| if (currentScore === undefined) |
| return false; |
| return currentScore > 0; |
| }); |
| isSelected = $derived.by(() => this.root.opts.value.current === this.trueValue && this.trueValue !== ""); |
| trueValue = $state(""); |
| constructor(opts, root) { |
| this.opts = opts; |
| this.root = root; |
| this.#group = CommandGroupContainerContext.getOr(null); |
| this.trueValue = opts.value.current; |
| this.attachment = attachRef(this.opts.ref); |
| watch([ |
| () => this.trueValue, |
| () => this.#group?.trueValue, |
| () => this.opts.forceMount.current, |
| ], () => { |
| if (this.opts.forceMount.current || !this.trueValue) |
| return; |
| return this.root.registerItem(this.trueValue, this.#group?.trueValue); |
| }); |
| watch([() => this.opts.value.current, () => this.opts.ref.current], () => { |
| if (this.opts.value.current) { |
| this.trueValue = this.opts.value.current; |
| } |
| else if (this.opts.ref.current?.textContent) { |
| this.trueValue = this.opts.ref.current.textContent.trim(); |
| } |
| if (this.trueValue) { |
| this.root.registerValue(this.trueValue, opts.keywords.current.map((kw) => kw.trim())); |
| this.opts.ref.current?.setAttribute(COMMAND_VALUE_ATTR, this.trueValue); |
| } |
| }); |
| |
| this.onclick = this.onclick.bind(this); |
| this.onpointermove = this.onpointermove.bind(this); |
| } |
| #onSelect() { |
| if (this.opts.disabled.current) |
| return; |
| this.#select(); |
| this.opts.onSelect?.current(); |
| } |
| #select() { |
| if (this.opts.disabled.current) |
| return; |
| this.root.setValue(this.trueValue, true); |
| } |
| onpointermove(_) { |
| if (this.opts.disabled.current || this.root.opts.disablePointerSelection.current) |
| return; |
| this.#select(); |
| } |
| onclick(_) { |
| if (this.opts.disabled.current) |
| return; |
| this.#onSelect(); |
| } |
| props = $derived.by(() => ({ |
| id: this.opts.id.current, |
| "aria-disabled": boolToStr(this.opts.disabled.current), |
| "aria-selected": boolToStr(this.isSelected), |
| "data-disabled": boolToEmptyStrOrUndef(this.opts.disabled.current), |
| "data-selected": boolToEmptyStrOrUndef(this.isSelected), |
| "data-value": this.trueValue, |
| "data-group": this.#group?.trueValue, |
| [commandAttrs.item]: "", |
| role: "option", |
| onpointermove: this.onpointermove, |
| onclick: this.onclick, |
| ...this.attachment, |
| })); |
| } |
| export class CommandLoadingState { |
| static create(opts) { |
| return new CommandLoadingState(opts); |
| } |
| opts; |
| attachment; |
| constructor(opts) { |
| this.opts = opts; |
| this.attachment = attachRef(this.opts.ref); |
| } |
| props = $derived.by(() => ({ |
| id: this.opts.id.current, |
| role: "progressbar", |
| "aria-valuenow": this.opts.progress.current, |
| "aria-valuemin": 0, |
| "aria-valuemax": 100, |
| "aria-label": "Loading...", |
| [commandAttrs.loading]: "", |
| ...this.attachment, |
| })); |
| } |
| export class CommandSeparatorState { |
| static create(opts) { |
| return new CommandSeparatorState(opts, CommandRootContext.get()); |
| } |
| opts; |
| root; |
| attachment; |
| shouldRender = $derived.by(() => !this.root._commandState.search || this.opts.forceMount.current); |
| constructor(opts, root) { |
| this.opts = opts; |
| this.root = root; |
| this.attachment = attachRef(this.opts.ref); |
| } |
| props = $derived.by(() => ({ |
| id: this.opts.id.current, |
| |
| "aria-hidden": "true", |
| [commandAttrs.separator]: "", |
| ...this.attachment, |
| })); |
| } |
| export class CommandListState { |
| static create(opts) { |
| return CommandListContext.set(new CommandListState(opts, CommandRootContext.get())); |
| } |
| opts; |
| root; |
| attachment; |
| constructor(opts, root) { |
| this.opts = opts; |
| this.root = root; |
| this.attachment = attachRef(this.opts.ref); |
| } |
| props = $derived.by(() => ({ |
| id: this.opts.id.current, |
| role: "listbox", |
| "aria-label": this.opts.ariaLabel.current, |
| [commandAttrs.list]: "", |
| ...this.attachment, |
| })); |
| } |
| export class CommandLabelState { |
| static create(opts) { |
| return new CommandLabelState(opts, CommandRootContext.get()); |
| } |
| opts; |
| root; |
| attachment; |
| constructor(opts, root) { |
| this.opts = opts; |
| this.root = root; |
| this.attachment = attachRef(this.opts.ref, (v) => (this.root.labelNode = v)); |
| } |
| props = $derived.by(() => ({ |
| id: this.opts.id.current, |
| [commandAttrs["input-label"]]: "", |
| for: this.opts.for?.current, |
| style: srOnlyStyles, |
| ...this.attachment, |
| })); |
| } |
| export class CommandViewportState { |
| static create(opts) { |
| return new CommandViewportState(opts, CommandListContext.get()); |
| } |
| opts; |
| list; |
| attachment; |
| constructor(opts, list) { |
| this.opts = opts; |
| this.list = list; |
| this.attachment = attachRef(this.opts.ref, (v) => (this.list.root.viewportNode = v)); |
| watch([() => this.opts.ref.current, () => this.list.opts.ref.current], ([node, listNode]) => { |
| if (node === null || listNode === null) |
| return; |
| let aF; |
| const observer = new ResizeObserver(() => { |
| aF = requestAnimationFrame(() => { |
| const height = node.offsetHeight; |
| listNode.style.setProperty("--bits-command-list-height", `${height.toFixed(1)}px`); |
| }); |
| }); |
| observer.observe(node); |
| return () => { |
| cancelAnimationFrame(aF); |
| observer.unobserve(node); |
| }; |
| }); |
| } |
| props = $derived.by(() => ({ |
| id: this.opts.id.current, |
| [commandAttrs.viewport]: "", |
| ...this.attachment, |
| })); |
| } |
|
|