├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── example ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ └── static │ └── demo_image.png ├── package-lock.json ├── package.json └── src ├── .eslintrc ├── AutocompleteCommandField.js ├── CommandRow.js ├── CommandsList.js └── index.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: saharmor 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /example/node_modules/ 3 | /node_modules/ 4 | /dist/ 5 | /example/build/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sahar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Commandline modal
3 |

React Super Command ⚡

4 |

5 | 6 | [![NPM](https://img.shields.io/npm/v/react-super-cmd.svg)](https://www.npmjs.com/package/react-super-cmd) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 7 | 8 | The command-line experience for the no-mouse generation. A blazing fast command line for your users to seamlessly interact with your React app. 9 |

10 | [Live demo](https://saharmor.github.io/react-super-cmd/) 11 | 12 | ## Installation 13 | 14 | ### npm 15 | 16 | ```bash 17 | npm install --save react-super-cmd 18 | ``` 19 | 20 | ### yarn 21 | 22 | ```bash 23 | yarn add react-super-cmd 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```jsx 29 | import React from 'react'; 30 | import CommandLineModal from "react-super-cmd"; 31 | 32 | import SearchOutlinedIcon from '@material-ui/icons/SearchOutlined'; 33 | import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline'; 34 | import RemoveCircleOutlineOutlinedIcon from '@material-ui/icons/RemoveCircleOutlineOutlined'; 35 | import OfflineBoltOutlined from "@material-ui/icons/OfflineBoltOutlined"; 36 | 37 | 38 | const App = () => { 39 | const [cmdLineModal, setCmdLineModal] = useState(true); 40 | 41 | const commands = { 42 | SEARCH_CONTACT: { 43 | name: 'Search', logo: , shortcut: 'S', callback: () => console.log('search') 44 | }, 45 | ADD_CONTACT: { 46 | name: 'Add', logo: , shortcut: '⌘ A', callback: () => console.log('add') 47 | }, 48 | }; 49 | 50 | function toggleIsOpen() { 51 | setCmdLineModal(previousState => !previousState); 52 | }; 53 | 54 | return ( 55 | } 60 | noOptionsText = "No commands found. Try a different search term." 61 | /> 62 | ); 63 | }; 64 | 65 | export default App; 66 | ``` 67 | 68 | ## Props 69 | ### commands 70 | Object representing the different commands to list. The key is command's name and value is another object containing command details. Example: 71 | ``` 72 | const commands = { 73 | SEARCH_CONTACT: { 74 | name: 'Search', logo: , shortcut: 'S', callback: () => console.log('search') 75 | }, 76 | ADD_CONTACT: { 77 | name: 'Add', logo: , shortcut: '⌘ A', callback: () => console.log('add') 78 | }, 79 | }; 80 | ``` 81 | Command details varibales 82 | 83 | | Parameter | Type | Description | Example | 84 | | :--------- | :-------- | :---------- | :----- | 85 | | name | `string` | The text to be displayed for this command| Search 86 | | logo | `component` | Component that will be next to command's name |`` from Material UI| 87 | | shortcut | `string` | Shortcut text to display next to command name |⌘ S| 88 | | callback | `func` | A function callback text to be displayed for this command|function searchCallback() {
     console.log("search called")
}| 89 | 90 | ### isOpen 91 | If true, command line modal will be visible. 92 | 93 | ### toggleIsModalOpen 94 | A function to be called to toggle modal state. Used to control isOpen state within the external component (e.g. `Super Command` in above example). 95 | 96 | ### title 97 | The title to be displayed for the command line modal (e.g. `` in above example) 98 | 99 | ### logo 100 | Optional
101 | A logo component to display as part of the title 102 | 103 | ### noOptionsTest 104 | Optional
105 | Text to show when no commands were found based on input search term 106 | 107 | ## Development 108 | Follow create-react-library's [development guide](https://www.npmjs.com/package/create-react-library#development) 109 | 110 | ## License 111 | `react-super-cmd` is released under MIT license © [saharmor](https://github.com/saharmor). 112 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # react-super-cmd example ⚡ 2 | 3 | ## Getting started 4 | 5 | ### Clone 6 | If you want to run the example project locally, you first need to clone the repository: 7 | 8 | ``` 9 | git clone git@github.com:saharmor/react-super-cmd.git 10 | ``` 11 | 12 | ### Build 13 | 14 | ``` 15 | yarn install && yarn run prepublish 16 | ``` 17 | 18 | ### Start 19 | Start the example app: 20 | 21 | ``` 22 | cd example && yarn start 23 | ``` 24 | 25 | The example app will automatically open in your default browser. 26 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-super-cmd-example", 3 | "homepage": ".", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "start": "node ../node_modules/react-scripts/bin/react-scripts.js start", 8 | "build": "node ../node_modules/react-scripts/bin/react-scripts.js build", 9 | "test": "node ../node_modules/react-scripts/bin/react-scripts.js test", 10 | "eject": "node ../node_modules/react-scripts/bin/react-scripts.js eject" 11 | }, 12 | "dependencies": { 13 | "@material-ui/icons": "^4.11.2", 14 | "react": "file:../node_modules/react", 15 | "react-dom": "file:../node_modules/react-dom", 16 | "react-hotkeys": "^2.0.0", 17 | "react-scripts": "file:../node_modules/react-scripts", 18 | "react-super-cmd": "file:.." 19 | }, 20 | "devDependencies": { 21 | "@babel/plugin-syntax-object-rest-spread": "^7.8.3" 22 | }, 23 | "eslintConfig": { 24 | "extends": "react-app" 25 | }, 26 | "browserslist": [ 27 | ">0.2%", 28 | "not dead", 29 | "not ie <= 11", 30 | "not op_mini all" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saharmor/react-super-cmd/b617375b28479f9d2516cb4899213b0f9edfe669/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 17 | 18 | 19 | 28 | react-super-cmd 29 | 30 | 31 | 32 | 35 | 36 |
37 | 38 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "react-super-cmd", 3 | "name": "react-super-cmd", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /example/src/App.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import withStyles from "@material-ui/core/styles/withStyles"; 3 | import {configure, GlobalHotKeys} from "react-hotkeys"; 4 | import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline'; 5 | import RemoveCircleOutlineOutlinedIcon from '@material-ui/icons/RemoveCircleOutlineOutlined'; 6 | import SearchOutlinedIcon from '@material-ui/icons/SearchOutlined'; 7 | import UpdateOutlinedIcon from '@material-ui/icons/UpdateOutlined'; 8 | import OfflineBoltOutlined from "@material-ui/icons/OfflineBoltOutlined"; 9 | import CommandLineModal from "react-super-cmd"; 10 | import Grid from "@material-ui/core/Grid"; 11 | import Typography from "@material-ui/core/Typography"; 12 | import {Link, List, ListItem, ListItemText} from "@material-ui/core"; 13 | import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; 14 | import GitHubIcon from '@material-ui/icons/GitHub'; 15 | import LinkedInIcon from "@material-ui/icons/LinkedIn"; 16 | 17 | const useStyles = (theme) => ({ 18 | root: { 19 | backgroundColor: '#fbfafa', 20 | height: '100vh', 21 | display: 'flex', 22 | flexDirection: 'column', 23 | flex: '1', 24 | padding: '30px 30px 10px 30px', 25 | }, 26 | headline: { 27 | marginBottom: '10px', 28 | }, 29 | ctaText: { 30 | fontWeight: 'bold', 31 | }, 32 | cmd: { 33 | backgroundColor: '#dedede', 34 | }, 35 | listBullet: { 36 | fontSize: '10px', 37 | marginRight: '3px', 38 | }, 39 | listItem: { 40 | paddingBottom: '0px', 41 | paddingTop: '0px', 42 | }, 43 | footer: { 44 | marginTop: '20px', 45 | } 46 | }); 47 | 48 | const App = ({classes}) => { 49 | const [cmdLineModal, setCmdLineModal] = useState(false); 50 | const [lastRunCmd, setLastRunCmd] = useState(null); 51 | 52 | const commands = { 53 | SEARCH_CONTACT: { 54 | name: 'Search', logo: , shortcut: 'S', callback: () => setLastRunCmd('SEARCH_CONTACT') 55 | }, 56 | ADD_CONTACT: { 57 | name: 'Add', logo: , shortcut: '⌘ A', callback: () => setLastRunCmd('ADD_CONTACT') 58 | }, 59 | DELETE_CONTACT: { 60 | name: 'Delete', logo: , shortcut: '⌘ D', callback: () => setLastRunCmd('DELETE_CONTACT') 61 | }, 62 | UPDATE_CONTACT: { 63 | name: 'Update', logo: , shortcut: null, callback: () => setLastRunCmd('UPDATE_CONTACT') 64 | }, 65 | REACH_OUT_CONTACT: { 66 | name: 'Reach out', logo: , shortcut: 'ctrl R', callback: () => setLastRunCmd('REACH_OUT_CONTACT') 67 | } 68 | } 69 | 70 | function toggleIsOpen() { 71 | setCmdLineModal(previousState => !previousState); 72 | } 73 | 74 | const keyMap = {TOGGLE_MODAL: "cmd+k"}; 75 | const handlers = { 76 | TOGGLE_MODAL: () => { 77 | toggleIsOpen(); 78 | }, 79 | }; 80 | 81 | configure({ 82 | ignoreTags: ['input', 'select', 'textarea'], 83 | ignoreEventsCondition: function () { 84 | } 85 | }); 86 | 87 | return ( 88 | 89 | 90 | 91 | 92 | react-super-cmd 93 | 94 | The command line experience for the no-mouse generation. 95 | A blazing fast command line for your users to seamlessly interact with your React app. 96 | 97 | {!lastRunCmd && 98 | 99 | Give it a spin 👉 Press cmd+k 100 | 101 | } 102 | {lastRunCmd && 103 | Command selected {lastRunCmd} 104 | } 105 | 106 | Command 107 | 108 | 109 | Possible applications 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | By Sahar Mor 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | }/> 148 | 149 | 150 | ) 151 | } 152 | 153 | export default withStyles(useStyles)(App); -------------------------------------------------------------------------------- /example/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div') 7 | ReactDOM.render(, div) 8 | ReactDOM.unmountComponentAtNode(div) 9 | }) 10 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | 16 | #root { 17 | height: 100vh; 18 | } 19 | 20 | html { 21 | height: 100%; 22 | } 23 | 24 | body { 25 | min-height: 100%; 26 | } 27 | 28 | /* MaterialUI overrides */ 29 | a:-webkit-any-link { 30 | text-decoration: none !important; 31 | } 32 | 33 | a, a:visited, a:hover, a:active { 34 | color: inherit; 35 | } 36 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import App from './App' 6 | 7 | ReactDOM.render(, document.getElementById('root')) 8 | -------------------------------------------------------------------------------- /example/src/static/demo_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saharmor/react-super-cmd/b617375b28479f9d2516cb4899213b0f9edfe669/example/src/static/demo_image.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-super-cmd", 3 | "version": "1.0.6", 4 | "description": "A sleek command line modal for React apps", 5 | "author": "saharmor", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/saharmor/react-super-cmd.git" 10 | }, 11 | "main": "dist/index.js", 12 | "module": "dist/index.modern.js", 13 | "source": "src/index.js", 14 | "engines": { 15 | "node": ">=10" 16 | }, 17 | "keywords": [ 18 | "react-component", 19 | "command-line", 20 | "command", 21 | "superhuman", 22 | "shortcuts", 23 | "react" 24 | ], 25 | "bugs": { 26 | "url": "https://github.com/saharmor/react-super-cmd/issues" 27 | }, 28 | "scripts": { 29 | "build": "microbundle-crl --no-compress --format modern,cjs", 30 | "start": "microbundle-crl watch --no-compress --format modern,cjs", 31 | "prepare": "run-s build", 32 | "test": "run-s test:unit test:lint test:build", 33 | "test:build": "run-s build", 34 | "test:lint": "eslint react-super-cmd", 35 | "test:unit": "cross-env CI=1 react-scripts test --env=jsdom", 36 | "test:watch": "react-scripts test --env=jsdom", 37 | "predeploy": "cd example && npm install && npm run build", 38 | "deploy": "gh-pages -d example/build" 39 | }, 40 | "peerDependencies": { 41 | "react": "^16.13.0", 42 | "react-dom": "^16.13.0", 43 | "@material-ui/core": "^4.11.2", 44 | "@material-ui/icons": "^4.11.2" 45 | }, 46 | "devDependencies": { 47 | "microbundle-crl": "^0.13.10", 48 | "babel-eslint": "^10.0.3", 49 | "cross-env": "^7.0.2", 50 | "eslint": "^6.8.0", 51 | "eslint-config-prettier": "^6.7.0", 52 | "eslint-config-standard": "^14.1.0", 53 | "eslint-config-standard-react": "^9.2.0", 54 | "eslint-plugin-import": "^2.18.2", 55 | "eslint-plugin-node": "^11.0.0", 56 | "eslint-plugin-prettier": "^3.1.1", 57 | "eslint-plugin-promise": "^4.2.1", 58 | "eslint-plugin-react": "^7.17.0", 59 | "eslint-plugin-standard": "^4.0.1", 60 | "gh-pages": "^2.2.0", 61 | "npm-run-all": "^4.1.5", 62 | "prettier": "^2.0.4", 63 | "react": "^16.13.0", 64 | "react-dom": "^16.13.0", 65 | "react-scripts": "^3.4.1", 66 | "@material-ui/core": "^4.11.2", 67 | "@material-ui/icons": "^4.11.2" 68 | }, 69 | "files": [ 70 | "dist" 71 | ], 72 | "homepage": "https://github.com/saharmor/react-super-cmd#readme", 73 | "directories": { 74 | "example": "example" 75 | }, 76 | "dependencies": {} 77 | } 78 | -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/AutocompleteCommandField.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from "react"; 2 | import withStyles from "@material-ui/core/styles/withStyles"; 3 | import TextField from '@material-ui/core/TextField'; 4 | 5 | 6 | const useStyles = (theme) => ({ 7 | root: { 8 | backgroundColor: '#212121', 9 | display: 'flex', 10 | width: '100%', 11 | margin: '24px 0', 12 | }, 13 | inputField: { 14 | flex: '1', 15 | border: 'none', 16 | boxShadow: 'inset 3px 0 0 #dcb865', 17 | backgroundColor: 'inherit', 18 | color: '#b7babe', 19 | }, 20 | input: { 21 | color: '#f1f2f2', 22 | paddingLeft: '30px', 23 | fontSize: '21px', 24 | lineHeight: '28px', 25 | } 26 | }); 27 | 28 | const AutocompleteCommandField = ({classes, fieldValue, onChange, onKeyPress}) => { 29 | return ( 30 |
31 | 33 |
34 | ) 35 | } 36 | 37 | export default withStyles(useStyles)(AutocompleteCommandField); 38 | -------------------------------------------------------------------------------- /src/CommandRow.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import withStyles from "@material-ui/core/styles/withStyles"; 3 | import Grid from "@material-ui/core/Grid"; 4 | import ButtonBase from '@material-ui/core/ButtonBase'; 5 | import Typography from "@material-ui/core/Typography"; 6 | 7 | const useStyles = (theme) => ({ 8 | root: { 9 | color: '#b7babe', 10 | minHeight: '60px', 11 | padding: '20px 36px', 12 | width: '100%', 13 | textAlign: 'left', 14 | }, 15 | rootHighlighted: { 16 | minHeight: '60px', 17 | padding: '20px 36px', 18 | width: '100%', 19 | textAlign: 'left', 20 | backgroundColor: 'rgba(255, 255, 255, 0.1)', 21 | color: '#ffffffe6', 22 | }, 23 | button: { 24 | flexGrow: '1', 25 | }, 26 | cmdIcon: { 27 | width: '20px', 28 | height: '20px', 29 | }, 30 | shortcut: { 31 | background: 'rgba(255, 255, 255, 0.15)', 32 | borderRadius: '3px', 33 | maxHeight: '18px', 34 | minWidth: '19px', 35 | marginLeft: '4px', 36 | padding: "1px 5px 5px 5px", 37 | marginTop: "3px", 38 | } 39 | }); 40 | 41 | const CommandRow = ({classes, command, commandName, isHighlighted, onHover, onKeyPress, onClick, ref}) => { 42 | const rootStyle = isHighlighted ? classes.rootHighlighted : classes.root; 43 | 44 | function shortcutToKeys(shortcut) { 45 | return ( 46 | 47 | { 48 | shortcut.split(" ").map((keyboardKey) => { 49 | return {keyboardKey} 50 | }) 51 | } 52 | 53 | ) 54 | } 55 | 56 | return ( 57 | onHover(commandName)} onKeyPress={onKeyPress}> 58 | 59 | 60 | 61 | 62 | {command['logo']} 63 | 64 | 65 | {command['name']} 66 | 67 | 68 | 69 | 70 | {command['shortcut'] && shortcutToKeys(command['shortcut'])} 71 | 72 | 73 | 74 | ) 75 | } 76 | 77 | export default withStyles(useStyles)(CommandRow); 78 | -------------------------------------------------------------------------------- /src/CommandsList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import CommandRow from "./CommandRow"; 3 | import withStyles from "@material-ui/core/styles/withStyles"; 4 | 5 | const useStyles = (theme) => ({ 6 | root: { 7 | maxHeight: '220px', 8 | overflow: 'auto', 9 | 10 | "&::-webkit-scrollbar": { 11 | width: '8px', 12 | borderRadius: '5px', 13 | paddingRight: '3px', 14 | }, 15 | "&::-webkit-scrollbar-track": { 16 | borderRadius: '5px', 17 | }, 18 | "&::-webkit-scrollbar-thumb": { 19 | borderRadius: '5px', 20 | backgroundColor: "#757474", 21 | }, 22 | }, 23 | child: { 24 | height: '100%', 25 | margin: '0 auto', 26 | } 27 | }) 28 | 29 | const CommandsList = ({classes, commands, highlightedCmdName, setHighlightedCallback, handleEnter, ignoreHover, toggleIgnoreHover}) => { 30 | function onHoverCallback(hoveredCmdName) { 31 | if (!ignoreHover) { 32 | setHighlightedCallback(hoveredCmdName); 33 | } else { 34 | toggleIgnoreHover(); 35 | } 36 | } 37 | 38 | return ( 39 |
40 |
41 | {Object.keys(commands).map((command, _) => { 42 | return 45 | } 46 | )} 47 |
48 |
49 | ) 50 | }; 51 | 52 | export default withStyles(useStyles)(CommandsList); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useRef} from "react"; 2 | import withStyles from "@material-ui/core/styles/withStyles"; 3 | import Dialog from "@material-ui/core/Dialog"; 4 | import DialogTitle from "@material-ui/core/DialogTitle"; 5 | import DialogContent from "@material-ui/core/DialogContent"; 6 | import Grid from "@material-ui/core/Grid"; 7 | import Divider from "@material-ui/core/Divider"; 8 | import Typography from "@material-ui/core/Typography"; 9 | import AutocompleteCommandField from "./AutocompleteCommandField"; 10 | import CommandsList from "./CommandsList"; 11 | import OfflineBoltOutlined from "@material-ui/icons/OfflineBoltOutlined"; 12 | 13 | const useStyles = (theme) => ({ 14 | paperRoot: { 15 | padding: '10px 0 10px 0', 16 | minWidth: '760px', 17 | maxHeight: '380px', 18 | backgroundColor: '#212121', 19 | borderRadius: '4px', 20 | boxShadow: '0 5px 10px 0 rgba(0, 0, 0, 0.7), 0 0px 10px 0 #000, 0.15px 0.5px 0 0 rgba(255, 255, 255, 0.1) inset' 21 | }, 22 | backdropRoot: { 23 | backgroundColor: 'transparent', 24 | }, 25 | dialogTitle: { 26 | margin: '12px 36px', 27 | padding: '0px', 28 | }, 29 | titleSection: { 30 | color: '#d4d6d8', 31 | display: 'flex', 32 | flexDirection: 'row', 33 | marginBottom: '20px', 34 | }, 35 | title: { 36 | marginLeft: '8px', 37 | }, 38 | dialogBody: { 39 | padding: '0px', 40 | overflow: 'hidden', 41 | }, 42 | divider: { 43 | backgroundColor: 'rgba(255, 255, 255, 0.1)', 44 | }, 45 | noOptionsText: { 46 | color: '#d4d6d8', 47 | padding: '12px 36px', 48 | }, 49 | commandsList:{ 50 | paddingRight: '3px', 51 | } 52 | }); 53 | 54 | 55 | const CommandLineModal = ({ 56 | classes, commands, isOpen, toggleIsModalOpen, title, 57 | noOptionsText = "No commands found. Try a different search term.", 58 | logo = 59 | }) => { 60 | let commandsInternal = {}; 61 | Object.entries(commands).map(([commandName, properties]) => { 62 | commandsInternal[commandName] = {...properties, ref: useRef(null)} 63 | }); 64 | 65 | const [inputValue, setInputValue] = useState(''); 66 | const [possibleCommands, setPossibleCommands] = useState(commandsInternal); 67 | const [highlightedCmdName, setHighlightedCmdName] = useState(Object.keys(possibleCommands)[0]); 68 | const [ignoreHover, setIgnoreHover] = useState(false); 69 | 70 | function changeHighlightedCmd(commandName) { 71 | setHighlightedCmdName(commandName); 72 | } 73 | 74 | function toggleIgnoreHover() { 75 | setIgnoreHover(previousState => !previousState); 76 | } 77 | 78 | function setPossibleCommandsWithSearchTerm(searchTerm) { 79 | const inputToSearch = searchTerm.toLowerCase(); 80 | let tempPossibleCommands = {}; 81 | Object.entries(commandsInternal).map(([commandName, properties]) => { 82 | if (properties.name.toLowerCase().includes(inputToSearch)) { 83 | tempPossibleCommands[commandName] = properties 84 | } 85 | }); 86 | setPossibleCommands(tempPossibleCommands); 87 | changeHighlightedCmd(Object.keys(tempPossibleCommands)[0]); 88 | } 89 | 90 | function handleInputChange(event) { 91 | const newSearchTerm = event.target.value; 92 | setPossibleCommandsWithSearchTerm(newSearchTerm); 93 | setInputValue(newSearchTerm); 94 | } 95 | 96 | function reset() { 97 | setInputValue(''); 98 | setPossibleCommands(commandsInternal); 99 | setHighlightedCmdName(Object.keys(commandsInternal)[0]); 100 | } 101 | 102 | function handleCommandSelected() { 103 | if (highlightedCmdName) { 104 | possibleCommands[highlightedCmdName]['callback'](); 105 | toggleIsModalOpen(); 106 | reset(); 107 | } 108 | } 109 | 110 | function onArrowsPress(direction) { 111 | setIgnoreHover(true); 112 | const keysArray = Object.keys(possibleCommands); 113 | const currSelectedIndex = keysArray.indexOf(highlightedCmdName); 114 | 115 | let selectedCommandName = null; 116 | if (direction === "down") { 117 | if (currSelectedIndex + 1 === keysArray.length) { 118 | selectedCommandName = keysArray[0]; 119 | } else { 120 | selectedCommandName = keysArray[currSelectedIndex + 1]; 121 | } 122 | } else if (direction === "up") { 123 | if (currSelectedIndex - 1 < 0) { 124 | selectedCommandName = keysArray[keysArray.length - 1]; 125 | } else { 126 | selectedCommandName = keysArray[currSelectedIndex - 1]; 127 | } 128 | } 129 | 130 | changeHighlightedCmd(selectedCommandName); 131 | commandsInternal[selectedCommandName].ref.current.scrollIntoView(); 132 | } 133 | 134 | function onKeyPress(event) { 135 | if (event.which === 13 /* Enter */) { 136 | handleCommandSelected(); 137 | } else if (event.which === 38 /* Arrow Up */) { 138 | onArrowsPress('up'); 139 | } else if (event.which === 40 /* Arrow Down*/) { 140 | onArrowsPress('down'); 141 | } 142 | } 143 | 144 | return ( 145 | 147 | 148 |
149 | {logo} 150 | {title} 151 |
152 | 153 |
154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | {Object.keys(possibleCommands).length > 0 && 162 | 165 | } 166 | {Object.keys(possibleCommands).length === 0 && 167 | {noOptionsText} 168 | } 169 | 170 | 171 | 172 |
173 | ) 174 | } 175 | 176 | export default withStyles(useStyles)(CommandLineModal); 177 | --------------------------------------------------------------------------------