table-header.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. import Vue from 'vue';
  2. import { hasClass, addClass, removeClass } from 'element-ui/src/utils/dom';
  3. import ElCheckbox from 'element-ui/packages/checkbox';
  4. import FilterPanel from './filter-panel.vue';
  5. import LayoutObserver from './layout-observer';
  6. import { mapStates } from './store/helper';
  7. const getAllColumns = (columns) => {
  8. const result = [];
  9. columns.forEach((column) => {
  10. if (column.children) {
  11. result.push(column);
  12. result.push.apply(result, getAllColumns(column.children));
  13. } else {
  14. result.push(column);
  15. }
  16. });
  17. return result;
  18. };
  19. const convertToRows = (originColumns) => {
  20. let maxLevel = 1;
  21. const traverse = (column, parent) => {
  22. if (parent) {
  23. column.level = parent.level + 1;
  24. if (maxLevel < column.level) {
  25. maxLevel = column.level;
  26. }
  27. }
  28. if (column.children) {
  29. let colSpan = 0;
  30. column.children.forEach((subColumn) => {
  31. traverse(subColumn, column);
  32. colSpan += subColumn.colSpan;
  33. });
  34. column.colSpan = colSpan;
  35. } else {
  36. column.colSpan = 1;
  37. }
  38. };
  39. originColumns.forEach((column) => {
  40. column.level = 1;
  41. traverse(column);
  42. });
  43. const rows = [];
  44. for (let i = 0; i < maxLevel; i++) {
  45. rows.push([]);
  46. }
  47. const allColumns = getAllColumns(originColumns);
  48. allColumns.forEach((column) => {
  49. if (!column.children) {
  50. column.rowSpan = maxLevel - column.level + 1;
  51. } else {
  52. column.rowSpan = 1;
  53. }
  54. rows[column.level - 1].push(column);
  55. });
  56. return rows;
  57. };
  58. export default {
  59. name: 'ElTableHeader',
  60. mixins: [LayoutObserver],
  61. render(h) {
  62. const originColumns = this.store.states.originColumns;
  63. const columnRows = convertToRows(originColumns, this.columns);
  64. // 是否拥有多级表头
  65. const isGroup = columnRows.length > 1;
  66. if (isGroup) this.$parent.isGroup = true;
  67. return (
  68. <table
  69. class="el-table__header"
  70. cellspacing="0"
  71. cellpadding="0"
  72. border="0">
  73. <colgroup>
  74. {
  75. this.columns.map(column => <col name={ column.id } key={column.id} />)
  76. }
  77. {
  78. this.hasGutter ? <col name="gutter" /> : ''
  79. }
  80. </colgroup>
  81. <thead class={ [{ 'is-group': isGroup, 'has-gutter': this.hasGutter }] }>
  82. {
  83. this._l(columnRows, (columns, rowIndex) =>
  84. <tr
  85. style={ this.getHeaderRowStyle(rowIndex) }
  86. class={ this.getHeaderRowClass(rowIndex) }
  87. >
  88. {
  89. columns.map((column, cellIndex) => (<th
  90. colspan={ column.colSpan }
  91. rowspan={ column.rowSpan }
  92. on-mousemove={ ($event) => this.handleMouseMove($event, column) }
  93. on-mouseout={ this.handleMouseOut }
  94. on-mousedown={ ($event) => this.handleMouseDown($event, column) }
  95. on-click={ ($event) => this.handleHeaderClick($event, column) }
  96. on-contextmenu={ ($event) => this.handleHeaderContextMenu($event, column) }
  97. style={ this.getHeaderCellStyle(rowIndex, cellIndex, columns, column) }
  98. class={ this.getHeaderCellClass(rowIndex, cellIndex, columns, column) }
  99. key={ column.id }>
  100. <div class={ ['cell', column.filteredValue && column.filteredValue.length > 0 ? 'highlight' : '', column.labelClassName] }>
  101. {
  102. column.renderHeader
  103. ? column.renderHeader.call(this._renderProxy, h, { column, $index: cellIndex, store: this.store, _self: this.$parent.$vnode.context })
  104. : column.label
  105. }
  106. {
  107. column.sortable ? (<span
  108. class="caret-wrapper"
  109. on-click={ ($event) => this.handleSortClick($event, column) }>
  110. <i class="sort-caret ascending"
  111. on-click={ ($event) => this.handleSortClick($event, column, 'ascending') }>
  112. </i>
  113. <i class="sort-caret descending"
  114. on-click={ ($event) => this.handleSortClick($event, column, 'descending') }>
  115. </i>
  116. </span>) : ''
  117. }
  118. {
  119. column.filterable ? (<span
  120. class="el-table__column-filter-trigger"
  121. on-click={ ($event) => this.handleFilterClick($event, column) }>
  122. <i class={ ['el-icon-arrow-down', column.filterOpened ? 'el-icon-arrow-up' : ''] }></i>
  123. </span>) : ''
  124. }
  125. </div>
  126. </th>))
  127. }
  128. {
  129. this.hasGutter ? <th class="gutter"></th> : ''
  130. }
  131. </tr>
  132. )
  133. }
  134. </thead>
  135. </table>
  136. );
  137. },
  138. props: {
  139. fixed: String,
  140. store: {
  141. required: true
  142. },
  143. border: Boolean,
  144. defaultSort: {
  145. type: Object,
  146. default() {
  147. return {
  148. prop: '',
  149. order: ''
  150. };
  151. }
  152. }
  153. },
  154. components: {
  155. ElCheckbox
  156. },
  157. computed: {
  158. table() {
  159. return this.$parent;
  160. },
  161. hasGutter() {
  162. return !this.fixed && this.tableLayout.gutterWidth;
  163. },
  164. ...mapStates({
  165. columns: 'columns',
  166. isAllSelected: 'isAllSelected',
  167. leftFixedLeafCount: 'fixedLeafColumnsLength',
  168. rightFixedLeafCount: 'rightFixedLeafColumnsLength',
  169. columnsCount: states => states.columns.length,
  170. leftFixedCount: states => states.fixedColumns.length,
  171. rightFixedCount: states => states.rightFixedColumns.length
  172. })
  173. },
  174. created() {
  175. this.filterPanels = {};
  176. },
  177. mounted() {
  178. // nextTick 是有必要的 https://github.com/ElemeFE/element/pull/11311
  179. this.$nextTick(() => {
  180. const { prop, order } = this.defaultSort;
  181. const init = true;
  182. this.store.commit('sort', { prop, order, init });
  183. });
  184. },
  185. beforeDestroy() {
  186. const panels = this.filterPanels;
  187. for (let prop in panels) {
  188. if (panels.hasOwnProperty(prop) && panels[prop]) {
  189. panels[prop].$destroy(true);
  190. }
  191. }
  192. },
  193. methods: {
  194. isCellHidden(index, columns) {
  195. let start = 0;
  196. for (let i = 0; i < index; i++) {
  197. start += columns[i].colSpan;
  198. }
  199. const after = start + columns[index].colSpan - 1;
  200. if (this.fixed === true || this.fixed === 'left') {
  201. return after >= this.leftFixedLeafCount;
  202. } else if (this.fixed === 'right') {
  203. return start < this.columnsCount - this.rightFixedLeafCount;
  204. } else {
  205. return (after < this.leftFixedLeafCount) || (start >= this.columnsCount - this.rightFixedLeafCount);
  206. }
  207. },
  208. getHeaderRowStyle(rowIndex) {
  209. const headerRowStyle = this.table.headerRowStyle;
  210. if (typeof headerRowStyle === 'function') {
  211. return headerRowStyle.call(null, { rowIndex });
  212. }
  213. return headerRowStyle;
  214. },
  215. getHeaderRowClass(rowIndex) {
  216. const classes = [];
  217. const headerRowClassName = this.table.headerRowClassName;
  218. if (typeof headerRowClassName === 'string') {
  219. classes.push(headerRowClassName);
  220. } else if (typeof headerRowClassName === 'function') {
  221. classes.push(headerRowClassName.call(null, { rowIndex }));
  222. }
  223. return classes.join(' ');
  224. },
  225. getHeaderCellStyle(rowIndex, columnIndex, row, column) {
  226. const headerCellStyle = this.table.headerCellStyle;
  227. if (typeof headerCellStyle === 'function') {
  228. return headerCellStyle.call(null, {
  229. rowIndex,
  230. columnIndex,
  231. row,
  232. column
  233. });
  234. }
  235. return headerCellStyle;
  236. },
  237. getHeaderCellClass(rowIndex, columnIndex, row, column) {
  238. const classes = [column.id, column.order, column.headerAlign, column.className, column.labelClassName];
  239. if (rowIndex === 0 && this.isCellHidden(columnIndex, row)) {
  240. classes.push('is-hidden');
  241. }
  242. if (!column.children) {
  243. classes.push('is-leaf');
  244. }
  245. if (column.sortable) {
  246. classes.push('is-sortable');
  247. }
  248. const headerCellClassName = this.table.headerCellClassName;
  249. if (typeof headerCellClassName === 'string') {
  250. classes.push(headerCellClassName);
  251. } else if (typeof headerCellClassName === 'function') {
  252. classes.push(headerCellClassName.call(null, {
  253. rowIndex,
  254. columnIndex,
  255. row,
  256. column
  257. }));
  258. }
  259. return classes.join(' ');
  260. },
  261. toggleAllSelection(event) {
  262. event.stopPropagation();
  263. this.store.commit('toggleAllSelection');
  264. },
  265. handleFilterClick(event, column) {
  266. event.stopPropagation();
  267. const target = event.target;
  268. let cell = target.tagName === 'TH' ? target : target.parentNode;
  269. if (hasClass(cell, 'noclick')) return;
  270. cell = cell.querySelector('.el-table__column-filter-trigger') || cell;
  271. const table = this.$parent;
  272. let filterPanel = this.filterPanels[column.id];
  273. if (filterPanel && column.filterOpened) {
  274. filterPanel.showPopper = false;
  275. return;
  276. }
  277. if (!filterPanel) {
  278. filterPanel = new Vue(FilterPanel);
  279. this.filterPanels[column.id] = filterPanel;
  280. if (column.filterPlacement) {
  281. filterPanel.placement = column.filterPlacement;
  282. }
  283. filterPanel.table = table;
  284. filterPanel.cell = cell;
  285. filterPanel.column = column;
  286. !this.$isServer && filterPanel.$mount(document.createElement('div'));
  287. }
  288. setTimeout(() => {
  289. filterPanel.showPopper = true;
  290. }, 16);
  291. },
  292. handleHeaderClick(event, column) {
  293. if (!column.filters && column.sortable) {
  294. this.handleSortClick(event, column);
  295. } else if (column.filterable && !column.sortable) {
  296. this.handleFilterClick(event, column);
  297. }
  298. this.$parent.$emit('header-click', column, event);
  299. },
  300. handleHeaderContextMenu(event, column) {
  301. this.$parent.$emit('header-contextmenu', column, event);
  302. },
  303. handleMouseDown(event, column) {
  304. if (this.$isServer) return;
  305. if (column.children && column.children.length > 0) return;
  306. /* istanbul ignore if */
  307. if (this.draggingColumn && this.border) {
  308. this.dragging = true;
  309. this.$parent.resizeProxyVisible = true;
  310. const table = this.$parent;
  311. const tableEl = table.$el;
  312. const tableLeft = tableEl.getBoundingClientRect().left;
  313. const columnEl = this.$el.querySelector(`th.${column.id}`);
  314. const columnRect = columnEl.getBoundingClientRect();
  315. const minLeft = columnRect.left - tableLeft + 30;
  316. addClass(columnEl, 'noclick');
  317. this.dragState = {
  318. startMouseLeft: event.clientX,
  319. startLeft: columnRect.right - tableLeft,
  320. startColumnLeft: columnRect.left - tableLeft,
  321. tableLeft
  322. };
  323. const resizeProxy = table.$refs.resizeProxy;
  324. resizeProxy.style.left = this.dragState.startLeft + 'px';
  325. document.onselectstart = function() { return false; };
  326. document.ondragstart = function() { return false; };
  327. const handleMouseMove = (event) => {
  328. const deltaLeft = event.clientX - this.dragState.startMouseLeft;
  329. const proxyLeft = this.dragState.startLeft + deltaLeft;
  330. resizeProxy.style.left = Math.max(minLeft, proxyLeft) + 'px';
  331. };
  332. const handleMouseUp = () => {
  333. if (this.dragging) {
  334. const {
  335. startColumnLeft,
  336. startLeft
  337. } = this.dragState;
  338. const finalLeft = parseInt(resizeProxy.style.left, 10);
  339. const columnWidth = finalLeft - startColumnLeft;
  340. column.width = column.realWidth = columnWidth;
  341. table.$emit('header-dragend', column.width, startLeft - startColumnLeft, column, event);
  342. this.store.scheduleLayout();
  343. document.body.style.cursor = '';
  344. this.dragging = false;
  345. this.draggingColumn = null;
  346. this.dragState = {};
  347. table.resizeProxyVisible = false;
  348. }
  349. document.removeEventListener('mousemove', handleMouseMove);
  350. document.removeEventListener('mouseup', handleMouseUp);
  351. document.onselectstart = null;
  352. document.ondragstart = null;
  353. setTimeout(function() {
  354. removeClass(columnEl, 'noclick');
  355. }, 0);
  356. };
  357. document.addEventListener('mousemove', handleMouseMove);
  358. document.addEventListener('mouseup', handleMouseUp);
  359. }
  360. },
  361. handleMouseMove(event, column) {
  362. if (column.children && column.children.length > 0) return;
  363. let target = event.target;
  364. while (target && target.tagName !== 'TH') {
  365. target = target.parentNode;
  366. }
  367. if (!column || !column.resizable) return;
  368. if (!this.dragging && this.border) {
  369. let rect = target.getBoundingClientRect();
  370. const bodyStyle = document.body.style;
  371. if (rect.width > 12 && rect.right - event.pageX < 8) {
  372. bodyStyle.cursor = 'col-resize';
  373. if (hasClass(target, 'is-sortable')) {
  374. target.style.cursor = 'col-resize';
  375. }
  376. this.draggingColumn = column;
  377. } else if (!this.dragging) {
  378. bodyStyle.cursor = '';
  379. if (hasClass(target, 'is-sortable')) {
  380. target.style.cursor = 'pointer';
  381. }
  382. this.draggingColumn = null;
  383. }
  384. }
  385. },
  386. handleMouseOut() {
  387. if (this.$isServer) return;
  388. document.body.style.cursor = '';
  389. },
  390. toggleOrder({ order, sortOrders }) {
  391. if (order === '') return sortOrders[0];
  392. const index = sortOrders.indexOf(order || null);
  393. return sortOrders[index > sortOrders.length - 2 ? 0 : index + 1];
  394. },
  395. handleSortClick(event, column, givenOrder) {
  396. event.stopPropagation();
  397. let order = column.order === givenOrder
  398. ? null
  399. : (givenOrder || this.toggleOrder(column));
  400. let target = event.target;
  401. while (target && target.tagName !== 'TH') {
  402. target = target.parentNode;
  403. }
  404. if (target && target.tagName === 'TH') {
  405. if (hasClass(target, 'noclick')) {
  406. removeClass(target, 'noclick');
  407. return;
  408. }
  409. }
  410. if (!column.sortable) return;
  411. const states = this.store.states;
  412. let sortProp = states.sortProp;
  413. let sortOrder;
  414. const sortingColumn = states.sortingColumn;
  415. if (sortingColumn !== column || (sortingColumn === column && sortingColumn.order === null)) {
  416. if (sortingColumn) {
  417. sortingColumn.order = null;
  418. }
  419. states.sortingColumn = column;
  420. sortProp = column.property;
  421. }
  422. if (!order) {
  423. sortOrder = column.order = null;
  424. } else {
  425. sortOrder = column.order = order;
  426. }
  427. states.sortProp = sortProp;
  428. states.sortOrder = sortOrder;
  429. this.store.commit('changeSortCondition');
  430. }
  431. },
  432. data() {
  433. return {
  434. draggingColumn: null,
  435. dragging: false,
  436. dragState: {}
  437. };
  438. }
  439. };