102 lines
3.8 KiB
TypeScript
102 lines
3.8 KiB
TypeScript
import { h } from 'preact';
|
|
import { useState, useEffect } from 'preact/hooks';
|
|
import { wsClient } from '../lib/ws';
|
|
import { getSignalStore, getMetaStore } from '../lib/stores';
|
|
import { logicEngine } from '../lib/logic';
|
|
import { useAuth, canWrite } from '../lib/auth';
|
|
import type { Widget, SignalMeta } from '../lib/types';
|
|
|
|
interface Props { widget: Widget; onContextMenu?: (e: MouseEvent) => void; }
|
|
|
|
export default function Button({ widget, onContextMenu }: Props) {
|
|
const sigRef = widget.signals[0];
|
|
const label = widget.options['label'] ?? 'Button';
|
|
const sendValue = widget.options['value'] ?? '1';
|
|
const resetValue = widget.options['resetValue'] ?? '0';
|
|
// mode: oneshot | setReset | persistent
|
|
const mode = widget.options['mode'] ?? 'oneshot';
|
|
const resetTimeSec = parseFloat(widget.options['resetTime'] ?? '1');
|
|
const confirmOpt = widget.options['confirm'] === 'true';
|
|
// Optional panel-logic sequence fired on click (in addition to any signal write).
|
|
const action = widget.options['action'] ?? '';
|
|
|
|
const me = useAuth();
|
|
const [signalVal, setSignalVal] = useState<any>(null);
|
|
const [meta, setMeta] = useState<SignalMeta | null>(null);
|
|
|
|
// Subscribe to signal only for persistent mode (to track whether it is active)
|
|
useEffect(() => {
|
|
if (!sigRef || mode !== 'persistent') return;
|
|
return getSignalStore(sigRef).subscribe(sv => setSignalVal(sv.value));
|
|
}, [sigRef?.ds, sigRef?.name, mode]);
|
|
|
|
// Track the target signal's writability so the button can be disabled when
|
|
// the user has no permission to write it (real signals only; local panel
|
|
// vars are written client-side and have no backend metadata).
|
|
useEffect(() => {
|
|
if (!sigRef || sigRef.ds === 'local') return;
|
|
return getMetaStore(sigRef).subscribe(setMeta);
|
|
}, [sigRef?.ds, sigRef?.name]);
|
|
|
|
// A button that drives a real signal is disabled when the user cannot write
|
|
// it (read-only access, or the signal reports it is not writable). Buttons
|
|
// that only run a logic action — or target a local var — stay enabled.
|
|
const drivesRealSignal = !!sigRef && sigRef.ds !== 'local';
|
|
const writeAllowed = !drivesRealSignal || (canWrite(me.level) && meta?.writable !== false);
|
|
|
|
const parsedSend = isNaN(parseFloat(sendValue)) ? sendValue : parseFloat(sendValue);
|
|
const parsedReset = isNaN(parseFloat(resetValue)) ? resetValue : parseFloat(resetValue);
|
|
|
|
// Persistent mode: button appears pressed while signal value matches sendValue
|
|
const isPressed = mode === 'persistent'
|
|
&& signalVal !== null
|
|
&& String(signalVal) === String(parsedSend);
|
|
|
|
function doWrite(val: any) {
|
|
if (!sigRef) return;
|
|
wsClient.write(sigRef, val);
|
|
}
|
|
|
|
function execute() {
|
|
if (sigRef) {
|
|
if (mode === 'setReset') {
|
|
doWrite(parsedSend);
|
|
setTimeout(() => doWrite(parsedReset), Math.max(0, resetTimeSec) * 1000);
|
|
} else if (mode === 'persistent') {
|
|
doWrite(isPressed ? parsedReset : parsedSend);
|
|
} else {
|
|
doWrite(parsedSend);
|
|
}
|
|
}
|
|
if (action) logicEngine.runAction(action);
|
|
}
|
|
|
|
function handleClick() {
|
|
// A button may drive a signal, a logic sequence, or both.
|
|
if (!sigRef && !action) return;
|
|
if (!writeAllowed) return;
|
|
if (confirmOpt) {
|
|
if (window.confirm(`Send ${label}?`)) execute();
|
|
} else {
|
|
execute();
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
class="button-widget"
|
|
style={`left:${widget.x}px;top:${widget.y}px;width:${widget.w}px;height:${widget.h}px;`}
|
|
onContextMenu={onContextMenu}
|
|
>
|
|
<button class={`btn${isPressed ? ' btn-pressed' : ''}`} onClick={handleClick}
|
|
disabled={!writeAllowed}
|
|
title={writeAllowed ? '' : 'You do not have permission to write this signal'}>
|
|
{label}
|
|
{mode === 'setReset' && (
|
|
<span class="btn-mode-hint">{resetTimeSec}s</span>
|
|
)}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|