import React from 'react'; import {findDOMNode} from 'react-dom'; import {Renderer, RendererProps} from '../../factory'; import {SchemaNode, Action, Schema} from '../../types'; import forEach from 'lodash/forEach'; import {filter} from '../../utils/tpl'; import DropDownButton from '../DropDownButton'; import Checkbox from '../../components/Checkbox'; import Button from '../../components/Button'; import {TableStore, ITableStore, IColumn, IRow} from '../../store/table'; import { anyChanged, getScrollParent, difference, noop, autobind, isArrayChildrenModified, getVariable } from '../../utils/helper'; import {resolveVariable} from '../../utils/tpl-builtin'; import debounce from 'lodash/debounce'; import Sortable from 'sortablejs'; import {resizeSensor} from '../../utils/resize-sensor'; import find from 'lodash/find'; import {Icon} from '../../components/icons'; import {TableCell} from './TableCell'; import {TableRow} from './TableRow'; import {HeadCellFilterDropDown} from './HeadCellFilterDropdown'; import {HeadCellSearchDropDown} from './HeadCellSearchDropdown'; import {TableContent} from './TableContent'; import { BaseSchema, SchemaClassName, SchemaObject, SchemaTokenizeableString, SchemaType } from '../../Schema'; import {SchemaPopOver} from '../PopOver'; import {SchemaQuickEdit} from '../QuickEdit'; import {SchemaCopyable} from '../Copyable'; import {SchemaRemark} from '../Remark'; import {toDataURL, getImageDimensions} from '../../utils/image'; import {TableBody} from './TableBody'; import {TplSchema} from '../Tpl'; import {MappingSchema} from '../Mapping'; import {isAlive, getSnapshot} from 'mobx-state-tree'; /** * 表格列,不指定类型时默认为文本类型。 */ export type TableColumnObject = { /** * 列标题 */ label: string; /** * 配置是否固定当前列 */ fixed?: 'left' | 'right' | 'none'; /** * 绑定字段名 */ name?: string; /** * 配置查看详情功能 */ popOver?: SchemaPopOver; /** * 配置快速编辑功能 */ quickEdit?: SchemaQuickEdit; /** * 作为表单项时,可以单独配置编辑时的快速编辑面板。 */ quickEditOnUpdate?: SchemaQuickEdit; /** * 配置点击复制功能 */ copyable?: SchemaCopyable; /** * 配置是否可以排序 */ sortable?: boolean; /** * 是否可快速搜索 */ searchable?: boolean; /** * 配置是否默认展示 */ toggled?: boolean; /** * 列宽度 */ width?: number | string; /** * todo */ filterable?: | boolean | { source?: string; options?: Array; }; /** * 结合表格的 footable 一起使用。 * 填写 *、xs、sm、md、lg指定 footable 的触发条件,可以填写多个用空格隔开 */ breakpoint?: '*' | 'xs' | 'sm' | 'md' | 'lg'; /** * 提示信息 */ remark?: SchemaRemark; }; export type TableColumnWithType = SchemaObject & TableColumnObject; export type TableColumn = TableColumnWithType | TableColumnObject; /** * Table 表格渲染器。 * 文档:https://baidu.gitee.io/amis/docs/components/table */ export interface TableSchema extends BaseSchema { /** * 指定为表格渲染器。 */ type: 'table' | 'static-table'; /** * 是否固定表头 */ affixHeader?: boolean; /** * 表格的列信息 */ columns?: Array; /** * 展示列显示开关,自动即:列数量大于或等于5个时自动开启 */ columnsTogglable?: boolean | 'auto'; /** * 是否开启底部展示功能,适合移动端展示 */ footable?: | boolean | { expand?: 'first' | 'all' | 'none'; /** * 是否为手风琴模式 */ accordion?: boolean; }; /** * 底部外层 CSS 类名 */ footerClassName?: SchemaClassName; /** * 顶部外层 CSS 类名 */ headerClassName?: SchemaClassName; /** * 占位符 */ placeholder?: string; /** * 是否显示底部 */ showFooter?: boolean; /** * 是否显示头部 */ showHeader?: boolean; /** * 数据源:绑定当前环境变量 */ source?: SchemaTokenizeableString; /** * 表格 CSS 类名 */ tableClassName?: SchemaClassName; /** * 标题 */ title?: string; /** * 工具栏 CSS 类名 */ toolbarClassName?: SchemaClassName; /** * 合并单元格配置,配置数字表示从左到右的多少列自动合并单元格。 */ combineNum?: number; /** * 顶部总结行 */ prefixRow?: Array; /** * 底部总结行 */ affixRow?: Array; } export interface TableProps extends RendererProps { title?: string; // 标题 header?: SchemaNode; footer?: SchemaNode; actions?: Action[]; className?: string; headerClassName?: string; footerClassName?: string; store: ITableStore; columns?: Array; headingClassName?: string; toolbarClassName?: string; headerToolbarClassName?: string; footerToolbarClassName?: string; tableClassName?: string; source?: string; selectable?: boolean; selected?: Array; maxKeepItemSelectionLength?: number; valueField?: string; draggable?: boolean; columnsTogglable?: boolean | 'auto'; affixHeader?: boolean; affixColumns?: boolean; combineNum?: number; footable?: | boolean | { expand?: 'first' | 'all' | 'none'; expandAll?: boolean; accordion?: boolean; }; expandConfig?: { expand?: 'first' | 'all' | 'none'; expandAll?: boolean; accordion?: boolean; }; itemCheckableOn?: string; itemDraggableOn?: string; itemActions?: Array; onSelect: ( selectedItems: Array, unSelectedItems: Array ) => void; onSave?: ( items: Array | object, diff: Array | object, rowIndexes: Array | string, unModifiedItems?: Array, rowOrigins?: Array | object, resetOnFailed?: boolean ) => void; onSaveOrder?: (moved: Array, items: Array) => void; onQuery: (values: object) => void; onImageEnlarge?: (data: any, target: any) => void; buildItemProps?: (item: any, index: number) => any; checkOnItemClick?: boolean; hideCheckToggler?: boolean; rowClassName?: string; rowClassNameExpr?: string; popOverContainer?: any; canAccessSuperData?: boolean; } /** * 将 url 转成绝对地址 */ const getAbsoluteUrl = (function () { let link: HTMLAnchorElement; return function (url: string) { if (!link) link = document.createElement('a'); link.href = url; return link.href; }; })(); export default class Table extends React.Component { static propsList: Array = [ 'header', 'headerToolbarRender', 'footer', 'footerToolbarRender', 'footable', 'expandConfig', 'placeholder', 'tableClassName', 'headingClassName', 'source', 'selectable', 'columnsTogglable', 'affixHeader', 'affixColumns', 'headerClassName', 'footerClassName', 'selected', 'multiple', 'primaryField', 'hideQuickSaveBtn', 'itemCheckableOn', 'itemDraggableOn', 'checkOnItemClick', 'hideCheckToggler', 'itemActions', 'combineNum', 'items', 'columns', 'valueField', 'saveImmediately', 'rowClassName', 'rowClassNameExpr', 'popOverContainer', 'headerToolbarClassName', 'toolbarClassName', 'footerToolbarClassName' ]; static defaultProps: Partial = { className: '', placeholder: 'placeholder.noData', tableClassName: '', source: '$items', selectable: false, columnsTogglable: 'auto', affixHeader: true, headerClassName: '', footerClassName: '', toolbarClassName: '', headerToolbarClassName: '', footerToolbarClassName: '', primaryField: 'id', itemCheckableOn: '', itemDraggableOn: '', hideCheckToggler: false, canAccessSuperData: false }; table?: HTMLTableElement; sortable?: Sortable; dragTip?: HTMLElement; affixedTable?: HTMLTableElement; parentNode?: HTMLElement | Window; lastScrollLeft: number = -1; totalWidth: number = 0; totalHeight: number = 0; outterWidth: number = 0; outterHeight: number = 0; unSensor?: Function; updateTableInfoLazy: () => void; widths: { [propName: string]: number; } = {}; heights: { [propName: string]: number; } = {}; renderedToolbars: Array = []; subForms: any = {}; constructor(props: TableProps) { super(props); this.handleOutterScroll = this.handleOutterScroll.bind(this); this.affixDetect = this.affixDetect.bind(this); this.updateTableInfoLazy = debounce(this.updateTableInfo.bind(this), 250, { trailing: true, leading: true }); this.tableRef = this.tableRef.bind(this); this.affixedTableRef = this.affixedTableRef.bind(this); this.handleAction = this.handleAction.bind(this); this.handleCheck = this.handleCheck.bind(this); this.handleCheckAll = this.handleCheckAll.bind(this); this.handleQuickChange = this.handleQuickChange.bind(this); this.handleSave = this.handleSave.bind(this); this.handleSaveOrder = this.handleSaveOrder.bind(this); this.reset = this.reset.bind(this); this.dragTipRef = this.dragTipRef.bind(this); this.getPopOverContainer = this.getPopOverContainer.bind(this); this.renderCell = this.renderCell.bind(this); this.renderHeadCell = this.renderHeadCell.bind(this); this.renderToolbar = this.renderToolbar.bind(this); this.handleMouseMove = this.handleMouseMove.bind(this); this.handleMouseLeave = this.handleMouseLeave.bind(this); this.subFormRef = this.subFormRef.bind(this); } static syncRows( store: ITableStore, props: TableProps, prevProps?: TableProps ) { const source = props.source; const value = props.value || props.items; let rows: Array = []; let updateRows = true; if (Array.isArray(value)) { rows = value; } else if (typeof source === 'string') { const resolved = resolveVariable(source, props.data); const prev = prevProps ? resolveVariable(source, prevProps.data) : null; if (prev && prev === resolved) { updateRows = false; } else if (Array.isArray(resolved)) { rows = resolved; } } updateRows && store.initRows(rows, props.getEntryId); typeof props.selected !== 'undefined' && store.updateSelected(props.selected, props.valueField); } componentWillMount() { const { store, columns, selectable, columnsTogglable, draggable, orderBy, orderDir, multiple, footable, primaryField, itemCheckableOn, itemDraggableOn, hideCheckToggler, combineNum, expandConfig, formItem, keepItemSelectionOnPageChange, maxKeepItemSelectionLength } = this.props; store.update({ selectable, draggable, columns, columnsTogglable, orderBy, orderDir, multiple, footable, expandConfig, primaryField, itemCheckableOn, itemDraggableOn, hideCheckToggler, combineNum, keepItemSelectionOnPageChange, maxKeepItemSelectionLength }); formItem && isAlive(formItem) && formItem.setSubStore(store); Table.syncRows(store, this.props); this.syncSelected(); } componentDidMount() { let parent: HTMLElement | Window | null = getScrollParent( findDOMNode(this) as HTMLElement ); if (!parent || parent === document.body) { parent = window; } this.parentNode = parent; this.updateTableInfo(); const dom = findDOMNode(this) as HTMLElement; if (dom.closest('.modal-body')) { return; } this.affixDetect(); parent.addEventListener('scroll', this.affixDetect); window.addEventListener('resize', this.affixDetect); } componentWillReceiveProps(nextProps: TableProps) { const props = this.props; const store = nextProps.store; if ( anyChanged( [ 'selectable', 'columnsTogglable', 'draggable', 'orderBy', 'orderDir', 'multiple', 'footable', 'primaryField', 'itemCheckableOn', 'itemDraggableOn', 'hideCheckToggler', 'combineNum', 'expandConfig' ], props, nextProps ) ) { store.update({ selectable: nextProps.selectable, columnsTogglable: nextProps.columnsTogglable, draggable: nextProps.draggable, orderBy: nextProps.orderBy, orderDir: nextProps.orderDir, multiple: nextProps.multiple, primaryField: nextProps.primaryField, footable: nextProps.footable, itemCheckableOn: nextProps.itemCheckableOn, itemDraggableOn: nextProps.itemDraggableOn, hideCheckToggler: nextProps.hideCheckToggler, combineNum: nextProps.combineNum, expandConfig: nextProps.expandConfig }); } if (props.columns !== nextProps.columns) { store.update({ columns: nextProps.columns }); } if ( anyChanged(['source', 'value', 'items'], props, nextProps) || (!nextProps.value && !nextProps.items && nextProps.data !== props.data) ) { Table.syncRows(store, nextProps, props); this.syncSelected(); } else if (isArrayChildrenModified(props.selected!, nextProps.selected!)) { store.updateSelected(nextProps.selected || [], nextProps.valueField); this.syncSelected(); } } componentDidUpdate() { this.updateTableInfoLazy(); } componentWillUnmount() { const {formItem} = this.props; const parent = this.parentNode; parent && parent.removeEventListener('scroll', this.affixDetect); window.removeEventListener('resize', this.affixDetect); (this.updateTableInfoLazy as any).cancel(); this.unSensor && this.unSensor(); formItem && isAlive(formItem) && formItem.setSubStore(null); } subFormRef(form: any, x: number, y: number) { const {quickEditFormRef} = this.props; quickEditFormRef && quickEditFormRef(form, x, y); this.subForms[`${x}-${y}`] = form; form && this.props.store.addForm(form.props.store, y); } handleAction(e: React.UIEvent, action: Action, ctx: object) { const {onAction} = this.props; // todo onAction(e, action, ctx); } handleCheck(item: IRow) { item.toggle(); this.syncSelected(); } handleCheckAll() { const {store} = this.props; store.toggleAll(); this.syncSelected(); } handleQuickChange( item: IRow, values: object, saveImmediately?: boolean | any, savePristine?: boolean, resetOnFailed?: boolean ) { if (!isAlive(item)) { return; } const { onSave, saveImmediately: propsSaveImmediately, primaryField } = this.props; item.change(values, savePristine); // 值发生变化了,需要通过 onSelect 通知到外面,否则会出现数据不同步的问题 item.modified && this.syncSelected(); if ((!saveImmediately && !propsSaveImmediately) || savePristine) { return; } if (saveImmediately && saveImmediately.api) { this.props.onAction( null, { actionType: 'ajax', api: saveImmediately.api }, values ); return; } if (!onSave) { return; } onSave( item.data, difference(item.data, item.pristine, ['id', primaryField]), item.path, undefined, item.pristine, resetOnFailed ); } async handleSave() { const {store, onSave, primaryField} = this.props; if (!onSave || !store.modifiedRows.length) { return; } // 验证所有表单项,没有错误才继续 const subForms: Array = []; Object.keys(this.subForms).forEach( key => this.subForms[key] && subForms.push(this.subForms[key]) ); if (subForms.length) { const result = await Promise.all(subForms.map(item => item.validate())); if (~result.indexOf(false)) { return; } } const rows = store.modifiedRows.map(item => item.data); const rowIndexes = store.modifiedRows.map(item => item.path); const diff = store.modifiedRows.map(item => difference(item.data, item.pristine, ['id', primaryField]) ); const unModifiedRows = store.rows .filter(item => !item.modified) .map(item => item.data); onSave( rows, diff, rowIndexes, unModifiedRows, store.modifiedRows.map(item => item.pristine) ); } handleSaveOrder() { const {store, onSaveOrder} = this.props; if (!onSaveOrder || !store.movedRows.length) { return; } onSaveOrder( store.movedRows.map(item => item.data), store.rows.map(item => item.getDataWithModifiedChilden()) ); } syncSelected() { const {store, onSelect} = this.props; onSelect && onSelect( store.selectedRows.map(item => item.data), store.unSelectedRows.map(item => item.data) ); } reset() { const {store} = this.props; store.reset(); const subForms: Array = []; Object.keys(this.subForms).forEach( key => this.subForms[key] && subForms.push(this.subForms[key]) ); subForms.forEach(item => item.clearErrors()); } bulkUpdate(value: any, items: Array) { const {store, primaryField} = this.props; if (primaryField && value.ids) { const ids = value.ids.split(','); const rows = store.rows.filter(item => find(ids, (id: any) => id && id == item.data[primaryField]) ); const newValue = {...value, ids: undefined}; rows.forEach(row => row.change(newValue)); } else { const rows = store.rows.filter(item => ~items.indexOf(item.pristine)); rows.forEach(row => row.change(value)); } } getSelected() { const {store} = this.props; return store.selectedRows.map(item => item.data); } affixDetect() { if (!this.props.affixHeader || !this.table) { return; } const ns = this.props.classPrefix; const dom = findDOMNode(this) as HTMLElement; const clip = (this.table as HTMLElement).getBoundingClientRect(); const offsetY = this.props.affixOffsetTop ?? this.props.env.affixOffsetTop ?? 0; const headingHeight = dom.querySelector(`.${ns}Table-heading`)?.getBoundingClientRect() .height || 0; const headerHeight = dom.querySelector(`.${ns}Table-headToolbar`)?.getBoundingClientRect() .height || 0; const affixed = clip.top - headerHeight - headingHeight < offsetY && clip.top + clip.height - 40 > offsetY; const affixedDom = dom.querySelector(`.${ns}Table-fixedTop`) as HTMLElement; affixedDom.style.cssText += `top: ${offsetY}px;width: ${ (this.table.parentNode as HTMLElement).offsetWidth }px`; affixed ? affixedDom.classList.add('in') : affixedDom.classList.remove('in'); // store.markHeaderAffix(clip.top < offsetY && (clip.top + clip.height - 40) > offsetY); } updateTableInfo() { if (!this.table) { return; } const table = this.table; const outter = table.parentNode as HTMLElement; const affixHeader = this.props.affixHeader; const ns = this.props.classPrefix; // 完成宽高都没有变化就直接跳过了。 // if (this.totalWidth === table.scrollWidth && this.totalHeight === table.scrollHeight) { // return; // } this.totalWidth = table.scrollWidth; this.totalHeight = table.scrollHeight; this.outterWidth = outter.offsetWidth; this.outterHeight = outter.offsetHeight; let widths: { [propName: string]: number; } = (this.widths = {}); let heights: { [propName: string]: number; } = (this.heights = {}); heights.header || (heights.header = table.querySelector('thead')!.offsetHeight); forEach( table.querySelectorAll('thead>tr:last-child>th'), (item: HTMLElement) => { widths[item.getAttribute('data-index') as string] = item.offsetWidth; } ); forEach( table.querySelectorAll('tbody>tr>*:last-child'), (item: HTMLElement, index: number) => (heights[index] = item.offsetHeight) ); // 让 react 去更新非常慢,还是手动更新吧。 const dom = findDOMNode(this) as HTMLElement; forEach( // 折叠 footTable 不需要改变 dom.querySelectorAll( `.${ns}Table-fixedTop table, .${ns}Table-fixedLeft>table, .${ns}Table-fixedRight>table` ), (table: HTMLTableElement) => { let totalWidth = 0; forEach( table.querySelectorAll('thead>tr:last-child>th'), (item: HTMLElement) => { const width = widths[item.getAttribute('data-index') as string]; item.style.cssText += `width: ${width}px; height: ${heights.header}px`; totalWidth += width; } ); forEach(table.querySelectorAll('colgroup>col'), (item: HTMLElement) => { const width = widths[item.getAttribute('data-index') as string]; item.setAttribute('width', `${width}`); }); forEach( table.querySelectorAll('tbody>tr'), (item: HTMLElement, index) => { item.style.cssText += `height: ${heights[index]}px`; } ); table.style.cssText += `width: ${totalWidth}px;table-layout: fixed;`; } ); if (affixHeader) { (dom.querySelector( `.${ns}Table-fixedTop>.${ns}Table-wrapper` ) as HTMLElement).style.cssText += `width: ${this.outterWidth}px`; } this.lastScrollLeft = -1; this.handleOutterScroll(); } handleOutterScroll() { const outter = (this.table as HTMLElement).parentNode as HTMLElement; const scrollLeft = outter.scrollLeft; if (scrollLeft === this.lastScrollLeft) { return; } this.lastScrollLeft = scrollLeft; let leading = scrollLeft === 0; let trailing = Math.ceil(scrollLeft) + this.outterWidth >= this.totalWidth; // console.log(scrollLeft, store.outterWidth, store.totalWidth, (scrollLeft + store.outterWidth) === store.totalWidth); // store.setLeading(leading); // store.setTrailing(trailing); const ns = this.props.classPrefix; const dom = findDOMNode(this) as HTMLElement; const fixedLeft = dom.querySelectorAll(`.${ns}Table-fixedLeft`); if (fixedLeft && fixedLeft.length) { for (let i = 0, len = fixedLeft.length; i < len; i++) { let node = fixedLeft[i]; leading ? node.classList.remove('in') : node.classList.add('in'); } } const fixedRight = dom.querySelectorAll(`.${ns}Table-fixedRight`); if (fixedRight && fixedRight.length) { for (let i = 0, len = fixedRight.length; i < len; i++) { let node = fixedRight[i]; trailing ? node.classList.remove('in') : node.classList.add('in'); } } const table = this.affixedTable; if (table) { table.style.cssText += `transform: translateX(-${scrollLeft}px)`; } } tableRef(ref: HTMLTableElement) { this.table = ref; if (ref) { this.unSensor = resizeSensor( ref.parentNode as HTMLElement, this.updateTableInfoLazy ); } else { this.unSensor && this.unSensor(); delete this.unSensor; } } dragTipRef(ref: any) { if (!this.dragTip && ref) { this.initDragging(); } else if (this.dragTip && !ref) { this.destroyDragging(); } this.dragTip = ref; } affixedTableRef(ref: HTMLTableElement) { this.affixedTable = ref; } initDragging() { const store = this.props.store; const ns = this.props.classPrefix; this.sortable = new Sortable( (this.table as HTMLElement).querySelector('tbody') as HTMLElement, { group: 'table', animation: 150, handle: `.${ns}Table-dragCell`, ghostClass: 'is-dragging', onEnd: (e: any) => { // 没有移动 if (e.newIndex === e.oldIndex) { return; } const parent = e.to as HTMLElement; if (e.oldIndex < parent.childNodes.length - 1) { parent.insertBefore(e.item, parent.childNodes[e.oldIndex]); } else { parent.appendChild(e.item); } store.exchange(e.oldIndex, e.newIndex); } } ); } destroyDragging() { this.sortable && this.sortable.destroy(); } getPopOverContainer() { return findDOMNode(this); } handleMouseMove(e: React.MouseEvent) { const tr: HTMLElement = (e.target as HTMLElement).closest( 'tr[data-index]' ) as HTMLElement; if (!tr) { return; } const {store, affixColumns, itemActions} = this.props; if ( (affixColumns === false || (store.leftFixedColumns.length === 0 && store.rightFixedColumns.length === 0)) && (!itemActions || !itemActions.filter(item => !item.hiddenOnHover).length) ) { return; } const index = parseInt(tr.getAttribute('data-index') as string, 10); if (store.hoverIndex === index) { return; } store.rows.forEach((item, key) => item.setIsHover(index === key)); } handleMouseLeave() { const store = this.props.store; if (~store.hoverIndex) { store.rows[store.hoverIndex].setIsHover(false); } } draggingTr: HTMLTableRowElement; originIndex: number; draggingSibling: Array; @autobind handleDragStart(e: React.DragEvent) { const store = this.props.store; const target = e.currentTarget; const tr = (this.draggingTr = target.closest('tr')!); const id = tr.getAttribute('data-id')!; const tbody = tr.parentNode!; this.originIndex = Array.prototype.indexOf.call(tbody.childNodes, tr); tr.classList.add('is-dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', id); e.dataTransfer.setDragImage(tr, 0, 0); const item = store.getRowById(id)!; store.collapseAllAtDepth(item.depth); let siblings: Array = store.rows; if (item.parentId) { const parent = store.getRowById(item.parentId)!; siblings = parent.children as any; } siblings = siblings.filter(sibling => sibling !== item); tbody.addEventListener('dragover', this.handleDragOver); tbody.addEventListener('drop', this.handleDrop); this.draggingSibling = siblings.map(item => { let tr: HTMLTableRowElement = tbody.querySelector( `tr[data-id="${item.id}"]` ) as HTMLTableRowElement; tr.classList.add('is-drop-allowed'); return tr; }); tr.addEventListener('dragend', this.handleDragEnd); } @autobind handleDragOver(e: any) { if (!e.target) { return; } e.preventDefault(); e.dataTransfer!.dropEffect = 'move'; const overTr: HTMLElement = (e.target as HTMLElement).closest('tr')!; if ( !overTr || !~overTr.className.indexOf('is-drop-allowed') || overTr === this.draggingTr ) { return; } const tbody = overTr.parentElement!; const dRect = this.draggingTr.getBoundingClientRect(); const tRect = overTr.getBoundingClientRect(); let ratio = dRect.top < tRect.top ? 0.1 : 0.9; const next = (e.clientY - tRect.top) / (tRect.bottom - tRect.top) > ratio; tbody.insertBefore(this.draggingTr, (next && overTr.nextSibling) || overTr); } @autobind handleDrop() { const store = this.props.store; const tr = this.draggingTr; const tbody = tr.parentElement!; const index = Array.prototype.indexOf.call(tbody.childNodes, tr); const item: IRow = store.getRowById(tr.getAttribute('data-id')!) as any; // destroy this.handleDragEnd(); store.exchange(this.originIndex, index, item); } @autobind handleDragEnd() { const tr = this.draggingTr; const tbody = tr.parentElement!; const index = Array.prototype.indexOf.call(tbody.childNodes, tr); tbody.insertBefore( tr, tbody.childNodes[ index < this.originIndex ? this.originIndex + 1 : this.originIndex ] ); tr.classList.remove('is-dragging'); tr.removeEventListener('dragend', this.handleDragEnd); tbody.removeEventListener('dragover', this.handleDragOver); tbody.removeEventListener('drop', this.handleDrop); this.draggingSibling.forEach(item => item.classList.remove('is-drop-allowed') ); } @autobind handleImageEnlarge(info: any, target: {rowIndex: number; colIndex: number}) { const onImageEnlarge = this.props.onImageEnlarge; // 如果已经是多张了,直接跳过 if (Array.isArray(info.list)) { return onImageEnlarge && onImageEnlarge(info, target); } // 从列表中收集所有图片,然后作为一个图片集合派送出去。 const store = this.props.store; const column = store.columns[target.colIndex].pristine; let index = target.rowIndex; const list: Array = []; store.rows.forEach((row, i) => { const src = resolveVariable(column.name, row.data); if (!src) { if (i < target.rowIndex) { index--; } return; } list.push({ src, originalSrc: column.originalSrc ? filter(column.originalSrc, row.data) : src, title: column.enlargeTitle ? filter(column.enlargeTitle, row.data) : column.title ? filter(column.title, row.data) : undefined, caption: column.enlargeCaption ? filter(column.enlargeCaption, row.data) : column.caption ? filter(column.caption, row.data) : undefined }); }); if (list.length > 1) { onImageEnlarge && onImageEnlarge( { ...info, list, index }, target ); } else { onImageEnlarge && onImageEnlarge(info, target); } } renderHeading() { let { title, store, hideQuickSaveBtn, data, classnames: cx, saveImmediately, headingClassName, translate: __ } = this.props; if ( title || (!saveImmediately && store.modified && !hideQuickSaveBtn) || store.moved ) { return (
{!saveImmediately && store.modified && !hideQuickSaveBtn ? ( {__('Table.modified', { modified: store.modified })} ) : store.moved ? ( {__('Table.moved', { moved: store.moved })} ) : title ? ( filter(title, data) ) : ( '' )}
); } return null; } renderHeadCell(column: IColumn, props?: any) { const { store, query, onQuery, multiple, env, render, classPrefix: ns, classnames: cx } = this.props; if (column.type === '__checkme') { return ( {store.rows.length && multiple ? ( ) : ( '\u00A0' )} ); } else if (column.type === '__dragme') { return ; } else if (column.type === '__expandme') { return ( {(store.footable && (store.footable.expandAll === false || store.footable.accordion)) || (store.expandConfig && (store.expandConfig.expandAll === false || store.expandConfig.accordion)) ? null : ( )} ); } let affix = null; if (column.searchable && column.name) { affix = ( ); } else if (column.sortable && column.name) { affix = ( { if (column.name === store.orderBy) { if (store.orderDir === 'desc') { // 降序之后取消排序 store.setOrderByInfo('', 'asc'); } else { // 升序之后降序 store.setOrderByInfo(column.name, 'desc'); } } else { store.setOrderByInfo(column.name as string, 'asc'); } onQuery && onQuery({ orderBy: store.orderBy, orderDir: store.orderDir }); }} > ); } else if (column.filterable && column.name) { affix = ( ); } if (column.pristine.width) { props.style = props.style || {}; props.style.width = column.pristine.width; } return (
{column.label ? render('tpl', column.label) : null} {column.remark ? render('remark', { type: 'remark', tooltip: column.remark, container: env && env.getModalContainer ? env.getModalContainer : undefined }) : null}
{affix} ); } renderCell( region: string, column: IColumn, item: IRow, props: any, ignoreDrag = false ) { const { render, store, multiple, classPrefix: ns, classnames: cx, checkOnItemClick, popOverContainer, canAccessSuperData } = this.props; if (column.name && item.rowSpans[column.name] === 0) { return null; } if (column.type === '__checkme') { return ( ); } else if (column.type === '__dragme') { return ( {item.draggable ? : null} ); } else if (column.type === '__expandme') { return ( {item.depth > 2 ? Array.from({length: item.depth - 2}).map((_, index) => ( )) : null} {item.expandable ? ( ) : null} ); } let prefix: React.ReactNode = null; if ( !ignoreDrag && column.isPrimary && store.isNested && store.draggable && item.draggable ) { prefix = ( ); } const subProps: any = { ...props, btnDisabled: store.dragging, data: item.locals, value: column.name ? resolveVariable( column.name, canAccessSuperData ? item.locals : item.data ) : column.value, popOverContainer: popOverContainer || this.getPopOverContainer, rowSpan: item.rowSpans[column.name as string], quickEditFormRef: this.subFormRef, prefix, onImageEnlarge: this.handleImageEnlarge, canAccessSuperData }; delete subProps.label; return render( region, { ...column.pristine, column: column.pristine, type: 'cell' }, subProps ); } renderAffixHeader(tableClassName: string) { const {store, affixHeader, render, classnames: cx} = this.props; const hideHeader = store.filteredColumns.every(column => !column.label); return affixHeader ? (
{this.renderHeading()} {this.renderHeader(false)}
{store.leftFixedColumns.length ? this.renderFixedColumns( store.rows, store.leftFixedColumns, true, tableClassName ) : null}
{store.rightFixedColumns.length ? this.renderFixedColumns( store.rows, store.rightFixedColumns, true, tableClassName ) : null}
{store.filteredColumns.map(column => ( ))} {store.columnGroup.length ? ( {store.columnGroup.map((item, index) => ( ))} ) : null} {store.filteredColumns.map(column => this.renderHeadCell(column, { 'key': column.index, 'data-index': column.index }) )}
{item.label ? render('tpl', item.label) : null}
) : null; } renderFixedColumns( rows: Array, columns: Array, headerOnly: boolean = false, tableClassName: string = '' ) { const { placeholder, store, classnames: cx, render, data, translate, locale, checkOnItemClick, buildItemProps, rowClassNameExpr, rowClassName } = this.props; const hideHeader = store.filteredColumns.every(column => !column.label); return ( 0 ? 'Table-table--withCombine' : '', tableClassName )} > {store.columnGroup.length ? ( {store.columnGroup.map((item, index) => { const renderColumns = columns.filter(a => ~item.has.indexOf(a)); return renderColumns.length ? ( ) : null; })} ) : null} {columns.map(column => this.renderHeadCell(column, { 'key': column.index, 'data-index': column.index }) )} {headerOnly ? null : !rows.length ? ( ) : ( 0 ? 'Table-table--withCombine' : '', tableClassName )} classnames={cx} render={render} renderCell={this.renderCell} onCheck={this.handleCheck} onQuickChange={store.dragging ? undefined : this.handleQuickChange} footable={store.footable} ignoreFootableContent footableColumns={store.footableColumns} checkOnItemClick={checkOnItemClick} buildItemProps={buildItemProps} onAction={this.handleAction} rowClassNameExpr={rowClassNameExpr} rowClassName={rowClassName} columns={columns} rows={rows} locale={locale} translate={translate} rowsProps={{ regionPrefix: 'fixed/', renderCell: ( region: string, column: IColumn, item: IRow, props: any ) => this.renderCell(region, column, item, props, true) }} /> )}
{'\u00A0'}
{render( 'placeholder', translate(placeholder || 'placeholder.noData') )}
); } renderToolbar(toolbar: SchemaNode) { const type = (toolbar as Schema).type || (toolbar as string); if (type === 'columns-toggler') { this.renderedToolbars.push(type); return this.renderColumnsToggler(toolbar as any); } else if (type === 'drag-toggler') { this.renderedToolbars.push(type); return this.renderDragToggler(); } else if (type === 'export-excel') { this.renderedToolbars.push(type); return this.renderExportExcel(toolbar); } return void 0; } renderColumnsToggler(config?: any) { const { className, store, classPrefix: ns, classnames: cx, ...rest } = this.props; const __ = rest.translate; const env = rest.env; const render = this.props.render; if (!store.columnsTogglable) { return null; } return ( } > {store.toggableColumns.map(column => (
  • {column.label ? render('tpl', column.label) : null}
  • ))}
    ); } renderDragToggler() { const {store, env, draggable, classPrefix: ns, translate: __} = this.props; if (!draggable || store.isNested) { return null; } return ( ); } renderExportExcel(toolbar: SchemaNode) { const { store, env, classPrefix: ns, classnames: cx, translate: __, columns, data } = this.props; if (!columns) { return null; } return ( ); } renderActions(region: string) { let {actions, render, store, classnames: cx, data} = this.props; actions = Array.isArray(actions) ? actions.concat() : []; if ( store.toggable && region === 'header' && !~this.renderedToolbars.indexOf('columns-toggler') ) { actions.push({ type: 'button', children: this.renderColumnsToggler() }); } if ( store.draggable && !store.isNested && region === 'header' && store.rows.length > 1 && !~this.renderedToolbars.indexOf('drag-toggler') ) { actions.push({ type: 'button', children: this.renderDragToggler() }); } return Array.isArray(actions) && actions.length ? (
    {actions.map((action, key) => render( `action/${key}`, { type: 'button', ...(action as any) }, { onAction: this.handleAction, key, btnDisabled: store.dragging, data: store.getData(data) } ) )}
    ) : null; } renderHeader(editable?: boolean) { const { header, headerClassName, toolbarClassName, headerToolbarClassName, headerToolbarRender, render, showHeader, store, classnames: cx, data, translate: __ } = this.props; if (showHeader === false) { return null; } const otherProps: any = {}; // editable === false && (otherProps.$$editable = false); const child = headerToolbarRender ? headerToolbarRender( { ...this.props, selectedItems: store.selectedRows.map(item => item.data), items: store.rows.map(item => item.data), unSelectedItems: store.unSelectedRows.map(item => item.data), ...otherProps }, this.renderToolbar ) : null; const actions = this.renderActions('header'); const toolbarNode = actions || child || store.dragging ? (
    {actions} {child} {store.dragging ? (
    {__('Table.dragTip')}
    ) : null}
    ) : null; const headerNode = header && (!Array.isArray(header) || header.length) ? (
    {render('header', header, { ...(editable === false ? otherProps : null), data: store.getData(data) })}
    ) : null; return headerNode && toolbarNode ? [headerNode, toolbarNode] : headerNode || toolbarNode || null; } renderFooter() { const { footer, toolbarClassName, footerToolbarClassName, footerClassName, footerToolbarRender, render, showFooter, store, data, classnames: cx } = this.props; if (showFooter === false) { return null; } const child = footerToolbarRender ? footerToolbarRender( { ...this.props, selectedItems: store.selectedRows.map(item => item.data), items: store.rows.map(item => item.data) }, this.renderToolbar ) : null; const actions = this.renderActions('footer'); const toolbarNode = actions || child ? (
    {actions} {child}
    ) : null; const footerNode = footer && (!Array.isArray(footer) || footer.length) ? (
    {render('footer', footer, { data: store.getData(data) })}
    ) : null; return footerNode && toolbarNode ? [toolbarNode, footerNode] : footerNode || toolbarNode || null; } renderItemActions() { const {itemActions, render, store, classnames: cx} = this.props; const finalActions = Array.isArray(itemActions) ? itemActions.filter(action => !action.hiddenOnHover) : []; if (!finalActions.length) { return null; } const rowIndex = store.hoverIndex; const heights = this.heights; let height = 40; let top = 0; if (heights && heights[rowIndex]) { height = heights[rowIndex]; top += heights.header; for (let i = rowIndex - 1; i >= 0; i--) { top += heights[i]; } } return (
    {finalActions.map((action, index) => render( `itemAction/${index}`, { ...(action as any), isMenuItem: true }, { key: index, item: store.rows[rowIndex], data: store.rows[rowIndex].locals, rowIndex } ) )}
    ); } renderTableContent() { const { classnames: cx, tableClassName, store, placeholder, render, checkOnItemClick, buildItemProps, rowClassNameExpr, rowClassName, prefixRow, locale, affixRow, translate } = this.props; return ( 0 ? 'Table-table--withCombine' : '', tableClassName )} classnames={cx} columns={store.filteredColumns} columnsGroup={store.columnGroup} rows={store.rows} placeholder={placeholder} render={render} onMouseMove={this.handleMouseMove} onScroll={this.handleOutterScroll} tableRef={this.tableRef} renderHeadCell={this.renderHeadCell} renderCell={this.renderCell} onCheck={this.handleCheck} onQuickChange={store.dragging ? undefined : this.handleQuickChange} footable={store.footable} footableColumns={store.footableColumns} checkOnItemClick={checkOnItemClick} buildItemProps={buildItemProps} onAction={this.handleAction} rowClassNameExpr={rowClassNameExpr} rowClassName={rowClassName} data={store.data} prefixRow={prefixRow} affixRow={affixRow} locale={locale} translate={translate} /> ); } render() { const {className, store, classnames: cx, affixColumns} = this.props; this.renderedToolbars = []; // 用来记录哪些 toolbar 已经渲染了,已经渲染了就不重复渲染了。 const heading = this.renderHeading(); const header = this.renderHeader(); const footer = this.renderFooter(); const tableClassName = cx( 'Table-table', store.combineNum > 0 ? 'Table-table--withCombine' : '', this.props.tableClassName ); return (
    {heading} {header}
    {affixColumns !== false && store.leftFixedColumns.length ? this.renderFixedColumns( store.rows, store.leftFixedColumns, false, tableClassName ) : null}
    {affixColumns !== false && store.rightFixedColumns.length ? this.renderFixedColumns( store.rows, store.rightFixedColumns, false, tableClassName ) : null}
    {this.renderTableContent()} {~store.hoverIndex ? this.renderItemActions() : null}
    {this.renderAffixHeader(tableClassName)} {footer}
    ); } } @Renderer({ test: (path: string) => /(^|\/)table$/.test(path) /* && !/(^|\/)table$/.test(path)*/, storeType: TableStore.name, name: 'table' }) export class TableRenderer extends Table {} export {TableCell};