|
|
@@ -0,0 +1,776 @@
|
|
|
+<template>
|
|
|
+ <div class="material-table" ref="table">
|
|
|
+ <div class="table-header">
|
|
|
+ <span class="table-title">{{ title }}</span>
|
|
|
+ <div class="actions">
|
|
|
+ <button
|
|
|
+ v-for="(button, index) in shownCustomButtons"
|
|
|
+ :key="index"
|
|
|
+ href="javascript:undefined"
|
|
|
+ class="btn small secondary"
|
|
|
+ @click="button.onclick"
|
|
|
+ >
|
|
|
+ <i class="material-icons">{{ button.icon }}</i>
|
|
|
+ </button>
|
|
|
+ <button v-if="exportable" class="btn icon small secondary" @click="exportExcel">
|
|
|
+ <i class="material-icons">download</i>
|
|
|
+ </button>
|
|
|
+ <button v-if="searchable" class="btn icon small secondary" @click="search">
|
|
|
+ <i class="material-icons">search</i>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-show="searching">
|
|
|
+ <div id="search-input-container" @click="focusSearchInput">
|
|
|
+ <input
|
|
|
+ ref="searchinput"
|
|
|
+ id="search-input"
|
|
|
+ type="search"
|
|
|
+ class="form-control"
|
|
|
+ :size="Math.max(searchInput.length, lang['search_data'].length)"
|
|
|
+ :placeholder="lang['search_data']"
|
|
|
+ v-model="searchInput"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <table class="datatable" ref="table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th
|
|
|
+ v-for="(column, index) in columns"
|
|
|
+ :key="index"
|
|
|
+ :class="columnClass(column, index)"
|
|
|
+ :style="{ width: column.width ? column.width : 'auto' }"
|
|
|
+ @click="sort(index)"
|
|
|
+ >
|
|
|
+ {{ column.label }}
|
|
|
+ </th>
|
|
|
+ <slot name="thead-tr" />
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+
|
|
|
+ <tbody>
|
|
|
+ <tr
|
|
|
+ v-for="(row, index) in paginated"
|
|
|
+ :key="index"
|
|
|
+ :class="{ clickable: clickable }"
|
|
|
+ @click="click(row)"
|
|
|
+ >
|
|
|
+ <td
|
|
|
+ v-for="(column, columnIndex) in columns"
|
|
|
+ :key="columnIndex"
|
|
|
+ :class="[column.numeric ? 'numeric' : '', column.class]"
|
|
|
+ >
|
|
|
+ <div v-if="!column.html">
|
|
|
+ {{ row[column.field] }}
|
|
|
+ </div>
|
|
|
+ <div v-if="column.html" v-html="row[column.field]" />
|
|
|
+ </td>
|
|
|
+ <slot name="tbody-tr" :row="row" />
|
|
|
+ </tr>
|
|
|
+
|
|
|
+ <template v-if="rows.length === 0">
|
|
|
+ <template v-if="loadingAnimation === true">
|
|
|
+ <tr v-for="n in currentPerPage === -1 ? 10 : currentPerPage" :key="n">
|
|
|
+ <td :colspan="columns.length">
|
|
|
+ <div class="loading-skeleton"></div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <tr>
|
|
|
+ <td :colspan="columns.length" style="text-align: center; font-size: 1.2rem">
|
|
|
+ {{ lang["empty"] }}
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+
|
|
|
+ <div v-if="paginate" class="table-footer">
|
|
|
+ <div :class="{ 'datatable-length': true, rtl: lang.__is_rtl }">
|
|
|
+ <label>
|
|
|
+ <span>{{ lang["rows_per_page"] }}:</span>
|
|
|
+ <select class="browser-default" @change="onTableLength">
|
|
|
+ <option
|
|
|
+ v-for="(option, index) in perPageOptions"
|
|
|
+ :key="index"
|
|
|
+ :value="option"
|
|
|
+ :selected="option == currentPerPage"
|
|
|
+ >
|
|
|
+ {{ option === -1 ? lang["all"] : option }}
|
|
|
+ </option>
|
|
|
+ </select>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ <div :class="{ 'datatable-info': true, rtl: lang.__is_rtl }">
|
|
|
+ {{ (currentPage - 1) * currentPerPage ? (currentPage - 1) * currentPerPage : 1 }} -
|
|
|
+ {{ Math.min(searchedRows.length, currentPerPage * currentPage) }}
|
|
|
+ {{ lang["out_of_pages"] }}
|
|
|
+ {{ searchedRows.length }}
|
|
|
+ </div>
|
|
|
+ <div class="material-pagination">
|
|
|
+ <button class="btn icon small ghost" tabindex="0" @click.prevent="previousPage">
|
|
|
+ <i class="material-icons">chevron_left</i>
|
|
|
+ </button>
|
|
|
+ <button class="btn icon small ghost" tabindex="0" @click.prevent="nextPage">
|
|
|
+ <i class="material-icons">chevron_right</i>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts">
|
|
|
+import { defineComponent, PropType } from "vue";
|
|
|
+import Fuse from "fuse.js";
|
|
|
+import { Dictionary } from "lodash";
|
|
|
+
|
|
|
+interface DataTableLocale {
|
|
|
+ rows_per_page: string;
|
|
|
+ out_of_pages: string;
|
|
|
+ all: string;
|
|
|
+ search_data: string;
|
|
|
+ empty: string;
|
|
|
+}
|
|
|
+var locales: Dictionary<DataTableLocale> = {
|
|
|
+ en: {
|
|
|
+ rows_per_page: "Rows per page",
|
|
|
+ out_of_pages: "of",
|
|
|
+ all: "All",
|
|
|
+ search_data: "Search data",
|
|
|
+ empty: "No data to be displayed",
|
|
|
+ },
|
|
|
+ fr: {
|
|
|
+ rows_per_page: "Lignes par page",
|
|
|
+ out_of_pages: "de",
|
|
|
+ all: "Tout",
|
|
|
+ search_data: "Rechercher des données",
|
|
|
+ empty: "Pas de donnée à afficher",
|
|
|
+ },
|
|
|
+};
|
|
|
+interface CustomButton {
|
|
|
+ icon: string;
|
|
|
+ hide: boolean;
|
|
|
+ onclick: (e: MouseEvent) => void;
|
|
|
+}
|
|
|
+interface DataTableColumn<T> {
|
|
|
+ label: string; // Column name
|
|
|
+ field: keyof T; // Field name from row
|
|
|
+ numeric: boolean; // Affects sorting
|
|
|
+ html: boolean;
|
|
|
+ export?: keyof T; // Exported field name from row
|
|
|
+ class?: string; // Exported field name from row
|
|
|
+ width?: number; // Fixed width of the column
|
|
|
+}
|
|
|
+export interface DataTableData<T> {
|
|
|
+ columns: Array<DataTableColumn<T>>;
|
|
|
+ items: Array<T>;
|
|
|
+}
|
|
|
+export interface DataTableObject {
|
|
|
+ [k: string]: string | number;
|
|
|
+}
|
|
|
+
|
|
|
+export default defineComponent({
|
|
|
+ name: "DataTable",
|
|
|
+ emits: ["row-click"],
|
|
|
+ props: {
|
|
|
+ title: {
|
|
|
+ type: String,
|
|
|
+ required: true,
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ type: Object as PropType<DataTableData<DataTableObject>>,
|
|
|
+ required: true,
|
|
|
+ },
|
|
|
+ clickable: {
|
|
|
+ type: Boolean,
|
|
|
+ required: false,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+ customButtons: {
|
|
|
+ type: Array as PropType<Array<CustomButton>>,
|
|
|
+ required: false,
|
|
|
+ default: () => [],
|
|
|
+ },
|
|
|
+ perPage: {
|
|
|
+ type: Array as PropType<Array<number>>,
|
|
|
+ required: false,
|
|
|
+ default: () => [10, 20, 30, 40, 50],
|
|
|
+ },
|
|
|
+ defaultPerPage: {
|
|
|
+ type: Number,
|
|
|
+ required: false,
|
|
|
+ default: null,
|
|
|
+ },
|
|
|
+ sortable: {
|
|
|
+ type: Boolean,
|
|
|
+ required: false,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+ searchable: {
|
|
|
+ type: Boolean,
|
|
|
+ required: false,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+ exactSearch: {
|
|
|
+ type: Boolean,
|
|
|
+ required: false,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ serverSearch: {
|
|
|
+ type: Boolean,
|
|
|
+ required: false,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ serverSearchFunc: {
|
|
|
+ type: Function,
|
|
|
+ required: false,
|
|
|
+ },
|
|
|
+ paginate: {
|
|
|
+ type: Boolean,
|
|
|
+ required: false,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+ exportable: {
|
|
|
+ type: Boolean,
|
|
|
+ required: false,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+ locale: {
|
|
|
+ type: String,
|
|
|
+ required: false,
|
|
|
+ default: "en",
|
|
|
+ },
|
|
|
+ initSortCol: {
|
|
|
+ type: Number,
|
|
|
+ required: false,
|
|
|
+ default: -1,
|
|
|
+ },
|
|
|
+ loadingAnimation: {
|
|
|
+ type: Boolean,
|
|
|
+ required: false,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ data: () => ({
|
|
|
+ currentPage: 1,
|
|
|
+ currentPerPage: 10,
|
|
|
+ sortColumn: -1,
|
|
|
+ sortType: "asc",
|
|
|
+ searching: false,
|
|
|
+ searchInput: "",
|
|
|
+ }),
|
|
|
+ methods: {
|
|
|
+ nextPage() {
|
|
|
+ if (this.searchedRows.length > this.currentPerPage * this.currentPage) ++this.currentPage;
|
|
|
+ },
|
|
|
+ previousPage() {
|
|
|
+ if (this.currentPage > 1) --this.currentPage;
|
|
|
+ },
|
|
|
+ onTableLength(e: { target: { value: string } }) {
|
|
|
+ this.currentPerPage = parseInt(e.target.value);
|
|
|
+ },
|
|
|
+ sort(index: number) {
|
|
|
+ if (!this.sortable) return;
|
|
|
+ if (this.sortColumn === index) {
|
|
|
+ this.sortType = this.sortType === "asc" ? "desc" : "asc";
|
|
|
+ } else {
|
|
|
+ this.sortType = "asc";
|
|
|
+ this.sortColumn = index;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ search() {
|
|
|
+ this.searching = !this.searching;
|
|
|
+ setTimeout(this.focusSearchInput, 100);
|
|
|
+ },
|
|
|
+ focusSearchInput() {
|
|
|
+ const searchbox = this.$refs["searchinput"] as HTMLElement;
|
|
|
+ if (searchbox) searchbox.focus();
|
|
|
+ },
|
|
|
+ columnClass(column: DataTableColumn<DataTableObject>, index: number) {
|
|
|
+ const output: Dictionary<boolean> = {
|
|
|
+ sorting: this.sortable,
|
|
|
+ "sorting-desc": this.sortColumn === index && this.sortType === "desc",
|
|
|
+ "sorting-asc": this.sortColumn === index && this.sortType !== "desc",
|
|
|
+ numeric: column.numeric,
|
|
|
+ };
|
|
|
+ if (column.class) {
|
|
|
+ output[column.class] = true;
|
|
|
+ }
|
|
|
+ return output;
|
|
|
+ },
|
|
|
+ click(row: DataTableObject): void {
|
|
|
+ if (!this.clickable) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (getSelection()?.toString()) {
|
|
|
+ // Return if some text is selected instead of firing the row-click event.
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.$emit("row-click", row);
|
|
|
+ },
|
|
|
+ exportExcel() {
|
|
|
+ const mimeType = "data:text/csv;charset=utf-8";
|
|
|
+ const strSpeparator = ",";
|
|
|
+ const sepReg = new RegExp("[" + strSpeparator + '\r\n"]');
|
|
|
+ let csvContent =
|
|
|
+ this.columns.map((o: DataTableColumn<DataTableObject>) => o.label).join(strSpeparator) +
|
|
|
+ "\r\n";
|
|
|
+ const escapeText = function (str: string) {
|
|
|
+ return sepReg.test(str)
|
|
|
+ ? '"' + str.replaceAll(/"/g, '""').replaceAll(/\r/g, "\\r").replaceAll(/\n/g, "\\n") + '"'
|
|
|
+ : str;
|
|
|
+ };
|
|
|
+ this.rows.forEach((r) => {
|
|
|
+ csvContent +=
|
|
|
+ this.columns
|
|
|
+ .map((c) =>
|
|
|
+ escapeText(c.export == undefined ? r[c.field].toString() : r[c.export].toString())
|
|
|
+ )
|
|
|
+ .join(strSpeparator) + "\r\n";
|
|
|
+ });
|
|
|
+ const documentPrefix = this.title ? this.title.replace(/ /g, "-") : "Sheet";
|
|
|
+ const d = new Date();
|
|
|
+ const dummy = document.createElement("a");
|
|
|
+ dummy.href = mimeType + ", " + encodeURI(csvContent);
|
|
|
+ dummy.download =
|
|
|
+ documentPrefix +
|
|
|
+ "-" +
|
|
|
+ d.getFullYear() +
|
|
|
+ "-" +
|
|
|
+ (d.getMonth() + 1) +
|
|
|
+ "-" +
|
|
|
+ d.getDate() +
|
|
|
+ "-" +
|
|
|
+ d.getHours() +
|
|
|
+ "-" +
|
|
|
+ d.getMinutes() +
|
|
|
+ "-" +
|
|
|
+ d.getSeconds() +
|
|
|
+ ".csv";
|
|
|
+ document.body.appendChild(dummy);
|
|
|
+ dummy.click();
|
|
|
+ },
|
|
|
+ renderTable() {
|
|
|
+ let table = "<table><thead>";
|
|
|
+ table += "<tr>";
|
|
|
+ for (let i = 0; i < this.columns.length; i++) {
|
|
|
+ const column = this.columns[i];
|
|
|
+ table += "<th>";
|
|
|
+ table += column.label;
|
|
|
+ table += "</th>";
|
|
|
+ }
|
|
|
+ table += "</tr>";
|
|
|
+ table += "</thead><tbody>";
|
|
|
+ for (let i = 0; i < this.rows.length; i++) {
|
|
|
+ const row = this.rows[i];
|
|
|
+ table += "<tr>";
|
|
|
+ for (let j = 0; j < this.columns.length; j++) {
|
|
|
+ const column = this.columns[j];
|
|
|
+ table += "<td>";
|
|
|
+ table += row[column.field];
|
|
|
+ table += "</td>";
|
|
|
+ }
|
|
|
+ table += "</tr>";
|
|
|
+ }
|
|
|
+ table += "</tbody></table>";
|
|
|
+ return table;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ perPageOptions(newOptions) {
|
|
|
+ // If defaultPerPage is provided and it's a valid option, set as current per page
|
|
|
+ if (newOptions.indexOf(this.defaultPerPage) > -1) {
|
|
|
+ this.currentPerPage = this.defaultPerPage;
|
|
|
+ } else {
|
|
|
+ // Set current page to first value
|
|
|
+ this.currentPerPage = newOptions[0];
|
|
|
+ }
|
|
|
+ },
|
|
|
+ searchInput(newSearchInput) {
|
|
|
+ if (this.searching && this.serverSearch && this.serverSearchFunc)
|
|
|
+ this.serverSearchFunc(newSearchInput);
|
|
|
+ },
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ rows(): Array<DataTableObject> {
|
|
|
+ return this.data.items;
|
|
|
+ },
|
|
|
+ columns(): Array<DataTableColumn<DataTableObject>> {
|
|
|
+ return this.data.columns;
|
|
|
+ },
|
|
|
+ shownCustomButtons(): Array<CustomButton> {
|
|
|
+ return this.customButtons.filter((b) => !b.hide);
|
|
|
+ },
|
|
|
+ perPageOptions() {
|
|
|
+ let options: Array<number> = (Array.isArray(this.perPage) && this.perPage) || [
|
|
|
+ 10,
|
|
|
+ 20,
|
|
|
+ 30,
|
|
|
+ 40,
|
|
|
+ 50,
|
|
|
+ ];
|
|
|
+ // Sort options
|
|
|
+ options.sort((a: number, b: number) => a - b);
|
|
|
+ // And add "All"
|
|
|
+ options.push(-1);
|
|
|
+ return options;
|
|
|
+ },
|
|
|
+ sortedRows(): Array<DataTableObject> {
|
|
|
+ let rows = this.rows;
|
|
|
+ if (this.sortable !== false)
|
|
|
+ if (this.sortColumn > -1) {
|
|
|
+ const col = this.columns[this.sortColumn];
|
|
|
+ let cook: (x: DataTableObject) => string | number;
|
|
|
+ if (col.numeric) {
|
|
|
+ cook = (x: DataTableObject) => x[col.field] as number;
|
|
|
+ } else {
|
|
|
+ cook = (x: DataTableObject) => (x[col.field] as string).toLowerCase();
|
|
|
+ }
|
|
|
+ return rows.sort((x, y) => {
|
|
|
+ const key_x = cook(x);
|
|
|
+ const key_y = cook(y);
|
|
|
+ return (
|
|
|
+ (key_x < key_y ? -1 : key_x > key_y ? 1 : 0) * (this.sortType === "desc" ? -1 : 1)
|
|
|
+ );
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return rows;
|
|
|
+ },
|
|
|
+ searchedRows(): Array<DataTableObject> {
|
|
|
+ if (this.searching && !this.serverSearch && this.searchInput) {
|
|
|
+ const searchConfig: Fuse.IFuseOptions<DataTableObject> = {
|
|
|
+ keys: this.columns.map((c) => c.field.toString()),
|
|
|
+ };
|
|
|
+ if (this.exactSearch) {
|
|
|
+ //return only exact matches
|
|
|
+ (searchConfig.threshold = 0), (searchConfig.distance = 0);
|
|
|
+ }
|
|
|
+ return new Fuse(this.sortedRows, searchConfig)
|
|
|
+ .search(this.searchInput)
|
|
|
+ .map((o: { item: DataTableObject }) => o.item);
|
|
|
+ }
|
|
|
+ return this.sortedRows;
|
|
|
+ },
|
|
|
+ paginated(): Array<DataTableObject> {
|
|
|
+ let paginatedRows: DataTableObject[] = this.searchedRows;
|
|
|
+ if (this.paginate && this.currentPerPage !== -1)
|
|
|
+ paginatedRows = paginatedRows.slice(
|
|
|
+ (this.currentPage - 1) * this.currentPerPage,
|
|
|
+ this.currentPerPage === -1
|
|
|
+ ? paginatedRows.length + 1
|
|
|
+ : this.currentPage * this.currentPerPage
|
|
|
+ );
|
|
|
+ return paginatedRows;
|
|
|
+ },
|
|
|
+ lang(): DataTableLocale {
|
|
|
+ return this.locale in locales ? locales[this.locale] : locales["en"];
|
|
|
+ },
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ if (!(this.locale in locales))
|
|
|
+ console.error(`vue-materialize-datable: Invalid locale '${this.locale}'`);
|
|
|
+ this.sortColumn = this.initSortCol;
|
|
|
+ },
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style>
|
|
|
+.material-table {
|
|
|
+ padding: 0;
|
|
|
+ box-shadow: 0 0 2px 2px var(--color-neutral-600);
|
|
|
+}
|
|
|
+
|
|
|
+tr.clickable {
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+.actions > .btn:not(:last-child) {
|
|
|
+ margin-right: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+#search-input-container {
|
|
|
+ position: relative;
|
|
|
+ padding: 0 14px 0 24px;
|
|
|
+ border-bottom: solid 1px var(--color-accent-800);
|
|
|
+ background: var(--color-accent-950);
|
|
|
+ cursor: text;
|
|
|
+}
|
|
|
+#search-input-container:focus-within {
|
|
|
+ box-shadow: 0 0 0 2px var(--color-accent-800);
|
|
|
+ border-color: transparent;
|
|
|
+ background-color: var(--color-accent-950);
|
|
|
+}
|
|
|
+
|
|
|
+#search-input {
|
|
|
+ margin: 0;
|
|
|
+ border: transparent 0 !important;
|
|
|
+ height: 30px;
|
|
|
+ color: rgba(0, 0, 0, 0.84);
|
|
|
+ outline: 0;
|
|
|
+ background: transparent;
|
|
|
+}
|
|
|
+search-input-container::after {
|
|
|
+ content: "search";
|
|
|
+ font-family: "Material Icons";
|
|
|
+ font-weight: normal;
|
|
|
+ font-style: normal;
|
|
|
+}
|
|
|
+
|
|
|
+.datatable {
|
|
|
+ table-layout: auto;
|
|
|
+ width: 100%;
|
|
|
+ border-collapse: collapse;
|
|
|
+}
|
|
|
+
|
|
|
+.table-header {
|
|
|
+ height: 64px;
|
|
|
+ padding-left: 24px;
|
|
|
+ padding-right: 14px;
|
|
|
+ -webkit-align-items: center;
|
|
|
+ -ms-flex-align: center;
|
|
|
+ align-items: center;
|
|
|
+ display: flex;
|
|
|
+ -webkit-display: flex;
|
|
|
+ border-bottom: solid 1px #dddddd;
|
|
|
+}
|
|
|
+
|
|
|
+.table-header .actions {
|
|
|
+ display: -webkit-flex;
|
|
|
+ margin-left: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.table-footer {
|
|
|
+ height: 56px;
|
|
|
+ padding-left: 24px;
|
|
|
+ padding-right: 14px;
|
|
|
+ display: -webkit-flex;
|
|
|
+ display: flex;
|
|
|
+ -webkit-flex-direction: row;
|
|
|
+ flex-direction: row;
|
|
|
+ -webkit-justify-content: flex-end;
|
|
|
+ justify-content: flex-end;
|
|
|
+ -webkit-align-items: center;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 12px !important;
|
|
|
+ color: rgba(0, 0, 0, 0.54);
|
|
|
+}
|
|
|
+
|
|
|
+.table-footer .datatable-length {
|
|
|
+ display: -webkit-flex;
|
|
|
+ display: flex;
|
|
|
+}
|
|
|
+
|
|
|
+.table-footer .datatable-length select {
|
|
|
+ outline: none;
|
|
|
+}
|
|
|
+
|
|
|
+.table-footer label {
|
|
|
+ font-size: 12px;
|
|
|
+ color: rgba(0, 0, 0, 0.54);
|
|
|
+ display: -webkit-flex;
|
|
|
+ display: flex;
|
|
|
+ -webkit-flex-direction: row;
|
|
|
+ /* works with row or column */
|
|
|
+ flex-direction: row;
|
|
|
+ -webkit-align-items: center;
|
|
|
+ align-items: center;
|
|
|
+ -webkit-justify-content: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.table-footer .select-wrapper {
|
|
|
+ display: -webkit-flex;
|
|
|
+ display: flex;
|
|
|
+ -webkit-flex-direction: row;
|
|
|
+ /* works with row or column */
|
|
|
+ flex-direction: row;
|
|
|
+ -webkit-align-items: center;
|
|
|
+ align-items: center;
|
|
|
+ -webkit-justify-content: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.table-footer .datatable-info,
|
|
|
+.table-footer .datatable-length {
|
|
|
+ margin-right: 32px;
|
|
|
+}
|
|
|
+
|
|
|
+.table-footer .material-pagination {
|
|
|
+ display: flex;
|
|
|
+ -webkit-display: flex;
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.table-footer .material-pagination li a {
|
|
|
+ color: rgba(0, 0, 0, 0.54);
|
|
|
+ padding: 0 8px;
|
|
|
+ font-size: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.table-footer .select-wrapper input.select-dropdown {
|
|
|
+ margin: 0;
|
|
|
+ border-bottom: none;
|
|
|
+ height: auto;
|
|
|
+ line-height: normal;
|
|
|
+ font-size: 12px;
|
|
|
+ width: 40px;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+.table-footer select {
|
|
|
+ background-color: transparent;
|
|
|
+ width: auto;
|
|
|
+ padding: 0;
|
|
|
+ border: 0;
|
|
|
+ border-radius: 0;
|
|
|
+ height: auto;
|
|
|
+ margin-left: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.table-title {
|
|
|
+ font-size: 20px;
|
|
|
+ color: #000;
|
|
|
+}
|
|
|
+
|
|
|
+.datatable tr td {
|
|
|
+ padding: 0 0 0 24px;
|
|
|
+ height: 48px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: rgba(0, 0, 0, 0.87);
|
|
|
+ border-bottom: solid 1px #dddddd;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+
|
|
|
+.datatable td,
|
|
|
+.datatable th {
|
|
|
+ border-radius: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.datatable th {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #757575;
|
|
|
+ cursor: pointer;
|
|
|
+ white-space: nowrap;
|
|
|
+ padding: 0;
|
|
|
+ height: 56px;
|
|
|
+ padding-left: 24px;
|
|
|
+ vertical-align: middle;
|
|
|
+ outline: none !important;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: initial;
|
|
|
+ border-bottom: solid 1px #dddddd;
|
|
|
+}
|
|
|
+
|
|
|
+.datatable th:hover {
|
|
|
+ overflow: visible;
|
|
|
+ text-overflow: initial;
|
|
|
+}
|
|
|
+
|
|
|
+.datatable th.sorting-asc,
|
|
|
+.datatable th.sorting-desc {
|
|
|
+ color: rgba(0, 0, 0, 0.87);
|
|
|
+}
|
|
|
+
|
|
|
+.datatable th.sorting:after {
|
|
|
+ font-family: "Material Icons";
|
|
|
+ font-weight: normal;
|
|
|
+ font-style: normal;
|
|
|
+ font-size: 18px;
|
|
|
+ line-height: 1.2rem;
|
|
|
+ letter-spacing: normal;
|
|
|
+ text-transform: none;
|
|
|
+ display: inline-block;
|
|
|
+ word-wrap: normal;
|
|
|
+ -webkit-font-feature-settings: "liga";
|
|
|
+ font-feature-settings: "liga";
|
|
|
+ -webkit-font-smoothing: antialiased;
|
|
|
+ content: "arrow_back";
|
|
|
+ -webkit-transform: rotate(90deg);
|
|
|
+ transform: rotate(90deg);
|
|
|
+ display: inline-block;
|
|
|
+ vertical-align: middle;
|
|
|
+ opacity: 0;
|
|
|
+ margin-left: 2px;
|
|
|
+ margin-top: -2px;
|
|
|
+}
|
|
|
+
|
|
|
+.datatable th.sorting:hover:after,
|
|
|
+.datatable th.sorting-asc:after,
|
|
|
+.datatable th.sorting-desc:after {
|
|
|
+ opacity: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.datatable th.sorting-desc:after {
|
|
|
+ content: "arrow_forward";
|
|
|
+}
|
|
|
+
|
|
|
+.datatable tbody tr:hover {
|
|
|
+ background-color: #eee;
|
|
|
+}
|
|
|
+
|
|
|
+.datatable th:last-child,
|
|
|
+.datatable td:last-child {
|
|
|
+ padding-right: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.datatable th:first-child,
|
|
|
+.datatable td:first-child {
|
|
|
+ padding-left: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.rtl {
|
|
|
+ direction: rtl;
|
|
|
+}
|
|
|
+.loading-skeleton {
|
|
|
+ background: var(--color-neutral-800);
|
|
|
+ height: 20px;
|
|
|
+ margin: 12px 16px;
|
|
|
+ border-radius: 4px;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.loading-skeleton::before {
|
|
|
+ content: "";
|
|
|
+ display: block;
|
|
|
+ position: absolute;
|
|
|
+ left: -200px;
|
|
|
+ top: 0;
|
|
|
+ height: 100%;
|
|
|
+ width: 200px;
|
|
|
+ background: linear-gradient(
|
|
|
+ to right,
|
|
|
+ transparent 0%,
|
|
|
+ rgba(255, 255, 255, 0.4) 50%,
|
|
|
+ transparent 100%
|
|
|
+ );
|
|
|
+ animation: load 3s linear infinite;
|
|
|
+}
|
|
|
+@keyframes load {
|
|
|
+ 0% {
|
|
|
+ left: -200px;
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ left: 100%;
|
|
|
+ }
|
|
|
+}
|
|
|
+td.fitwidth {
|
|
|
+ width: 1px;
|
|
|
+ white-space: nowrap;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.overflow-cell {
|
|
|
+ max-width: 70px;
|
|
|
+}
|
|
|
+
|
|
|
+.overflow-cell div {
|
|
|
+ max-width: 100%;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+</style>
|