import * as React from 'react'; import { connect } from 'unistore/react'; import { IStoreState, ID, IRowDimensions, IColDimensions, ISelectionType, IRow, IColumn, ICell, } from '../index.data'; import Row from '../Row'; import { getCell, getCellType, getPresentCell } from '../store/selectors'; import ColGroup from '../ColGroup'; import { queryColDimensions, queryRowDimensions, getCellAttrsFromEvent, queryContainerBox, } from './helper'; import { setDimensions, setContainerBox, tryToggleCollapse } from '../store/actions'; import { ID_CELL, ID_ROW_INDICATOR, FSM_IGNORE, ID_COLUMN_INDICATOR, ID_SHEET_INDICATOR, } from '../constants'; import * as Rx from 'rambdax'; import * as R from 'rambda'; import { Store } from 'unistore'; import { EditEvent } from '../StateMachine/index.data'; import RowGroup from '../RowGroup'; import RuntimeContext from '../RuntimeContext'; import { getTypedId, pickPatch } from '../utils'; import { shallowEqualsArray } from 'valor-app-utils'; /* interface Props {} interface MappedProps { selectionType: ISelectionType | null; selectionCellRange: [ID, ID]; mode: any; } */ interface Props { selectionType: ISelectionType | null; selectionCellRange: [ID, ID]; mode: any; // for RowGroup rows: IRow[]; columns: IColumn[]; loadableRowFlags: boolean[]; loadableColumnFlags: boolean[]; cells: Record; } interface MappedActions { setDimensions: ( rowDimensions: IRowDimensions, colDimensions: IColDimensions, ) => { rowDimensions: IRowDimensions; colDimensions: IColDimensions }; setContainerDimensions: ( containerDimensions: IStoreState['containerBox'], ) => { containerDimensions: IStoreState['containerBox'] }; tryToggleCollapse: ( rowId: ID, ) => { collapsedRowIds: []; } | null; // send: (event: EditEvent) => void; // attachToStore: (propName: string, body: () => void) => void; } class DataSheet extends React.Component { ref = React.createRef(); dragging: boolean = false; static contextType = RuntimeContext; context!: React.ContextType; lastSelection: { selectionType: ISelectionType | null; selectionCellRange: [ID, ID] | null } = { selectionType: null, selectionCellRange: null, }; observer: ResizeObserver; constructor(props: Props & MappedActions) { super(props); this.handleMouseDown = this.handleMouseDown.bind(this); this.handleMouseMove = this.handleMouseMove.bind(this); this.handleMouseMove = Rx.throttle(this.handleMouseMove, 16); this.handleMouseUp = this.handleMouseUp.bind(this); this.handleDoubleClick = this.handleDoubleClick.bind(this); this.setSheetDimensions = this.setSheetDimensions.bind(this); this.setContainerDimensions = this.setContainerDimensions.bind(this); this.observer = new ResizeObserver(() => { this.setContainerDimensions(); }); } handleMouseDown(e: MouseEvent) { const { selectionType, selectionCellRange } = this.props; this.lastSelection = { selectionType, selectionCellRange, }; // if (this.ref!.contains(e.target! as any)) { // 计算每行和每列dim ( 注意是每行 和 每列, 一次性计算完成 ) // this.setSheetDimensions(); // } if (this.sendEvent(e, 'MOUSE.DOWN')) { // 绑定事件 document.body.addEventListener('mousemove', this.handleMouseMove); document.body.addEventListener('mouseup', this.handleMouseUp); } } setContainerDimensions() { const containerDimensions = queryContainerBox(this.ref.current!); this.props.setContainerDimensions(containerDimensions); // 容器大小变更后, 可能重算 setTimeout(() => { this.setSheetDimensions(); }); } setSheetDimensions() { // 计算每行和每列dim ( 注意是每行 和 每列, 一次性计算完成 ) const colDimensions = queryColDimensions(this.ref.current!); const rowDimensions = queryRowDimensions(this.ref.current!); this.props.setDimensions(rowDimensions, colDimensions); } handleMouseMove(e: MouseEvent) { this.dragging = true; this.sendEvent(e, 'MOUSE.MOVE'); } handleMouseUp(e: MouseEvent) { this.sendEvent(e, 'MOUSE.UP'); document.body.removeEventListener('mousemove', this.handleMouseMove); document.body.removeEventListener('mouseup', this.handleMouseUp); this.dragging = false; } private sendEvent(e: MouseEvent, eventType: 'MOUSE.DOWN' | 'MOUSE.MOVE' | 'MOUSE.UP'): boolean { // 注意: 这里需要防止以下元素发出事件: 比如 input , 比如 dialog // 这些事件会被解释为: clickoutside 事件 // 所以, 如果不想发出事件, 那么应标记组件为 className='fsm-ignore' if ((e.target as HTMLElement).closest(`.${FSM_IGNORE}`)) return false; const [cellType, id] = getCellAttrsFromEvent(e); // 暂不支持选择整个sheet if (ID_SHEET_INDICATOR === cellType) return false; const selectedKey = cellType === ID_ROW_INDICATOR ? 'selectedRow' : cellType === ID_COLUMN_INDICATOR ? 'selectedColumn' : 'selectedCell'; const context = this.context!; const state = context.store.getState(); if ( // 排除picking状态, 此时不应重选单元格 !(state.mode.editing && state.mode.editing.picking === 'on') && eventType === 'MOUSE.DOWN' && state.isActiving ) { // 意外情况: 当前正在编辑(Active), 用户点了其它单元格 , 或表格之外 if (cellType === ID_CELL && id !== null && id + '' !== getPresentCell(state) + '') { // 1. 正在编辑, 点了其它单元格 context.sendFsmEvent({ type: 'SELECT.CELL', selectedCell: id, state, }); } else if (cellType === ID_ROW_INDICATOR && id !== null) { // 2. 正在编辑, 点了行标头 context.sendFsmEvent({ type: 'SELECT.ROW', selectedRow: id, state, }); } else if (cellType === ID_COLUMN_INDICATOR && id !== null) { // 2. 正在编辑, 点了行标头 context.sendFsmEvent({ type: 'SELECT.COLUMN', selectedColumn: id, state, }); } } else { // 正常情况, 发送鼠标事件到状态机 ( 主要确保可以多选 ) context.sendFsmEvent({ type: eventType as any, [selectedKey]: id, state, }); } return true; } handleDoubleClick(e: React.MouseEvent) { // 对于可编辑单元格, 不会触发! (因为editor实际上并不在dataSheet内部) // 所以以下代码安全 const [cellType, id_] = getCellAttrsFromEvent(e.nativeEvent); const state = this.context!.store.getState(); const id = id_ && getTypedId(id_); if (cellType === ID_CELL && id) { if (getCellType(state, id) === 'row-no') { this.props.tryToggleCollapse(id!); setTimeout(() => { this.context!.setSheetDimensions(); }); } else { this.context!.sendFsmEvent({ type: 'EDITING' }); } } } shouldComponentUpdate(nextProps: Props) { const props = this.props; return ( nextProps.rows !== props.rows || nextProps.cells !== props.cells || !shallowEqualsArray(nextProps.loadableRowFlags, props.loadableRowFlags) || !shallowEqualsArray(nextProps.loadableColumnFlags, props.loadableColumnFlags) ); } componentDidMount() { const self = this; // self.props.attachToStore('setSheetDimensions', self.setSheetDimensions); self.context!.setSheetDimensions = self.setSheetDimensions; document.body.addEventListener('mousedown', self.handleMouseDown); // 让安装好的列列立即可查询dim setTimeout(() => { // 立即计算容器宽高一次 self.setContainerDimensions(); }, 0); if (!this.ref.current) { throw new Error('Dastasheet 未找到ref节点'); } const container = this.ref.current.closest('.spread-sheet'); if (!container) { throw new Error('没有找到class-name = spread-sheet 的容器节点'); } this.observer.observe(container); } componentWillUnmount() { document.body.removeEventListener('mousedown', this.handleMouseDown); this.observer.disconnect(); const container = this.ref.current!.closest('.spread-sheet')!; this.observer.observe(container); } render() { //console.log('dataSheet render '); const { rows, cells, columns, loadableRowFlags, loadableColumnFlags } = this.props; return (
); } } /* const mapState = (state: IStoreState, props: Props): MappedProps => ({ selectionType: state.selectionType, selectionCellRange: state.selectedCellRange, mode: state.mode, }); */ const mapState = (state: IStoreState) => ({}); const actions = (store: Store) => ({ setDimensions: ( state: IStoreState, rowDimensions: IRowDimensions, colDimensions: IColDimensions, ) => { return setDimensions(store.getState(), rowDimensions, colDimensions); }, setContainerDimensions: ( state: IStoreState, containerDimensions: IStoreState['containerBox'], ) => { return setContainerBox(store.getState(), containerDimensions); }, tryToggleCollapse: (state: IStoreState, cellId: ID) => { return tryToggleCollapse(state, cellId); }, }); export default connect( mapState, actions, )(DataSheet);