29a284766d34cdeb9bd61c24932017e9e0c6ab4828dc06a719ba0462822f60bbc2b48e592f11e2adf88cd998b640b31aa45306ab2384f3ab1a4450ed769138 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. import { arrayFindIndex } from 'element-ui/src/utils/util';
  2. import { getCell, getColumnByCell, getRowIdentity, objectEquals } from './util';
  3. import { getStyle, hasClass, removeClass, addClass } from 'element-ui/src/utils/dom';
  4. import ElCheckbox from 'element-ui/packages/checkbox';
  5. import ElTooltip from 'element-ui/packages/tooltip';
  6. import debounce from 'throttle-debounce/debounce';
  7. import LayoutObserver from './layout-observer';
  8. import { mapStates } from './store/helper';
  9. import TableRow from './table-row.js';
  10. export default {
  11. name: 'ElTableBody',
  12. mixins: [LayoutObserver],
  13. components: {
  14. ElCheckbox,
  15. ElTooltip,
  16. TableRow
  17. },
  18. props: {
  19. store: {
  20. required: true
  21. },
  22. stripe: Boolean,
  23. context: {},
  24. rowClassName: [String, Function],
  25. rowStyle: [Object, Function],
  26. fixed: String,
  27. highlight: Boolean
  28. },
  29. render(h) {
  30. const data = this.data || [];
  31. return (
  32. <table
  33. class="el-table__body"
  34. cellspacing="0"
  35. cellpadding="0"
  36. border="0">
  37. <colgroup>
  38. {
  39. this.columns.map(column => <col name={column.id} key={column.id} />)
  40. }
  41. </colgroup>
  42. <tbody>
  43. {
  44. data.reduce((acc, row) => {
  45. return acc.concat(this.wrappedRowRender(row, acc.length));
  46. }, [])
  47. }
  48. <el-tooltip effect={this.table.tooltipEffect} placement="top" ref="tooltip" content={this.tooltipContent}></el-tooltip>
  49. </tbody>
  50. </table>
  51. );
  52. },
  53. computed: {
  54. table() {
  55. return this.$parent;
  56. },
  57. ...mapStates({
  58. data: 'data',
  59. columns: 'columns',
  60. treeIndent: 'indent',
  61. leftFixedLeafCount: 'fixedLeafColumnsLength',
  62. rightFixedLeafCount: 'rightFixedLeafColumnsLength',
  63. columnsCount: states => states.columns.length,
  64. leftFixedCount: states => states.fixedColumns.length,
  65. rightFixedCount: states => states.rightFixedColumns.length,
  66. hasExpandColumn: states => states.columns.some(({ type }) => type === 'expand')
  67. }),
  68. columnsHidden() {
  69. return this.columns.map((column, index) => this.isColumnHidden(index));
  70. },
  71. firstDefaultColumnIndex() {
  72. return arrayFindIndex(this.columns, ({ type }) => type === 'default');
  73. }
  74. },
  75. watch: {
  76. // don't trigger getter of currentRow in getCellClass. see https://jsfiddle.net/oe2b4hqt/
  77. // update DOM manually. see https://github.com/ElemeFE/element/pull/13954/files#diff-9b450c00d0a9dec0ffad5a3176972e40
  78. 'store.states.hoverRow'(newVal, oldVal) {
  79. if (!this.store.states.isComplex || this.$isServer) return;
  80. let raf = window.requestAnimationFrame;
  81. if (!raf) {
  82. raf = (fn) => setTimeout(fn, 16);
  83. }
  84. raf(() => {
  85. const rows = this.$el.querySelectorAll('.el-table__row');
  86. const oldRow = rows[oldVal];
  87. const newRow = rows[newVal];
  88. if (oldRow) {
  89. removeClass(oldRow, 'hover-row');
  90. }
  91. if (newRow) {
  92. addClass(newRow, 'hover-row');
  93. }
  94. });
  95. }
  96. },
  97. data() {
  98. return {
  99. tooltipContent: ''
  100. };
  101. },
  102. created() {
  103. this.activateTooltip = debounce(50, tooltip => tooltip.handleShowPopper());
  104. },
  105. methods: {
  106. getKeyOfRow(row, index) {
  107. const rowKey = this.table.rowKey;
  108. if (rowKey) {
  109. return getRowIdentity(row, rowKey);
  110. }
  111. return index;
  112. },
  113. isColumnHidden(index) {
  114. if (this.fixed === true || this.fixed === 'left') {
  115. return index >= this.leftFixedLeafCount;
  116. } else if (this.fixed === 'right') {
  117. return index < this.columnsCount - this.rightFixedLeafCount;
  118. } else {
  119. return (index < this.leftFixedLeafCount) || (index >= this.columnsCount - this.rightFixedLeafCount);
  120. }
  121. },
  122. getSpan(row, column, rowIndex, columnIndex) {
  123. let rowspan = 1;
  124. let colspan = 1;
  125. const fn = this.table.spanMethod;
  126. if (typeof fn === 'function') {
  127. const result = fn({
  128. row,
  129. column,
  130. rowIndex,
  131. columnIndex
  132. });
  133. if (Array.isArray(result)) {
  134. rowspan = result[0];
  135. colspan = result[1];
  136. } else if (typeof result === 'object') {
  137. rowspan = result.rowspan;
  138. colspan = result.colspan;
  139. }
  140. }
  141. return { rowspan, colspan };
  142. },
  143. getRowStyle(row, rowIndex) {
  144. const rowStyle = this.table.rowStyle;
  145. if (typeof rowStyle === 'function') {
  146. return rowStyle.call(null, {
  147. row,
  148. rowIndex
  149. });
  150. }
  151. return rowStyle || null;
  152. },
  153. getRowClass(row, rowIndex) {
  154. let selection = this.store.states.selection;
  155. const classes = ['el-table__row'];
  156. if (this.table.highlightCurrentRow && row === this.store.states.currentRow) {
  157. classes.push('current-row');
  158. }
  159. if (this.table.highlightSelectionRow) {
  160. for (let i = 0; i < selection.length; i++) {
  161. if (objectEquals(row, selection[i])) {
  162. classes.push('selection-row');
  163. }
  164. };
  165. }
  166. if (this.stripe && rowIndex % 2 === 1) {
  167. classes.push('el-table__row--striped');
  168. }
  169. const rowClassName = this.table.rowClassName;
  170. if (typeof rowClassName === 'string') {
  171. classes.push(rowClassName);
  172. } else if (typeof rowClassName === 'function') {
  173. classes.push(rowClassName.call(null, {
  174. row,
  175. rowIndex
  176. }));
  177. }
  178. if (this.store.states.expandRows.indexOf(row) > -1) {
  179. classes.push('expanded');
  180. }
  181. return classes;
  182. },
  183. getCellStyle(rowIndex, columnIndex, row, column) {
  184. const cellStyle = this.table.cellStyle;
  185. if (typeof cellStyle === 'function') {
  186. return cellStyle.call(null, {
  187. rowIndex,
  188. columnIndex,
  189. row,
  190. column
  191. });
  192. }
  193. return cellStyle;
  194. },
  195. getCellClass(rowIndex, columnIndex, row, column) {
  196. const classes = [column.id, column.align, column.className];
  197. if (this.isColumnHidden(columnIndex)) {
  198. classes.push('is-hidden');
  199. }
  200. const cellClassName = this.table.cellClassName;
  201. if (typeof cellClassName === 'string') {
  202. classes.push(cellClassName);
  203. } else if (typeof cellClassName === 'function') {
  204. classes.push(cellClassName.call(null, {
  205. rowIndex,
  206. columnIndex,
  207. row,
  208. column
  209. }));
  210. }
  211. classes.push('el-table__cell');
  212. return classes.join(' ');
  213. },
  214. getColspanRealWidth(columns, colspan, index) {
  215. if (colspan < 1) {
  216. return columns[index].realWidth;
  217. }
  218. const widthArr = columns.map(({ realWidth }) => realWidth).slice(index, index + colspan);
  219. return widthArr.reduce((acc, width) => acc + width, -1);
  220. },
  221. handleCellMouseEnter(event, row) {
  222. const table = this.table;
  223. const cell = getCell(event);
  224. if (cell) {
  225. const column = getColumnByCell(table, cell);
  226. const hoverState = table.hoverState = { cell, column, row };
  227. table.$emit('cell-mouse-enter', hoverState.row, hoverState.column, hoverState.cell, event);
  228. }
  229. // 判断是否text-overflow, 如果是就显示tooltip
  230. const cellChild = event.target.querySelector('.cell');
  231. if (!(hasClass(cellChild, 'el-tooltip') && cellChild.childNodes.length)) {
  232. return;
  233. }
  234. // use range width instead of scrollWidth to determine whether the text is overflowing
  235. // to address a potential FireFox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1074543#c3
  236. const range = document.createRange();
  237. range.setStart(cellChild, 0);
  238. range.setEnd(cellChild, cellChild.childNodes.length);
  239. const rangeWidth = range.getBoundingClientRect().width;
  240. const padding = (parseInt(getStyle(cellChild, 'paddingLeft'), 10) || 0) +
  241. (parseInt(getStyle(cellChild, 'paddingRight'), 10) || 0);
  242. if ((rangeWidth + padding > cellChild.offsetWidth || cellChild.scrollWidth > cellChild.offsetWidth) && this.$refs.tooltip) {
  243. const tooltip = this.$refs.tooltip;
  244. // TODO 会引起整个 Table 的重新渲染,需要优化
  245. this.tooltipContent = cell.innerText || cell.textContent;
  246. tooltip.referenceElm = cell;
  247. tooltip.$refs.popper && (tooltip.$refs.popper.style.display = 'none');
  248. tooltip.doDestroy();
  249. tooltip.setExpectedState(true);
  250. this.activateTooltip(tooltip);
  251. }
  252. },
  253. handleCellMouseLeave(event) {
  254. const tooltip = this.$refs.tooltip;
  255. if (tooltip) {
  256. tooltip.setExpectedState(false);
  257. tooltip.handleClosePopper();
  258. }
  259. const cell = getCell(event);
  260. if (!cell) return;
  261. const oldHoverState = this.table.hoverState || {};
  262. this.table.$emit('cell-mouse-leave', oldHoverState.row, oldHoverState.column, oldHoverState.cell, event);
  263. },
  264. handleMouseEnter: debounce(30, function(index) {
  265. this.store.commit('setHoverRow', index);
  266. }),
  267. handleMouseLeave: debounce(30, function() {
  268. this.store.commit('setHoverRow', null);
  269. }),
  270. handleContextMenu(event, row) {
  271. this.handleEvent(event, row, 'contextmenu');
  272. },
  273. handleDoubleClick(event, row) {
  274. this.handleEvent(event, row, 'dblclick');
  275. },
  276. handleClick(event, row) {
  277. this.store.commit('setCurrentRow', row);
  278. this.handleEvent(event, row, 'click');
  279. },
  280. handleEvent(event, row, name) {
  281. const table = this.table;
  282. const cell = getCell(event);
  283. let column;
  284. if (cell) {
  285. column = getColumnByCell(table, cell);
  286. if (column) {
  287. table.$emit(`cell-${name}`, row, column, cell, event);
  288. }
  289. }
  290. table.$emit(`row-${name}`, row, column, event);
  291. },
  292. rowRender(row, $index, treeRowData) {
  293. const { treeIndent, columns, firstDefaultColumnIndex } = this;
  294. const rowClasses = this.getRowClass(row, $index);
  295. let display = true;
  296. if (treeRowData) {
  297. rowClasses.push('el-table__row--level-' + treeRowData.level);
  298. display = treeRowData.display;
  299. }
  300. // 指令 v-show 会覆盖 row-style 中 display
  301. // 使用 :style 代替 v-show https://github.com/ElemeFE/element/issues/16995
  302. let displayStyle = display ? null : {
  303. display: 'none'
  304. };
  305. return (
  306. <TableRow
  307. style={[displayStyle, this.getRowStyle(row, $index)]}
  308. class={rowClasses}
  309. key={this.getKeyOfRow(row, $index)}
  310. nativeOn-dblclick={($event) => this.handleDoubleClick($event, row)}
  311. nativeOn-click={($event) => this.handleClick($event, row)}
  312. nativeOn-contextmenu={($event) => this.handleContextMenu($event, row)}
  313. nativeOn-mouseenter={_ => this.handleMouseEnter($index)}
  314. nativeOn-mouseleave={this.handleMouseLeave}
  315. columns={columns}
  316. row={row}
  317. index={$index}
  318. store={this.store}
  319. context={this.context || this.table.$vnode.context}
  320. firstDefaultColumnIndex={firstDefaultColumnIndex}
  321. treeRowData={treeRowData}
  322. treeIndent={treeIndent}
  323. columnsHidden={this.columnsHidden}
  324. getSpan={this.getSpan}
  325. getColspanRealWidth={this.getColspanRealWidth}
  326. getCellStyle={this.getCellStyle}
  327. getCellClass={this.getCellClass}
  328. handleCellMouseEnter={this.handleCellMouseEnter}
  329. handleCellMouseLeave={this.handleCellMouseLeave}
  330. isSelected={this.store.isSelected(row)}
  331. isExpanded={this.store.states.expandRows.indexOf(row) > -1}
  332. fixed={this.fixed}
  333. >
  334. </TableRow>
  335. );
  336. },
  337. wrappedRowRender(row, $index) {
  338. const store = this.store;
  339. const { isRowExpanded, assertRowKey } = store;
  340. const { treeData, lazyTreeNodeMap, childrenColumnName, rowKey } = store.states;
  341. if (this.hasExpandColumn && isRowExpanded(row)) {
  342. const renderExpanded = this.table.renderExpanded;
  343. const tr = this.rowRender(row, $index);
  344. if (!renderExpanded) {
  345. console.error('[Element Error]renderExpanded is required.');
  346. return tr;
  347. }
  348. // 使用二维数组,避免修改 $index
  349. return [[
  350. tr,
  351. <tr key={'expanded-row__' + tr.key}>
  352. <td colspan={ this.columnsCount } class="el-table__cell el-table__expanded-cell">
  353. { renderExpanded(this.$createElement, { row, $index, store: this.store }) }
  354. </td>
  355. </tr>]];
  356. } else if (Object.keys(treeData).length) {
  357. assertRowKey();
  358. // TreeTable 时,rowKey 必须由用户设定,不使用 getKeyOfRow 计算
  359. // 在调用 rowRender 函数时,仍然会计算 rowKey,不太好的操作
  360. const key = getRowIdentity(row, rowKey);
  361. let cur = treeData[key];
  362. let treeRowData = null;
  363. if (cur) {
  364. treeRowData = {
  365. expanded: cur.expanded,
  366. level: cur.level,
  367. display: true
  368. };
  369. if (typeof cur.lazy === 'boolean') {
  370. if (typeof cur.loaded === 'boolean' && cur.loaded) {
  371. treeRowData.noLazyChildren = !(cur.children && cur.children.length);
  372. }
  373. treeRowData.loading = cur.loading;
  374. }
  375. }
  376. const tmp = [this.rowRender(row, $index, treeRowData)];
  377. // 渲染嵌套数据
  378. if (cur) {
  379. // currentRow 记录的是 index,所以还需主动增加 TreeTable 的 index
  380. let i = 0;
  381. const traverse = (children, parent) => {
  382. if (!(children && children.length && parent)) return;
  383. children.forEach(node => {
  384. // 父节点的 display 状态影响子节点的显示状态
  385. const innerTreeRowData = {
  386. display: parent.display && parent.expanded,
  387. level: parent.level + 1
  388. };
  389. const childKey = getRowIdentity(node, rowKey);
  390. if (childKey === undefined || childKey === null) {
  391. throw new Error('for nested data item, row-key is required.');
  392. }
  393. cur = { ...treeData[childKey] };
  394. // 对于当前节点,分成有无子节点两种情况。
  395. // 如果包含子节点的,设置 expanded 属性。
  396. // 对于它子节点的 display 属性由它本身的 expanded 与 display 共同决定。
  397. if (cur) {
  398. innerTreeRowData.expanded = cur.expanded;
  399. // 懒加载的某些节点,level 未知
  400. cur.level = cur.level || innerTreeRowData.level;
  401. cur.display = !!(cur.expanded && innerTreeRowData.display);
  402. if (typeof cur.lazy === 'boolean') {
  403. if (typeof cur.loaded === 'boolean' && cur.loaded) {
  404. innerTreeRowData.noLazyChildren = !(cur.children && cur.children.length);
  405. }
  406. innerTreeRowData.loading = cur.loading;
  407. }
  408. }
  409. i++;
  410. tmp.push(this.rowRender(node, $index + i, innerTreeRowData));
  411. if (cur) {
  412. const nodes = lazyTreeNodeMap[childKey] || node[childrenColumnName];
  413. traverse(nodes, cur);
  414. }
  415. });
  416. };
  417. // 对于 root 节点,display 一定为 true
  418. cur.display = true;
  419. const nodes = lazyTreeNodeMap[key] || row[childrenColumnName];
  420. traverse(nodes, cur);
  421. }
  422. return tmp;
  423. } else {
  424. return this.rowRender(row, $index);
  425. }
  426. }
  427. }
  428. };