Unify list and edit-mode handler flows in input helpers
This commit is contained in:
7
client/src/input/editSession.ts
Normal file
7
client/src/input/editSession.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export type EditSessionAction = 'submit' | 'cancel' | null;
|
||||||
|
|
||||||
|
export function getEditSessionAction(code: string): EditSessionAction {
|
||||||
|
if (code === 'Enter') return 'submit';
|
||||||
|
if (code === 'Escape') return 'cancel';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
34
client/src/input/listController.ts
Normal file
34
client/src/input/listController.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { cycleIndex, findNextIndexByInitial } from './listNavigation';
|
||||||
|
|
||||||
|
export type ListControlResult =
|
||||||
|
| { type: 'move'; index: number; reason: 'arrow' | 'initial' }
|
||||||
|
| { type: 'select' }
|
||||||
|
| { type: 'cancel' }
|
||||||
|
| { type: 'none' };
|
||||||
|
|
||||||
|
export function handleListControlKey<T>(
|
||||||
|
code: string,
|
||||||
|
key: string,
|
||||||
|
entries: readonly T[],
|
||||||
|
currentIndex: number,
|
||||||
|
labelFor: (entry: T) => string,
|
||||||
|
): ListControlResult {
|
||||||
|
if (entries.length === 0) return { type: 'none' };
|
||||||
|
|
||||||
|
if (code === 'ArrowDown' || code === 'ArrowUp') {
|
||||||
|
return {
|
||||||
|
type: 'move',
|
||||||
|
index: cycleIndex(currentIndex, entries.length, code === 'ArrowDown' ? 'next' : 'prev'),
|
||||||
|
reason: 'arrow',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextByInitial = findNextIndexByInitial(entries, currentIndex, key, labelFor);
|
||||||
|
if (nextByInitial >= 0) {
|
||||||
|
return { type: 'move', index: nextByInitial, reason: 'initial' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === 'Enter') return { type: 'select' };
|
||||||
|
if (code === 'Escape') return { type: 'cancel' };
|
||||||
|
return { type: 'none' };
|
||||||
|
}
|
||||||
@@ -29,7 +29,8 @@ import {
|
|||||||
shouldReplaceCurrentText,
|
shouldReplaceCurrentText,
|
||||||
} from './input/textInput';
|
} from './input/textInput';
|
||||||
import { resolveMainModeCommand } from './input/mainCommandRouter';
|
import { resolveMainModeCommand } from './input/mainCommandRouter';
|
||||||
import { cycleIndex, findNextIndexByInitial } from './input/listNavigation';
|
import { handleListControlKey } from './input/listController';
|
||||||
|
import { getEditSessionAction } from './input/editSession';
|
||||||
import { formatSteppedNumber, snapNumberToStep } from './input/numeric';
|
import { formatSteppedNumber, snapNumberToStep } from './input/numeric';
|
||||||
import { type IncomingMessage, type OutgoingMessage } from './network/protocol';
|
import { type IncomingMessage, type OutgoingMessage } from './network/protocol';
|
||||||
import { SignalingClient } from './network/signalingClient';
|
import { SignalingClient } from './network/signalingClient';
|
||||||
@@ -1786,7 +1787,8 @@ function handleHelpViewModeInput(code: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleChatModeInput(code: string, key: string, ctrlKey: boolean): void {
|
function handleChatModeInput(code: string, key: string, ctrlKey: boolean): void {
|
||||||
if (code === 'Enter') {
|
const editAction = getEditSessionAction(code);
|
||||||
|
if (editAction === 'submit') {
|
||||||
const message = state.nicknameInput.trim();
|
const message = state.nicknameInput.trim();
|
||||||
if (message.length > 0) {
|
if (message.length > 0) {
|
||||||
signaling.send({ type: 'chat_message', message });
|
signaling.send({ type: 'chat_message', message });
|
||||||
@@ -1802,7 +1804,7 @@ function handleChatModeInput(code: string, key: string, ctrlKey: boolean): void
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code === 'Escape') {
|
if (editAction === 'cancel') {
|
||||||
state.mode = 'normal';
|
state.mode = 'normal';
|
||||||
state.nicknameInput = '';
|
state.nicknameInput = '';
|
||||||
state.cursorPos = 0;
|
state.cursorPos = 0;
|
||||||
@@ -1833,7 +1835,8 @@ function handleMicGainEditModeInput(code: string, key: string, ctrlKey: boolean)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code === 'Enter') {
|
const editAction = getEditSessionAction(code);
|
||||||
|
if (editAction === 'submit') {
|
||||||
const value = Number(state.nicknameInput.trim());
|
const value = Number(state.nicknameInput.trim());
|
||||||
if (!Number.isFinite(value)) {
|
if (!Number.isFinite(value)) {
|
||||||
updateStatus(`Volume must be between ${MIC_CALIBRATION_MIN_GAIN} and ${MIC_CALIBRATION_MAX_GAIN}.`);
|
updateStatus(`Volume must be between ${MIC_CALIBRATION_MIN_GAIN} and ${MIC_CALIBRATION_MAX_GAIN}.`);
|
||||||
@@ -1855,7 +1858,7 @@ function handleMicGainEditModeInput(code: string, key: string, ctrlKey: boolean)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code === 'Escape') {
|
if (editAction === 'cancel') {
|
||||||
state.mode = 'normal';
|
state.mode = 'normal';
|
||||||
replaceTextOnNextType = false;
|
replaceTextOnNextType = false;
|
||||||
updateStatus('Cancelled.');
|
updateStatus('Cancelled.');
|
||||||
@@ -1867,27 +1870,15 @@ function handleMicGainEditModeInput(code: string, key: string, ctrlKey: boolean)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleEffectSelectModeInput(code: string, key: string): void {
|
function handleEffectSelectModeInput(code: string, key: string): void {
|
||||||
if (code === 'ArrowDown' || code === 'ArrowUp') {
|
const control = handleListControlKey(code, key, EFFECT_SEQUENCE, state.effectSelectIndex, (effect) => effect.label);
|
||||||
state.effectSelectIndex = cycleIndex(state.effectSelectIndex, EFFECT_SEQUENCE.length, code === 'ArrowDown' ? 'next' : 'prev');
|
if (control.type === 'move') {
|
||||||
|
state.effectSelectIndex = control.index;
|
||||||
updateStatus(EFFECT_SEQUENCE[state.effectSelectIndex].label);
|
updateStatus(EFFECT_SEQUENCE[state.effectSelectIndex].label);
|
||||||
audio.sfxUiBlip();
|
audio.sfxUiBlip();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextByInitial = findNextIndexByInitial(
|
if (control.type === 'select') {
|
||||||
EFFECT_SEQUENCE,
|
|
||||||
state.effectSelectIndex,
|
|
||||||
key,
|
|
||||||
(effect) => effect.label,
|
|
||||||
);
|
|
||||||
if (nextByInitial >= 0) {
|
|
||||||
state.effectSelectIndex = nextByInitial;
|
|
||||||
updateStatus(EFFECT_SEQUENCE[state.effectSelectIndex].label);
|
|
||||||
audio.sfxUiBlip();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code === 'Enter') {
|
|
||||||
const selected = EFFECT_SEQUENCE[state.effectSelectIndex];
|
const selected = EFFECT_SEQUENCE[state.effectSelectIndex];
|
||||||
const effect = audio.setOutboundEffect(selected.id);
|
const effect = audio.setOutboundEffect(selected.id);
|
||||||
state.mode = 'normal';
|
state.mode = 'normal';
|
||||||
@@ -1896,7 +1887,7 @@ function handleEffectSelectModeInput(code: string, key: string): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code === 'Escape') {
|
if (control.type === 'cancel') {
|
||||||
state.mode = 'normal';
|
state.mode = 'normal';
|
||||||
updateStatus('Cancelled.');
|
updateStatus('Cancelled.');
|
||||||
audio.sfxUiCancel();
|
audio.sfxUiCancel();
|
||||||
@@ -1909,33 +1900,21 @@ function handleListModeInput(code: string, key: string): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code === 'ArrowDown' || code === 'ArrowUp') {
|
const control = handleListControlKey(code, key, state.sortedPeerIds, state.listIndex, (peerId) => state.peers.get(peerId)?.nickname ?? '');
|
||||||
state.listIndex = cycleIndex(state.listIndex, state.sortedPeerIds.length, code === 'ArrowDown' ? 'next' : 'prev');
|
if (control.type === 'move') {
|
||||||
const peer = state.peers.get(state.sortedPeerIds[state.listIndex]);
|
state.listIndex = control.index;
|
||||||
if (!peer) return;
|
|
||||||
updateStatus(
|
|
||||||
`${peer.nickname}, ${distanceDirectionPhrase(state.player.x, state.player.y, peer.x, peer.y)}, ${peer.x}, ${peer.y}`,
|
|
||||||
);
|
|
||||||
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]);
|
const peer = state.peers.get(state.sortedPeerIds[state.listIndex]);
|
||||||
if (!peer) return;
|
if (!peer) return;
|
||||||
updateStatus(
|
updateStatus(
|
||||||
`${peer.nickname}, ${distanceDirectionPhrase(state.player.x, state.player.y, peer.x, peer.y)}, ${peer.x}, ${peer.y}`,
|
`${peer.nickname}, ${distanceDirectionPhrase(state.player.x, state.player.y, peer.x, peer.y)}, ${peer.x}, ${peer.y}`,
|
||||||
);
|
);
|
||||||
|
if (control.reason === 'initial') {
|
||||||
audio.sfxUiBlip();
|
audio.sfxUiBlip();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code === 'Enter') {
|
if (control.type === 'select') {
|
||||||
const peer = state.peers.get(state.sortedPeerIds[state.listIndex]);
|
const peer = state.peers.get(state.sortedPeerIds[state.listIndex]);
|
||||||
if (!peer) return;
|
if (!peer) return;
|
||||||
if (state.player.x === peer.x && state.player.y === peer.y) {
|
if (state.player.x === peer.x && state.player.y === peer.y) {
|
||||||
@@ -1952,7 +1931,7 @@ function handleListModeInput(code: string, key: string): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code === 'Escape') {
|
if (control.type === 'cancel') {
|
||||||
state.mode = 'normal';
|
state.mode = 'normal';
|
||||||
updateStatus('Exit list mode.');
|
updateStatus('Exit list mode.');
|
||||||
audio.sfxUiCancel();
|
audio.sfxUiCancel();
|
||||||
@@ -1964,35 +1943,24 @@ function handleListItemsModeInput(code: string, key: string): void {
|
|||||||
state.mode = 'normal';
|
state.mode = 'normal';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (code === 'ArrowDown' || code === 'ArrowUp') {
|
|
||||||
state.itemListIndex = cycleIndex(state.itemListIndex, state.sortedItemIds.length, code === 'ArrowDown' ? 'next' : 'prev');
|
const control = handleListControlKey(code, key, state.sortedItemIds, state.itemListIndex, (itemId) => {
|
||||||
const item = state.items.get(state.sortedItemIds[state.itemListIndex]);
|
|
||||||
if (!item) return;
|
|
||||||
updateStatus(
|
|
||||||
`${itemLabel(item)}, ${distanceDirectionPhrase(state.player.x, state.player.y, item.x, item.y)}, ${item.x}, ${item.y}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextByInitial = findNextIndexByInitial(
|
|
||||||
state.sortedItemIds,
|
|
||||||
state.itemListIndex,
|
|
||||||
key,
|
|
||||||
(itemId) => {
|
|
||||||
const item = state.items.get(itemId);
|
const item = state.items.get(itemId);
|
||||||
return item ? itemLabel(item) : '';
|
return item ? itemLabel(item) : '';
|
||||||
},
|
});
|
||||||
);
|
if (control.type === 'move') {
|
||||||
if (nextByInitial >= 0) {
|
state.itemListIndex = control.index;
|
||||||
state.itemListIndex = nextByInitial;
|
|
||||||
const item = state.items.get(state.sortedItemIds[state.itemListIndex]);
|
const item = state.items.get(state.sortedItemIds[state.itemListIndex]);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
updateStatus(
|
updateStatus(
|
||||||
`${itemLabel(item)}, ${distanceDirectionPhrase(state.player.x, state.player.y, item.x, item.y)}, ${item.x}, ${item.y}`,
|
`${itemLabel(item)}, ${distanceDirectionPhrase(state.player.x, state.player.y, item.x, item.y)}, ${item.x}, ${item.y}`,
|
||||||
);
|
);
|
||||||
|
if (control.reason === 'initial') {
|
||||||
audio.sfxUiBlip();
|
audio.sfxUiBlip();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (code === 'Enter') {
|
if (control.type === 'select') {
|
||||||
const item = state.items.get(state.sortedItemIds[state.itemListIndex]);
|
const item = state.items.get(state.sortedItemIds[state.itemListIndex]);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
if (state.player.x === item.x && state.player.y === item.y) {
|
if (state.player.x === item.x && state.player.y === item.y) {
|
||||||
@@ -2008,7 +1976,7 @@ function handleListItemsModeInput(code: string, key: string): void {
|
|||||||
updateStatus(`Moved to ${itemLabel(item)}.`);
|
updateStatus(`Moved to ${itemLabel(item)}.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (code === 'Escape') {
|
if (control.type === 'cancel') {
|
||||||
state.mode = 'normal';
|
state.mode = 'normal';
|
||||||
updateStatus('Exit item list mode.');
|
updateStatus('Exit item list mode.');
|
||||||
audio.sfxUiCancel();
|
audio.sfxUiCancel();
|
||||||
@@ -2023,20 +1991,9 @@ function handleAddItemModeInput(code: string, key: string): void {
|
|||||||
audio.sfxUiCancel();
|
audio.sfxUiCancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (code === 'ArrowDown' || code === 'ArrowUp') {
|
const control = handleListControlKey(code, key, itemTypeSequence, state.addItemTypeIndex, (itemType) => itemTypeLabel(itemType));
|
||||||
state.addItemTypeIndex = cycleIndex(state.addItemTypeIndex, itemTypeSequence.length, code === 'ArrowDown' ? 'next' : 'prev');
|
if (control.type === 'move') {
|
||||||
updateStatus(`${itemTypeLabel(itemTypeSequence[state.addItemTypeIndex])}.`);
|
state.addItemTypeIndex = control.index;
|
||||||
audio.sfxUiBlip();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextByInitial = findNextIndexByInitial(
|
|
||||||
itemTypeSequence,
|
|
||||||
state.addItemTypeIndex,
|
|
||||||
key,
|
|
||||||
(itemType) => itemTypeLabel(itemType),
|
|
||||||
);
|
|
||||||
if (nextByInitial >= 0) {
|
|
||||||
state.addItemTypeIndex = nextByInitial;
|
|
||||||
updateStatus(`${itemTypeLabel(itemTypeSequence[state.addItemTypeIndex])}.`);
|
updateStatus(`${itemTypeLabel(itemTypeSequence[state.addItemTypeIndex])}.`);
|
||||||
audio.sfxUiBlip();
|
audio.sfxUiBlip();
|
||||||
return;
|
return;
|
||||||
@@ -2048,12 +2005,12 @@ function handleAddItemModeInput(code: string, key: string): void {
|
|||||||
audio.sfxUiBlip();
|
audio.sfxUiBlip();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (code === 'Enter') {
|
if (control.type === 'select') {
|
||||||
signaling.send({ type: 'item_add', itemType: itemTypeSequence[state.addItemTypeIndex] });
|
signaling.send({ type: 'item_add', itemType: itemTypeSequence[state.addItemTypeIndex] });
|
||||||
state.mode = 'normal';
|
state.mode = 'normal';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (code === 'Escape') {
|
if (control.type === 'cancel') {
|
||||||
state.mode = 'normal';
|
state.mode = 'normal';
|
||||||
updateStatus('Cancelled.');
|
updateStatus('Cancelled.');
|
||||||
audio.sfxUiCancel();
|
audio.sfxUiCancel();
|
||||||
@@ -2066,26 +2023,12 @@ function handleSelectItemModeInput(code: string, key: string): void {
|
|||||||
state.selectionContext = null;
|
state.selectionContext = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (code === 'ArrowDown' || code === 'ArrowUp') {
|
const control = handleListControlKey(code, key, state.selectedItemIds, state.selectedItemIndex, (itemId) => {
|
||||||
state.selectedItemIndex = cycleIndex(state.selectedItemIndex, state.selectedItemIds.length, code === 'ArrowDown' ? 'next' : 'prev');
|
|
||||||
const current = state.items.get(state.selectedItemIds[state.selectedItemIndex]);
|
|
||||||
if (current) {
|
|
||||||
updateStatus(itemLabel(current));
|
|
||||||
audio.sfxUiBlip();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextByInitial = findNextIndexByInitial(
|
|
||||||
state.selectedItemIds,
|
|
||||||
state.selectedItemIndex,
|
|
||||||
key,
|
|
||||||
(itemId) => {
|
|
||||||
const item = state.items.get(itemId);
|
const item = state.items.get(itemId);
|
||||||
return item ? itemLabel(item) : '';
|
return item ? itemLabel(item) : '';
|
||||||
},
|
});
|
||||||
);
|
if (control.type === 'move') {
|
||||||
if (nextByInitial >= 0) {
|
state.selectedItemIndex = control.index;
|
||||||
state.selectedItemIndex = nextByInitial;
|
|
||||||
const current = state.items.get(state.selectedItemIds[state.selectedItemIndex]);
|
const current = state.items.get(state.selectedItemIds[state.selectedItemIndex]);
|
||||||
if (current) {
|
if (current) {
|
||||||
updateStatus(itemLabel(current));
|
updateStatus(itemLabel(current));
|
||||||
@@ -2093,7 +2036,7 @@ function handleSelectItemModeInput(code: string, key: string): void {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (code === 'Enter') {
|
if (control.type === 'select') {
|
||||||
const selected = state.items.get(state.selectedItemIds[state.selectedItemIndex]);
|
const selected = state.items.get(state.selectedItemIds[state.selectedItemIndex]);
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
state.mode = 'normal';
|
state.mode = 'normal';
|
||||||
@@ -2125,7 +2068,7 @@ function handleSelectItemModeInput(code: string, key: string): void {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (code === 'Escape') {
|
if (control.type === 'cancel') {
|
||||||
state.mode = 'normal';
|
state.mode = 'normal';
|
||||||
state.selectionContext = null;
|
state.selectionContext = null;
|
||||||
updateStatus('Cancelled.');
|
updateStatus('Cancelled.');
|
||||||
@@ -2152,11 +2095,12 @@ function handleItemPropertiesModeInput(code: string, key: string): void {
|
|||||||
audio.sfxUiCancel();
|
audio.sfxUiCancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (code === 'ArrowDown' || code === 'ArrowUp') {
|
const control = handleListControlKey(code, key, state.itemPropertyKeys, state.itemPropertyIndex, (propertyKey) => propertyKey);
|
||||||
state.itemPropertyIndex = cycleIndex(state.itemPropertyIndex, state.itemPropertyKeys.length, code === 'ArrowDown' ? 'next' : 'prev');
|
if (control.type === 'move') {
|
||||||
const key = state.itemPropertyKeys[state.itemPropertyIndex];
|
state.itemPropertyIndex = control.index;
|
||||||
const value = getItemPropertyValue(item, key);
|
const selectedKey = state.itemPropertyKeys[state.itemPropertyIndex];
|
||||||
updateStatus(`${itemPropertyLabel(key)}: ${value}`);
|
const value = getItemPropertyValue(item, selectedKey);
|
||||||
|
updateStatus(`${itemPropertyLabel(selectedKey)}: ${value}`);
|
||||||
audio.sfxUiBlip();
|
audio.sfxUiBlip();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2166,21 +2110,7 @@ function handleItemPropertiesModeInput(code: string, key: string): void {
|
|||||||
audio.sfxUiBlip();
|
audio.sfxUiBlip();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nextByInitial = findNextIndexByInitial(
|
if (control.type === 'select') {
|
||||||
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(`${itemPropertyLabel(selectedKey)}: ${value}`);
|
|
||||||
audio.sfxUiBlip();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (code === 'Enter') {
|
|
||||||
const key = state.itemPropertyKeys[state.itemPropertyIndex];
|
const key = state.itemPropertyKeys[state.itemPropertyIndex];
|
||||||
if (!isItemPropertyEditable(item, key)) {
|
if (!isItemPropertyEditable(item, key)) {
|
||||||
updateStatus(`${itemPropertyLabel(key)} is not editable.`);
|
updateStatus(`${itemPropertyLabel(key)} is not editable.`);
|
||||||
@@ -2228,7 +2158,7 @@ function handleItemPropertiesModeInput(code: string, key: string): void {
|
|||||||
audio.sfxUiBlip();
|
audio.sfxUiBlip();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (code === 'Escape') {
|
if (control.type === 'cancel') {
|
||||||
state.mode = 'normal';
|
state.mode = 'normal';
|
||||||
state.selectedItemId = null;
|
state.selectedItemId = null;
|
||||||
state.itemPropertyKeys = [];
|
state.itemPropertyKeys = [];
|
||||||
@@ -2290,8 +2220,35 @@ function handleItemPropertyEditModeInput(code: string, key: string, ctrlKey: boo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (code === 'Enter') {
|
const editAction = getEditSessionAction(code);
|
||||||
|
if (editAction === 'submit') {
|
||||||
const value = state.nicknameInput.trim();
|
const value = state.nicknameInput.trim();
|
||||||
|
const sendItemParams = (params: Record<string, unknown>): void => {
|
||||||
|
signaling.send({ type: 'item_update', itemId, params });
|
||||||
|
};
|
||||||
|
const parseToggleValue = (raw: string, field: string): { ok: true; value: boolean } | { ok: false } => {
|
||||||
|
const normalized = raw.toLowerCase();
|
||||||
|
if (!['on', 'off', 'true', 'false', '1', '0', 'yes', 'no'].includes(normalized)) {
|
||||||
|
updateStatus(`${field} must be on or off.`);
|
||||||
|
audio.sfxUiCancel();
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
return { ok: true, value: ['on', 'true', '1', 'yes'].includes(normalized) };
|
||||||
|
};
|
||||||
|
const submitNumericParam = (
|
||||||
|
targetKey: string,
|
||||||
|
requireInteger: boolean,
|
||||||
|
transform?: (num: number) => number,
|
||||||
|
): boolean => {
|
||||||
|
const parsed = validateNumericItemPropertyInput(item, targetKey, value, requireInteger);
|
||||||
|
if (!parsed.ok) {
|
||||||
|
updateStatus(parsed.message);
|
||||||
|
audio.sfxUiCancel();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
sendItemParams({ [targetKey]: transform ? transform(parsed.value) : parsed.value });
|
||||||
|
return true;
|
||||||
|
};
|
||||||
if (propertyKey === 'title') {
|
if (propertyKey === 'title') {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
updateStatus('Value is required.');
|
updateStatus('Value is required.');
|
||||||
@@ -2300,57 +2257,25 @@ function handleItemPropertyEditModeInput(code: string, key: string, ctrlKey: boo
|
|||||||
}
|
}
|
||||||
signaling.send({ type: 'item_update', itemId, title: value });
|
signaling.send({ type: 'item_update', itemId, title: value });
|
||||||
} else if (propertyKey === 'streamUrl') {
|
} else if (propertyKey === 'streamUrl') {
|
||||||
signaling.send({ type: 'item_update', itemId, params: { streamUrl: value } });
|
sendItemParams({ streamUrl: value });
|
||||||
} else if (propertyKey === 'enabled') {
|
} else if (propertyKey === 'enabled' || propertyKey === 'directional') {
|
||||||
const normalized = value.toLowerCase();
|
const toggle = parseToggleValue(value, propertyKey);
|
||||||
if (!['on', 'off', 'true', 'false', '1', '0', 'yes', 'no'].includes(normalized)) {
|
if (!toggle.ok) {
|
||||||
updateStatus('enabled must be on or off.');
|
|
||||||
audio.sfxUiCancel();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const enabled = ['on', 'true', '1', 'yes'].includes(normalized);
|
sendItemParams({ [propertyKey]: toggle.value });
|
||||||
signaling.send({ type: 'item_update', itemId, params: { enabled } });
|
} else if (
|
||||||
} else if (propertyKey === 'directional') {
|
propertyKey === 'mediaVolume' ||
|
||||||
const normalized = value.toLowerCase();
|
propertyKey === 'emitVolume' ||
|
||||||
if (!['on', 'off', 'true', 'false', '1', '0', 'yes', 'no'].includes(normalized)) {
|
propertyKey === 'emitSoundSpeed' ||
|
||||||
updateStatus('directional must be on or off.');
|
propertyKey === 'emitSoundTempo' ||
|
||||||
audio.sfxUiCancel();
|
propertyKey === 'emitRange' ||
|
||||||
|
propertyKey === 'sides' ||
|
||||||
|
propertyKey === 'number'
|
||||||
|
) {
|
||||||
|
if (!submitNumericParam(propertyKey, true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const directional = ['on', 'true', '1', 'yes'].includes(normalized);
|
|
||||||
signaling.send({ type: 'item_update', itemId, params: { directional } });
|
|
||||||
} else if (propertyKey === 'mediaVolume') {
|
|
||||||
const parsed = validateNumericItemPropertyInput(item, propertyKey, value, true);
|
|
||||||
if (!parsed.ok) {
|
|
||||||
updateStatus(parsed.message);
|
|
||||||
audio.sfxUiCancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
signaling.send({ type: 'item_update', itemId, params: { mediaVolume: parsed.value } });
|
|
||||||
} else if (propertyKey === 'emitVolume') {
|
|
||||||
const parsed = validateNumericItemPropertyInput(item, propertyKey, value, true);
|
|
||||||
if (!parsed.ok) {
|
|
||||||
updateStatus(parsed.message);
|
|
||||||
audio.sfxUiCancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
signaling.send({ type: 'item_update', itemId, params: { emitVolume: parsed.value } });
|
|
||||||
} else if (propertyKey === 'emitSoundSpeed') {
|
|
||||||
const parsed = validateNumericItemPropertyInput(item, propertyKey, value, true);
|
|
||||||
if (!parsed.ok) {
|
|
||||||
updateStatus(parsed.message);
|
|
||||||
audio.sfxUiCancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
signaling.send({ type: 'item_update', itemId, params: { emitSoundSpeed: parsed.value } });
|
|
||||||
} else if (propertyKey === 'emitSoundTempo') {
|
|
||||||
const parsed = validateNumericItemPropertyInput(item, propertyKey, value, true);
|
|
||||||
if (!parsed.ok) {
|
|
||||||
updateStatus(parsed.message);
|
|
||||||
audio.sfxUiCancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
signaling.send({ type: 'item_update', itemId, params: { emitSoundTempo: parsed.value } });
|
|
||||||
} else if (propertyKey === 'mediaEffect' || propertyKey === 'emitEffect') {
|
} else if (propertyKey === 'mediaEffect' || propertyKey === 'emitEffect') {
|
||||||
const normalized = value.trim().toLowerCase() as EffectId;
|
const normalized = value.trim().toLowerCase() as EffectId;
|
||||||
if (!EFFECT_IDS.has(normalized)) {
|
if (!EFFECT_IDS.has(normalized)) {
|
||||||
@@ -2358,33 +2283,17 @@ function handleItemPropertyEditModeInput(code: string, key: string, ctrlKey: boo
|
|||||||
audio.sfxUiCancel();
|
audio.sfxUiCancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
signaling.send({ type: 'item_update', itemId, params: { [propertyKey]: normalized } });
|
sendItemParams({ [propertyKey]: normalized });
|
||||||
} else if (propertyKey === 'mediaEffectValue' || propertyKey === 'emitEffectValue') {
|
} else if (propertyKey === 'mediaEffectValue' || propertyKey === 'emitEffectValue') {
|
||||||
const parsed = validateNumericItemPropertyInput(item, propertyKey, value, false);
|
if (!submitNumericParam(propertyKey, false, (num) => clampEffectLevel(num))) {
|
||||||
if (!parsed.ok) {
|
|
||||||
updateStatus(parsed.message);
|
|
||||||
audio.sfxUiCancel();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
signaling.send({ type: 'item_update', itemId, params: { [propertyKey]: clampEffectLevel(parsed.value) } });
|
|
||||||
} else if (propertyKey === 'facing') {
|
} else if (propertyKey === 'facing') {
|
||||||
const parsed = validateNumericItemPropertyInput(item, propertyKey, value, false);
|
if (!submitNumericParam(propertyKey, false)) {
|
||||||
if (!parsed.ok) {
|
|
||||||
updateStatus(parsed.message);
|
|
||||||
audio.sfxUiCancel();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
signaling.send({ type: 'item_update', itemId, params: { facing: parsed.value } });
|
|
||||||
} else if (propertyKey === 'emitRange') {
|
|
||||||
const parsed = validateNumericItemPropertyInput(item, propertyKey, value, true);
|
|
||||||
if (!parsed.ok) {
|
|
||||||
updateStatus(parsed.message);
|
|
||||||
audio.sfxUiCancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
signaling.send({ type: 'item_update', itemId, params: { emitRange: parsed.value } });
|
|
||||||
} else if (propertyKey === 'useSound' || propertyKey === 'emitSound') {
|
} else if (propertyKey === 'useSound' || propertyKey === 'emitSound') {
|
||||||
signaling.send({ type: 'item_update', itemId, params: { [propertyKey]: value } });
|
sendItemParams({ [propertyKey]: value });
|
||||||
} else if (propertyKey === 'spaces') {
|
} else if (propertyKey === 'spaces') {
|
||||||
const spaces = value
|
const spaces = value
|
||||||
.split(',')
|
.split(',')
|
||||||
@@ -2405,22 +2314,14 @@ function handleItemPropertyEditModeInput(code: string, key: string, ctrlKey: boo
|
|||||||
audio.sfxUiCancel();
|
audio.sfxUiCancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
signaling.send({ type: 'item_update', itemId, params: { spaces: spaces.join(', ') } });
|
sendItemParams({ spaces: spaces.join(', ') });
|
||||||
} else if (propertyKey === 'sides' || propertyKey === 'number') {
|
|
||||||
const parsed = validateNumericItemPropertyInput(item, propertyKey, value, true);
|
|
||||||
if (!parsed.ok) {
|
|
||||||
updateStatus(parsed.message);
|
|
||||||
audio.sfxUiCancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
signaling.send({ type: 'item_update', itemId, params: { [propertyKey]: parsed.value } });
|
|
||||||
}
|
}
|
||||||
state.mode = 'itemProperties';
|
state.mode = 'itemProperties';
|
||||||
state.editingPropertyKey = null;
|
state.editingPropertyKey = null;
|
||||||
replaceTextOnNextType = false;
|
replaceTextOnNextType = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (code === 'Escape') {
|
if (editAction === 'cancel') {
|
||||||
state.mode = 'itemProperties';
|
state.mode = 'itemProperties';
|
||||||
state.editingPropertyKey = null;
|
state.editingPropertyKey = null;
|
||||||
replaceTextOnNextType = false;
|
replaceTextOnNextType = false;
|
||||||
@@ -2442,30 +2343,21 @@ function handleItemPropertyOptionSelectModeInput(code: string, key: string): voi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code === 'ArrowDown' || code === 'ArrowUp') {
|
const control = handleListControlKey(
|
||||||
state.itemPropertyOptionIndex = cycleIndex(
|
code,
|
||||||
state.itemPropertyOptionIndex,
|
key,
|
||||||
state.itemPropertyOptionValues.length,
|
|
||||||
code === 'ArrowDown' ? 'next' : 'prev',
|
|
||||||
);
|
|
||||||
updateStatus(state.itemPropertyOptionValues[state.itemPropertyOptionIndex]);
|
|
||||||
audio.sfxUiBlip();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextByInitial = findNextIndexByInitial(
|
|
||||||
state.itemPropertyOptionValues,
|
state.itemPropertyOptionValues,
|
||||||
state.itemPropertyOptionIndex,
|
state.itemPropertyOptionIndex,
|
||||||
key,
|
|
||||||
(value) => value,
|
(value) => value,
|
||||||
);
|
);
|
||||||
if (nextByInitial >= 0) {
|
if (control.type === 'move') {
|
||||||
state.itemPropertyOptionIndex = nextByInitial;
|
state.itemPropertyOptionIndex = control.index;
|
||||||
updateStatus(state.itemPropertyOptionValues[state.itemPropertyOptionIndex]);
|
updateStatus(state.itemPropertyOptionValues[state.itemPropertyOptionIndex]);
|
||||||
audio.sfxUiBlip();
|
audio.sfxUiBlip();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code === 'Enter') {
|
if (control.type === 'select') {
|
||||||
const selectedValue = state.itemPropertyOptionValues[state.itemPropertyOptionIndex];
|
const selectedValue = state.itemPropertyOptionValues[state.itemPropertyOptionIndex];
|
||||||
signaling.send({ type: 'item_update', itemId, params: { [propertyKey]: selectedValue } });
|
signaling.send({ type: 'item_update', itemId, params: { [propertyKey]: selectedValue } });
|
||||||
state.mode = 'itemProperties';
|
state.mode = 'itemProperties';
|
||||||
@@ -2475,7 +2367,7 @@ function handleItemPropertyOptionSelectModeInput(code: string, key: string): voi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code === 'Escape') {
|
if (control.type === 'cancel') {
|
||||||
state.mode = 'itemProperties';
|
state.mode = 'itemProperties';
|
||||||
state.editingPropertyKey = null;
|
state.editingPropertyKey = null;
|
||||||
state.itemPropertyOptionValues = [];
|
state.itemPropertyOptionValues = [];
|
||||||
@@ -2486,7 +2378,8 @@ function handleItemPropertyOptionSelectModeInput(code: string, key: string): voi
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleNicknameModeInput(code: string, key: string, ctrlKey: boolean): void {
|
function handleNicknameModeInput(code: string, key: string, ctrlKey: boolean): void {
|
||||||
if (code === 'Enter') {
|
const editAction = getEditSessionAction(code);
|
||||||
|
if (editAction === 'submit') {
|
||||||
const clean = sanitizeName(state.nicknameInput);
|
const clean = sanitizeName(state.nicknameInput);
|
||||||
if (clean) {
|
if (clean) {
|
||||||
const payload: OutgoingMessage = { type: 'update_nickname', nickname: clean };
|
const payload: OutgoingMessage = { type: 'update_nickname', nickname: clean };
|
||||||
@@ -2501,7 +2394,7 @@ function handleNicknameModeInput(code: string, key: string, ctrlKey: boolean): v
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code === 'Escape') {
|
if (editAction === 'cancel') {
|
||||||
state.mode = 'normal';
|
state.mode = 'normal';
|
||||||
replaceTextOnNextType = false;
|
replaceTextOnNextType = false;
|
||||||
updateStatus('Cancelled.');
|
updateStatus('Cancelled.');
|
||||||
|
|||||||
Reference in New Issue
Block a user