AdvancedTable.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335
  1. <template>
  2. <div>
  3. <div
  4. class="table-outer-container"
  5. @mousemove="columnResizingMouseMove($event)"
  6. >
  7. <div class="table-header">
  8. <div>
  9. <tippy
  10. v-if="filters.length > 0"
  11. :touch="true"
  12. :interactive="true"
  13. placement="bottom"
  14. theme="search"
  15. ref="search"
  16. trigger="click"
  17. >
  18. <button class="button is-info">
  19. <i class="material-icons icon-with-button"
  20. >filter_list</i
  21. >
  22. Filters
  23. </button>
  24. <template #content>
  25. <div class="control is-grouped input-with-button">
  26. <p class="control select is-expanded">
  27. <select v-model="addFilterValue">
  28. <option
  29. v-for="type in filters"
  30. :key="type.name"
  31. :value="type"
  32. >
  33. {{ type.displayName }}
  34. </option>
  35. </select>
  36. </p>
  37. <p class="control">
  38. <button
  39. class="button material-icons is-success"
  40. @click="addFilterItem()"
  41. >
  42. control_point
  43. </button>
  44. </p>
  45. </div>
  46. <div
  47. v-for="(filter, index) in editingFilters"
  48. :key="`filter-${index}`"
  49. class="control is-grouped is-expanded"
  50. >
  51. <div class="control select">
  52. <select
  53. v-model="filter.filter"
  54. @change="changeFilterType(index)"
  55. >
  56. <option
  57. v-for="type in filters"
  58. :key="type.name"
  59. :value="type"
  60. >
  61. {{ type.displayName }}
  62. </option>
  63. </select>
  64. </div>
  65. <div class="control select">
  66. <select
  67. v-model="filter.filterType"
  68. :disabled="!filter.filterType"
  69. >
  70. <option
  71. v-for="filterType in filterTypes(
  72. filter.filter
  73. )"
  74. :key="filterType.name"
  75. :value="filterType.name"
  76. :selected="
  77. filter.filter
  78. .defaultFilterType ===
  79. filterType.name
  80. "
  81. >
  82. {{ filterType.displayName }}
  83. </option>
  84. </select>
  85. </div>
  86. <p class="control is-expanded">
  87. <input
  88. v-model="filter.data"
  89. class="input"
  90. type="text"
  91. placeholder="Search value"
  92. :disabled="!filter.filterType"
  93. />
  94. </p>
  95. <div class="control">
  96. <button
  97. class="button material-icons is-danger"
  98. @click="removeFilterItem(index)"
  99. >
  100. remove_circle_outline
  101. </button>
  102. </div>
  103. </div>
  104. <div
  105. v-if="editingFilters.length > 1"
  106. class="control is-expanded is-grouped"
  107. >
  108. <label class="control label"
  109. >Filter operator</label
  110. >
  111. <div class="control select is-expanded">
  112. <select v-model="filterOperator">
  113. <option
  114. v-for="operator in filterOperators"
  115. :key="operator.name"
  116. :value="operator.name"
  117. >
  118. {{ operator.displayName }}
  119. </option>
  120. </select>
  121. </div>
  122. </div>
  123. <div
  124. class="advanced-filter-bottom"
  125. v-if="editingFilters.length > 0"
  126. >
  127. <div class="control is-expanded">
  128. <button
  129. class="button is-info"
  130. @click="applyFilterAndGetData()"
  131. >
  132. <i
  133. class="
  134. material-icons
  135. icon-with-button
  136. "
  137. >filter_list</i
  138. >
  139. Apply filters
  140. </button>
  141. </div>
  142. </div>
  143. <div
  144. class="advanced-filter-bottom"
  145. v-else-if="editingFilters.length === 0"
  146. >
  147. <div class="control is-expanded">
  148. <button
  149. class="button is-info"
  150. @click="applyFilterAndGetData()"
  151. >
  152. <i
  153. class="
  154. material-icons
  155. icon-with-button
  156. "
  157. >filter_list</i
  158. >
  159. Apply filters
  160. </button>
  161. </div>
  162. </div>
  163. </template>
  164. </tippy>
  165. <tippy
  166. v-if="appliedFilters.length > 0"
  167. :touch="true"
  168. :interactive="true"
  169. theme="info"
  170. ref="activeFilters"
  171. >
  172. <div class="filters-indicator">
  173. {{ appliedFilters.length }}
  174. <i class="material-icons" @click.prevent="true"
  175. >filter_list</i
  176. >
  177. </div>
  178. <template #content>
  179. <p
  180. v-for="(filter, index) in appliedFilters"
  181. :key="`filter-${index}`"
  182. >
  183. {{ filter.filter.displayName }}
  184. {{ filter.filterType }} "{{ filter.data }}"
  185. {{
  186. appliedFilters.length === index + 1
  187. ? ""
  188. : filterOperator
  189. }}
  190. </p>
  191. </template>
  192. </tippy>
  193. <i
  194. v-else
  195. class="filters-indicator material-icons"
  196. content="No active filters"
  197. v-tippy="{ theme: 'info' }"
  198. >
  199. filter_list_off
  200. </i>
  201. </div>
  202. <div>
  203. <tippy
  204. v-if="hidableSortedColumns.length > 0"
  205. :touch="true"
  206. :interactive="true"
  207. placement="bottom"
  208. theme="dropdown"
  209. ref="editColumns"
  210. trigger="click"
  211. >
  212. <a class="button is-info" @click.prevent="true">
  213. <i class="material-icons icon-with-button">tune</i>
  214. Columns
  215. </a>
  216. <template #content>
  217. <draggable
  218. item-key="name"
  219. v-model="orderedColumns"
  220. v-bind="columnDragOptions"
  221. tag="div"
  222. draggable=".item-draggable"
  223. class="nav-dropdown-items"
  224. >
  225. <template #item="{ element: column }">
  226. <button
  227. v-if="
  228. column.name !== 'select' &&
  229. column.name !== 'placeholder'
  230. "
  231. :class="{
  232. sortable: column.sortable,
  233. 'item-draggable': column.draggable,
  234. 'nav-item': true
  235. }"
  236. @click.prevent="
  237. toggleColumnVisibility(column)
  238. "
  239. >
  240. <p
  241. class="
  242. control
  243. is-expanded
  244. checkbox-control
  245. "
  246. >
  247. <label class="switch">
  248. <input
  249. v-if="column.hidable"
  250. type="checkbox"
  251. :id="index"
  252. :checked="
  253. shownColumns.indexOf(
  254. column.name
  255. ) !== -1
  256. "
  257. @click="
  258. toggleColumnVisibility(
  259. column
  260. )
  261. "
  262. />
  263. <span
  264. :class="{
  265. slider: true,
  266. round: true,
  267. disabled:
  268. !column.hidable
  269. }"
  270. ></span>
  271. </label>
  272. <label :for="index">
  273. <span></span>
  274. <p>{{ column.displayName }}</p>
  275. </label>
  276. </p>
  277. </button>
  278. </template>
  279. </draggable>
  280. </template>
  281. </tippy>
  282. </div>
  283. </div>
  284. <div class="table-container">
  285. <table class="table">
  286. <thead>
  287. <draggable
  288. item-key="name"
  289. v-model="orderedColumns"
  290. v-bind="columnDragOptions"
  291. tag="tr"
  292. draggable=".item-draggable"
  293. >
  294. <template #item="{ element: column }">
  295. <th
  296. v-if="
  297. !(
  298. column.name === 'select' &&
  299. data.length === 0
  300. ) &&
  301. shownColumns.indexOf(column.name) !== -1
  302. "
  303. :class="{
  304. sortable: column.sortable,
  305. 'item-draggable': column.draggable
  306. }"
  307. :style="{
  308. minWidth: Number.isNaN(column.minWidth)
  309. ? column.minWidth
  310. : `${column.minWidth}px`,
  311. width: Number.isNaN(column.width)
  312. ? column.width
  313. : `${column.width}px`,
  314. maxWidth: Number.isNaN(column.maxWidth)
  315. ? column.maxWidth
  316. : `${column.maxWidth}px`
  317. }"
  318. >
  319. <p
  320. v-if="column.name === 'select'"
  321. class="checkbox"
  322. >
  323. <input
  324. type="checkbox"
  325. :checked="
  326. data.length ===
  327. selectedRows.length
  328. "
  329. @click="toggleAllRows()"
  330. />
  331. </p>
  332. <div v-else>
  333. <span>
  334. {{ column.displayName }}
  335. </span>
  336. <span
  337. v-if="column.sortable"
  338. :content="`Sort by ${column.displayName}`"
  339. v-tippy
  340. >
  341. <span
  342. v-if="
  343. !sort[column.sortProperty]
  344. "
  345. class="material-icons"
  346. @click="changeSort(column)"
  347. >
  348. unfold_more
  349. </span>
  350. <span
  351. v-if="
  352. sort[
  353. column.sortProperty
  354. ] === 'ascending'
  355. "
  356. class="material-icons active"
  357. @click="changeSort(column)"
  358. >
  359. expand_more
  360. </span>
  361. <span
  362. v-if="
  363. sort[
  364. column.sortProperty
  365. ] === 'descending'
  366. "
  367. class="material-icons active"
  368. @click="changeSort(column)"
  369. >
  370. expand_less
  371. </span>
  372. </span>
  373. </div>
  374. <div
  375. class="resizer"
  376. v-if="column.resizable"
  377. @mousedown.prevent.stop="
  378. columnResizingMouseDown(
  379. column,
  380. $event
  381. )
  382. "
  383. @mouseup="columnResizingMouseUp()"
  384. @dblclick="columnResetWidth(column)"
  385. ></div>
  386. </th>
  387. </template>
  388. </draggable>
  389. </thead>
  390. <tbody>
  391. <tr
  392. v-for="(item, itemIndex) in data"
  393. :key="item._id"
  394. :class="{
  395. selected: item.selected,
  396. highlighted: item.highlighted
  397. }"
  398. >
  399. <td
  400. v-for="column in sortedFilteredColumns"
  401. :key="`${item._id}-${column.name}`"
  402. >
  403. <slot
  404. :name="`column-${column.name}`"
  405. :item="item"
  406. v-if="
  407. column.properties.length === 0 ||
  408. column.properties.every(
  409. property =>
  410. item[property] !== undefined
  411. )
  412. "
  413. ></slot>
  414. <p
  415. class="checkbox"
  416. v-if="column.name === 'select'"
  417. >
  418. <input
  419. type="checkbox"
  420. :checked="item.selected"
  421. @click="
  422. toggleSelectedRow(itemIndex, $event)
  423. "
  424. />
  425. </p>
  426. <div
  427. class="resizer"
  428. v-if="column.resizable"
  429. @mousedown.prevent.stop="
  430. columnResizingMouseDown(column, $event)
  431. "
  432. @mouseup="columnResizingMouseUp()"
  433. @dblclick="columnResetWidth(column)"
  434. ></div>
  435. </td>
  436. </tr>
  437. </tbody>
  438. </table>
  439. </div>
  440. <div class="table-footer">
  441. <div class="page-controls">
  442. <button
  443. :class="{ disabled: page === 1 }"
  444. class="button is-primary material-icons"
  445. :disabled="page === 1"
  446. @click="changePage(1)"
  447. content="First Page"
  448. v-tippy
  449. >
  450. skip_previous
  451. </button>
  452. <button
  453. :class="{ disabled: page === 1 }"
  454. class="button is-primary material-icons"
  455. :disabled="page === 1"
  456. @click="changePage(page - 1)"
  457. content="Previous Page"
  458. v-tippy
  459. >
  460. fast_rewind
  461. </button>
  462. <p>Page {{ page }} / {{ lastPage }}</p>
  463. <button
  464. :class="{ disabled: page === lastPage }"
  465. class="button is-primary material-icons"
  466. :disabled="page === lastPage"
  467. @click="changePage(page + 1)"
  468. content="Next Page"
  469. v-tippy
  470. >
  471. fast_forward
  472. </button>
  473. <button
  474. :class="{ disabled: page === lastPage }"
  475. class="button is-primary material-icons"
  476. :disabled="page === lastPage"
  477. @click="changePage(lastPage)"
  478. content="Last Page"
  479. v-tippy
  480. >
  481. skip_next
  482. </button>
  483. </div>
  484. <div class="page-size">
  485. <div class="control">
  486. <label class="label">Items per page</label>
  487. <p class="control select">
  488. <select
  489. v-model.number="pageSize"
  490. @change="changePageSize()"
  491. >
  492. <option value="10">10</option>
  493. <option value="25">25</option>
  494. <option value="50">50</option>
  495. <option value="100">100</option>
  496. <option value="250">250</option>
  497. <option value="500">500</option>
  498. <option value="1000">1000</option>
  499. </select>
  500. </p>
  501. </div>
  502. </div>
  503. </div>
  504. </div>
  505. <div
  506. v-if="selectedRows.length > 0"
  507. class="bulk-popup"
  508. :style="{
  509. top: bulkPopup.top + 'px',
  510. left: bulkPopup.left + 'px'
  511. }"
  512. >
  513. <button
  514. class="button is-primary"
  515. :content="
  516. selectedRows.length === 1
  517. ? `${selectedRows.length} row selected`
  518. : `${selectedRows.length} rows selected`
  519. "
  520. v-tippy="{ theme: 'info' }"
  521. >
  522. {{ selectedRows.length }}
  523. </button>
  524. <slot name="bulk-actions" :item="selectedRows" />
  525. <div class="right">
  526. <slot name="bulk-actions-right" :item="selectedRows" />
  527. <span
  528. class="material-icons drag-icon"
  529. @mousedown.left="onDragBox"
  530. @dblclick="resetBulkActionsPosition()"
  531. >
  532. drag_indicator
  533. </span>
  534. </div>
  535. </div>
  536. </div>
  537. </template>
  538. <script>
  539. import { mapGetters } from "vuex";
  540. import draggable from "vuedraggable";
  541. import Toast from "toasters";
  542. import ws from "@/ws";
  543. export default {
  544. components: {
  545. draggable
  546. },
  547. props: {
  548. /*
  549. Column properties:
  550. name: Unique lowercase name
  551. displayName: Nice name for the column header
  552. properties: The properties this column needs to show data
  553. sortable: Boolean for whether the order of a particular column can be changed
  554. sortProperty: The property the backend will sort on if this column gets sorted, e.g. title
  555. hidable: Boolean for whether a column can be hidden
  556. defaultVisibility: Default visibility for a column, either "shown" or "hidden"
  557. draggable: Boolean for whether a column can be dragged/reordered,
  558. resizable: Boolean for whether a column can be resized
  559. minWidth: Minimum width of column, e.g. 50px
  560. width: Width of column, e.g. 100px
  561. maxWidth: Maximum width of column, e.g. 150px
  562. */
  563. columnDefault: { type: Object, default: () => {} },
  564. columns: { type: Array, default: null },
  565. filters: { type: Array, default: null },
  566. dataAction: { type: String, default: null }
  567. },
  568. data() {
  569. return {
  570. page: 1,
  571. pageSize: 10,
  572. data: [],
  573. count: 0, // TODO Rename
  574. sort: {},
  575. orderedColumns: [],
  576. shownColumns: [],
  577. columnDragOptions() {
  578. return {
  579. animation: 200,
  580. group: "columns",
  581. disabled: false,
  582. ghostClass: "draggable-list-ghost",
  583. filter: ".ignore-elements",
  584. fallbackTolerance: 50
  585. };
  586. },
  587. editingFilters: [],
  588. appliedFilters: [],
  589. filterOperator: "or",
  590. appliedFilterOperator: "or",
  591. filterOperators: [
  592. {
  593. name: "or",
  594. displayName: "OR"
  595. },
  596. {
  597. name: "and",
  598. displayName: "AND"
  599. },
  600. {
  601. name: "nor",
  602. displayName: "NOR"
  603. }
  604. ],
  605. resizing: {},
  606. allFilterTypes: {
  607. contains: {
  608. name: "contains",
  609. displayName: "Contains"
  610. },
  611. exact: {
  612. name: "exact",
  613. displayName: "Exact"
  614. },
  615. regex: {
  616. name: "regex",
  617. displayName: "Regex"
  618. }
  619. },
  620. bulkPopup: {
  621. top: 0,
  622. left: 0,
  623. pos1: 0,
  624. pos2: 0,
  625. pos3: 0,
  626. pos4: 0
  627. }
  628. };
  629. },
  630. computed: {
  631. properties() {
  632. return Array.from(
  633. new Set(
  634. this.sortedFilteredColumns.flatMap(
  635. column => column.properties
  636. )
  637. )
  638. );
  639. },
  640. lastPage() {
  641. return Math.ceil(this.count / this.pageSize);
  642. },
  643. sortedFilteredColumns() {
  644. return this.orderedColumns.filter(
  645. column => this.shownColumns.indexOf(column.name) !== -1
  646. );
  647. },
  648. hidableSortedColumns() {
  649. return this.orderedColumns.filter(column => column.hidable);
  650. },
  651. lastSelectedItemIndex() {
  652. return this.data.findIndex(item => item.highlighted);
  653. },
  654. selectedRows() {
  655. return this.data.filter(data => data.selected);
  656. },
  657. ...mapGetters({
  658. socket: "websockets/getSocket"
  659. })
  660. },
  661. mounted() {
  662. this.orderedColumns = [
  663. {
  664. name: "select",
  665. displayName: "",
  666. properties: [],
  667. sortable: false,
  668. hidable: false,
  669. draggable: false,
  670. resizable: false,
  671. minWidth: 47,
  672. defaultWidth: 47,
  673. maxWidth: 47
  674. },
  675. ...this.columns.map(column => ({
  676. ...this.columnDefault,
  677. ...column
  678. })),
  679. {
  680. name: "placeholder",
  681. displayName: "",
  682. properties: [],
  683. sortable: false,
  684. hidable: false,
  685. draggable: false,
  686. resizable: false,
  687. minWidth: "auto",
  688. width: "auto",
  689. maxWidth: "auto"
  690. }
  691. ];
  692. // A column will be shown if the defaultVisibility is set to shown, OR if the defaultVisibility is not set to shown and hidable is false
  693. this.shownColumns = this.orderedColumns
  694. .filter(column => column.defaultVisibility !== "hidden")
  695. .map(column => column.name);
  696. this.recalculateWidths();
  697. const pageSize = parseInt(localStorage.getItem("adminPageSize"));
  698. if (!Number.isNaN(pageSize)) this.pageSize = pageSize;
  699. this.resetBulkActionsPosition();
  700. ws.onConnect(this.init);
  701. },
  702. methods: {
  703. init() {
  704. this.getData();
  705. },
  706. getData() {
  707. this.socket.dispatch(
  708. this.dataAction,
  709. this.page,
  710. this.pageSize,
  711. this.properties,
  712. this.sort,
  713. this.appliedFilters,
  714. this.appliedFilterOperator,
  715. res => {
  716. console.log(111, res);
  717. if (res.status === "success") {
  718. const { data, count } = res.data;
  719. this.data = data;
  720. this.count = count;
  721. } else {
  722. new Toast(res.message);
  723. }
  724. }
  725. );
  726. },
  727. changePageSize() {
  728. this.getData();
  729. localStorage.setItem("adminPageSize", this.pageSize);
  730. },
  731. changePage(page) {
  732. if (page < 1) return;
  733. if (page > this.lastPage) return;
  734. if (page === this.page) return;
  735. this.page = page;
  736. this.getData();
  737. },
  738. changeSort(column) {
  739. if (column.sortable) {
  740. const { sortProperty } = column;
  741. if (this.sort[sortProperty] === undefined)
  742. this.sort[sortProperty] = "ascending";
  743. else if (this.sort[sortProperty] === "ascending")
  744. this.sort[sortProperty] = "descending";
  745. else if (this.sort[sortProperty] === "descending")
  746. delete this.sort[sortProperty];
  747. this.getData();
  748. }
  749. },
  750. toggleColumnVisibility(column) {
  751. if (this.shownColumns.indexOf(column.name) !== -1) {
  752. if (this.shownColumns.length <= 3)
  753. return new Toast(
  754. `Unable to hide column ${column.displayName}, there must be at least 1 visibile column`
  755. );
  756. this.shownColumns.splice(
  757. this.shownColumns.indexOf(column.name),
  758. 1
  759. );
  760. } else {
  761. this.shownColumns.push(column.name);
  762. }
  763. this.recalculateWidths();
  764. return this.getData();
  765. },
  766. toggleSelectedRow(itemIndex, event) {
  767. const { shiftKey, ctrlKey } = event;
  768. // Shift was pressed, so attempt to select all items between the clicked item and last clicked item
  769. if (shiftKey) {
  770. // If there is a last clicked item
  771. if (this.lastSelectedItemIndex >= 0) {
  772. // Clicked item is lower than last item, so select upwards until it reaches the last selected item
  773. if (itemIndex > this.lastSelectedItemIndex) {
  774. for (
  775. let itemIndexUp = itemIndex;
  776. itemIndexUp > this.lastSelectedItemIndex;
  777. itemIndexUp -= 1
  778. ) {
  779. this.data[itemIndexUp].selected = true;
  780. }
  781. }
  782. // Clicked item is higher than last item, so select downwards until it reaches the last selected item
  783. else if (itemIndex < this.lastSelectedItemIndex) {
  784. for (
  785. let itemIndexDown = itemIndex;
  786. itemIndexDown < this.lastSelectedItemIndex;
  787. itemIndexDown += 1
  788. ) {
  789. this.data[itemIndexDown].selected = true;
  790. }
  791. }
  792. }
  793. }
  794. // Ctrl was pressed, so attempt to unselect all items between the clicked item and last clicked item
  795. else if (ctrlKey) {
  796. // If there is a last clicked item
  797. if (this.lastSelectedItemIndex >= 0) {
  798. // Clicked item is lower than last item, so unselect upwards until it reaches the last selected item
  799. if (itemIndex > this.lastSelectedItemIndex) {
  800. for (
  801. let itemIndexUp = itemIndex;
  802. itemIndexUp > this.lastSelectedItemIndex;
  803. itemIndexUp -= 1
  804. ) {
  805. this.data[itemIndexUp].selected = false;
  806. }
  807. }
  808. // Clicked item is higher than last item, so unselect downwards until it reaches the last selected item
  809. else if (itemIndex < this.lastSelectedItemIndex) {
  810. for (
  811. let itemIndexDown = itemIndex;
  812. itemIndexDown < this.lastSelectedItemIndex;
  813. itemIndexDown += 1
  814. ) {
  815. this.data[itemIndexDown].selected = false;
  816. }
  817. }
  818. }
  819. }
  820. // Neither ctrl nor shift were pressed, so toggle clicked item
  821. else {
  822. this.data[itemIndex].selected = !this.data[itemIndex].selected;
  823. }
  824. // Set the last clicked item to no longer be highlighted, if it exists
  825. if (this.lastSelectedItemIndex >= 0)
  826. this.data[this.lastSelectedItemIndex].highlighted = false;
  827. // Set the clicked item to be highlighted
  828. this.data[itemIndex].highlighted = true;
  829. },
  830. toggleAllRows() {
  831. if (this.data.length > this.selectedRows.length) {
  832. this.data = this.data.map(row => ({ ...row, selected: true }));
  833. } else {
  834. this.data = this.data.map(row => ({ ...row, selected: false }));
  835. }
  836. },
  837. addFilterItem() {
  838. this.editingFilters.push({
  839. data: "",
  840. filter: this.addFilterValue,
  841. filterType: this.addFilterValue.defaultFilterType
  842. });
  843. },
  844. removeFilterItem(index) {
  845. this.editingFilters.splice(index, 1);
  846. },
  847. columnResizingMouseDown(column, event) {
  848. this.resizing.resizing = true;
  849. this.resizing.resizingColumn = column;
  850. this.resizing.width = event.target.parentElement.offsetWidth;
  851. this.resizing.lastX = event.x;
  852. },
  853. columnResizingMouseMove(event) {
  854. if (this.resizing.resizing) {
  855. if (event.buttons !== 1) {
  856. this.resizing.resizing = false;
  857. }
  858. this.resizing.width -= this.resizing.lastX - event.x;
  859. this.resizing.lastX = event.x;
  860. if (
  861. this.resizing.resizingColumn.minWidth &&
  862. this.resizing.resizingColumn.maxWidth
  863. ) {
  864. this.resizing.resizingColumn.width = Math.max(
  865. Math.min(
  866. this.resizing.resizingColumn.maxWidth,
  867. this.resizing.width
  868. ),
  869. this.resizing.resizingColumn.minWidth
  870. );
  871. } else if (this.resizing.resizingColumn.minWidth) {
  872. this.resizing.resizingColumn.width = Math.max(
  873. this.resizing.width,
  874. this.resizing.resizingColumn.minWidth
  875. );
  876. } else if (this.resizing.resizingColumn.maxWidth) {
  877. this.resizing.resizingColumn.width = Math.min(
  878. this.resizing.resizingColumn.maxWidth,
  879. this.resizing.width
  880. );
  881. } else {
  882. this.resizing.resizingColumn.width = this.resizing.width;
  883. }
  884. this.resizing.width = this.resizing.resizingColumn.width;
  885. console.log(`New width: ${this.resizing.width}px`);
  886. }
  887. },
  888. columnResizingMouseUp() {
  889. this.resizing.resizing = false;
  890. },
  891. columnResetWidth(column) {
  892. const index = this.orderedColumns.indexOf(column);
  893. if (column.defaultWidth && !Number.isNaN(column.defaultWidth))
  894. this.orderedColumns[index].width = column.defaultWidth;
  895. else if (
  896. column.calculatedWidth &&
  897. !Number.isNaN(column.calculatedWidth)
  898. )
  899. this.orderedColumns[index].width = column.calculatedWidth;
  900. },
  901. filterTypes(filter) {
  902. if (!filter || !filter.filterTypes) return [];
  903. return filter.filterTypes.map(
  904. filterType => this.allFilterTypes[filterType]
  905. );
  906. },
  907. changeFilterType(index) {
  908. this.editingFilters[index].filterType =
  909. this.editingFilters[index].filter.defaultFilterType;
  910. },
  911. onDragBox(e) {
  912. const e1 = e || window.event;
  913. e1.preventDefault();
  914. this.bulkPopup.pos3 = e1.clientX;
  915. this.bulkPopup.pos4 = e1.clientY;
  916. document.onmousemove = e => {
  917. const e2 = e || window.event;
  918. e2.preventDefault();
  919. // calculate the new cursor position:
  920. this.bulkPopup.pos1 = this.bulkPopup.pos3 - e.clientX;
  921. this.bulkPopup.pos2 = this.bulkPopup.pos4 - e.clientY;
  922. this.bulkPopup.pos3 = e.clientX;
  923. this.bulkPopup.pos4 = e.clientY;
  924. // set the element's new position:
  925. this.bulkPopup.top -= this.bulkPopup.pos2;
  926. this.bulkPopup.left -= this.bulkPopup.pos1;
  927. if (this.bulkPopup.top < 0) this.bulkPopup.top = 0;
  928. if (this.bulkPopup.top > document.body.clientHeight - 50)
  929. this.bulkPopup.top = document.body.clientHeight - 50;
  930. if (this.bulkPopup.left < 0) this.bulkPopup.left = 0;
  931. if (this.bulkPopup.left > document.body.clientWidth - 400)
  932. this.bulkPopup.left = document.body.clientWidth - 400;
  933. };
  934. document.onmouseup = () => {
  935. document.onmouseup = null;
  936. document.onmousemove = null;
  937. };
  938. },
  939. resetBulkActionsPosition() {
  940. this.bulkPopup.top = document.body.clientHeight - 56;
  941. this.bulkPopup.left = document.body.clientWidth / 2 - 200;
  942. },
  943. applyFilterAndGetData() {
  944. this.appliedFilters = JSON.parse(
  945. JSON.stringify(this.editingFilters)
  946. );
  947. this.appliedFilterOperator = this.filterOperator;
  948. this.getData();
  949. },
  950. recalculateWidths() {
  951. let noWidthCount = 0;
  952. let calculatedWidth = 0;
  953. this.orderedColumns.forEach(column => {
  954. if (this.shownColumns.indexOf(column.name) !== -1)
  955. if (Number.isFinite(column.width)) {
  956. calculatedWidth += column.width;
  957. } else if (Number.isFinite(column.defaultWidth)) {
  958. calculatedWidth += column.defaultWidth;
  959. } else {
  960. noWidthCount += 1;
  961. }
  962. });
  963. calculatedWidth = Math.floor(
  964. (Math.min(1880, document.body.clientWidth) - calculatedWidth) /
  965. (noWidthCount - 1)
  966. );
  967. this.orderedColumns = this.orderedColumns.map(column => {
  968. const orderedColumn = column;
  969. if (
  970. this.shownColumns.indexOf(orderedColumn.name) !== -1 &&
  971. !Number.isFinite(orderedColumn.width)
  972. ) {
  973. if (Number.isFinite(orderedColumn.defaultWidth)) {
  974. orderedColumn.width = orderedColumn.defaultWidth;
  975. } else {
  976. // eslint-disable-next-line no-param-reassign
  977. orderedColumn.width = orderedColumn.calculatedWidth =
  978. Math.min(
  979. Math.max(
  980. orderedColumn.minWidth || 100,
  981. calculatedWidth
  982. ),
  983. orderedColumn.maxWidth || 1000
  984. );
  985. }
  986. }
  987. return orderedColumn;
  988. });
  989. }
  990. }
  991. };
  992. </script>
  993. <style lang="scss" scoped>
  994. .night-mode {
  995. .table-outer-container {
  996. .table-container .table {
  997. &,
  998. thead th {
  999. background-color: var(--dark-grey-3);
  1000. color: var(--light-grey-2);
  1001. }
  1002. tr {
  1003. th,
  1004. td {
  1005. border-color: var(--dark-grey) !important;
  1006. &:first-child {
  1007. background-color: var(--dark-grey-3) !important;
  1008. }
  1009. }
  1010. &:nth-child(even) {
  1011. &,
  1012. td:first-child {
  1013. background-color: var(--dark-grey-2) !important;
  1014. }
  1015. }
  1016. &:hover,
  1017. &:focus,
  1018. &.highlighted {
  1019. th,
  1020. td {
  1021. &,
  1022. &:first-child {
  1023. background-color: var(--dark-grey-4) !important;
  1024. }
  1025. }
  1026. }
  1027. }
  1028. }
  1029. .table-header,
  1030. .table-footer {
  1031. background-color: var(--dark-grey-3);
  1032. color: var(--light-grey-2);
  1033. }
  1034. }
  1035. .bulk-popup {
  1036. border: 0;
  1037. background-color: var(--dark-grey-2);
  1038. color: var(--white);
  1039. .material-icons {
  1040. color: var(--white);
  1041. }
  1042. }
  1043. }
  1044. .table-outer-container {
  1045. border-radius: 5px;
  1046. box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1);
  1047. margin: 10px 0;
  1048. overflow: hidden;
  1049. .table-container {
  1050. overflow-x: auto;
  1051. table {
  1052. border-collapse: separate;
  1053. table-layout: fixed;
  1054. thead {
  1055. tr {
  1056. th {
  1057. height: 40px;
  1058. line-height: 40px;
  1059. border: 1px solid var(--light-grey-2);
  1060. border-width: 1px 1px 1px 0;
  1061. &:last-child {
  1062. border-width: 1px 0 1px;
  1063. }
  1064. &.sortable {
  1065. cursor: pointer;
  1066. }
  1067. & > div {
  1068. display: flex;
  1069. white-space: nowrap;
  1070. & > span {
  1071. margin-left: 5px;
  1072. &:first-child {
  1073. margin-left: 0;
  1074. margin-right: auto;
  1075. }
  1076. & > .material-icons {
  1077. font-size: 22px;
  1078. position: relative;
  1079. top: 6px;
  1080. cursor: pointer;
  1081. &.active {
  1082. color: var(--primary-color);
  1083. }
  1084. &:hover,
  1085. &:focus {
  1086. filter: brightness(90%);
  1087. }
  1088. }
  1089. }
  1090. }
  1091. }
  1092. }
  1093. }
  1094. tbody {
  1095. tr {
  1096. &.highlighted {
  1097. background-color: var(--light-grey);
  1098. }
  1099. td {
  1100. border: 1px solid var(--light-grey-2);
  1101. border-width: 0 1px 1px 0;
  1102. &:last-child {
  1103. border-width: 0 0 1px;
  1104. }
  1105. }
  1106. }
  1107. }
  1108. }
  1109. table thead tr,
  1110. table tbody tr {
  1111. th,
  1112. td {
  1113. position: relative;
  1114. white-space: nowrap;
  1115. text-overflow: ellipsis;
  1116. overflow: hidden;
  1117. &:first-child {
  1118. position: sticky;
  1119. left: 0;
  1120. background-color: var(--white);
  1121. z-index: 2;
  1122. }
  1123. .resizer {
  1124. height: 100%;
  1125. width: 5px;
  1126. background-color: transparent;
  1127. cursor: col-resize;
  1128. position: absolute;
  1129. right: 0;
  1130. top: 0;
  1131. }
  1132. }
  1133. &:nth-child(even) td:first-child {
  1134. background-color: #fafafa;
  1135. }
  1136. &:hover,
  1137. &:focus,
  1138. &.highlighted {
  1139. th,
  1140. td {
  1141. &,
  1142. &:first-child {
  1143. background-color: var(--light-grey);
  1144. }
  1145. }
  1146. }
  1147. }
  1148. }
  1149. .table-header,
  1150. .table-footer {
  1151. display: flex;
  1152. flex-direction: row;
  1153. flex-wrap: wrap;
  1154. justify-content: space-between;
  1155. line-height: 36px;
  1156. background-color: var(--white);
  1157. }
  1158. .table-header > div {
  1159. display: flex;
  1160. flex-direction: row;
  1161. > span > .button {
  1162. margin: 5px;
  1163. }
  1164. .filters-indicator {
  1165. line-height: 46px;
  1166. display: flex;
  1167. align-items: center;
  1168. column-gap: 4px;
  1169. }
  1170. }
  1171. .table-footer {
  1172. .page-controls,
  1173. .page-size > .control {
  1174. display: flex;
  1175. flex-direction: row;
  1176. margin-bottom: 0 !important;
  1177. button {
  1178. margin: 5px;
  1179. font-size: 20px;
  1180. }
  1181. p,
  1182. label {
  1183. margin: 5px;
  1184. font-size: 14px;
  1185. font-weight: 600;
  1186. }
  1187. &.select::after {
  1188. top: 18px;
  1189. }
  1190. }
  1191. }
  1192. }
  1193. .control.is-grouped {
  1194. display: flex;
  1195. & > .control {
  1196. &.label {
  1197. height: 36px;
  1198. background-color: var(--white);
  1199. border: 1px solid var(--light-grey-2);
  1200. color: var(--dark-grey-2);
  1201. appearance: none;
  1202. border-radius: 3px;
  1203. font-size: 14px;
  1204. line-height: 34px;
  1205. padding-left: 8px;
  1206. padding-right: 8px;
  1207. }
  1208. &.select.is-expanded > select {
  1209. width: 100%;
  1210. }
  1211. & > input,
  1212. & > select,
  1213. & > .button,
  1214. &.label {
  1215. border-radius: 0;
  1216. }
  1217. &:first-child {
  1218. & > input,
  1219. & > select,
  1220. & > .button,
  1221. &.label {
  1222. border-radius: 5px 0 0 5px;
  1223. }
  1224. }
  1225. &:last-child {
  1226. & > input,
  1227. & > select,
  1228. & > .button,
  1229. &.label {
  1230. border-radius: 0 5px 5px 0;
  1231. }
  1232. }
  1233. & > .button {
  1234. font-size: 22px;
  1235. }
  1236. }
  1237. }
  1238. .advanced-filter-bottom {
  1239. display: flex;
  1240. .button {
  1241. font-size: 16px !important;
  1242. width: 100%;
  1243. }
  1244. .control {
  1245. margin: 0 !important;
  1246. }
  1247. }
  1248. .bulk-popup {
  1249. display: flex;
  1250. position: fixed;
  1251. flex-direction: row;
  1252. width: 100%;
  1253. max-width: 400px;
  1254. line-height: 36px;
  1255. z-index: 5;
  1256. border: 1px solid var(--light-grey-3);
  1257. border-radius: 5px;
  1258. box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
  1259. background-color: var(--white);
  1260. color: var(--dark-grey);
  1261. padding: 5px;
  1262. .right {
  1263. display: flex;
  1264. flex-direction: row;
  1265. margin-left: auto;
  1266. }
  1267. .drag-icon {
  1268. position: relative;
  1269. top: 6px;
  1270. color: var(--dark-grey);
  1271. cursor: move;
  1272. }
  1273. }
  1274. </style>