Files
Nitro-V3/src/components/wired/views/extras/WiredExtraVariableReferenceView.tsx
T
2026-04-08 16:18:16 +02:00

250 lines
9.8 KiB
TypeScript

import { FC, useEffect, useMemo, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { NitroInput } from '../../../../layout';
import { WiredExtraBaseView } from './WiredExtraBaseView';
const TARGET_USER = 0;
const TARGET_ROOM = 3;
const MAX_NAME_LENGTH = 40;
interface IVariableReferenceEditorVariable
{
hasValue: boolean;
itemId: number;
name: string;
targetType: number;
}
interface IVariableReferenceEditorRoom
{
roomId: number;
roomName: string;
variables: IVariableReferenceEditorVariable[];
}
interface IVariableReferenceEditorData
{
readOnly?: boolean;
rooms?: IVariableReferenceEditorRoom[];
sourceRoomId?: number;
sourceRoomName?: string;
sourceTargetType?: number;
sourceVariableItemId?: number;
sourceVariableName?: string;
variableName?: string;
}
const normalizeVariableName = (value: string) =>
{
let normalizedValue = (value ?? '').replace(/[\t\r\n]/g, '');
if(normalizedValue.includes('=')) normalizedValue = normalizedValue.substring(0, normalizedValue.indexOf('=')).trim();
while(normalizedValue.startsWith('@') || normalizedValue.startsWith('~'))
{
normalizedValue = normalizedValue.substring(1);
}
normalizedValue = normalizedValue.replace(/\s+/g, '_');
normalizedValue = normalizedValue.replace(/[^A-Za-z0-9_]/g, '');
return normalizedValue.slice(0, MAX_NAME_LENGTH);
};
const handleVariableNameKeyDown = (event: React.KeyboardEvent<HTMLInputElement>, setValue: (value: string) => void) =>
{
if(event.key !== ' ') return;
event.preventDefault();
const input = event.currentTarget;
const start = (input.selectionStart ?? input.value.length);
const end = (input.selectionEnd ?? start);
const nextValue = `${ input.value.substring(0, start) }_${ input.value.substring(end) }`;
setValue(normalizeVariableName(nextValue));
window.requestAnimationFrame(() => input.setSelectionRange(Math.min(start + 1, input.value.length + 1), Math.min(start + 1, input.value.length + 1)));
};
const parseEditorData = (value: string): IVariableReferenceEditorData =>
{
if(!value?.trim().startsWith('{')) return {};
try
{
return (JSON.parse(value) as IVariableReferenceEditorData) || {};
}
catch
{
return {};
}
};
export const WiredExtraVariableReferenceView: FC<{}> = () =>
{
const { trigger = null, setIntParams = null, setStringParam = null } = useWired();
const [ variableName, setVariableName ] = useState('');
const [ sourceRoomId, setSourceRoomId ] = useState(0);
const [ sourceVariableItemId, setSourceVariableItemId ] = useState(0);
const [ sourceTargetType, setSourceTargetType ] = useState(TARGET_USER);
const [ readOnly, setReadOnly ] = useState(true);
const [ roomOptions, setRoomOptions ] = useState<IVariableReferenceEditorRoom[]>([]);
const [ fallbackRoomName, setFallbackRoomName ] = useState('');
const [ fallbackVariableName, setFallbackVariableName ] = useState('');
useEffect(() =>
{
if(!trigger)
{
setVariableName('');
setSourceRoomId(0);
setSourceVariableItemId(0);
setSourceTargetType(TARGET_USER);
setReadOnly(true);
setRoomOptions([]);
setFallbackRoomName('');
setFallbackVariableName('');
return;
}
const editorData = parseEditorData(trigger.stringData);
setVariableName(normalizeVariableName(editorData.variableName || ''));
setSourceRoomId(editorData.sourceRoomId || 0);
setSourceVariableItemId(editorData.sourceVariableItemId || 0);
setSourceTargetType((editorData.sourceTargetType === TARGET_ROOM) ? TARGET_ROOM : TARGET_USER);
setReadOnly(editorData.readOnly !== false);
setRoomOptions([ ...(editorData.rooms || []) ]);
setFallbackRoomName((editorData.sourceRoomName || '').trim());
setFallbackVariableName((editorData.sourceVariableName || '').trim());
}, [ trigger ]);
const mergedRoomOptions = useMemo(() =>
{
const nextValue = [ ...roomOptions ];
if(sourceRoomId <= 0) return nextValue;
if(nextValue.some(room => (room.roomId === sourceRoomId))) return nextValue;
nextValue.push({
roomId: sourceRoomId,
roomName: (fallbackRoomName || `#${ sourceRoomId }`),
variables: sourceVariableItemId > 0
? [ {
itemId: sourceVariableItemId,
name: (fallbackVariableName || `#${ sourceVariableItemId }`),
targetType: sourceTargetType,
hasValue: true
} ]
: []
});
return nextValue;
}, [ fallbackRoomName, fallbackVariableName, roomOptions, sourceRoomId, sourceTargetType, sourceVariableItemId ]);
const selectedRoom = useMemo(() => mergedRoomOptions.find(option => (option.roomId === sourceRoomId)) ?? null, [ mergedRoomOptions, sourceRoomId ]);
const selectedRoomVariables = (selectedRoom?.variables || []);
useEffect(() =>
{
if(!selectedRoom)
{
if(!sourceRoomId && mergedRoomOptions.length) setSourceRoomId(mergedRoomOptions[0].roomId);
return;
}
const hasSelectedVariable = selectedRoomVariables.some(variable => (variable.itemId === sourceVariableItemId) && (variable.targetType === sourceTargetType));
if(hasSelectedVariable) return;
const fallbackVariable = selectedRoomVariables[0];
if(!fallbackVariable)
{
setSourceVariableItemId(0);
setSourceTargetType(TARGET_USER);
return;
}
setSourceVariableItemId(fallbackVariable.itemId);
setSourceTargetType(fallbackVariable.targetType);
}, [ mergedRoomOptions, selectedRoom, selectedRoomVariables, sourceRoomId, sourceTargetType, sourceVariableItemId ]);
const save = () =>
{
setIntParams([]);
setStringParam(JSON.stringify({
variableName: normalizeVariableName(variableName),
sourceRoomId,
sourceVariableItemId,
sourceTargetType,
readOnly
}));
};
const validate = () => !!normalizeVariableName(variableName).length && (sourceRoomId > 0) && (sourceVariableItemId > 0);
const getTargetLabel = (targetType: number) =>
{
if(targetType === TARGET_ROOM)
{
const globalLabel = LocalizeText('wiredfurni.params.sources.global');
return ((globalLabel && (globalLabel !== 'wiredfurni.params.sources.global')) ? globalLabel : 'Global');
}
return 'User';
};
return (
<WiredExtraBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save } validate={ validate } cardStyle={ { width: 400 } }>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-1">
<Text>{ LocalizeText('wiredfurni.params.variables.variable_name') }</Text>
<NitroInput maxLength={ MAX_NAME_LENGTH } type="text" value={ variableName } onChange={ event => setVariableName(normalizeVariableName(event.target.value)) } onKeyDown={ event => handleVariableNameKeyDown(event, setVariableName) } />
</div>
<div className="flex flex-col gap-1">
<Text>{ LocalizeText('wiredfurni.params.variables.room_selection') }</Text>
<select className="form-select form-select-sm" value={ sourceRoomId } onChange={ event => setSourceRoomId(parseInt(event.target.value, 10) || 0) }>
<option value={ 0 }>{ LocalizeText('wiredfurni.variable_picker.search') }</option>
{ mergedRoomOptions.map(option => <option key={ option.roomId } value={ option.roomId }>{ option.roomName }</option>) }
</select>
</div>
<div className="flex flex-col gap-1">
<Text>{ LocalizeText('wiredfurni.params.variables.variable_ref_selection') }</Text>
<select
className="form-select form-select-sm"
value={ `${ sourceVariableItemId }:${ sourceTargetType }` }
onChange={ event =>
{
const [ nextItemId, nextTargetType ] = event.target.value.split(':').map(value => parseInt(value, 10) || 0);
setSourceVariableItemId(nextItemId);
setSourceTargetType((nextTargetType === TARGET_ROOM) ? TARGET_ROOM : TARGET_USER);
} }>
<option value="0:0">{ LocalizeText('wiredfurni.variable_picker.search') }</option>
{ selectedRoomVariables.map(variable => (
<option key={ `${ variable.itemId }:${ variable.targetType }` } value={ `${ variable.itemId }:${ variable.targetType }` }>
{ `${ variable.name } (${ getTargetLabel(variable.targetType) })` }
</option>
)) }
</select>
</div>
<div className="flex flex-col gap-1">
<Text>{ LocalizeText('wiredfurni.params.variables.settings') }</Text>
<label className="flex items-center gap-1 cursor-pointer">
<input checked={ readOnly } className="form-check-input" type="checkbox" onChange={ event => setReadOnly(event.target.checked) } />
<Text>{ LocalizeText('wiredfurni.params.variables.settings.read_only') }</Text>
</label>
</div>
</div>
</WiredExtraBaseView>
);
};