mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
stateful night vision rework
This commit is contained in:
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -81,6 +81,12 @@ func (n *NightVisionLight) HandleAction(action string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NightVisionLight) NightVisionOn() bool {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
return !n.on
|
||||
}
|
||||
|
||||
func (n *NightVisionLight) setLocked(on bool) error {
|
||||
if err := n.line.SetValue(boolToGPIO(on)); err != nil {
|
||||
return err
|
||||
|
||||
@@ -18,3 +18,7 @@ func (n *NightVisionLight) Close() {}
|
||||
func (n *NightVisionLight) HandleAction(action string) error {
|
||||
return fmt.Errorf("night vision not supported in dummy build")
|
||||
}
|
||||
|
||||
func (n *NightVisionLight) NightVisionOn() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -184,7 +184,13 @@ func (c *WSClient) dispatch(ctx context.Context, msg *inboundMessage) error {
|
||||
if c.nightVision == nil {
|
||||
return fmt.Errorf("night vision disabled")
|
||||
}
|
||||
return c.nightVision.HandleAction(msg.NightVision.Action)
|
||||
if err := c.nightVision.HandleAction(msg.NightVision.Action); err != nil {
|
||||
return err
|
||||
}
|
||||
c.emitEvent("nightVision.state", map[string]any{
|
||||
"nightVisionOn": c.nightVision.NightVisionOn(),
|
||||
})
|
||||
return nil
|
||||
case msg.Song != nil:
|
||||
slot := 0
|
||||
if msg.Song.Slot != nil {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,8 +11,8 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||
<title>Multi Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-sxx9hCHI.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-VF8LslAQ.css">
|
||||
<script type="module" crossorigin src="/assets/index-DCabXYAC.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-lg9md4T4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -17,6 +17,10 @@ function handleMessage(roverId, msg) {
|
||||
roverManager.handleSensorFrame(roverId, msg);
|
||||
break;
|
||||
case 'event':
|
||||
if (msg.event === 'nightVision.state' && typeof msg.data?.nightVisionOn === 'boolean') {
|
||||
roverManager.setNightVisionState(roverId, msg.data.nightVisionOn);
|
||||
break;
|
||||
}
|
||||
sendAlert({ color: ALERT_COLOR, title: `${roverId} event`, message: msg.event });
|
||||
break;
|
||||
default:
|
||||
@@ -69,7 +73,11 @@ roverWSS.on('connection', (ws) => {
|
||||
} else if (msg.type === 'ack') {
|
||||
handleAck(msg);
|
||||
} else if (msg.type === 'event') {
|
||||
sendAlert({ color: ALERT_COLOR, title: `${roverId}`, message: msg.event });
|
||||
if (msg.event === 'nightVision.state' && typeof msg.data?.nightVisionOn === 'boolean') {
|
||||
roverManager.setNightVisionState(roverId, msg.data.nightVisionOn);
|
||||
} else {
|
||||
sendAlert({ color: ALERT_COLOR, title: `${roverId}`, message: msg.event });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ function ensureRecord(id) {
|
||||
locked: false,
|
||||
lockReason: null,
|
||||
batteryState: null,
|
||||
nightVisionState: null,
|
||||
room: `rover:${id}`,
|
||||
lastSeen: Date.now(),
|
||||
lastMovementAt: Date.now(),
|
||||
@@ -57,6 +58,13 @@ function upsertRover(meta, ws) {
|
||||
record.meta = meta;
|
||||
record.ws = ws;
|
||||
record.lastSeen = Date.now();
|
||||
if (record.nightVisionState == null && meta?.nightVision?.enabled) {
|
||||
const ledOn = Boolean(meta.nightVision.initialOn);
|
||||
record.nightVisionState = {
|
||||
nightVisionOn: !ledOn,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
rovers.set(id, record);
|
||||
spectatorSockets.forEach((socketId) => {
|
||||
const sock = io.sockets.sockets.get(socketId);
|
||||
@@ -143,7 +151,9 @@ function getRoster() {
|
||||
media: record.meta?.media,
|
||||
cameraServo: record.meta?.cameraServo,
|
||||
audio: record.meta?.audio,
|
||||
nightVision: record.meta?.nightVision,
|
||||
nightVision: record.meta?.nightVision
|
||||
? { ...record.meta.nightVision, state: record.nightVisionState }
|
||||
: record.meta?.nightVision,
|
||||
locked: record.locked,
|
||||
lockReason: record.lockReason,
|
||||
lastSeen: record.lastSeen,
|
||||
@@ -154,6 +164,18 @@ function broadcastRoster() {
|
||||
io.emit('rovers', getRoster());
|
||||
}
|
||||
|
||||
function setNightVisionState(roverId, nightVisionOn) {
|
||||
const record = rovers.get(roverId);
|
||||
if (!record) return;
|
||||
if (typeof nightVisionOn !== 'boolean') return;
|
||||
record.nightVisionState = {
|
||||
nightVisionOn,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
broadcastRoster();
|
||||
managerEvents.emit('rover', { roverId, action: 'nightVision', record });
|
||||
}
|
||||
|
||||
function computeBatteryState(record, sensors) {
|
||||
if (!record) return null;
|
||||
if (!sensors) return record.batteryState;
|
||||
@@ -557,6 +579,7 @@ module.exports = {
|
||||
lockRover,
|
||||
getRoster,
|
||||
broadcastRoster,
|
||||
setNightVisionState,
|
||||
handleSensorFrame,
|
||||
requestControl,
|
||||
releaseControl,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useControlSystem } from '../controls/index.js';
|
||||
import { formatKeyLabel } from '../controls/keymapUtils.js';
|
||||
import NightVisionControl from './NightVisionControl.jsx';
|
||||
|
||||
const SLIDER_THROTTLE_MS = 150;
|
||||
|
||||
@@ -10,11 +12,15 @@ function formatDegrees(value) {
|
||||
|
||||
export default function CameraServoPanel() {
|
||||
const {
|
||||
state: { roverId, camera },
|
||||
actions: { setServoAngle, nudgeServo, goServoHome },
|
||||
state: { roverId, camera, keymap },
|
||||
pipeline,
|
||||
actions: { setServoAngle, nudgeServo, goServoHome, setNightVision },
|
||||
} = useControlSystem();
|
||||
const config = camera?.config;
|
||||
const enabled = Boolean(roverId && camera?.enabled && config);
|
||||
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
|
||||
const nightVisionState = pipeline?.nightVisionState;
|
||||
const nightVisionKey = formatKeyLabel(keymap?.nightVisionToggle?.[0]);
|
||||
const min = typeof config?.minAngle === 'number' ? config.minAngle : -30;
|
||||
const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
|
||||
const value =
|
||||
@@ -24,7 +30,7 @@ export default function CameraServoPanel() {
|
||||
? config.homeAngle
|
||||
: (min + max) / 2;
|
||||
|
||||
if (!enabled) return null;
|
||||
if (!enabled && !nightVisionAvailable) return null;
|
||||
|
||||
const [pendingAngle, setPendingAngle] = useState(value);
|
||||
const throttleRef = useRef(null);
|
||||
@@ -80,30 +86,48 @@ export default function CameraServoPanel() {
|
||||
nudgeServo(delta);
|
||||
};
|
||||
|
||||
const handleNightVisionToggle = (nextOn) => {
|
||||
if (!nightVisionAvailable) return;
|
||||
setNightVision(nextOn);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="panel-section space-y-0.5 text-base">
|
||||
<div className="flex items-center justify-between text-sm text-slate-300">
|
||||
<span>Camera Tilt</span>
|
||||
<span className="font-mono text-sm text-slate-100">{formatDegrees(value)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="range"
|
||||
className="w-full accent-emerald-400"
|
||||
min={min}
|
||||
max={max}
|
||||
step={0.5}
|
||||
value={pendingAngle}
|
||||
onChange={handleSlider}
|
||||
onMouseUp={commitSlider}
|
||||
onTouchEnd={commitSlider}
|
||||
onPointerUp={commitSlider}
|
||||
{enabled && (
|
||||
<>
|
||||
<div className="flex items-center justify-between text-sm text-slate-300">
|
||||
<span>Camera Tilt</span>
|
||||
<span className="font-mono text-sm text-slate-100">{formatDegrees(value)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="range"
|
||||
className="w-full accent-emerald-400"
|
||||
min={min}
|
||||
max={max}
|
||||
step={0.5}
|
||||
value={pendingAngle}
|
||||
onChange={handleSlider}
|
||||
onMouseUp={commitSlider}
|
||||
onTouchEnd={commitSlider}
|
||||
onPointerUp={commitSlider}
|
||||
/>
|
||||
<div className="mt-0 flex justify-between text-xs text-slate-400">
|
||||
<span>{formatDegrees(min)}</span>
|
||||
<span>{formatDegrees(max)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{nightVisionAvailable && (
|
||||
<NightVisionControl
|
||||
nightVisionOn={nightVisionState?.nightVisionOn}
|
||||
disabled={!roverId}
|
||||
onToggle={handleNightVisionToggle}
|
||||
keyLabel={nightVisionKey}
|
||||
className={enabled ? 'mt-1' : ''}
|
||||
/>
|
||||
<div className="mt-0 flex justify-between text-xs text-slate-400">
|
||||
<span>{formatDegrees(min)}</span>
|
||||
<span>{formatDegrees(max)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* <div className="flex gap-0.5 text-sm">
|
||||
<button type="button" className="flex-1 button-dark" onClick={() => handleNudge(-1)}>
|
||||
Tilt Down
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useControlSystem } from '../controls/index.js';
|
||||
import { clampUnit } from '../controls/controlMath.js';
|
||||
import DriveDockAction, { useDriveDockState } from './DriveDockAction.jsx';
|
||||
import NightVisionControl from './NightVisionControl.jsx';
|
||||
|
||||
const SOURCE = 'mobile-joystick';
|
||||
const JOYSTICK_RADIUS = 80;
|
||||
@@ -134,7 +135,7 @@ function MobileJoystickPanel({ layout }) {
|
||||
const {
|
||||
state: { roverId, camera },
|
||||
pipeline,
|
||||
actions: { setDriveVector, registerInputState, setServoAngle, toggleNightVision },
|
||||
actions: { setDriveVector, registerInputState, setServoAngle, setNightVision },
|
||||
} = useControlSystem();
|
||||
const driveDockState = useDriveDockState(roverId);
|
||||
const dockedNotDriving = driveDockState.docked && !driveDockState.driving;
|
||||
@@ -142,6 +143,7 @@ function MobileJoystickPanel({ layout }) {
|
||||
const cameraConfig = camera?.config;
|
||||
const cameraEnabled = Boolean(roverId && camera?.enabled && cameraConfig);
|
||||
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
|
||||
const nightVisionState = pipeline?.nightVisionState;
|
||||
const cameraMin = typeof cameraConfig?.minAngle === 'number' ? cameraConfig.minAngle : -45;
|
||||
const cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45;
|
||||
const cameraValue =
|
||||
@@ -198,6 +200,14 @@ function MobileJoystickPanel({ layout }) {
|
||||
setServoAngle(next);
|
||||
};
|
||||
|
||||
const handleNightVisionToggle = useCallback(
|
||||
(nextOn) => {
|
||||
if (!nightVisionAvailable) return;
|
||||
setNightVision(nextOn);
|
||||
},
|
||||
[nightVisionAvailable, setNightVision],
|
||||
);
|
||||
|
||||
const fillClass = dockedNotDriving ? 'max-h-screen self-start' : '';
|
||||
const containerClass = `flex h-full flex-col gap-0.5 text-slate-100 ${fillClass}`;
|
||||
|
||||
@@ -206,16 +216,6 @@ function MobileJoystickPanel({ layout }) {
|
||||
<DriveDockAction layout="mobile" expand={dockedNotDriving} driveDockState={driveDockState} />
|
||||
{!dockedNotDriving ? (
|
||||
<>
|
||||
{nightVisionAvailable && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleNightVision()}
|
||||
disabled={disabled}
|
||||
className="bg-amber-600 px-0.5 py-1 text-sm font-semibold text-amber-50 transition hover:bg-amber-500 disabled:opacity-40"
|
||||
>
|
||||
Toggle Night Vision
|
||||
</button>
|
||||
)}
|
||||
{cameraEnabled && (
|
||||
<div className="bg-zinc-950 p-0.5 text-xs">
|
||||
<div className="flex items-center justify-between text-[0.75rem] text-slate-400">
|
||||
@@ -233,6 +233,14 @@ function MobileJoystickPanel({ layout }) {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{nightVisionAvailable && (
|
||||
<NightVisionControl
|
||||
nightVisionOn={nightVisionState?.nightVisionOn}
|
||||
disabled={disabled}
|
||||
onToggle={handleNightVisionToggle}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
)}
|
||||
<FloatingJoystick
|
||||
disabled={disabled}
|
||||
layout={layout}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
function isBoolean(value) {
|
||||
return typeof value === 'boolean';
|
||||
}
|
||||
|
||||
export default function NightVisionControl({
|
||||
nightVisionOn,
|
||||
disabled,
|
||||
onToggle,
|
||||
keyLabel,
|
||||
className = '',
|
||||
}) {
|
||||
const [optimistic, setOptimistic] = useState(
|
||||
isBoolean(nightVisionOn) ? nightVisionOn : null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isBoolean(nightVisionOn)) {
|
||||
setOptimistic(nightVisionOn);
|
||||
}
|
||||
}, [nightVisionOn]);
|
||||
|
||||
const hasState = isBoolean(optimistic);
|
||||
const displayOn = hasState ? optimistic : false;
|
||||
const statusLabel = hasState ? (displayOn ? 'On' : 'Off') : '—';
|
||||
const statusClasses = displayOn
|
||||
? 'bg-emerald-600 text-emerald-50'
|
||||
: 'bg-slate-700 text-slate-200';
|
||||
|
||||
const handleToggle = () => {
|
||||
if (disabled) return;
|
||||
const next = hasState ? !displayOn : true;
|
||||
setOptimistic(next);
|
||||
onToggle?.(next);
|
||||
};
|
||||
|
||||
const buttonClasses = useMemo(
|
||||
() =>
|
||||
[
|
||||
'flex w-full items-center justify-between rounded bg-zinc-950 px-1 py-0.5 text-xs text-slate-300',
|
||||
'transition hover:bg-zinc-900 disabled:opacity-50',
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
[className],
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggle}
|
||||
disabled={disabled}
|
||||
aria-pressed={displayOn}
|
||||
className={buttonClasses}
|
||||
>
|
||||
<span className="text-slate-400">Night Vision</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className={`rounded px-1 py-0.5 text-[0.65rem] font-semibold ${statusClasses}`}>
|
||||
{statusLabel}
|
||||
</span>
|
||||
{keyLabel ? (
|
||||
<span className="rounded bg-slate-800 px-1 py-0.5 text-[0.6rem] font-semibold text-slate-200">
|
||||
{keyLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -312,10 +312,23 @@ export function ControlSystemProvider({ children }) {
|
||||
[pipeline],
|
||||
);
|
||||
|
||||
const setNightVision = useCallback(
|
||||
(nightVisionOn) => {
|
||||
if (!pipeline.nightVision) return;
|
||||
if (typeof nightVisionOn === 'boolean') {
|
||||
const action = nightVisionOn ? 'off' : 'on';
|
||||
pipeline.sendNightVision(action);
|
||||
} else {
|
||||
pipeline.sendNightVision('toggle');
|
||||
}
|
||||
recordControlIntent();
|
||||
},
|
||||
[pipeline, recordControlIntent],
|
||||
);
|
||||
|
||||
const toggleNightVision = useCallback(() => {
|
||||
pipeline.sendNightVision('toggle');
|
||||
recordControlIntent();
|
||||
}, [pipeline, recordControlIntent]);
|
||||
setNightVision();
|
||||
}, [setNightVision]);
|
||||
|
||||
const setSongNote = useCallback(
|
||||
(note) => {
|
||||
@@ -354,6 +367,7 @@ export function ControlSystemProvider({ children }) {
|
||||
stopAllMotion,
|
||||
sendOiCommand,
|
||||
setSensorStream,
|
||||
setNightVision,
|
||||
toggleNightVision,
|
||||
updateKeyBinding,
|
||||
resetKeyBindings,
|
||||
@@ -376,6 +390,7 @@ export function ControlSystemProvider({ children }) {
|
||||
stopAllMotion,
|
||||
sendOiCommand,
|
||||
setSensorStream,
|
||||
setNightVision,
|
||||
toggleNightVision,
|
||||
updateKeyBinding,
|
||||
resetKeyBindings,
|
||||
|
||||
@@ -32,6 +32,8 @@ export function useCommandPipeline(options = {}) {
|
||||
return rosterEntry.nightVision;
|
||||
}, [rosterEntry]);
|
||||
|
||||
const nightVisionState = useMemo(() => rosterEntry?.nightVision?.state ?? null, [rosterEntry]);
|
||||
|
||||
const emitCommand = useCallback(
|
||||
(payload, cb) => {
|
||||
if (!roverId) return;
|
||||
@@ -202,6 +204,7 @@ export function useCommandPipeline(options = {}) {
|
||||
rosterEntry,
|
||||
servoConfig,
|
||||
nightVision,
|
||||
nightVisionState,
|
||||
emitCommand,
|
||||
enableSensorStream,
|
||||
sendDriveDirect,
|
||||
@@ -217,6 +220,7 @@ export function useCommandPipeline(options = {}) {
|
||||
rosterEntry,
|
||||
servoConfig,
|
||||
nightVision,
|
||||
nightVisionState,
|
||||
emitCommand,
|
||||
enableSensorStream,
|
||||
sendDriveDirect,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getPadSignature,
|
||||
} from './gamepadBindings.js';
|
||||
import { subscribeGamepadHub } from './gamepadHub.js';
|
||||
import { isTextEntryActive } from './inputFocusUtils.js';
|
||||
|
||||
const SOURCE = 'gamepad';
|
||||
const ZERO_VECTOR = { x: 0, y: 0, boost: false };
|
||||
@@ -154,6 +155,21 @@ export default function GamepadInputManager() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTextEntryActive()) {
|
||||
if (!areVectorsEqual(lastVectorRef.current, ZERO_VECTOR)) {
|
||||
lastVectorRef.current = ZERO_VECTOR;
|
||||
setDriveVector(ZERO_VECTOR, { source: SOURCE });
|
||||
}
|
||||
if (!areAuxEqual(lastAuxRef.current, ZERO_AUX)) {
|
||||
lastAuxRef.current = ZERO_AUX;
|
||||
setAuxMotors(ZERO_AUX);
|
||||
}
|
||||
buttonStateRef.current = new Map();
|
||||
reverseStateRef.current = { main: false, side: false };
|
||||
registerInputState(SOURCE, { connected: true, blocked: true });
|
||||
return;
|
||||
}
|
||||
|
||||
ensureProfile(activePad);
|
||||
const signature = activePad.signature;
|
||||
const profile =
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useControlSystem } from '../ControlContext.jsx';
|
||||
import { useChat } from '../../context/ChatContext.jsx';
|
||||
import { useSession } from '../../context/SessionContext.jsx';
|
||||
import { normalizeKeymapEntries, tokensForEvent } from '../keymapUtils.js';
|
||||
import { isTextInputElement } from './inputFocusUtils.js';
|
||||
import { useSettingsNamespace } from '../../settings/index.js';
|
||||
import { INPUT_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
||||
import {
|
||||
@@ -55,15 +56,7 @@ function mapTiltIntervalToSpeed(interval) {
|
||||
}
|
||||
|
||||
function shouldIgnoreEvent(event) {
|
||||
const target = event.target;
|
||||
if (!target) return false;
|
||||
const tag = target.tagName;
|
||||
return (
|
||||
tag === 'INPUT' ||
|
||||
tag === 'TEXTAREA' ||
|
||||
target.isContentEditable ||
|
||||
tag === 'SELECT'
|
||||
);
|
||||
return isTextInputElement(event?.target);
|
||||
}
|
||||
|
||||
function bindingActive(bindingSet, keys) {
|
||||
@@ -395,12 +388,12 @@ export default function KeyboardInputManager() {
|
||||
resetAll();
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('keyup', handleKeyUp);
|
||||
window.addEventListener('keydown', handleKeyDown, { capture: true });
|
||||
window.addEventListener('keyup', handleKeyUp, { capture: true });
|
||||
window.addEventListener('blur', handleBlur);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('keyup', handleKeyUp);
|
||||
window.removeEventListener('keydown', handleKeyDown, { capture: true });
|
||||
window.removeEventListener('keyup', handleKeyUp, { capture: true });
|
||||
window.removeEventListener('blur', handleBlur);
|
||||
};
|
||||
}, [
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
const TEXT_INPUT_TYPES = new Set([
|
||||
'',
|
||||
'text',
|
||||
'search',
|
||||
'email',
|
||||
'password',
|
||||
'url',
|
||||
'tel',
|
||||
'number',
|
||||
'date',
|
||||
'datetime-local',
|
||||
'month',
|
||||
'time',
|
||||
'week',
|
||||
]);
|
||||
|
||||
export function isTextInputElement(target) {
|
||||
if (!target || target.nodeType !== 1) return false;
|
||||
const tag = target.tagName;
|
||||
if (tag === 'TEXTAREA') return true;
|
||||
if (target.isContentEditable) return true;
|
||||
if (tag !== 'INPUT') return false;
|
||||
const type = target.type ? target.type.toLowerCase() : '';
|
||||
return TEXT_INPUT_TYPES.has(type);
|
||||
}
|
||||
|
||||
export function isTextEntryActive() {
|
||||
if (typeof document === 'undefined') return false;
|
||||
return isTextInputElement(document.activeElement);
|
||||
}
|
||||
Reference in New Issue
Block a user