mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Code] shortcut modal and shortcuts (#29061)
This commit is contained in:
parent
97b4c1cc93
commit
b4d714a9a7
11 changed files with 423 additions and 9 deletions
|
@ -15,6 +15,7 @@ export * from './user';
|
|||
export * from './commit';
|
||||
export * from './status';
|
||||
export * from './project_config';
|
||||
export * from './shortcuts';
|
||||
|
||||
export interface Match {
|
||||
isExact?: boolean;
|
||||
|
|
13
x-pack/plugins/code/public/actions/shortcuts.ts
Normal file
13
x-pack/plugins/code/public/actions/shortcuts.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { createAction } from 'redux-actions';
|
||||
import { HotKey } from '../components/shortcuts';
|
||||
|
||||
export const registerShortcut = createAction<HotKey>('REGISTER SHORTCUT');
|
||||
export const unregisterShortcut = createAction<HotKey>('UNREGISTER SHORTCUT');
|
||||
|
||||
export const toggleHelp = createAction<boolean | null>('TOGGLE SHORTCUTS HELP');
|
|
@ -13,6 +13,7 @@ import { closeReferences, findReferences, hoverResult } from '../../actions';
|
|||
import { MonacoHelper } from '../../monaco/monaco_helper';
|
||||
import { RootState } from '../../reducers';
|
||||
import { refUrlSelector } from '../../selectors';
|
||||
import { Modifier, Shortcut } from '../shortcuts';
|
||||
import { ReferencesPanel } from './references_panel';
|
||||
|
||||
export interface EditorActions {
|
||||
|
@ -87,6 +88,13 @@ export class EditorComponent extends React.Component<Props> {
|
|||
public render() {
|
||||
return (
|
||||
<EuiFlexItem className="noOverflow" grow={1}>
|
||||
<Shortcut
|
||||
keyCode="f"
|
||||
help="With editor ‘active’ Find in file"
|
||||
linuxModifier={[Modifier.ctrl]}
|
||||
macModifier={[Modifier.meta]}
|
||||
winModifier={[Modifier.ctrl]}
|
||||
/>
|
||||
<EuiFlexItem tabIndex={0} grow={1} className="editorContainer noOverflow" id="mainEditor" />
|
||||
{this.renderReferences()}
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -14,6 +14,7 @@ import { CloneProgress, WorkerReservedProgress } from '../../../model';
|
|||
import { MainRouteParams } from '../../common/types';
|
||||
import { RootState } from '../../reducers';
|
||||
import { cloneProgressSelector, progressSelector } from '../../selectors';
|
||||
import { ShortcutsProvider } from '../shortcuts';
|
||||
import { CloneStatus } from './clone_status';
|
||||
import { Content } from './content';
|
||||
import { NotFound } from './not_found';
|
||||
|
@ -76,6 +77,7 @@ class CodeMain extends React.Component<Props> {
|
|||
<Root>
|
||||
<TopBar routeParams={this.props.match.params} />
|
||||
<Container>{this.renderContent()}</Container>
|
||||
<ShortcutsProvider />
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
RepositorySuggestionsProvider,
|
||||
SymbolSuggestionsProvider,
|
||||
} from '../query_bar';
|
||||
import { Shortcut } from '../shortcuts';
|
||||
|
||||
const SearchBarContainer = styled.div`
|
||||
width: ${pxToRem(600)};
|
||||
|
@ -51,11 +52,15 @@ export class CodeSearchBar extends React.Component<RouteComponentProps<MainRoute
|
|||
public render() {
|
||||
return (
|
||||
<SearchBarContainer>
|
||||
<Shortcut keyCode="p" help="Search projects" />
|
||||
<Shortcut keyCode="y" help="Search symbols" />
|
||||
<Shortcut keyCode="s" help="Search everything" />
|
||||
<QueryBar
|
||||
query=""
|
||||
onSubmit={this.onSubmit}
|
||||
onSelect={this.onSelect}
|
||||
appName="code"
|
||||
disableAutoFocus={true}
|
||||
suggestionProviders={this.suggestionProviders}
|
||||
/>
|
||||
</SearchBarContainer>
|
||||
|
|
|
@ -13,6 +13,7 @@ import { QueryString } from 'ui/utils/query_string';
|
|||
import { MainRouteParams, PathTypes } from '../../common/types';
|
||||
import { colors } from '../../style/variables';
|
||||
import { FileTree } from '../file_tree/file_tree';
|
||||
import { Shortcut } from '../shortcuts';
|
||||
import { SymbolTree } from '../symbol_tree/symbol_tree';
|
||||
|
||||
enum Tabs {
|
||||
|
@ -35,15 +36,6 @@ const FileTreeContainer = styled.div`
|
|||
`;
|
||||
|
||||
class CodeSideTabs extends React.PureComponent<RouteComponentProps<MainRouteParams>> {
|
||||
public tabContentMap = {
|
||||
[Tabs.file]: (
|
||||
<FileTreeContainer>
|
||||
<FileTree />
|
||||
</FileTreeContainer>
|
||||
),
|
||||
[Tabs.structure]: <SymbolTree />,
|
||||
};
|
||||
|
||||
public get sideTab(): Tabs {
|
||||
const { search } = this.props.location;
|
||||
let qs = search;
|
||||
|
@ -53,6 +45,14 @@ class CodeSideTabs extends React.PureComponent<RouteComponentProps<MainRoutePara
|
|||
const tab = parseQuery(qs).tab;
|
||||
return tab === Tabs.structure ? Tabs.structure : Tabs.file;
|
||||
}
|
||||
public tabContentMap = {
|
||||
[Tabs.file]: (
|
||||
<FileTreeContainer>
|
||||
<FileTree />
|
||||
</FileTreeContainer>
|
||||
),
|
||||
[Tabs.structure]: <SymbolTree />,
|
||||
};
|
||||
|
||||
public switchTab = (tab: Tabs) => {
|
||||
const { history } = this.props;
|
||||
|
@ -87,9 +87,22 @@ class CodeSideTabs extends React.PureComponent<RouteComponentProps<MainRoutePara
|
|||
<EuiTabs>{this.renderTabs()}</EuiTabs>
|
||||
</div>
|
||||
{this.tabContentMap[this.sideTab]}
|
||||
<Shortcut
|
||||
keyCode="t"
|
||||
help="Toggle tree and symbol view in sidebar"
|
||||
onPress={this.toggleTab}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
private toggleTab = () => {
|
||||
const currentTab = this.sideTab;
|
||||
if (currentTab === Tabs.file) {
|
||||
this.switchTab(Tabs.structure);
|
||||
} else {
|
||||
this.switchTab(Tabs.file);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const SideTabs = withRouter(CodeSideTabs);
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { Shortcut, OS, HotKey, Modifier } from './shortcut';
|
||||
export { ShortcutsProvider } from './shortcuts_provider';
|
84
x-pack/plugins/code/public/components/shortcuts/shortcut.tsx
Normal file
84
x-pack/plugins/code/public/components/shortcuts/shortcut.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { registerShortcut, unregisterShortcut } from '../../actions';
|
||||
|
||||
export enum OS {
|
||||
win,
|
||||
mac,
|
||||
linux,
|
||||
}
|
||||
|
||||
export enum Modifier {
|
||||
ctrl,
|
||||
meta,
|
||||
alt,
|
||||
shift,
|
||||
}
|
||||
|
||||
export interface HotKey {
|
||||
key: string;
|
||||
modifier: Map<OS, Modifier[]>;
|
||||
help: string;
|
||||
onPress?: (dispatch: any) => void;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
keyCode: string;
|
||||
help: string;
|
||||
onPress?: (dispatch: any) => void;
|
||||
winModifier?: Modifier[];
|
||||
macModifier?: Modifier[];
|
||||
linuxModifier?: Modifier[];
|
||||
registerShortcut(hotKey: HotKey): void;
|
||||
unregisterShortcut(hotKey: HotKey): void;
|
||||
}
|
||||
|
||||
class ShortcutsComponent extends React.Component<Props> {
|
||||
private readonly hotKey: HotKey;
|
||||
constructor(props: Props, context: any) {
|
||||
super(props, context);
|
||||
this.hotKey = {
|
||||
key: props.keyCode,
|
||||
help: props.help,
|
||||
onPress: props.onPress,
|
||||
modifier: new Map(),
|
||||
};
|
||||
if (props.winModifier) {
|
||||
this.hotKey.modifier.set(OS.win, props.winModifier);
|
||||
}
|
||||
if (props.macModifier) {
|
||||
this.hotKey.modifier.set(OS.mac, props.macModifier);
|
||||
}
|
||||
if (props.linuxModifier) {
|
||||
this.hotKey.modifier.set(OS.linux, props.linuxModifier);
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.props.registerShortcut(this.hotKey);
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.props.unregisterShortcut(this.hotKey);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
registerShortcut,
|
||||
unregisterShortcut,
|
||||
};
|
||||
|
||||
export const Shortcut = connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(ShortcutsComponent);
|
|
@ -0,0 +1,220 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiOverlayMask,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import { toggleHelp } from '../../actions';
|
||||
import { RootState } from '../../reducers';
|
||||
import { HotKey, Modifier, OS } from './shortcut';
|
||||
|
||||
const Key = styled.span`
|
||||
background: #ffffff;
|
||||
border: 1px solid #d3dae6;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 2px 0px #eeeeee;
|
||||
border-radius: 4px;
|
||||
min-width: 24px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
margin: 4px;
|
||||
line-height: 24px;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
const HelpText = styled.span`
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
line-height: 21px;
|
||||
font-size: 14px;
|
||||
margin-left: 12px;
|
||||
color: #000000;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
showHelp: boolean;
|
||||
shortcuts: HotKey[];
|
||||
dispatch(action: any): void;
|
||||
}
|
||||
|
||||
class ShortcutsComponent extends React.Component<Props> {
|
||||
private readonly os: OS;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
if (navigator.appVersion.indexOf('Win') !== -1) {
|
||||
this.os = OS.win;
|
||||
} else if (navigator.appVersion.indexOf('Mac') !== -1) {
|
||||
this.os = OS.mac;
|
||||
} else {
|
||||
this.os = OS.linux;
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
document.addEventListener('keydown', this.handleKeydown);
|
||||
document.addEventListener('keypress', this.handleKeyPress);
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
document.removeEventListener('keydown', this.handleKeydown);
|
||||
document.removeEventListener('keypress', this.handleKeyPress);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.props.showHelp && (
|
||||
<EuiOverlayMask>
|
||||
<EuiModal onClose={this.closeModal}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>Keyboard Shortcuts</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>{this.renderShortcuts()}</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButton onClick={this.closeModal} fill>
|
||||
Close
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private handleKeydown = (event: KeyboardEvent) => {
|
||||
const target = event.target;
|
||||
const key = event.key;
|
||||
// @ts-ignore
|
||||
if (target && target.tagName === 'INPUT') {
|
||||
if (key === 'Escape') {
|
||||
// @ts-ignore
|
||||
target.blur();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private handleKeyPress = (event: KeyboardEvent) => {
|
||||
const target = event.target;
|
||||
const key = event.key;
|
||||
// @ts-ignore
|
||||
if (target && target.tagName === 'INPUT') {
|
||||
return;
|
||||
}
|
||||
|
||||
const isPressed = (s: HotKey) => {
|
||||
if (s.modifier) {
|
||||
const mods = s.modifier.get(this.os) || [];
|
||||
for (const mod of mods) {
|
||||
switch (mod) {
|
||||
case Modifier.alt:
|
||||
if (!event.altKey) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case Modifier.ctrl:
|
||||
if (!event.ctrlKey) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case Modifier.meta:
|
||||
if (!event.metaKey) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case Modifier.shift:
|
||||
if (!event.shiftKey) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return key === s.key;
|
||||
};
|
||||
for (const shortcut of this.props.shortcuts) {
|
||||
if (isPressed(shortcut) && shortcut.onPress) {
|
||||
shortcut.onPress(this.props.dispatch);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private closeModal = () => {
|
||||
this.props.dispatch(toggleHelp(false));
|
||||
};
|
||||
|
||||
private showModifier(mod: Modifier): string {
|
||||
switch (mod) {
|
||||
case Modifier.meta:
|
||||
if (this.os === OS.mac) {
|
||||
return '⌘';
|
||||
} else if (this.os === OS.win) {
|
||||
return '⊞ Win';
|
||||
} else {
|
||||
return 'meta';
|
||||
}
|
||||
|
||||
case Modifier.shift:
|
||||
if (this.os === OS.mac) {
|
||||
return '⇧';
|
||||
} else {
|
||||
return 'shift';
|
||||
}
|
||||
case Modifier.ctrl:
|
||||
if (this.os === OS.mac) {
|
||||
return '⌃';
|
||||
} else {
|
||||
return 'ctrl';
|
||||
}
|
||||
case Modifier.alt:
|
||||
if (this.os === OS.mac) {
|
||||
return '⌥';
|
||||
} else {
|
||||
return 'alt';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private renderShortcuts() {
|
||||
return this.props.shortcuts.map((s, idx) => {
|
||||
return (
|
||||
<div key={'shortcuts_' + idx}>
|
||||
{this.renderModifier(s)}
|
||||
<Key>{s.key}</Key>
|
||||
<HelpText>{s.help}</HelpText>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private renderModifier(hotKey: HotKey) {
|
||||
if (hotKey.modifier) {
|
||||
const modifiers = hotKey.modifier.get(this.os) || [];
|
||||
return modifiers.map(m => <Key>{this.showModifier(m)}</Key>);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: RootState) => ({
|
||||
shortcuts: state.shortcuts.shortcuts,
|
||||
showHelp: state.shortcuts.showHelp,
|
||||
});
|
||||
|
||||
export const ShortcutsProvider = connect(mapStateToProps)(ShortcutsComponent);
|
|
@ -14,6 +14,7 @@ import { languageServer, LanguageServerState } from './language_server';
|
|||
import { repository, RepositoryState } from './repository';
|
||||
import { route, RouteState } from './route';
|
||||
import { search, SearchState } from './search';
|
||||
import { shortcuts, ShortcutsState } from './shortcuts';
|
||||
import { status, StatusState } from './status';
|
||||
import { symbol, SymbolState } from './symbol';
|
||||
import { userConfig, UserConfigState } from './user';
|
||||
|
@ -29,6 +30,7 @@ export interface RootState {
|
|||
commit: CommitState;
|
||||
blame: BlameState;
|
||||
languageServer: LanguageServerState;
|
||||
shortcuts: ShortcutsState;
|
||||
}
|
||||
|
||||
const reducers = {
|
||||
|
@ -43,6 +45,7 @@ const reducers = {
|
|||
commit,
|
||||
blame,
|
||||
languageServer,
|
||||
shortcuts,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
|
|
57
x-pack/plugins/code/public/reducers/shortcuts.ts
Normal file
57
x-pack/plugins/code/public/reducers/shortcuts.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import produce from 'immer';
|
||||
import { Action, handleActions } from 'redux-actions';
|
||||
|
||||
import { registerShortcut, toggleHelp, unregisterShortcut } from '../actions';
|
||||
import { HotKey } from '../components/shortcuts';
|
||||
|
||||
export interface ShortcutsState {
|
||||
showHelp: boolean;
|
||||
shortcuts: HotKey[];
|
||||
}
|
||||
|
||||
const helpShortcut: HotKey = {
|
||||
key: '?',
|
||||
help: 'Display this page',
|
||||
modifier: new Map(),
|
||||
onPress: dispatch => {
|
||||
dispatch(toggleHelp(null));
|
||||
},
|
||||
};
|
||||
|
||||
const initialState: ShortcutsState = {
|
||||
showHelp: false,
|
||||
shortcuts: [helpShortcut],
|
||||
};
|
||||
|
||||
export const shortcuts = handleActions<ShortcutsState, any>(
|
||||
{
|
||||
[String(toggleHelp)]: (state: ShortcutsState, action: Action<boolean | null>) =>
|
||||
produce<ShortcutsState>(state, (draft: ShortcutsState) => {
|
||||
if (action.payload === null) {
|
||||
draft.showHelp = !state.showHelp;
|
||||
} else {
|
||||
draft.showHelp = action.payload!;
|
||||
}
|
||||
}),
|
||||
[String(registerShortcut)]: (state: ShortcutsState, action: Action<HotKey>) =>
|
||||
produce<ShortcutsState>(state, (draft: ShortcutsState) => {
|
||||
const hotKey = action.payload as HotKey;
|
||||
draft.shortcuts.push(hotKey);
|
||||
}),
|
||||
[String(unregisterShortcut)]: (state: ShortcutsState, action: Action<HotKey>) =>
|
||||
produce<ShortcutsState>(state, (draft: ShortcutsState) => {
|
||||
const hotKey = action.payload as HotKey;
|
||||
const idx = state.shortcuts.indexOf(hotKey);
|
||||
if (idx >= 0) {
|
||||
draft.shortcuts.splice(idx, 1);
|
||||
}
|
||||
}),
|
||||
},
|
||||
initialState
|
||||
);
|
Loading…
Add table
Add a link
Reference in a new issue