1
0

AdvancedTable.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. <template>
  2. <div>
  3. <div>
  4. <button
  5. v-for="column in orderedColumns.filter(
  6. c => c.name !== 'select'
  7. )"
  8. :key="column.name"
  9. class="button"
  10. @click="toggleColumnVisibility(column)"
  11. >
  12. {{
  13. `${
  14. this.enabledColumns.indexOf(column.name) !== -1
  15. ? "Hide"
  16. : "Show"
  17. } ${column.name} column`
  18. }}
  19. </button>
  20. </div>
  21. <div class="table-outer-container">
  22. <div class="table-container">
  23. <table class="table">
  24. <thead>
  25. <draggable
  26. item-key="name"
  27. v-model="orderedColumns"
  28. v-bind="columnDragOptions"
  29. tag="tr"
  30. draggable=".item-draggable"
  31. >
  32. <template #item="{ element: column }">
  33. <th
  34. :class="{
  35. sortable: column.sortable,
  36. 'item-draggable':
  37. column.name !== 'select'
  38. }"
  39. v-if="
  40. enabledColumns.indexOf(column.name) !==
  41. -1
  42. "
  43. >
  44. <span @click="changeSort(column)">
  45. {{ column.displayName }}
  46. <span
  47. v-if="
  48. column.sortable &&
  49. sort[column.sortProperty]
  50. "
  51. >({{
  52. sort[column.sortProperty]
  53. }})</span
  54. >
  55. </span>
  56. <tippy
  57. v-if="column.sortable"
  58. :touch="true"
  59. :interactive="true"
  60. placement="bottom"
  61. theme="search"
  62. ref="search"
  63. trigger="click"
  64. >
  65. <i
  66. class="
  67. material-icons
  68. action-dropdown-icon
  69. "
  70. :content="`Filter by ${column.displayName}`"
  71. v-tippy
  72. @click.prevent="true"
  73. >search</i
  74. >
  75. <template #content>
  76. <div
  77. class="
  78. control
  79. is-grouped
  80. input-with-button
  81. "
  82. >
  83. <p class="control is-expanded">
  84. <input
  85. class="input"
  86. type="text"
  87. :placeholder="`Filter by ${column.displayName}`"
  88. :value="
  89. column.filterProperty !==
  90. null
  91. ? filter[
  92. column
  93. .filterProperty
  94. ]
  95. : ''
  96. "
  97. @keyup.enter="
  98. changeFilter(
  99. column,
  100. $event
  101. )
  102. "
  103. />
  104. </p>
  105. <p class="control">
  106. <a class="button is-info">
  107. <i
  108. class="
  109. material-icons
  110. icon-with-button
  111. "
  112. >search</i
  113. >
  114. </a>
  115. </p>
  116. </div>
  117. </template>
  118. </tippy>
  119. </th>
  120. </template>
  121. </draggable>
  122. </thead>
  123. <tbody>
  124. <tr
  125. v-for="(item, itemIndex) in data"
  126. :key="item._id"
  127. :class="{
  128. selected: item.selected,
  129. highlighted: item.highlighted
  130. }"
  131. @click="clickItem(itemIndex, $event)"
  132. >
  133. <td
  134. v-for="column in sortedFilteredColumns"
  135. :key="`${item._id}-${column.name}`"
  136. >
  137. <slot
  138. :name="`column-${column.name}`"
  139. :item="item"
  140. ></slot>
  141. </td>
  142. </tr>
  143. </tbody>
  144. </table>
  145. </div>
  146. <div class="table-footer">
  147. <div>
  148. <button
  149. v-if="page > 1"
  150. class="button is-primary material-icons"
  151. @click="changePage(1)"
  152. content="First Page"
  153. v-tippy
  154. >
  155. skip_previous
  156. </button>
  157. <button
  158. v-if="page > 1"
  159. class="button is-primary material-icons"
  160. @click="changePage(page - 1)"
  161. content="Previous Page"
  162. v-tippy
  163. >
  164. fast_rewind
  165. </button>
  166. <p>Page {{ page }} / {{ lastPage }}</p>
  167. <button
  168. v-if="page < lastPage"
  169. class="button is-primary material-icons"
  170. @click="changePage(page + 1)"
  171. content="Next Page"
  172. v-tippy
  173. >
  174. fast_forward
  175. </button>
  176. <button
  177. v-if="page < lastPage"
  178. class="button is-primary material-icons"
  179. @click="changePage(lastPage)"
  180. content="Last Page"
  181. v-tippy
  182. >
  183. skip_next
  184. </button>
  185. </div>
  186. <div>
  187. <div class="control">
  188. <label class="label">Items per page</label>
  189. <p class="control select">
  190. <select
  191. v-model.number="pageSize"
  192. @change="getData()"
  193. >
  194. <option value="10">10</option>
  195. <option value="25">25</option>
  196. <option value="50">50</option>
  197. <option value="100">100</option>
  198. <option value="250">250</option>
  199. <option value="500">500</option>
  200. <option value="1000">1000</option>
  201. </select>
  202. </p>
  203. </div>
  204. </div>
  205. </div>
  206. </div>
  207. </div>
  208. </template>
  209. <script>
  210. import { mapGetters } from "vuex";
  211. import draggable from "vuedraggable";
  212. import Toast from "toasters";
  213. import ws from "@/ws";
  214. export default {
  215. components: {
  216. draggable
  217. },
  218. props: {
  219. columns: { type: Array, default: null },
  220. dataAction: { type: String, default: null }
  221. },
  222. data() {
  223. return {
  224. page: 1,
  225. pageSize: 10,
  226. data: [],
  227. count: 0, // TODO Rename
  228. sort: {},
  229. filter: {},
  230. orderedColumns: [],
  231. enabledColumns: [],
  232. columnDragOptions() {
  233. return {
  234. animation: 200,
  235. group: "columns",
  236. disabled: false,
  237. ghostClass: "draggable-list-ghost",
  238. filter: ".ignore-elements",
  239. fallbackTolerance: 50
  240. };
  241. }
  242. };
  243. },
  244. computed: {
  245. properties() {
  246. return Array.from(
  247. new Set(
  248. this.sortedFilteredColumns.flatMap(
  249. column => column.properties
  250. )
  251. )
  252. );
  253. },
  254. lastPage() {
  255. return Math.ceil(this.count / this.pageSize);
  256. },
  257. sortedFilteredColumns() {
  258. return this.orderedColumns.filter(
  259. column => this.enabledColumns.indexOf(column.name) !== -1
  260. );
  261. },
  262. lastSelectedItemIndex() {
  263. return this.data.findIndex(item => item.highlighted);
  264. },
  265. ...mapGetters({
  266. socket: "websockets/getSocket"
  267. })
  268. },
  269. mounted() {
  270. const columns = [
  271. {
  272. name: "select",
  273. displayName: "",
  274. properties: [],
  275. sortable: false,
  276. filterable: false
  277. },
  278. ...this.columns
  279. ];
  280. this.orderedColumns = columns;
  281. this.enabledColumns = columns.map(column => column.name);
  282. ws.onConnect(this.init);
  283. },
  284. methods: {
  285. init() {
  286. this.getData();
  287. },
  288. getData() {
  289. this.socket.dispatch(
  290. this.dataAction,
  291. this.page,
  292. this.pageSize,
  293. this.properties,
  294. this.sort,
  295. this.filter,
  296. res => {
  297. console.log(111, res);
  298. new Toast(res.message);
  299. if (res.status === "success") {
  300. const { data, count } = res.data;
  301. this.data = data;
  302. this.count = count;
  303. }
  304. }
  305. );
  306. },
  307. changePage(page) {
  308. if (page < 1) return;
  309. if (page > this.lastPage) return;
  310. if (page === this.page) return;
  311. this.page = page;
  312. this.getData();
  313. },
  314. changeSort(column) {
  315. if (column.sortable) {
  316. const { sortProperty } = column;
  317. if (this.sort[sortProperty] === undefined)
  318. this.sort[sortProperty] = "ascending";
  319. else if (this.sort[sortProperty] === "ascending")
  320. this.sort[sortProperty] = "descending";
  321. else if (this.sort[sortProperty] === "descending")
  322. delete this.sort[sortProperty];
  323. this.getData();
  324. }
  325. },
  326. changeFilter(column, event) {
  327. if (column.filterable) {
  328. const { value } = event.target;
  329. const { filterProperty } = column;
  330. if (this.filter[filterProperty] !== undefined && value === "") {
  331. delete this.filter[filterProperty];
  332. } else if (this.filter[filterProperty] !== value) {
  333. this.filter[filterProperty] = value;
  334. } else return;
  335. this.getData();
  336. }
  337. },
  338. toggleColumnVisibility(column) {
  339. if (this.enabledColumns.indexOf(column.name) !== -1) {
  340. this.enabledColumns.splice(
  341. this.enabledColumns.indexOf(column.name),
  342. 1
  343. );
  344. } else {
  345. this.enabledColumns.push(column.name);
  346. }
  347. this.getData();
  348. },
  349. clickItem(itemIndex, event) {
  350. const { shiftKey, ctrlKey } = event;
  351. // Shift was pressed, so attempt to select all items between the clicked item and last clicked item
  352. if (shiftKey) {
  353. // If there is a last clicked item
  354. if (this.lastSelectedItemIndex >= 0) {
  355. // Clicked item is lower than last item, so select upwards until it reaches the last selected item
  356. if (itemIndex > this.lastSelectedItemIndex) {
  357. for (
  358. let itemIndexUp = itemIndex;
  359. itemIndexUp > this.lastSelectedItemIndex;
  360. itemIndexUp -= 1
  361. ) {
  362. this.data[itemIndexUp].selected = true;
  363. }
  364. }
  365. // Clicked item is higher than last item, so select downwards until it reaches the last selected item
  366. else if (itemIndex < this.lastSelectedItemIndex) {
  367. for (
  368. let itemIndexDown = itemIndex;
  369. itemIndexDown < this.lastSelectedItemIndex;
  370. itemIndexDown += 1
  371. ) {
  372. this.data[itemIndexDown].selected = true;
  373. }
  374. }
  375. }
  376. }
  377. // Ctrl was pressed, so toggle selected on the clicked item
  378. else if (ctrlKey) {
  379. this.data[itemIndex].selected = !this.data[itemIndex].selected;
  380. }
  381. // Neither ctrl nor shift were pressed, so unselect all items and set the clicked item to selected
  382. else {
  383. this.data = this.data.map(item => ({
  384. ...item,
  385. selected: false
  386. }));
  387. this.data[itemIndex].selected = true;
  388. }
  389. // Set the last clicked item to no longer be highlighted, if it exists
  390. if (this.lastSelectedItemIndex >= 0)
  391. this.data[this.lastSelectedItemIndex].highlighted = false;
  392. // Set the clicked item to be highlighted
  393. this.data[itemIndex].highlighted = true;
  394. }
  395. }
  396. };
  397. </script>
  398. <style lang="scss" scoped>
  399. .night-mode .table-outer-container {
  400. .table-container .table {
  401. &,
  402. thead th {
  403. background-color: var(--dark-grey-3);
  404. color: var(--light-grey-2);
  405. }
  406. tr {
  407. &:nth-child(even) {
  408. background-color: var(--dark-grey-2);
  409. }
  410. &:hover,
  411. &:focus,
  412. &.highlighted {
  413. background-color: var(--dark-grey);
  414. }
  415. }
  416. }
  417. .table-footer {
  418. background-color: var(--dark-grey-3);
  419. color: var(--light-grey-2);
  420. }
  421. }
  422. .table-outer-container {
  423. border-radius: 5px;
  424. box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1);
  425. margin: 10px 0;
  426. overflow: hidden;
  427. .table-container {
  428. overflow-x: auto;
  429. table {
  430. border-collapse: separate;
  431. thead {
  432. tr {
  433. th {
  434. white-space: nowrap;
  435. &.sortable {
  436. cursor: pointer;
  437. }
  438. span > .material-icons {
  439. font-size: 22px;
  440. position: relative;
  441. top: 6px;
  442. &:first-child {
  443. margin-left: auto;
  444. }
  445. }
  446. }
  447. }
  448. }
  449. tbody {
  450. tr {
  451. &.selected td:first-child {
  452. border-left: 5px solid var(--primary-color);
  453. padding-left: 0;
  454. }
  455. &.highlighted {
  456. background-color: var(--light-grey);
  457. td:first-child {
  458. border-left: 5px solid var(--red);
  459. padding-left: 0;
  460. }
  461. }
  462. &.selected.highlighted td:first-child {
  463. border-left: 5px solid var(--green);
  464. padding-left: 0;
  465. }
  466. }
  467. }
  468. }
  469. table thead tr th:first-child,
  470. table tbody tr td:first-child {
  471. position: sticky;
  472. left: 0;
  473. z-index: 2;
  474. padding: 0;
  475. padding-left: 5px;
  476. }
  477. }
  478. .table-footer {
  479. display: flex;
  480. flex-direction: row;
  481. flex-wrap: wrap;
  482. justify-content: space-between;
  483. line-height: 36px;
  484. background-color: var(--white);
  485. & > div:first-child,
  486. div .control {
  487. display: flex;
  488. flex-direction: row;
  489. margin-bottom: 0 !important;
  490. button {
  491. margin: 5px;
  492. font-size: 20px;
  493. }
  494. p,
  495. label {
  496. margin: 5px;
  497. font-size: 14px;
  498. font-weight: 600;
  499. }
  500. &.select::after {
  501. top: 18px;
  502. }
  503. }
  504. }
  505. }
  506. </style>