diff options
author | Shin'ya Ueoka <ueokande@i-beam.org> | 2021-04-04 11:01:40 +0900 |
---|---|---|
committer | Shin'ya Ueoka <ueokande@i-beam.org> | 2021-04-04 17:34:57 +0900 |
commit | de4e651196502cffa56cedfdaf84d9bb665559e1 (patch) | |
tree | 9ede8350e941a8754cb99a6fc55d5124ea41c5ae /src | |
parent | d0495ce30e9aee853c23d70d512d845d6d131bb0 (diff) |
Replace Console component with a React Hooks
Diffstat (limited to 'src')
-rw-r--r-- | src/console/components/AppContext.ts | 13 | ||||
-rw-r--r-- | src/console/components/Console.tsx | 235 | ||||
-rw-r--r-- | src/console/index.tsx | 79 | ||||
-rw-r--r-- | src/console/reducers/index.ts | 2 |
4 files changed, 174 insertions, 155 deletions
diff --git a/src/console/components/AppContext.ts b/src/console/components/AppContext.ts new file mode 100644 index 0000000..878d00b --- /dev/null +++ b/src/console/components/AppContext.ts @@ -0,0 +1,13 @@ +import React from "react"; +import { State, defaultState } from "../reducers"; +import { ConsoleAction } from "../actions"; + +const AppContext = React.createContext<{ + state: State; + dispatch: React.Dispatch<Promise<ConsoleAction> | ConsoleAction>; +}>({ + state: defaultState, + dispatch: () => null, +}); + +export default AppContext; diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx index bb9aee7..3cccc43 100644 --- a/src/console/components/Console.tsx +++ b/src/console/components/Console.tsx @@ -1,10 +1,8 @@ -import { connect } from "react-redux"; import React from "react"; import Input from "./console/Input"; import Completion from "./console/Completion"; import Message from "./console/Message"; import * as consoleActions from "../../console/actions/console"; -import { State as AppState } from "../reducers"; import CommandLineParser, { InputPhase, } from "../commandline/CommandLineParser"; @@ -14,6 +12,7 @@ import { LightTheme, DarkTheme } from "./Theme"; import styled from "./Theme"; import { ThemeProvider } from "styled-components"; import ConsoleFrameClient from "../clients/ConsoleFrameClient"; +import AppContext from "./AppContext"; const ConsoleWrapper = styled.div` border-top: 1px solid gray; @@ -21,63 +20,54 @@ const ConsoleWrapper = styled.div` const COMPLETION_MAX_ITEMS = 33; -type StateProps = ReturnType<typeof mapStateToProps>; -interface DispatchProps { - dispatch: (action: any) => void; -} -type Props = StateProps & DispatchProps; +const Console: React.FC = () => { + const { state, dispatch } = React.useContext(AppContext); + const commandLineParser = new CommandLineParser(); + const consoleFrameClient = new ConsoleFrameClient(); -class Console extends React.Component<Props> { - private commandLineParser: CommandLineParser = new CommandLineParser(); - private consoleFrameClient = new ConsoleFrameClient(); - - constructor(props: Props) { - super(props); - } - - onBlur() { - if (this.props.mode === "command" || this.props.mode === "find") { - return this.props.dispatch(consoleActions.hideCommand()); + const onBlur = () => { + if (state.mode === "command" || state.mode === "find") { + dispatch(consoleActions.hideCommand()); } - } + }; - doEnter(e: React.KeyboardEvent<HTMLInputElement>) { + const doEnter = (e: React.KeyboardEvent<HTMLInputElement>) => { e.stopPropagation(); e.preventDefault(); const value = (e.target as HTMLInputElement).value; - if (this.props.mode === "command") { - return this.props.dispatch(consoleActions.enterCommand(value)); - } else if (this.props.mode === "find") { - return this.props.dispatch( - consoleActions.enterFind(value === "" ? undefined : value) - ); + if (state.mode === "command") { + dispatch(consoleActions.enterCommand(value)); + } else if (state.mode === "find") { + dispatch(consoleActions.enterFind(value === "" ? undefined : value)); } - } + }; - selectNext(e: React.KeyboardEvent<HTMLInputElement>) { - this.props.dispatch(consoleActions.completionNext()); + const selectNext = (e: React.KeyboardEvent<HTMLInputElement>) => { + dispatch(consoleActions.completionNext()); e.stopPropagation(); e.preventDefault(); - } + }; - selectPrev(e: React.KeyboardEvent<HTMLInputElement>) { - this.props.dispatch(consoleActions.completionPrev()); + const selectPrev = (e: React.KeyboardEvent<HTMLInputElement>) => { + dispatch(consoleActions.completionPrev()); e.stopPropagation(); e.preventDefault(); - } + }; - onKeyDown(e: React.KeyboardEvent<HTMLInputElement>) { + const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { switch (e.key) { case "Escape": - return this.props.dispatch(consoleActions.hideCommand()); + dispatch(consoleActions.hideCommand()); + break; case "Enter": - return this.doEnter(e); + doEnter(e); + break; case "Tab": if (e.shiftKey) { - this.props.dispatch(consoleActions.completionPrev()); + dispatch(consoleActions.completionPrev()); } else { - this.props.dispatch(consoleActions.completionNext()); + dispatch(consoleActions.completionNext()); } e.stopPropagation(); e.preventDefault(); @@ -85,126 +75,79 @@ class Console extends React.Component<Props> { case "[": if (e.ctrlKey) { e.preventDefault(); - return this.props.dispatch(consoleActions.hideCommand()); + dispatch(consoleActions.hideCommand()); } break; case "c": if (e.ctrlKey) { e.preventDefault(); - return this.props.dispatch(consoleActions.hideCommand()); + dispatch(consoleActions.hideCommand()); } break; case "m": if (e.ctrlKey) { - return this.doEnter(e); + doEnter(e); } break; case "n": if (e.ctrlKey) { - this.selectNext(e); + selectNext(e); } break; case "p": if (e.ctrlKey) { - this.selectPrev(e); + selectPrev(e); } break; } - } + }; - onChange(e: React.ChangeEvent<HTMLInputElement>) { + const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { const text = e.target.value; - this.props.dispatch(consoleActions.setConsoleText(text)); - if (this.props.mode !== "command") { + dispatch(consoleActions.setConsoleText(text)); + if (state.mode !== "command") { return; } - this.updateCompletions(text); - } - - componentDidUpdate(prevProps: Props) { - if (prevProps.mode !== "command" && this.props.mode === "command") { - this.updateCompletions(this.props.consoleText); - this.focus(); - } else if (prevProps.mode !== "find" && this.props.mode === "find") { - this.focus(); + updateCompletions(text); + }; + + const prevState = React.useRef(state); + React.useEffect(() => { + if (prevState.current.mode !== "command" && state.mode === "command") { + updateCompletions(state.consoleText); + focus(); + } else if (prevState.current.mode !== "find" && state.mode === "find") { + focus(); } const { scrollWidth: width, scrollHeight: height, } = document.getElementById("vimvixen-console")!; - this.consoleFrameClient.resize(width, height); - } - - render() { - let theme = this.props.colorscheme; - if (this.props.colorscheme === ColorScheme.System) { - if ( - window.matchMedia && - window.matchMedia("(prefers-color-scheme: dark)").matches - ) { - theme = ColorScheme.Dark; - } else { - theme = ColorScheme.Light; - } - } + consoleFrameClient.resize(width, height); - switch (this.props.mode) { - case "command": - case "find": - return ( - <ThemeProvider - theme={theme === ColorScheme.Dark ? DarkTheme : LightTheme} - > - <ConsoleWrapper> - <Completion - size={COMPLETION_MAX_ITEMS} - completions={this.props.completions} - select={this.props.select} - /> - <Input - mode={this.props.mode} - onBlur={this.onBlur.bind(this)} - onKeyDown={this.onKeyDown.bind(this)} - onChange={this.onChange.bind(this)} - value={this.props.consoleText} - /> - </ConsoleWrapper> - </ThemeProvider> - ); - case "info": - case "error": - return ( - <ThemeProvider - theme={theme === ColorScheme.Dark ? DarkTheme : LightTheme} - > - <Message mode={this.props.mode}>{this.props.messageText}</Message> - </ThemeProvider> - ); - default: - return null; - } - } + prevState.current = state; + }); - async focus() { - this.props.dispatch(consoleActions.setColorScheme()); + const focus = () => { + dispatch(consoleActions.setColorScheme()); window.focus(); - } + }; - private updateCompletions(text: string) { - const phase = this.commandLineParser.inputPhase(text); + const updateCompletions = (text: string) => { + const phase = commandLineParser.inputPhase(text); if (phase === InputPhase.OnCommand) { - return this.props.dispatch(consoleActions.getCommandCompletions(text)); + dispatch(consoleActions.getCommandCompletions(text)); } else { - const cmd = this.commandLineParser.parse(text); + const cmd = commandLineParser.parse(text); switch (cmd.command) { case Command.Open: case Command.TabOpen: case Command.WindowOpen: - this.props.dispatch( + dispatch( consoleActions.getOpenCompletions( - this.props.completionTypes, + state.completionTypes, text, cmd.command, cmd.args @@ -212,32 +155,78 @@ class Console extends React.Component<Props> { ); break; case Command.Buffer: - this.props.dispatch( + dispatch( consoleActions.getTabCompletions(text, cmd.command, cmd.args, false) ); break; case Command.BufferDelete: case Command.BuffersDelete: - this.props.dispatch( + dispatch( consoleActions.getTabCompletions(text, cmd.command, cmd.args, true) ); break; case Command.BufferDeleteForce: case Command.BuffersDeleteForce: - this.props.dispatch( + dispatch( consoleActions.getTabCompletions(text, cmd.command, cmd.args, false) ); break; case Command.Set: - this.props.dispatch( + dispatch( consoleActions.getPropertyCompletions(text, cmd.command, cmd.args) ); break; } } + }; + + let theme = state.colorscheme; + if (state.colorscheme === ColorScheme.System) { + if ( + window.matchMedia && + window.matchMedia("(prefers-color-scheme: dark)").matches + ) { + theme = ColorScheme.Dark; + } else { + theme = ColorScheme.Light; + } } -} -const mapStateToProps = (state: AppState) => ({ ...state }); + switch (state.mode) { + case "command": + case "find": + return ( + <ThemeProvider + theme={theme === ColorScheme.Dark ? DarkTheme : LightTheme} + > + <ConsoleWrapper> + <Completion + size={COMPLETION_MAX_ITEMS} + completions={state.completions} + select={state.select} + /> + <Input + mode={state.mode} + onBlur={onBlur} + onKeyDown={onKeyDown} + onChange={onChange} + value={state.consoleText} + /> + </ConsoleWrapper> + </ThemeProvider> + ); + case "info": + case "error": + return ( + <ThemeProvider + theme={theme === ColorScheme.Dark ? DarkTheme : LightTheme} + > + <Message mode={state.mode}>{state.messageText}</Message> + </ThemeProvider> + ); + default: + return null; + } +}; -export default connect(mapStateToProps)(Console); +export default Console; diff --git a/src/console/index.tsx b/src/console/index.tsx index f9313a0..cf9367b 100644 --- a/src/console/index.tsx +++ b/src/console/index.tsx @@ -1,42 +1,59 @@ import * as messages from "../shared/messages"; -import reducers from "./reducers"; -import { createStore, applyMiddleware } from "redux"; -import promise from "redux-promise"; +import reducers, { defaultState } from "./reducers"; import * as consoleActions from "./actions/console"; -import { Provider } from "react-redux"; import Console from "./components/Console"; +import AppContext from "./components/AppContext"; import "./index.css"; import React from "react"; import ReactDOM from "react-dom"; -const store = createStore(reducers, applyMiddleware(promise)); +const wrapAsync = <T extends unknown>( + dispatch: React.Dispatch<T> +): React.Dispatch<T | Promise<T>> => { + return (action: T | Promise<T>) => { + if (action instanceof Promise) { + action.then((a) => dispatch(a)).catch(console.error); + } else { + dispatch(action); + } + }; +}; -window.addEventListener("DOMContentLoaded", () => { - const wrapper = document.getElementById("vimvixen-console"); - ReactDOM.render( - <Provider store={store}> - <Console></Console> - </Provider>, - wrapper - ); -}); +const RootComponent: React.FC = () => { + const [state, dispatch] = React.useReducer(reducers, defaultState); + + React.useEffect(() => { + const onMessage = async (message: any): Promise<any> => { + const msg = messages.valueOf(message); + switch (msg.type) { + case messages.CONSOLE_SHOW_COMMAND: + return dispatch(await consoleActions.showCommand(msg.command)); + case messages.CONSOLE_SHOW_FIND: + return dispatch(consoleActions.showFind()); + case messages.CONSOLE_SHOW_ERROR: + return dispatch(consoleActions.showError(msg.text)); + case messages.CONSOLE_SHOW_INFO: + return dispatch(consoleActions.showInfo(msg.text)); + case messages.CONSOLE_HIDE: + return dispatch(consoleActions.hide()); + } + }; -const onMessage = async (message: any): Promise<any> => { - const msg = messages.valueOf(message); - switch (msg.type) { - case messages.CONSOLE_SHOW_COMMAND: - return store.dispatch(await consoleActions.showCommand(msg.command)); - case messages.CONSOLE_SHOW_FIND: - return store.dispatch(consoleActions.showFind()); - case messages.CONSOLE_SHOW_ERROR: - return store.dispatch(consoleActions.showError(msg.text)); - case messages.CONSOLE_SHOW_INFO: - return store.dispatch(consoleActions.showInfo(msg.text)); - case messages.CONSOLE_HIDE: - return store.dispatch(consoleActions.hide()); - } + browser.runtime.onMessage.addListener(onMessage); + const port = browser.runtime.connect(undefined, { + name: "vimvixen-console", + }); + port.onMessage.addListener(onMessage); + }, []); + + return ( + <AppContext.Provider value={{ state, dispatch: wrapAsync(dispatch) }}> + <Console /> + </AppContext.Provider> + ); }; -browser.runtime.onMessage.addListener(onMessage); -const port = browser.runtime.connect(undefined, { name: "vimvixen-console" }); -port.onMessage.addListener(onMessage); +window.addEventListener("DOMContentLoaded", () => { + const wrapper = document.getElementById("vimvixen-console"); + ReactDOM.render(<RootComponent />, wrapper); +}); diff --git a/src/console/reducers/index.ts b/src/console/reducers/index.ts index dbaf97d..49d0de1 100644 --- a/src/console/reducers/index.ts +++ b/src/console/reducers/index.ts @@ -14,7 +14,7 @@ export interface State { colorscheme: ColorScheme; } -const defaultState = { +export const defaultState = { mode: "", messageText: "", consoleText: "", |