mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
Control keybindings (#489)
* fixed inline settings textedit and added datepicker keybindings * added dropdown keybindings * added observable to make sure that keybindings aren't double registered * added enter and escape keybindings for datepicker * dropdown closure fix
This commit is contained in:
parent
c0c53edb84
commit
d3c48e3a3e
@ -23,6 +23,54 @@
|
|||||||
"command": "generic:deleteItem",
|
"command": "generic:deleteItem",
|
||||||
"keys": ["Backspace", "Delete"]
|
"keys": ["Backspace", "Delete"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "generic:space",
|
||||||
|
"keys": ["Space"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "generic:tab",
|
||||||
|
"keys": ["Tab"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "generic:numpad-0",
|
||||||
|
"keys": ["0"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "generic:numpad-1",
|
||||||
|
"keys": ["1"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "generic:numpad-2",
|
||||||
|
"keys": ["2"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "generic:numpad-3",
|
||||||
|
"keys": ["3"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "generic:numpad-4",
|
||||||
|
"keys": ["4"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "generic:numpad-5",
|
||||||
|
"keys": ["5"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "generic:numpad-6",
|
||||||
|
"keys": ["6"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "generic:numpad-7",
|
||||||
|
"keys": ["7"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "generic:numpad-8",
|
||||||
|
"keys": ["8"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "generic:numpad-9",
|
||||||
|
"keys": ["9"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "generic:selectAbove",
|
"command": "generic:selectAbove",
|
||||||
"keys": ["ArrowUp"]
|
"keys": ["ArrowUp"]
|
||||||
@ -31,6 +79,14 @@
|
|||||||
"command": "generic:selectBelow",
|
"command": "generic:selectBelow",
|
||||||
"keys": ["ArrowDown"]
|
"keys": ["ArrowDown"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "generic:selectLeft",
|
||||||
|
"keys": ["ArrowLeft"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "generic:selectRight",
|
||||||
|
"keys": ["ArrowRight"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "generic:selectPageAbove",
|
"command": "generic:selectPageAbove",
|
||||||
"keys": ["PageUp"]
|
"keys": ["PageUp"]
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import React, { useState, useEffect, useRef, createRef } from "react";
|
import React, { useState, useEffect, useRef, createRef } from "react";
|
||||||
|
import * as mobx from "mobx";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import cs from "classnames";
|
import cs from "classnames";
|
||||||
import { Button } from "@/elements";
|
import { Button } from "@/elements";
|
||||||
import { If } from "tsx-control-statements/components";
|
import { If } from "tsx-control-statements/components";
|
||||||
|
import { GlobalModel } from "@/models";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
import "./datepicker.less";
|
import "./datepicker.less";
|
||||||
|
|
||||||
@ -36,6 +39,10 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
|
|||||||
MM: selDate.format("MM"),
|
MM: selDate.format("MM"),
|
||||||
DD: selDate.format("DD"),
|
DD: selDate.format("DD"),
|
||||||
});
|
});
|
||||||
|
let curUuid = uuidv4();
|
||||||
|
let keybindsRegistered = mobx.observable.box(false, {
|
||||||
|
name: "datepicker-keybinds-registered",
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputRefs.current = {
|
inputRefs.current = {
|
||||||
@ -273,6 +280,11 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
|
|||||||
setShowYearAccordion(false);
|
setShowYearAccordion(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setShowYearAccordion(false);
|
||||||
|
};
|
||||||
|
|
||||||
const dayPickerModal = isOpen
|
const dayPickerModal = isOpen
|
||||||
? ReactDOM.createPortal(
|
? ReactDOM.createPortal(
|
||||||
<div ref={modalRef} className="day-picker-modal" style={calculatePosition()}>
|
<div ref={modalRef} className="day-picker-modal" style={calculatePosition()}>
|
||||||
@ -300,13 +312,13 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleArrowNavigation = (event, currentPart) => {
|
const handleArrowNavigation = (key, currentPart) => {
|
||||||
const currentIndex = formatParts.indexOf(currentPart);
|
const currentIndex = formatParts.indexOf(currentPart);
|
||||||
let targetInput;
|
let targetInput;
|
||||||
|
|
||||||
if (event.key === "ArrowLeft" && currentIndex > 0) {
|
if (key == "ArrowLeft" && currentIndex > 0) {
|
||||||
targetInput = inputRefs.current[formatParts[currentIndex - 1]].current;
|
targetInput = inputRefs.current[formatParts[currentIndex - 1]].current;
|
||||||
} else if (event.key === "ArrowRight" && currentIndex < formatParts.length - 1) {
|
} else if (key == "ArrowRight" && currentIndex < formatParts.length - 1) {
|
||||||
targetInput = inputRefs.current[formatParts[currentIndex + 1]].current;
|
targetInput = inputRefs.current[formatParts[currentIndex + 1]].current;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,51 +327,91 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (event, currentPart) => {
|
const handleKeyDown = (event, currentPart) => {};
|
||||||
const key = event.key;
|
|
||||||
|
|
||||||
if (key === "ArrowLeft" || key === "ArrowRight") {
|
const handleFocus = (event, part) => {
|
||||||
// Handle arrow navigation without selecting text
|
event.target.select();
|
||||||
handleArrowNavigation(event, currentPart);
|
registerKeybindings(event, part);
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerKeybindings = (event: any, part: string) => {
|
||||||
|
if (keybindsRegistered.get() == true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
mobx.action(() => {
|
||||||
if (key === " ") {
|
keybindsRegistered.set(true);
|
||||||
// Handle spacebar press to toggle the modal
|
})();
|
||||||
|
let keybindManager = GlobalModel.keybindManager;
|
||||||
|
let domain = "datepicker-" + curUuid + "-" + part;
|
||||||
|
keybindManager.registerKeybinding("control", domain, "generic:selectLeft", (waveEvent) => {
|
||||||
|
handleArrowNavigation("ArrowLeft", part);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
keybindManager.registerKeybinding("control", domain, "generic:selectRight", (waveEvent) => {
|
||||||
|
handleArrowNavigation("ArrowRight", part);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
keybindManager.registerKeybinding("control", domain, "generic:space", (waveEvent) => {
|
||||||
toggleModal();
|
toggleModal();
|
||||||
return;
|
return true;
|
||||||
}
|
});
|
||||||
|
keybindManager.registerKeybinding("control", domain, "generic:confirm", (waveEvent) => {
|
||||||
|
toggleModal();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
keybindManager.registerKeybinding("control", domain, "generic:cancel", (waveEvent) => {
|
||||||
|
closeModal();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
keybindManager.registerKeybinding("control", domain, "generic:tab", (waveEvent) => {
|
||||||
|
handleArrowNavigation("ArrowRight", part);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
for (let numpadKey = 0; numpadKey <= 9; numpadKey++) {
|
||||||
|
keybindManager.registerKeybinding(
|
||||||
|
"control",
|
||||||
|
domain,
|
||||||
|
"generic:numpad-" + numpadKey.toString(),
|
||||||
|
(waveEvent) => {
|
||||||
|
let currentPart = part;
|
||||||
|
const maxLength = currentPart === "YYYY" ? 4 : 2;
|
||||||
|
const newValue = event.target.value.length < maxLength ? event.target.value + numpadKey : numpadKey;
|
||||||
|
let selectionTimeoutId = null;
|
||||||
|
handleDatePartChange(currentPart, newValue);
|
||||||
|
|
||||||
if (key.match(/[0-9]/)) {
|
// Clear any existing timeout
|
||||||
// Handle numeric keys
|
if (selectionTimeoutId !== null) {
|
||||||
event.preventDefault();
|
clearTimeout(selectionTimeoutId);
|
||||||
const maxLength = currentPart === "YYYY" ? 4 : 2;
|
}
|
||||||
const newValue = event.target.value.length < maxLength ? event.target.value + key : key;
|
|
||||||
let selectionTimeoutId = null;
|
|
||||||
handleDatePartChange(currentPart, newValue);
|
|
||||||
|
|
||||||
// Clear any existing timeout
|
// Re-focus and select the input after state update
|
||||||
if (selectionTimeoutId !== null) {
|
selectionTimeoutId = setTimeout(() => {
|
||||||
clearTimeout(selectionTimeoutId);
|
event.target.focus();
|
||||||
}
|
event.target.select();
|
||||||
|
}, 0);
|
||||||
// Re-focus and select the input after state update
|
return true;
|
||||||
selectionTimeoutId = setTimeout(() => {
|
}
|
||||||
event.target.focus();
|
);
|
||||||
event.target.select();
|
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFocus = (event) => {
|
const handleBlur = (event, part) => {
|
||||||
event.target.select();
|
unregisterKeybindings(part);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unregisterKeybindings = (part) => {
|
||||||
|
mobx.action(() => {
|
||||||
|
keybindsRegistered.set(false);
|
||||||
|
})();
|
||||||
|
let domain = "datepicker-" + curUuid + "-" + part;
|
||||||
|
GlobalModel.keybindManager.unregisterDomain(domain);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prevent use from selecting text in the input
|
// Prevent use from selecting text in the input
|
||||||
const handleMouseDown = (event) => {
|
const handleMouseDown = (event, part) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
handleFocus(event);
|
handleFocus(event, part);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleIconKeyDown = (event) => {
|
const handleIconKeyDown = (event) => {
|
||||||
@ -413,8 +465,9 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
|
|||||||
value={dateParts[part]}
|
value={dateParts[part]}
|
||||||
onChange={(e) => handleDatePartChange(part, e.target.value)}
|
onChange={(e) => handleDatePartChange(part, e.target.value)}
|
||||||
onKeyDown={(e) => handleKeyDown(e, part)}
|
onKeyDown={(e) => handleKeyDown(e, part)}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={(e) => handleMouseDown(e, part)}
|
||||||
onFocus={handleFocus}
|
onFocus={(e) => handleFocus(e, part)}
|
||||||
|
onBlur={(e) => handleBlur(e, part)}
|
||||||
maxLength={part === "YYYY" ? 4 : 2}
|
maxLength={part === "YYYY" ? 4 : 2}
|
||||||
className="date-input"
|
className="date-input"
|
||||||
placeholder={part}
|
placeholder={part}
|
||||||
|
@ -7,6 +7,8 @@ import { boundMethod } from "autobind-decorator";
|
|||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { If } from "tsx-control-statements/components";
|
import { If } from "tsx-control-statements/components";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { GlobalModel } from "@/models";
|
||||||
|
|
||||||
import "./dropdown.less";
|
import "./dropdown.less";
|
||||||
|
|
||||||
@ -39,6 +41,7 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
|
|||||||
wrapperRef: React.RefObject<HTMLDivElement>;
|
wrapperRef: React.RefObject<HTMLDivElement>;
|
||||||
menuRef: React.RefObject<HTMLDivElement>;
|
menuRef: React.RefObject<HTMLDivElement>;
|
||||||
timeoutId: any;
|
timeoutId: any;
|
||||||
|
curUuid: string;
|
||||||
|
|
||||||
constructor(props: DropdownProps) {
|
constructor(props: DropdownProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -50,6 +53,7 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
|
|||||||
};
|
};
|
||||||
this.wrapperRef = React.createRef();
|
this.wrapperRef = React.createRef();
|
||||||
this.menuRef = React.createRef();
|
this.menuRef = React.createRef();
|
||||||
|
this.curUuid == uuidv4();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -102,49 +106,76 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
|
|||||||
@boundMethod
|
@boundMethod
|
||||||
handleFocus() {
|
handleFocus() {
|
||||||
this.setState({ isTouched: true });
|
this.setState({ isTouched: true });
|
||||||
|
this.registerKeybindings();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerKeybindings() {
|
||||||
|
let keybindManager = GlobalModel.keybindManager;
|
||||||
|
let domain = "dropdown-" + this.curUuid;
|
||||||
|
keybindManager.registerKeybinding("control", domain, "generic:confirm", (waveEvent) => {
|
||||||
|
this.handleConfirm();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
keybindManager.registerKeybinding("control", domain, "generic:space", (waveEvent) => {
|
||||||
|
this.handleConfirm();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
keybindManager.registerKeybinding("control", domain, "generic:cancel", (waveEvent) => {
|
||||||
|
this.setState({ isOpen: false });
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
keybindManager.registerKeybinding("control", domain, "generic:selectAbove", (waveEvent) => {
|
||||||
|
const { isOpen } = this.state;
|
||||||
|
const { options } = this.props;
|
||||||
|
if (isOpen) {
|
||||||
|
this.setState((prevState) => ({
|
||||||
|
highlightedIndex:
|
||||||
|
prevState.highlightedIndex > 0 ? prevState.highlightedIndex - 1 : options.length - 1,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
keybindManager.registerKeybinding("control", domain, "generic:selectBelow", (waveEvent) => {
|
||||||
|
const { isOpen } = this.state;
|
||||||
|
const { options } = this.props;
|
||||||
|
if (isOpen) {
|
||||||
|
this.setState((prevState) => ({
|
||||||
|
highlightedIndex:
|
||||||
|
prevState.highlightedIndex < options.length - 1 ? prevState.highlightedIndex + 1 : 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
keybindManager.registerKeybinding("control", domain, "generic:tab", (waveEvent) => {
|
||||||
|
this.setState({ isOpen: false });
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConfirm() {
|
||||||
|
const { options } = this.props;
|
||||||
|
const { isOpen, highlightedIndex } = this.state;
|
||||||
|
if (isOpen) {
|
||||||
|
const option = options[highlightedIndex];
|
||||||
|
if (option) {
|
||||||
|
this.handleSelect(option.value, undefined);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.toggleDropdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBlur() {
|
||||||
|
this.unregisterKeybindings();
|
||||||
|
}
|
||||||
|
|
||||||
|
unregisterKeybindings() {
|
||||||
|
let domain = "dropdown-" + this.curUuid;
|
||||||
|
GlobalModel.keybindManager.unregisterDomain(domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
handleKeyDown(event: React.KeyboardEvent) {
|
handleKeyDown(event: React.KeyboardEvent) {}
|
||||||
const { options } = this.props;
|
|
||||||
const { isOpen, highlightedIndex } = this.state;
|
|
||||||
|
|
||||||
switch (event.key) {
|
|
||||||
case "Enter":
|
|
||||||
case " ":
|
|
||||||
if (isOpen) {
|
|
||||||
const option = options[highlightedIndex];
|
|
||||||
if (option) {
|
|
||||||
this.handleSelect(option.value, undefined);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.toggleDropdown();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "Escape":
|
|
||||||
this.setState({ isOpen: false });
|
|
||||||
break;
|
|
||||||
case "ArrowUp":
|
|
||||||
if (isOpen) {
|
|
||||||
this.setState((prevState) => ({
|
|
||||||
highlightedIndex:
|
|
||||||
prevState.highlightedIndex > 0 ? prevState.highlightedIndex - 1 : options.length - 1,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "ArrowDown":
|
|
||||||
if (isOpen) {
|
|
||||||
this.setState((prevState) => ({
|
|
||||||
highlightedIndex:
|
|
||||||
prevState.highlightedIndex < options.length - 1 ? prevState.highlightedIndex + 1 : 0,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "Tab":
|
|
||||||
this.setState({ isOpen: false });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
handleSelect(value: string, event?: React.MouseEvent | React.KeyboardEvent) {
|
handleSelect(value: string, event?: React.MouseEvent | React.KeyboardEvent) {
|
||||||
@ -228,7 +259,8 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
onFocus={this.handleFocus}
|
onFocus={this.handleFocus.bind(this)}
|
||||||
|
onBlur={this.handleBlur.bind(this)}
|
||||||
>
|
>
|
||||||
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
||||||
<If condition={label}>
|
<If condition={label}>
|
||||||
|
@ -78,11 +78,11 @@ class InlineSettingsTextEdit extends React.Component<
|
|||||||
registerKeybindings() {
|
registerKeybindings() {
|
||||||
let keybindManager = GlobalModel.keybindManager;
|
let keybindManager = GlobalModel.keybindManager;
|
||||||
let domain = "inline-settings" + this.curId;
|
let domain = "inline-settings" + this.curId;
|
||||||
keybindManager.registerKeybinding("mainview", domain, "generic:confirm", (waveEvent) => {
|
keybindManager.registerKeybinding("control", domain, "generic:confirm", (waveEvent) => {
|
||||||
this.confirmChange();
|
this.confirmChange();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
keybindManager.registerKeybinding("mainview", domain, "generic:cancel", (waveEvent) => {
|
keybindManager.registerKeybinding("control", domain, "generic:cancel", (waveEvent) => {
|
||||||
this.cancelChange();
|
this.cancelChange();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
@ -38,7 +38,7 @@ type Keybind = {
|
|||||||
commandStr: string;
|
commandStr: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const KeybindLevels = ["system", "modal", "app", "mainview", "pane", "plugin"];
|
const KeybindLevels = ["system", "modal", "app", "mainview", "pane", "plugin", "control"];
|
||||||
|
|
||||||
class KeybindManager {
|
class KeybindManager {
|
||||||
domainCallbacks: Map<string, KeybindCallback>;
|
domainCallbacks: Map<string, KeybindCallback>;
|
||||||
@ -178,7 +178,12 @@ class KeybindManager {
|
|||||||
if (modalLevel.length != 0) {
|
if (modalLevel.length != 0) {
|
||||||
// console.log("processing modal");
|
// console.log("processing modal");
|
||||||
// special case when modal keybindings are present
|
// special case when modal keybindings are present
|
||||||
let shouldReturn = this.processLevel(nativeEvent, event, modalLevel);
|
let controlLevel = this.levelMap.get("control");
|
||||||
|
let shouldReturn = this.processLevel(nativeEvent, event, controlLevel);
|
||||||
|
if (shouldReturn) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
shouldReturn = this.processLevel(nativeEvent, event, modalLevel);
|
||||||
if (shouldReturn) {
|
if (shouldReturn) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user