table-header.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  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="el-table__cell 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. classes.push('el-table__cell');
  260. return classes.join(' ');
  261. },
  262. toggleAllSelection(event) {
  263. event.stopPropagation();
  264. this.store.commit('toggleAllSelection');
  265. },
  266. handleFilterClick(event, column) {
  267. event.stopPropagation();
  268. const target = event.target;
  269. let cell = target.tagName === 'TH' ? target : target.parentNode;
  270. if (hasClass(cell, 'noclick')) return;
  271. cell = cell.querySelector('.el-table__column-filter-trigger') || cell;
  272. const table = this.$parent;
  273. let filterPanel = this.filterPanels[column.id];
  274. if (filterPanel && column.filterOpened) {
  275. filterPanel.showPopper = false;
  276. return;
  277. }
  278. if (!filterPanel) {
  279. filterPanel = new Vue(FilterPanel);
  280. this.filterPanels[column.id] = filterPanel;
  281. if (column.filterPlacement) {
  282. filterPanel.placement = column.filterPlacement;
  283. }
  284. filterPanel.table = table;
  285. filterPanel.cell = cell;
  286. filterPanel.column = column;
  287. !this.$isServer && filterPanel.$mount(document.createElement('div'));
  288. }
  289. setTimeout(() => {
  290. filterPanel.showPopper = true;
  291. }, 16);
  292. },
  293. handleHeaderClick(event, column) {
  294. if (!column.filters && column.sortable) {
  295. this.handleSortClick(event, column);
  296. } else if (column.filterable && !column.sortable) {
  297. this.handleFilterClick(event, column);
  298. }
  299. this.$parent.$emit('header-click', column, event);
  300. },
  301. handleHeaderContextMenu(event, column) {
  302. this.$parent.$emit('header-contextmenu', column, event);
  303. },
  304. handleMouseDown(event, column) {
  305. if (this.$isServer) return;
  306. if (column.children && column.children.length > 0) return;
  307. /* istanbul ignore if */
  308. if (this.draggingColumn && this.border) {
  309. this.dragging = true;
  310. this.$parent.resizeProxyVisible = true;
  311. const table = this.$parent;
  312. const tableEl = table.$el;
  313. const tableLeft = tableEl.getBoundingClientRect().left;
  314. const columnEl = this.$el.querySelector(`th.${column.id}`);
  315. const columnRect = columnEl.getBoundingClientRect();
  316. const minLeft = columnRect.left - tableLeft + 30;
  317. addClass(columnEl, 'noclick');
  318. this.dragState = {
  319. startMouseLeft: event.clientX,
  320. startLeft: columnRect.right - tableLeft,
  321. startColumnLeft: columnRect.left - tableLeft,
  322. tableLeft
  323. };
  324. const resizeProxy = table.$refs.resizeProxy;
  325. resizeProxy.style.left = this.dragState.startLeft + 'px';
  326. document.onselectstart = function() { return false; };
  327. document.ondragstart = function() { return false; };
  328. const handleMouseMove = (event) => {
  329. const deltaLeft = event.clientX - this.dragState.startMouseLeft;
  330. const proxyLeft = this.dragState.startLeft + deltaLeft;
  331. resizeProxy.style.left = Math.max(minLeft, proxyLeft) + 'px';
  332. };
  333. const handleMouseUp = () => {
  334. if (this.dragging) {
  335. const {
  336. startColumnLeft,
  337. startLeft
  338. } = this.dragState;
  339. const finalLeft = parseInt(resizeProxy.style.left, 10);
  340. const columnWidth = finalLeft - startColumnLeft;
  341. column.width = column.realWidth = columnWidth;
  342. table.$emit('header-dragend', column.width, startLeft - startColumnLeft, column, event);
  343. this.store.scheduleLayout();
  344. document.body.style.cursor = '';
  345. this.dragging = false;
  346. this.draggingColumn = null;
  347. this.dragState = {};
  348. table.resizeProxyVisible = false;
  349. }
  350. document.removeEventListener('mousemove', handleMouseMove);
  351. document.removeEventListener('mouseup', handleMouseUp);
  352. document.onselectstart = null;
  353. document.ondragstart = null;
  354. setTimeout(function() {
  355. removeClass(columnEl, 'noclick');
  356. }, 0);
  357. };
  358. document.addEventListener('mousemove', handleMouseMove);
  359. document.addEventListener('mouseup', handleMouseUp);
  360. }
  361. },
  362. handleMouseMove(event, column) {
  363. if (column.children && column.children.length > 0) return;
  364. let target = event.target;
  365. while (target && target.tagName !== 'TH') {
  366. target = target.parentNode;
  367. }
  368. if (!column || !column.resizable) return;
  369. if (!this.dragging && this.border) {
  370. let rect = target.getBoundingClientRect();
  371. const bodyStyle = document.body.style;
  372. if (rect.width > 12 && rect.right - event.pageX < 8) {
  373. bodyStyle.cursor = 'col-resize';
  374. if (hasClass(target, 'is-sortable')) {
  375. target.style.cursor = 'col-resize';
  376. }
  377. this.draggingColumn = column;
  378. } else if (!this.dragging) {
  379. bodyStyle.cursor = '';
  380. if (hasClass(target, 'is-sortable')) {
  381. target.style.cursor = 'pointer';
  382. }
  383. this.draggingColumn = null;
  384. }
  385. }
  386. },
  387. handleMouseOut() {
  388. if (this.$isServer) return;
  389. document.body.style.cursor = '';
  390. },
  391. toggleOrder({ order, sortOrders }) {
  392. if (order === '') return sortOrders[0];
  393. const index = sortOrders.indexOf(order || null);
  394. return sortOrders[index > sortOrders.length - 2 ? 0 : index + 1];
  395. },
  396. handleSortClick(event, column, givenOrder) {
  397. event.stopPropagation();
  398. let order = column.order === givenOrder
  399. ? null
  400. : (givenOrder || this.toggleOrder(column));
  401. let target = event.target;
  402. while (target && target.tagName !== 'TH') {
  403. target = target.parentNode;
  404. }
  405. if (target && target.tagName === 'TH') {
  406. if (hasClass(target, 'noclick')) {
  407. removeClass(target, 'noclick');
  408. return;
  409. }
  410. }
  411. if (!column.sortable) return;
  412. const states = this.store.states;
  413. let sortProp = states.sortProp;
  414. let sortOrder;
  415. const sortingColumn = states.sortingColumn;
  416. if (sortingColumn !== column || (sortingColumn === column && sortingColumn.order === null)) {
  417. if (sortingColumn) {
  418. sortingColumn.order = null;
  419. }
  420. states.sortingColumn = column;
  421. sortProp = column.property;
  422. }
  423. if (!order) {
  424. sortOrder = column.order = null;
  425. } else {
  426. sortOrder = column.order = order;
  427. }
  428. states.sortProp = sortProp;
  429. states.sortOrder = sortOrder;
  430. this.store.commit('changeSortCondition');
  431. }
  432. },
  433. data() {
  434. return {
  435. draggingColumn: null,
  436. dragging: false,
  437. dragState: {}
  438. };
  439. }
  440. };