initial mousetrap keyboard shortcuts

This commit is contained in:
Ashley Stanton-Nurse 2024-04-15 15:02:10 -07:00
parent d35e2a325e
commit e441b75325
No known key found for this signature in database
4 changed files with 192 additions and 43 deletions

13
package-lock.json generated
View File

@ -82,6 +82,7 @@
"knockout": "3.5.1",
"mkdirp": "1.0.4",
"monaco-editor": "0.44.0",
"mousetrap": "1.6.5",
"ms": "2.1.3",
"p-retry": "4.6.2",
"patch-package": "8.0.0",
@ -128,6 +129,7 @@
"@types/hasher": "0.0.31",
"@types/jest": "26.0.20",
"@types/jquery": "3.5.29",
"@types/mousetrap": "1.6.15",
"@types/node": "12.11.1",
"@types/post-robot": "10.0.1",
"@types/q": "1.5.1",
@ -12792,6 +12794,12 @@
"@types/node": "*"
}
},
"node_modules/@types/mousetrap": {
"version": "1.6.15",
"resolved": "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.15.tgz",
"integrity": "sha512-qL0hyIMNPow317QWW/63RvL1x5MVMV+Ru3NaY9f/CuEpCqrmb7WeuK2071ZY5hczOnm38qExWM2i2WtkXLSqFw==",
"dev": true
},
"node_modules/@types/node": {
"version": "12.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.1.tgz",
@ -31631,6 +31639,11 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/mousetrap": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz",
"integrity": "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA=="
},
"node_modules/mrmime": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz",

View File

@ -77,6 +77,7 @@
"knockout": "3.5.1",
"mkdirp": "1.0.4",
"monaco-editor": "0.44.0",
"mousetrap": "1.6.5",
"ms": "2.1.3",
"p-retry": "4.6.2",
"patch-package": "8.0.0",
@ -123,6 +124,7 @@
"@types/hasher": "0.0.31",
"@types/jest": "26.0.20",
"@types/jquery": "3.5.29",
"@types/mousetrap": "1.6.15",
"@types/node": "12.11.1",
"@types/post-robot": "10.0.1",
"@types/q": "1.5.1",

131
src/KeyboardShortcuts.tsx Normal file
View File

@ -0,0 +1,131 @@
import { useSelectedNode } from "Explorer/useSelectedNode";
import { userContext } from "UserContext";
import Mousetrap, { ExtendedKeyboardEvent } from "mousetrap";
import * as React from "react";
import * as ViewModels from "../Contracts/ViewModels";
type KeyboardShortcutRootProps = React.PropsWithChildren<unknown>;
type KeyboardShortcutHandler = (e: ExtendedKeyboardEvent, combo: string) => boolean | void;
export interface KeyboardShortcutBinding {
/**
* The keyboard shortcut to bind to. This can be a single string or an array of strings.
* Any combination supported by Mousetrap (https://craig.is/killing/mice#api.bind) is valid here.
*/
keys: string | string[],
/**
* The handler to run when the keyboard shortcut is pressed.
* @param e The keyboard event that triggered the shortcut.
* @param combo The specific keyboard combination that was matched (in case a single handler is used for multiple shortcuts).
* @returns If the handler returns `false`, the default action for the keyboard shortcut will be prevented AND propagation of the event will be stopped.
*/
handler: KeyboardShortcutHandler,
/**
* The event to bind the keyboard shortcut to (keydown, keyup, etc.).
* The default is 'keydown'
*/
action?: string,
}
/**
* Wraps the provided keyboard shortcut handler in one that only runs if a collection is selected.
* @param callback The callback to run if a collection is selected.
* @returns If the handler returns `false`, the default action for the keyboard shortcut will be prevented AND propagation of the event will be stopped.
*/
function withSelectedCollection(callback: (selectedCollection: ViewModels.Collection, e: ExtendedKeyboardEvent, combo: string) => boolean | void): KeyboardShortcutHandler {
return (e, combo) => {
const state = useSelectedNode.getState();
if (!state.selectedNode) {
return;
}
const selectedCollection = state.findSelectedCollection();
if (selectedCollection) {
return callback(selectedCollection, e, combo);
}
};
}
const bindings: KeyboardShortcutBinding[] = [
{
keys: ["ctrl+j"],
handler: withSelectedCollection((selectedCollection) => {
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
selectedCollection.onNewQueryClick(selectedCollection);
return false;
} else if (userContext.apiType === "Mongo") {
selectedCollection.onNewMongoQueryClick(selectedCollection);
return false;
}
return true;
}),
},
{
keys: ["shift+enter"],
handler: () => {
alert("TODO: Execute Item");
return false;
},
},
{
keys: ["esc"],
handler: () => {
alert("TODO: Cancel Query");
return false;
},
},
{
keys: ["mod+s"],
handler: () => {
alert("TODO: Save Query");
return false;
},
},
{
keys: ["mod+o"],
handler: () => {
alert("TODO: Open Query");
return false;
},
},
{
keys: ["mod+shift+o"],
handler: () => {
alert("TODO: Open Query from Disk");
return false;
},
},
{
keys: ["mod+s"],
handler: () => {
alert("TODO: Save");
return false;
},
},
]
export function KeyboardShortcutRoot({ children }: KeyboardShortcutRootProps) {
React.useEffect(() => {
const m = new Mousetrap(document.body);
const existingStopCallback = m.stopCallback;
m.stopCallback = (e, element, combo) => {
// Don't block mousetrap callback in the Monaco editor.
if (element.matches(".monaco-editor textarea")) {
return false;
}
return existingStopCallback(e, element, combo);
};
bindings.forEach(b => {
m.bind(b.keys, b.handler, b.action);
});
}, []); // Using an empty dependency array means React will only run this _once_ when the component is mounted.
return <>
{children}
</>;
}

