/** * @file scoped.jsx. * @author fex */ import React from 'react'; import {findDOMNode} from 'react-dom'; import find from 'lodash/find'; import PropTypes from 'prop-types'; import isPlainObject from 'lodash/isPlainObject'; import {RendererProps} from '../factory'; import cx from 'classnames'; import hoistNonReactStatic from 'hoist-non-react-statics'; import onClickOutside from 'react-onclickoutside'; import {Action} from '../types'; import keycode from 'keycode'; import matches from 'dom-helpers/query/matches'; import Overlay from '../components/Overlay'; import PopOver from '../components/PopOver'; import {Icon} from '../components/icons'; import {SchemaCollection, SchemaObject} from '../Schema'; export type SchemaQuickEditObject = /** * 直接就是个表单项 */ | ({ /** * 是否立即保存 */ saveImmediately?: boolean; /** * 接口保存失败后,是否重置组件编辑状态 */ resetOnFailed?: boolean; /** * 是否直接内嵌 */ mode?: 'inline'; } & SchemaObject) /** * 表单项集合 */ | { /** * 是否立即保存 */ saveImmediately?: boolean; /** * 接口保存失败后,是否重置组件编辑状态 */ resetOnFailed?: boolean; /** * 是否直接内嵌 */ mode?: 'inline'; body: SchemaCollection; }; export type SchemaQuickEdit = boolean | SchemaQuickEditObject; export interface QuickEditConfig { saveImmediately?: boolean; resetOnFailed?: boolean; mode?: 'inline' | 'dialog' | 'popOver' | 'append'; type?: string; body?: any; focusable?: boolean; popOverClassName?: string; [propName: string]: any; } export interface QuickEditProps extends RendererProps { name?: string; label?: string; quickEdit: boolean | QuickEditConfig; quickEditEnabled?: boolean; } export interface QuickEditState { isOpened: boolean; } let inited: boolean = false; let currentOpened: any; export const HocQuickEdit = (config: Partial = {}) => ( Component: React.ComponentType ): any => { class QuickEditComponent extends React.PureComponent< QuickEditProps, QuickEditState > { target: HTMLElement; overlay: HTMLElement; static ComposedComponent = Component; constructor(props: QuickEditProps) { super(props); this.openQuickEdit = this.openQuickEdit.bind(this); this.closeQuickEdit = this.closeQuickEdit.bind(this); this.handleAction = this.handleAction.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleKeyUp = this.handleKeyUp.bind(this); this.overlayRef = this.overlayRef.bind(this); this.handleWindowKeyPress = this.handleWindowKeyPress.bind(this); this.handleWindowKeyDown = this.handleWindowKeyDown.bind(this); this.formRef = this.formRef.bind(this); this.handleInit = this.handleInit.bind(this); this.handleChange = this.handleChange.bind(this); this.state = { isOpened: false }; } componentDidMount() { this.target = findDOMNode(this) as HTMLElement; if (inited) { return; } inited = true; document.body.addEventListener('keypress', this.handleWindowKeyPress); document.body.addEventListener('keydown', this.handleWindowKeyDown); } formRef(ref: any) { const {quickEditFormRef, rowIndex, colIndex} = this.props; if (quickEditFormRef) { while (ref && ref.getWrappedInstance) { ref = ref.getWrappedInstance(); } quickEditFormRef(ref, colIndex, rowIndex); } } handleWindowKeyPress(e: Event) { const ns = this.props.classPrefix; let el: HTMLElement = (e.target as HTMLElement).closest( `.${ns}Field--quickEditable` ) as HTMLElement; if (!el) { return; } const table = el.closest('table'); if (!table) { return; } if ( keycode(e) === 'space' && !~['INPUT', 'TEXTAREA'].indexOf(el.tagName) ) { e.preventDefault(); e.stopPropagation(); } } handleWindowKeyDown(e: Event) { const code = keycode(e); if (code === 'esc' && currentOpened) { currentOpened.closeQuickEdit(); } else if ( ~['INPUT', 'TEXTAREA'].indexOf((e.target as HTMLElement).tagName) || (e.target as HTMLElement).contentEditable === 'true' || !~['up', 'down', 'left', 'right'].indexOf(code) ) { return; } e.preventDefault(); const ns = this.props.classPrefix; let el: HTMLElement = ((e.target as HTMLElement).closest( `.${ns}Field--quickEditable` ) as HTMLElement) || document.querySelector(`.${ns}Field--quickEditable`); if (!el) { return; } let table = el.closest('table'); if (!table) { return; } let current = table.querySelector( `.${ns}Field--quickEditable:focus` ) as HTMLTableDataCellElement; if (!current) { let dom = table.querySelector( `.${ns}Field--quickEditable[tabindex]` ) as HTMLElement; dom && dom.focus(); } else { let prevTr, nextTr, prevTd, nextTd; switch (code) { case 'up': prevTr = (current.parentNode as HTMLElement) .previousSibling as HTMLTableCellElement; if (prevTr) { let index = current.cellIndex; (prevTr.children[index] as HTMLElement).focus(); } break; case 'down': nextTr = (current.parentNode as HTMLElement) .nextSibling as HTMLTableCellElement; if (nextTr) { let index = current.cellIndex; (nextTr.children[index] as HTMLElement).focus(); } break; case 'left': prevTd = current.previousElementSibling as HTMLTableCellElement; while (prevTd) { if (matches(prevTd, `.${ns}Field--quickEditable[tabindex]`)) { break; } prevTd = prevTd.previousElementSibling; } if (prevTd) { (prevTd as HTMLElement).focus(); } else if ((current.parentNode as HTMLElement).previousSibling) { let tds = ((current.parentNode as HTMLElement) .previousSibling as HTMLElement).querySelectorAll( `.${ns}Field--quickEditable[tabindex]` ); if (tds.length) { (tds[tds.length - 1] as HTMLElement).focus(); } } break; case 'right': nextTd = current.nextSibling; while (nextTd) { if ( matches( nextTd as Element, `.${ns}Field--quickEditable[tabindex]` ) ) { break; } nextTd = nextTd.nextSibling; } if (nextTd) { (nextTd as HTMLElement).focus(); } else if ((current.parentNode as HTMLElement).nextSibling) { nextTd = ((current.parentNode as HTMLElement) .nextSibling as HTMLElement).querySelector( `.${ns}Field--quickEditable[tabindex]` ); if (nextTd) { (nextTd as any).focus(); } } break; } } } // handleClickOutside() { // this.closeQuickEdit(); // } overlayRef(ref: any) { this.overlay = ref; } handleAction(e: any, action: Action, ctx: object) { const {onAction} = this.props; if (action.actionType === 'cancel' || action.actionType === 'close') { this.closeQuickEdit(); return; } onAction && onAction(e, action, ctx); } handleSubmit(values: object) { const {onQuickChange, quickEdit} = this.props; this.closeQuickEdit(); onQuickChange( values, (quickEdit as QuickEditConfig).saveImmediately, false, (quickEdit as QuickEditConfig).resetOnFailed ); } handleInit(values: object) { const {onQuickChange} = this.props; onQuickChange(values, false, true); } handleChange(values: object) { const {onQuickChange, quickEdit} = this.props; onQuickChange( values, (quickEdit as QuickEditConfig).saveImmediately, false, (quickEdit as QuickEditConfig).resetOnFailed ); } openQuickEdit() { currentOpened = this; this.setState({ isOpened: true }); } closeQuickEdit() { if (!this.state.isOpened) { return; } currentOpened = null; const ns = this.props.classPrefix; this.setState( { isOpened: false }, () => { let el = findDOMNode(this) as HTMLElement; let table = el.closest('table') as HTMLElement; ((table && table.querySelectorAll(`td.${ns}Field--quickEditable:focus`) .length) || el) && el.focus(); } ); } buildSchema() { const {quickEdit, name, label, translate: __} = this.props; let schema; if (quickEdit === true) { schema = { type: 'form', title: '', autoFocus: true, body: [ { type: 'input-text', name, placeholder: label, label: false } ] }; } else if (quickEdit) { if ( quickEdit.body && !~['combo', 'group', 'panel', 'fieldSet', 'fieldset'].indexOf( (quickEdit as any).type ) ) { schema = { title: '', autoFocus: (quickEdit as QuickEditConfig).mode !== 'inline', ...quickEdit, mode: 'normal', type: 'form' }; } else { schema = { title: '', className: quickEdit.formClassName, type: 'form', autoFocus: (quickEdit as QuickEditConfig).mode !== 'inline', mode: 'normal', body: [ { type: quickEdit.type || 'text', name: quickEdit.name || name, ...quickEdit, mode: undefined } ] }; } } if (schema) { schema = { ...schema, wrapWithPanel: (quickEdit as QuickEditConfig).mode !== 'inline', actions: (quickEdit as QuickEditConfig).mode === 'inline' ? [] : [ { type: 'button', label: __('cancel'), actionType: 'cancel' }, { label: __('confirm'), type: 'submit', primary: true } ] }; } return schema || 'error'; } handleKeyUp(e: Event) { const code = keycode(e); if ( code === 'space' && !~['INPUT', 'TEXTAREA'].indexOf((e.target as HTMLElement).tagName) ) { e.preventDefault(); e.stopPropagation(); this.openQuickEdit(); } } renderPopOver() { let { quickEdit, render, popOverContainer, classPrefix: ns, classnames: cx, canAccessSuperData } = this.props; const content = (
{render('quick-edit-form', this.buildSchema(), { value: undefined, onSubmit: this.handleSubmit, onAction: this.handleAction, onChange: null, formLazyChange: false, ref: this.formRef, popOverContainer: () => this.overlay, canAccessSuperData })}
); popOverContainer = popOverContainer || (() => findDOMNode(this)); return ( this.target} onHide={this.closeQuickEdit} placement="left-top right-top left-bottom right-bottom left-top" show > {content} ); } render() { const { onQuickChange, quickEdit, quickEditEnabled, className, classnames: cx, render, noHoc, canAccessSuperData } = this.props; if (!quickEdit || !onQuickChange || quickEditEnabled === false || noHoc) { return ; } if ((quickEdit as QuickEditConfig).mode === 'inline') { return ( {render('inline-form', this.buildSchema(), { value: undefined, wrapperComponent: 'div', className: cx('Form--quickEdit'), ref: this.formRef, simpleMode: true, onInit: this.handleInit, onChange: this.handleChange, formLazyChange: false, canAccessSuperData })} ); } else { return ( {this.state.isOpened ? this.renderPopOver() : null} ); } } } hoistNonReactStatic(QuickEditComponent, Component); return QuickEditComponent; }; export default HocQuickEdit;