AdvancedTable.vue 9.1 KB

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