[Code] shortcut modal and shortcuts (#29061)

This commit is contained in:
Yulong 2019-01-22 14:02:39 +08:00 committed by GitHub
parent 97b4c1cc93
commit b4d714a9a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 423 additions and 9 deletions

View file

@ -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;

View 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');

View file

@ -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>

View file

@ -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>
);
}

View file

@ -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>

View file

@ -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);

View file

@ -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';

View 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);

View file

@ -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);

View file

@ -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

View 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
);