Add first-letter navigation across list and menu modes
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
// Maintainer-controlled web client version.
|
||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
||||
window.CHGRID_WEB_VERSION = "2026.02.21 R76";
|
||||
window.CHGRID_WEB_VERSION = "2026.02.21 R77";
|
||||
|
||||
@@ -1513,7 +1513,7 @@ function handleChatModeInput(code: string, key: string): void {
|
||||
applyTextInputEdit(code, key, 500);
|
||||
}
|
||||
|
||||
function handleListModeInput(code: string): void {
|
||||
function handleListModeInput(code: string, key: string): void {
|
||||
if (state.sortedPeerIds.length === 0) {
|
||||
state.mode = 'normal';
|
||||
return;
|
||||
@@ -1532,6 +1532,23 @@ function handleListModeInput(code: string): void {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const nextByInitial = findNextIndexByInitial(
|
||||
state.sortedPeerIds,
|
||||
state.listIndex,
|
||||
key,
|
||||
(peerId) => state.peers.get(peerId)?.nickname ?? '',
|
||||
);
|
||||
if (nextByInitial >= 0) {
|
||||
state.listIndex = nextByInitial;
|
||||
const peer = state.peers.get(state.sortedPeerIds[state.listIndex]);
|
||||
if (!peer) return;
|
||||
const distance = Math.round(Math.hypot(peer.x - state.player.x, peer.y - state.player.y));
|
||||
updateStatus(
|
||||
`${peer.nickname}, ${distance} ${squareWord(distance)} ${getDirection(state.player.x, state.player.y, peer.x, peer.y)}, ${peer.x}, ${peer.y}`,
|
||||
);
|
||||
audio.sfxUiBlip();
|
||||
return;
|
||||
}
|
||||
|
||||
if (code === 'Enter') {
|
||||
const peer = state.peers.get(state.sortedPeerIds[state.listIndex]);
|
||||
@@ -1553,7 +1570,7 @@ function handleListModeInput(code: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function handleListItemsModeInput(code: string): void {
|
||||
function handleListItemsModeInput(code: string, key: string): void {
|
||||
if (state.sortedItemIds.length === 0) {
|
||||
state.mode = 'normal';
|
||||
return;
|
||||
@@ -1571,6 +1588,26 @@ function handleListItemsModeInput(code: string): void {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const nextByInitial = findNextIndexByInitial(
|
||||
state.sortedItemIds,
|
||||
state.itemListIndex,
|
||||
key,
|
||||
(itemId) => {
|
||||
const item = state.items.get(itemId);
|
||||
return item ? itemLabel(item) : '';
|
||||
},
|
||||
);
|
||||
if (nextByInitial >= 0) {
|
||||
state.itemListIndex = nextByInitial;
|
||||
const item = state.items.get(state.sortedItemIds[state.itemListIndex]);
|
||||
if (!item) return;
|
||||
const distance = Math.round(Math.hypot(item.x - state.player.x, item.y - state.player.y));
|
||||
updateStatus(
|
||||
`${itemLabel(item)}, ${distance} ${squareWord(distance)} ${getDirection(state.player.x, state.player.y, item.x, item.y)}, ${item.x}, ${item.y}`,
|
||||
);
|
||||
audio.sfxUiBlip();
|
||||
return;
|
||||
}
|
||||
if (code === 'Enter') {
|
||||
const item = state.items.get(state.sortedItemIds[state.itemListIndex]);
|
||||
if (!item) return;
|
||||
@@ -1590,7 +1627,7 @@ function handleListItemsModeInput(code: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddItemModeInput(code: string): void {
|
||||
function handleAddItemModeInput(code: string, key: string): void {
|
||||
if (code === 'ArrowDown' || code === 'ArrowUp') {
|
||||
state.addItemTypeIndex =
|
||||
code === 'ArrowDown'
|
||||
@@ -1600,6 +1637,18 @@ function handleAddItemModeInput(code: string): void {
|
||||
audio.sfxUiBlip();
|
||||
return;
|
||||
}
|
||||
const nextByInitial = findNextIndexByInitial(
|
||||
ITEM_TYPE_SEQUENCE,
|
||||
state.addItemTypeIndex,
|
||||
key,
|
||||
(itemType) => itemTypeLabel(itemType),
|
||||
);
|
||||
if (nextByInitial >= 0) {
|
||||
state.addItemTypeIndex = nextByInitial;
|
||||
updateStatus(`${itemTypeLabel(ITEM_TYPE_SEQUENCE[state.addItemTypeIndex])}.`);
|
||||
audio.sfxUiBlip();
|
||||
return;
|
||||
}
|
||||
if (code === 'Enter') {
|
||||
signaling.send({ type: 'item_add', itemType: ITEM_TYPE_SEQUENCE[state.addItemTypeIndex] });
|
||||
state.mode = 'normal';
|
||||
@@ -1612,7 +1661,7 @@ function handleAddItemModeInput(code: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectItemModeInput(code: string): void {
|
||||
function handleSelectItemModeInput(code: string, key: string): void {
|
||||
if (state.selectedItemIds.length === 0) {
|
||||
state.mode = 'normal';
|
||||
state.selectionContext = null;
|
||||
@@ -1630,6 +1679,24 @@ function handleSelectItemModeInput(code: string): void {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const nextByInitial = findNextIndexByInitial(
|
||||
state.selectedItemIds,
|
||||
state.selectedItemIndex,
|
||||
key,
|
||||
(itemId) => {
|
||||
const item = state.items.get(itemId);
|
||||
return item ? itemLabel(item) : '';
|
||||
},
|
||||
);
|
||||
if (nextByInitial >= 0) {
|
||||
state.selectedItemIndex = nextByInitial;
|
||||
const current = state.items.get(state.selectedItemIds[state.selectedItemIndex]);
|
||||
if (current) {
|
||||
updateStatus(itemLabel(current));
|
||||
audio.sfxUiBlip();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (code === 'Enter') {
|
||||
const selected = state.items.get(state.selectedItemIds[state.selectedItemIndex]);
|
||||
if (!selected) {
|
||||
@@ -1670,7 +1737,7 @@ function handleSelectItemModeInput(code: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function handleItemPropertiesModeInput(code: string): void {
|
||||
function handleItemPropertiesModeInput(code: string, key: string): void {
|
||||
const itemId = state.selectedItemId;
|
||||
if (!itemId) {
|
||||
state.mode = 'normal';
|
||||
@@ -1700,6 +1767,20 @@ function handleItemPropertiesModeInput(code: string): void {
|
||||
audio.sfxUiBlip();
|
||||
return;
|
||||
}
|
||||
const nextByInitial = findNextIndexByInitial(
|
||||
state.itemPropertyKeys,
|
||||
state.itemPropertyIndex,
|
||||
key,
|
||||
(propertyKey) => propertyKey,
|
||||
);
|
||||
if (nextByInitial >= 0) {
|
||||
state.itemPropertyIndex = nextByInitial;
|
||||
const selectedKey = state.itemPropertyKeys[state.itemPropertyIndex];
|
||||
const value = getItemPropertyValue(item, selectedKey);
|
||||
updateStatus(`${selectedKey}: ${value}`);
|
||||
audio.sfxUiBlip();
|
||||
return;
|
||||
}
|
||||
if (code === 'Enter') {
|
||||
const key = state.itemPropertyKeys[state.itemPropertyIndex];
|
||||
if (!EDITABLE_ITEM_PROPERTY_KEYS.has(key)) {
|
||||
@@ -1844,7 +1925,7 @@ function handleItemPropertyEditModeInput(code: string, key: string): void {
|
||||
applyTextInputEdit(code, key, 500, true);
|
||||
}
|
||||
|
||||
function handleItemPropertyOptionSelectModeInput(code: string): void {
|
||||
function handleItemPropertyOptionSelectModeInput(code: string, key: string): void {
|
||||
const itemId = state.selectedItemId;
|
||||
const propertyKey = state.editingPropertyKey;
|
||||
if (!itemId || !propertyKey || state.itemPropertyOptionValues.length === 0) {
|
||||
@@ -1864,6 +1945,18 @@ function handleItemPropertyOptionSelectModeInput(code: string): void {
|
||||
audio.sfxUiBlip();
|
||||
return;
|
||||
}
|
||||
const nextByInitial = findNextIndexByInitial(
|
||||
state.itemPropertyOptionValues,
|
||||
state.itemPropertyOptionIndex,
|
||||
key,
|
||||
(value) => value,
|
||||
);
|
||||
if (nextByInitial >= 0) {
|
||||
state.itemPropertyOptionIndex = nextByInitial;
|
||||
updateStatus(state.itemPropertyOptionValues[state.itemPropertyOptionIndex]);
|
||||
audio.sfxUiBlip();
|
||||
return;
|
||||
}
|
||||
|
||||
if (code === 'Enter') {
|
||||
const selectedValue = state.itemPropertyOptionValues[state.itemPropertyOptionIndex];
|
||||
@@ -1916,6 +2009,26 @@ function isTypingKey(code: string): boolean {
|
||||
return code.startsWith('Key') || code === 'Space';
|
||||
}
|
||||
|
||||
function findNextIndexByInitial<T>(
|
||||
entries: readonly T[],
|
||||
currentIndex: number,
|
||||
key: string,
|
||||
labelFor: (entry: T) => string,
|
||||
): number {
|
||||
if (entries.length === 0 || key.length !== 1 || !/[a-z]/i.test(key)) {
|
||||
return -1;
|
||||
}
|
||||
const target = key.toLowerCase();
|
||||
for (let step = 1; step <= entries.length; step += 1) {
|
||||
const candidateIndex = (currentIndex + step) % entries.length;
|
||||
const label = labelFor(entries[candidateIndex]).trim().toLowerCase();
|
||||
if (label.startsWith(target)) {
|
||||
return candidateIndex;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function setupInputHandlers(): void {
|
||||
document.addEventListener('keydown', (event) => {
|
||||
const code = event.code;
|
||||
@@ -1940,19 +2053,19 @@ function setupInputHandlers(): void {
|
||||
} else if (state.mode === 'chat') {
|
||||
handleChatModeInput(code, event.key);
|
||||
} else if (state.mode === 'listUsers') {
|
||||
handleListModeInput(code);
|
||||
handleListModeInput(code, event.key);
|
||||
} else if (state.mode === 'listItems') {
|
||||
handleListItemsModeInput(code);
|
||||
handleListItemsModeInput(code, event.key);
|
||||
} else if (state.mode === 'addItem') {
|
||||
handleAddItemModeInput(code);
|
||||
handleAddItemModeInput(code, event.key);
|
||||
} else if (state.mode === 'selectItem') {
|
||||
handleSelectItemModeInput(code);
|
||||
handleSelectItemModeInput(code, event.key);
|
||||
} else if (state.mode === 'itemProperties') {
|
||||
handleItemPropertiesModeInput(code);
|
||||
handleItemPropertiesModeInput(code, event.key);
|
||||
} else if (state.mode === 'itemPropertyEdit') {
|
||||
handleItemPropertyEditModeInput(code, event.key);
|
||||
} else if (state.mode === 'itemPropertyOptionSelect') {
|
||||
handleItemPropertyOptionSelectModeInput(code);
|
||||
handleItemPropertyOptionSelectModeInput(code, event.key);
|
||||
} else {
|
||||
handleNormalModeInput(code, event.shiftKey);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user