View File

@ -21,6 +21,7 @@ import "../externals/jquery.typeahead.min.js";
// Image Dependencies
import { Platform } from "ConfigContext";
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
import "../images/favicon.ico";
@ -91,52 +92,54 @@ const App: React.FunctionComponent = () => {
}
return (
<div className="flexContainer" aria-hidden="false">
<div id="divExplorer" className="flexContainer hideOverflows">
<div id="freeTierTeachingBubble"> </div>
{/* Main Command Bar - Start */}
<CommandBar container={explorer} />
{/* Collections Tree and Tabs - Begin */}
<div className="resourceTreeAndTabs">
{/* Collections Tree - Start */}
{userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && (
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
<div className="collectionsTreeWithSplitter">
{/* Collections Tree Expanded - Start */}
<ResourceTreeContainer
container={explorer}
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
isLeftPaneExpanded={isLeftPaneExpanded}
/>
{/* Collections Tree Expanded - End */}
{/* Collections Tree Collapsed - Start */}
<CollapsedResourceTree
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
isLeftPaneExpanded={isLeftPaneExpanded}
/>
{/* Collections Tree Collapsed - End */}
<KeyboardShortcutRoot>
<div className="flexContainer" aria-hidden="false">
<div id="divExplorer" className="flexContainer hideOverflows">
<div id="freeTierTeachingBubble"> </div>
{/* Main Command Bar - Start */}
<CommandBar container={explorer} />
{/* Collections Tree and Tabs - Begin */}
<div className="resourceTreeAndTabs">
{/* Collections Tree - Start */}
{userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && (
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
<div className="collectionsTreeWithSplitter">
{/* Collections Tree Expanded - Start */}
<ResourceTreeContainer
container={explorer}
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
isLeftPaneExpanded={isLeftPaneExpanded}
/>
{/* Collections Tree Expanded - End */}
{/* Collections Tree Collapsed - Start */}
<CollapsedResourceTree
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
isLeftPaneExpanded={isLeftPaneExpanded}
/>
{/* Collections Tree Collapsed - End */}
</div>
</div>
</div>
)}
<Tabs explorer={explorer} />
</div>
{/* Collections Tree and Tabs - End */}
<div
className="dataExplorerErrorConsoleContainer"
role="contentinfo"
aria-label="Notification console"
id="explorerNotificationConsole"
>
<NotificationConsole />
)}
<Tabs explorer={explorer} />
</div>
{/* Collections Tree and Tabs - End */}
<div
className="dataExplorerErrorConsoleContainer"
role="contentinfo"
aria-label="Notification console"
id="explorerNotificationConsole"
>
<NotificationConsole />
</div>
</div>
<SidePanel />
<Dialog />
{<QuickstartCarousel isOpen={isCarouselOpen} />}
{<SQLQuickstartTutorial />}
{<MongoQuickstartTutorial />}
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
</div>
<SidePanel />
<Dialog />
{<QuickstartCarousel isOpen={isCarouselOpen} />}
{<SQLQuickstartTutorial />}
{<MongoQuickstartTutorial />}
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
</div>
</KeyboardShortcutRoot>
);
};