import React, { cloneElement, Children, Component } from 'react'; import PropTypes from 'prop-types'; import Dock from 'react-dock'; import { Action, Dispatch } from 'redux'; import { LiftedState, Monitor } from 'redux-devtools'; import { POSITIONS } from './constants'; import { toggleVisibility, changeMonitor, changePosition, changeSize, DockMonitorAction, } from './actions'; import reducer, { DockMonitorState } from './reducers'; import parseKey from 'parse-key'; interface KeyObject { name: string; ctrl: boolean; meta: boolean; shift: boolean; alt: boolean; sequence: string; } export interface DockMonitorProps> extends LiftedState { defaultPosition: 'left' | 'top' | 'right' | 'bottom'; defaultIsVisible: boolean; defaultSize: number; toggleVisibilityKey: string; changePositionKey: string; changeMonitorKey?: string; fluid: boolean; dispatch: Dispatch; children: | Monitor, unknown, Action> | Monitor, unknown, Action>[]; } export default class DockMonitor< S, A extends Action > extends Component> { static update = reducer; static propTypes = { defaultPosition: PropTypes.oneOf(POSITIONS), defaultIsVisible: PropTypes.bool.isRequired, defaultSize: PropTypes.number.isRequired, toggleVisibilityKey: PropTypes.string.isRequired, changePositionKey: PropTypes.string.isRequired, changeMonitorKey: PropTypes.string, fluid: PropTypes.bool, dispatch: PropTypes.func, monitorState: PropTypes.shape({ position: PropTypes.oneOf(POSITIONS).isRequired, size: PropTypes.number.isRequired, isVisible: PropTypes.bool.isRequired, childMonitorState: PropTypes.any, }), }; static defaultProps = { defaultIsVisible: true, defaultPosition: 'right', defaultSize: 0.3, fluid: true, }; constructor(props: DockMonitorProps) { super(props); const childrenCount = Children.count(props.children); if (childrenCount === 0) { // eslint-disable-next-line no-console console.error( ' requires at least one monitor inside. ' + 'Why don’t you try ? You can get it at ' + 'https://github.com/gaearon/redux-devtools-log-monitor.' ); } else if (childrenCount > 1 && !props.changeMonitorKey) { // eslint-disable-next-line no-console console.error( 'You specified multiple monitors inside ' + 'but did not provide `changeMonitorKey` prop to change them. ' + 'Try specifying ' + 'and then press Ctrl-M.' ); } } componentDidMount() { window.addEventListener('keydown', this.handleKeyDown); } componentWillUnmount() { window.removeEventListener('keydown', this.handleKeyDown); } matchesKey(key: KeyObject | undefined, event: KeyboardEvent) { if (!key) { return false; } const charCode = event.keyCode || event.which; const char = String.fromCharCode(charCode); return ( key.name.toUpperCase() === char.toUpperCase() && key.alt === event.altKey && key.ctrl === event.ctrlKey && key.meta === event.metaKey && key.shift === event.shiftKey ); } handleKeyDown = (e: KeyboardEvent) => { // Ignore regular keys when focused on a field // and no modifiers are active. if ( !e.ctrlKey && !e.metaKey && !e.altKey && ((e.target! as { tagName?: string }).tagName === 'INPUT' || (e.target! as { tagName?: string }).tagName === 'SELECT' || (e.target! as { tagName?: string }).tagName === 'TEXTAREA' || (e.target! as { isContentEditable?: boolean }).isContentEditable) ) { return; } const visibilityKey = parseKey(this.props.toggleVisibilityKey); const positionKey = parseKey(this.props.changePositionKey); let monitorKey; if (this.props.changeMonitorKey) { monitorKey = parseKey(this.props.changeMonitorKey); } if (this.matchesKey(visibilityKey, e)) { e.preventDefault(); this.props.dispatch(toggleVisibility()); } else if (this.matchesKey(positionKey, e)) { e.preventDefault(); this.props.dispatch(changePosition()); } else if (this.matchesKey(monitorKey, e)) { e.preventDefault(); this.props.dispatch(changeMonitor()); } }; handleSizeChange = (requestedSize: number) => { this.props.dispatch(changeSize(requestedSize)); }; renderChild( child: Monitor, unknown, Action>, index: number, otherProps: Omit< DockMonitorProps, 'monitorState' | 'children' | 'fluid' > ) { const { monitorState } = this.props; const { childMonitorIndex, childMonitorStates } = monitorState; if (index !== childMonitorIndex) { return null; } return cloneElement(child, { monitorState: childMonitorStates[index], ...otherProps, }); } render() { const { monitorState, children, fluid, ...rest } = this.props; const { position, isVisible, size } = monitorState; return ( {Children.map( children as | Monitor< S, A, LiftedState, unknown, Action > | Monitor< S, A, LiftedState, unknown, Action >[], (child, index) => this.renderChild(child, index, rest) )} ); } }