Reworked Device Table and added table features

This commit is contained in:
2024-05-27 18:07:30 +02:00
parent 3e5166313e
commit fa3c1b8766
9 changed files with 522 additions and 393 deletions

View File

@@ -1,167 +0,0 @@
<?php
$pagination_baseurl = $this->getUrl($Mod, "Index");
$pagination_baseurl_params = ["filter" => $filter];
$pagination_entity_name = "Device";
?>
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?>
<link href="<?= self::getResourcePath() ?>assets/css/datatables-std.css?<?= date('U') ?>" rel="stylesheet"
type="text/css"/>
<style>
</style>
<!-- start page title -->
<div class="row">
<div class="col-12">
<div class="page-title-box">
<div class="page-title-right">
<ol class="breadcrumb m-0">
<li class="breadcrumb-item"><a href="<?= self::getUrl("Dashboard") ?>"><?= MFAPPNAME_SLUG ?></a>
</li>
<li class="breadcrumb-item active">Devices</li>
</ol>
</div>
<h4 class="page-title">Devices</h4>
</div>
</div>
</div>
<!-- end page title -->
<div class="card">
<div class="card-body mb-3">
<div class="row">
<div class="col-12">
<div class="float-left">
<h4 class="header-title">Liste aller Devices</h4>
</div>
<div class="float-right ">
<a class="btn btn-primary mb-2" href="<?= self::getUrl("Device", "add") ?>"><i
class="fas fa-plus"></i><span
class="d-none d-lg-inline"> Neues Device anlegen</span></a>
</div>
</div>
</div>
<table id="datatable" class="table table-striped table-hover table-sm font-13">
<thead>
<tr>
<th class="all">Device Name</th>
<th class="text-center ">Geräte Typ</th>
<th class="text-center ">Hersteller</th>
<th class="text-center ">Pop/Adresse</th>
<th class="text-center all">IP-Adresse</th>
<th class="text-center">Mac-Adresse</th>
<th class="text-center">Seriennummer</th>
<th class="text-center">Preis</th>
<th class="text-center text-nowrap ml-2 mr-2">max. Leistung</th>
<th class="text-center">Backup</th>
<th class="edit-width"></th>
</tr>
<tr id="filterrow">
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th class="pr-1"></th>
<th></th>
</tr>
</thead>
<tbody>
<?php
foreach ($devices as $device):
if ($device->price != "0.00") {
$price = $device->price;
} else {
$price = $device->devicetype->price;
}
if ($device->power != "0.0") {
$power = $device->power;
} else {
$power = $device->devicetype->power;
}
if ($device->last_config_backup) {
if (time() - $device->last_config_backup <= 172800) {
$backup = '<i class="fa-regular fa-circle-check"><span style="display: none">OK</span></i>';
} else {
$backup = '<i class="fa-regular fa-circle-xmark" title="Letztes Configbackup älter als 48 Stunden"><span style="display: none">AGED</span></i>';
}
} else {
$backup = '<i class="fa-regular fa-ban" title="Kein Configbackup"><span style="display: none">N/A</span></i>';
}
if ($device->autobackup==1) {
$backup = '<i class="fa-regular fa-circle-a mr-1"><span style="display: none">Auto</span></i>'.$backup;
}
if ($device->pop->id) {
$destination = '<a href="' . self::getUrl("Pop", "Detail", ["id" => $device->pop->id]) . '">' . $device->pop->name . '</a>';
} else if ($device->addr_street) {
$address = $device->addr_street . " " . $device->addr_number . ", " . $device->addr_zip . " " . $device->addr_city;
$destination = '<a title="Google-Maps: ' . $address . '" class="mapsLink" href="http://maps.google.com/?q=' . $address . '" target="_blank">' . $address . '</a>';
} else if ($device->gps_lat) {
$address = $device->gps_lat . " , " . $device->gps_long;
$destination = '<a title="Google-Maps: ' . $address . '" class="mapsLink" href="http://maps.google.com/?q=' . $address . '" target="_blank">' . $address . '</a>';
} else {
$destination = '';
}
?>
<tr>
<td class="text-nowrap">
<a class=" text-nowrap"
href="<?= self::getUrl("Device", "Detail", ["id" => $device->id]) ?>"><?= $device->name ?></a>
</td>
<td class="text-center"><?= $device->devicetype->name ?></td>
<td class="text-center"><?= $device->devicetype->devicemanufactor->name ?></td>
<td class="text-center text-nowrap"><?= $destination ?></td>
<td class="text-center"><?= $device->ip ?></td>
<td class="text-center"><?= $device->mac ?></td>
<td class="text-center"><?= $device->serial ?></td>
<td class="text-right text-nowrap"><?= $price ?> €</td>
<td class="text-right text-nowrap"><?= $power ?> Watt</td>
<td class="text-center"><?= $backup ?></td>
<td style="text-align: left; letter-spacing: 4px; font-size: 1.1em; width: 80px">
<a href="<?= self::getUrl("Device", "edit", ["id" => $device->id]) ?>"><i
class="far fa-edit" title="Bearbeiten"></i></a>
<a href="<?= self::getUrl("Device", "delete", ["id" => $device->id]) ?>"
onclick="if(!confirm('Device wirklich löschen?')) return false;" class="text-danger"
title="Löschen"><i class="fas fa-trash "></i></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script type="text/javascript">
var hidesearch = [7, 8, 10];
var columndefs = {type: 'ip-address', targets: 4};
var columnfilter = [9];
var columnoptions = '<option value=""></option><option value="OK">OK</option><option value="AGED">AGED</option><option value="N/A">N/A</option><option value="Auto">AUTO</option>';
$(document).ready(function () {
});
</script>
<script type="text/javascript"
src="<?= self::getResourcePath() ?>assets/js/datatables-std.js?<?= date('U') ?>"></script>
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/footer.php"); ?>

View File

@@ -17,11 +17,36 @@ class DeviceController extends mfBaseController
protected function indexAction()
{
$deviceManufacturers = array_map(function($manufacturer) {
return [
"text" => $manufacturer->name,
"value" => $manufacturer->name,
];
}, DevicemanufactorModel::getAll());
$this->layout()->setTemplate("Device/Index");
$devices = DeviceModel::getAll();
$this->layout()->set("devices", $devices);
$deviceTypes = array_map(function($deviceType) {
return [
"text" => $deviceType->name,
"value" => $deviceType->name,
];
}, DevicetypeModel::getAll());
$JSGlobals = ["BASE_URL" => self::getUrl("Device"),
"DASHBOARD_URL" => self::getUrl("Dashboard"),
"MFAPPNAME" => MFAPPNAME_SLUG,
"PAGE_TITLE" => "Devices",
"PATH" => [
["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")],
["text" => "Devices", "href" => self::getUrl("Device")]
],
"DEVICE_MANUFACTURERS" => $deviceManufacturers,
"DEVICE_TYPES" => $deviceTypes,
"DEVICE_API_URL" => self::getUrl("Device/api"),
];
$this->layout()->set("vueViewName", "Device");
$this->layout()->set("JSGlobals", $JSGlobals);
$this->layout()->setTemplate("VueViews/Vue");
}
@@ -83,7 +108,7 @@ class DeviceController extends mfBaseController
}
$this->layout()->set("device", $device);
return $this->addAction();
$this->addAction();
}
protected function saveAction()
@@ -253,57 +278,25 @@ class DeviceController extends mfBaseController
switch ($do) {
case "getDevices":
$devices = DeviceModel::getAll();
foreach ($devices as $device) {
$locationText = "";
$locationUrl = "";
if (trim($device->pop->name)) {
$locationText = $device->pop->name;
$locationUrl = self::getUrl("Pop", "Detail", ["id" => $device->pop->id]);
} else if (trim($device->addr_street)) {
$locationText = $device->addr_street . " " . $device->addr_number . ", " . $device->addr_zip . " " . $device->addr_city;
$locationUrl = "http://maps.google.com/?q=" . $locationText;
} else if (trim($device->gps_lat)) {
$locationText = $device->gps_lat . " , " . $device->gps_long;
$locationUrl = "http://maps.google.com/?q=" . $locationText;
}
$data[] = [
"name" => $device->name,
"devicetype" => $device->devicetype->name,
"devicemanufactor" => $device->devicetype->devicemanufactor->name,
"locationText" => $locationText,
"locationUrl" => $locationUrl,
"ip" => $device->ip,
"mac" => $device->mac,
"serial" => $device->serial,
"price" => $device->price != "0.0" ? $device->price : $device->devicetype->price,
"power" => $device->power != "0.0" ? $device->power : $device->devicetype->power,
"backup" => $device->last_config_backup ? (time() - $device->last_config_backup <= 172800 ? 'ok' : 'aged') : 'na',
];
}
die(json_encode($data));
break;
header('Content-Type: application/json');
die(json_encode($this->getDevices()));
case "getconfig":
$return = $this->getConfig($id, $format, $filename);
$this->getConfig($id, $format, $filename);
break;
case "createconfig":
$return = $this->createConfig($ip);
$this->createConfig($ip);
break;
case "getoltinfo":
$return = $this->getoltInfo($ip, $portid, $adv);
$this->getoltInfo($ip, $portid, $adv);
break;
case "getontinfo":
$return = $this->getontInfo($ip, $portid, $ont);
$this->getontInfo($ip, $portid, $ont);
break;
case "getontinfomac":
$return = $this->getontInfoMac($ip, $portid, $ont);
$this->getontInfoMac($ip, $portid, $ont);
break;
case "changeoltsplitter":
$return = $this->changeoltSplitter($id, $portid, $ports);
$this->changeoltSplitter($id, $portid, $ports);
break;
default:
$return = false;
@@ -352,7 +345,7 @@ class DeviceController extends mfBaseController
$returnUrl = "Device";
$returnAction = "Detail";
$returnVariables['id'] = $id;
return $this->redirect($returnUrl, $returnAction, $returnVariables, $returnAnker);
$this->redirect($returnUrl, $returnAction, $returnVariables);
}
private function changeoltSplitter($id, $portid, $ports)
@@ -390,4 +383,54 @@ class DeviceController extends mfBaseController
exit;
}
private function getDevices()
{
$devices = DeviceModel::getAll();
foreach ($devices as $device) {
$locationText = "";
$locationUrl = "";
if (trim($device->pop->name)) {
$locationText = $device->pop->name;
$locationUrl = self::getUrl("Pop", "Detail", ["id" => $device->pop->id]);
} else if (trim($device->addr_street)) {
$locationText = $device->addr_street . " " . $device->addr_number . ", " . $device->addr_zip . " " . $device->addr_city;
$locationUrl = "http://maps.google.com/?q=" . $locationText;
} else if (trim($device->gps_lat)) {
$locationText = $device->gps_lat . " , " . $device->gps_long;
$locationUrl = "http://maps.google.com/?q=" . $locationText;
}
$backup = 'na';
if ($device->last_config_backup) {
if (time() - $device->last_config_backup <= 172800) {
$backup = 'ok';
if ($device->autobackup==1) {
$backup = 'auto';
}
} else {
$backup = 'aged';
}
}
$data[] = [
"id" => $device->id,
"name" => $device->name,
"devicetype" => $device->devicetype->name,
"devicemanufactor" => $device->devicetype->devicemanufactor->name,
"locationText" => $locationText,
"locationUrl" => $locationUrl,
"ip" => $device->ip,
"mac" => $device->mac,
"serial" => $device->serial,
"price" => $device->price != "0.00" ? $device->price : $device->devicetype->price,
"power" => $device->power != "0.00" ? intval($device->power) : intval($device->devicetype->power),
"backup" => $backup,
];
}
return $data ?? [];
}
}

View File

@@ -0,0 +1,72 @@
Vue.component('Device', {
//language=Vue
template: `
<div>
<tt-page-title :title="window['TT_CONFIG']['PAGE_TITLE']" :path="window['TT_CONFIG']['PATH']"></tt-page-title>
<tt-table
:fetch-url="window['TT_CONFIG']['DEVICE_API_URL'] + '?do=getDevices'"
:config="DeviceTableConfig"
small ref="table" excel-export
>
<template v-slot:top-buttons>
<button type="button" class="btn btn-primary" @click="window.location = window['TT_CONFIG']['BASE_URL'] + '/add'">
<i class="fas fa-plus"></i>
Device hinzufügen
</button>
</template>
<template v-slot:name="{ row }">
<a target="_blank" :href="window['TT_CONFIG']['BASE_URL'] +'/Detail?id=' + row.id">{{row.name}}</a>
</template>
<template v-slot:locationtext="{ row }">
<a target="_blank" :href="row['locationUrl']">{{row.locationText}}</a>
</template>
<template v-slot:actions="{ row }">
<a :href="window['TT_CONFIG']['BASE_URL'] +'/edit/?id=' + row.id"><i class="far fa-edit" title="Bearbeiten"></i></a>
<a :href="window['TT_CONFIG']['BASE_URL'] +'/delete/?id=' + row.id" onclick="if(!confirm('Device wirklich löschen?')) return false;" class="text-danger" title="Löschen"><i class="fas fa-trash "></i></a>
</template>
</tt-table>
</div>
`,
data() {
return {
window: window,
DeviceTableConfig: {
key: 'DeviceTable',
tableHeader: 'Device-Liste',
defaultPageSize: 25,
headers: [
{ text: 'Device Name', key: 'name', sortable: true, class: 'text-nowrap', priority: 10 },
{ text: 'Hersteller', key: 'devicemanufactor', filter: 'select',class: 'text-nowrap text-center', filterOptions: window.TT_CONFIG["DEVICE_MANUFACTURERS"] },
{ text: 'Geräte Typ', key: 'devicetype', filter: 'autocomplete', class: 'text-nowrap text-center', filterOptions: window.TT_CONFIG["DEVICE_TYPES"] , priority: 7},
{ text: 'Pop/Adresse', key: 'locationText', filter: 'search' , class: 'text-nowrap text-center'},
{ text: 'IP-Adresse', key: 'ip', filter: 'search',class: 'text-center', priority: 8},
{ text: 'Mac-Adresse', key: 'mac', filter: 'search',class: 'text-center' },
{ text: 'Seriennummer', key: 'serial', filter: 'search',class: 'text-center' },
{ text: 'Preis', key: 'price', filter: 'numberRange',class: 'text-center', suffix: ' €' },
{ text: 'max. Leistung', key: 'power', filter: 'numberRange',class: 'text-center', suffix: ' W' },
{
text: 'Backup',
key: 'backup',
filter: 'iconSelect',
filterOptions: [
{ value: 'ok', text: 'Configbackup aktuell', icon: 'fa-regular fa-circle-check text-success' },
{ value: 'auto', text: 'Configbackup aktuell', icon: 'fa-regular fa-circle-a text-success' },
{ value: 'aged', text: 'Letztes Configbackup älter als 48 Stunden', icon: 'fa-regular fa-circle-xmark' },
{ value: 'na', text: 'N/A', icon: 'fa-regular fa-ban text-warning' }
],
priority: 6,
sortable: false,
},
{text: 'Aktionen', key: 'actions', class: 'text-center', sortable: false, filter: false, priority: 9},
],
},
};
},
});

View File

@@ -20,7 +20,7 @@ Vue.component('Domain', {
</button>
<div class="input-group">
<input type="text" class="form-control" v-model="checkDomainInput" placeholder="Neue Domain überprüfen">
<input type="text" class="form-control" v-model="checkDomainInput" style="height: 100%" placeholder="Neue Domain überprüfen">
<div class="input-group-append">
<button class="btn btn-primary" @click="checkDomainAvailability">
<template v-if="checkDomainLoading">
@@ -83,7 +83,7 @@ Vue.component('Domain', {
</thead>
<tbody>
<tr v-for="record in dnsRecordsModal.records">
<td>{{ record.class }}</td>
<td v-if="record">{{ record.class }}</td>
<td>{{ record.type }} {{ record.pri ? '(' + record.pri + ')' : '' }}</td>
<td>{{ record.host }}</td>
<td>{{ record.value }}</td>

View File

@@ -22,7 +22,7 @@
.tt-table-card {
overflow: auto;
/*overflow: auto;*/
}
.tt-table-card .page-link {
@@ -40,6 +40,7 @@
align-items: center;
justify-content: end;
}
.tt-table-pagination-wrapper {
display: grid;
grid-template-rows: auto auto;
@@ -48,28 +49,86 @@
grid-gap: 4px;
justify-content: end;
}
.tt-table-pagination {
margin: 0;
}
.tt-table-page-item {
cursor: pointer;
}
.tt-table-page-item.disabled {
pointer-events: none;
opacity: 0.5;
}
.tt-table-page-item.active {
font-weight: bold;
background-color: #007bff;
color: white;
}
.tt-table-select {
display: inline-block;
width: auto;
}
.tt-table-text-center {
text-align: center;
}
.tt-pointer {
cursor: pointer;
}
.tt-table.table-sm > tbody > tr > td * {
font-size: 13px !important;
}
.tt-table.table-sm > tbody > tr {
line-height: 1.15 !important;
}
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
margin: 0;
}
/*table {*/
/* width: 100%; !* Ensure table takes full available width *!*/
/* table-layout: auto; !* Crucial for automatic column scaling *!*/
/*}*/
/*td {*/
/* white-space: nowrap; !* Prevent text from wrapping within cells *!*/
/* overflow: hidden; !* Hide overflowing content *!*/
/* text-overflow: ellipsis; !* Show ellipsis (...) for long content *!*/
/*}*/
.tt-table-top-pagination-container {
display: grid;
grid-template-columns: 1fr 1fr;
padding-bottom: 8px;
align-items: center;
justify-content: space-between
}
@media (max-width: 486px) {
.tt-table-top-pagination-container {
grid-template-columns: 1fr;
}
.tt-table-top-pagination-container > * {
justify-self: center;
}
.tt-table-top-pagination-container > *:first-child {
margin-top: 8px;
margin-bottom: 8px;
}
}

View File

@@ -2,16 +2,16 @@ Vue.component('tt-autocomplete', {
template: `
<div class="form-group">
<slot name="prepend"></slot>
<label :for="label">{{ label }}</label>
<label v-if="label" :for="label">{{ label }}</label>
<div class="autocomplete position-relative">
<input
type="text"
class="form-control"
class="form-control form-control-sm"
v-model="displayValue"
@input="onInput"
@focus="onFocus"
@blur="onBlur"
style="padding-right: 30px;"
:style="{'padding-right': $slots.append ? '30px' : '0'}"
/>
<slot name="append"></slot>
@@ -20,9 +20,9 @@ Vue.component('tt-autocomplete', {
<div v-show="isLoading" class="loader"></div>
<template v-show="showSuggestions && items.length > 0 && isLoading !== true">
<template v-show="showSuggestions && displayingItems.length > 0 && isLoading !== true">
<li
v-for="(item, index) in items.slice(0, 10)"
v-for="(item) in displayingItems.slice(0, 10)"
:key="item.value"
:class="{'active': value === item.value}"
class="dropdown-item"
@@ -32,15 +32,16 @@ Vue.component('tt-autocomplete', {
{{ item.text }}
</li>
<!-- display more search results available define it more precisely in German -->
<li v-show="items.length > 10" class="dropdown-item disabled">
<li v-show="displayingItems.length > 10" class="dropdown-item disabled">
Mehr Suchergebnisse vorhanden. Bitte genauer eingeben
</li>
</template>
<li v-show="items.length === 0 && isLoading === false && displayValue.length > 3" class="dropdown-item disabled">
<li v-show="displayingItems.length === 0 && isLoading === false && displayValue.length > 3"
class="dropdown-item disabled">
Keine Suchergebnisse vorhanden.
</li>
<li v-show="displayValue.length < 3" class="dropdown-item disabled">
Bitte mindestens 3 Zeichen eingeben
Bitte mindestens 3 Zeichen eingeben
</li>
</ul>
</div>
@@ -54,19 +55,29 @@ Vue.component('tt-autocomplete', {
},
label: {
type: String,
required: true,
required: false,
},
apiUrl: String,
items: {
type: Array,
default: () => [],
}
},
async mounted() {
if (this.value) {
if (this.value && this.apiUrl) {
const response = await axios.get(`${this.apiUrl}&autocomplete=1&searchedID=${this.value}`);
this.displayValue = response.data[0].text;
} else if (this.value) {
const selectedItem = this.items.find(item => item.value === this.value);
this.displayValue = selectedItem ? selectedItem.text : '';
}
console.log(this.$slots.append ? 'append' : 'no append');
},
data() {
return {
items: [],
displayingItems: [],
displayValue: '',
isLoading: false,
showSuggestions: false,
@@ -76,7 +87,7 @@ Vue.component('tt-autocomplete', {
},
watch: {
value(newValue) {
const selectedItem = this.items.find(item => item.value === newValue);
const selectedItem = this.displayingItems.find(item => item.value === newValue);
this.displayValue = selectedItem ? selectedItem.text : '';
},
apiUrl() {
@@ -86,6 +97,7 @@ Vue.component('tt-autocomplete', {
methods: {
onInput(event) {
this.displayValue = event.target.value;
this.$emit('input', undefined);
this.fetchSuggestions();
},
onFocus() {
@@ -97,7 +109,28 @@ Vue.component('tt-autocomplete', {
}, 200);
},
fetchSuggestions() {
if (!this.apiUrl || this.displayValue.length < 3) return;
if (this.displayValue.length < 3) {
this.$set(this, 'displayingItems', []);
return this.displayingItems = [];
}
if (!this.apiUrl) {
const isNegated = this.displayValue.startsWith('!');
const substrings = (isNegated ? this.displayValue.slice(1) : this.displayValue).split(' ').map(s => s.toLowerCase());
this.displayingItems = this.items.filter(item => {
let substringMatch = true;
for (var k = 0, klen = substrings.length; k < klen; ++k) {
if (!item.text.toLowerCase().includes(substrings[k])) {
substringMatch = false;
break;
}
}
return (isNegated && !substringMatch) || (!isNegated && substringMatch);
})
return
}
this.isLoading = true;
clearTimeout(this.fetchSuggestionsDebounceTimer);
@@ -107,9 +140,9 @@ Vue.component('tt-autocomplete', {
setTimeout(async () => {
const response = await axios.get(`${this.apiUrl}&autocomplete=1&q=${this.displayValue}`);
if (response.data?.status === 'error') {
this.items = [];
this.displayingItems = [];
} else {
this.items = response.data
this.displayingItems = response.data
}
this.isLoading = false;

View File

@@ -8,10 +8,13 @@ Vue.component('tt-icon-select', {
};
},
mounted() {
// Dynamically add CSS to disable default dropdown caret
const style = document.createElement('style');
style.innerHTML = `.tt-select .dropdown-toggle::after { display: none; }`;
document.body.appendChild(style);
if (! document.getElementById('tt-icon-select-style')) {
// Dynamically add CSS to disable default dropdown caret
const style = document.createElement('style');
style.id = 'tt-icon-select-style';
style.innerHTML = `.tt-select .dropdown-toggle::after { display: none; }`;
document.body.appendChild(style);
}
document.addEventListener('click', this.handleClick);

View File

@@ -44,9 +44,11 @@ Vue.component('tt-number-range', {
}
}
}, template: `
<div style="display:grid;grid-template-columns: 75px 25px 75px;grid-gap: 4px;justify-content: center">
<div style="display:grid;grid-template-columns: minmax(35px, 75px) minmax(20px, 25px) minmax(35px, 75px);;grid-gap: 4px;justify-content: center">
<slot name="prepend"></slot>
<input type="number"
style="-webkit-appearance: none;
-moz-appearance: textfield;"
class="form-control form-control-sm"
v-model.number="inputValueFrom"
@input="updateValue"
@@ -60,3 +62,5 @@ Vue.component('tt-number-range', {
</div>
`
});

View File

@@ -1,22 +1,44 @@
/**
* @typedef {Object} ttTableColumnConfig
* @property {string} text - The display text of the column.
* @property {string} key - The unique key of the column.
* @property {string} filter - Indicates if filtering is enabled for the column.
* @property {boolean} sortable - Indicates if sorting is enabled for the column.
* @property {string} class - The CSS class(es) applied to the column.
* @property {string} text - The display text of the column header.
* @property {string} key - The unique key of the column (used for data access and filtering).
* @property {string} [filter] - (Optional) Determines the type of filter applied to the column. Default is 'search'.
* Possible values:
* - 'search': Basic text search (case-insensitive).
* - 'iconSelect': Filter with icons and associated text (requires 'filterOptions').
* - 'numberRange': Filter with numeric ranges (e.g., ">5", "10-20", etc.).
* - 'select': Dropdown filter with predefined options (requires 'filterOptions').
* - 'date': Date range filter.
* - 'false': Disables filtering for the column.
* @property {Array} [filterOptions] - (Optional) An array of filter options for 'iconSelect' or 'select' filters.
* Each option should be an object with 'text' (display text) and 'value' (filter value) properties.
* For 'iconSelect', an additional 'icon' property (CSS class for the icon) is required.
* @property {boolean} [sortable=true] - (Optional) Indicates whether the column is sortable. Default is true.
* @property {string} [class] - (Optional) Additional CSS classes to apply to the column.
* @property {string} [suffix] - (Optional) Additional CSS classes to apply to the column.
* @property {string} [prefix] - (Optional) Additional CSS classes to apply to the column.
* */
/**
* @typedef {Object} ttTableConfig
* @property {string} key - A unique key for the table instance (used for saving/loading settings).
* @property {string} tableHeader - The main header text displayed above the table.
* @property {ttTableColumnConfig[]} headers - An array of column configuration objects (see `ttTableColumnConfig` typedef).
* @property {Function} [customRowClass] - (Optional) A function that returns a CSS class string based on row data.
* @property {Function} [expandCondition] - (Optional) A function that determines if a row is expandable.
* @property {number} [defaultPageSize=10] - (Optional) The default number of rows to display per page.
* @property {Function} [customExcelProcessor] - (Optional) A function to preprocess row data before exporting to Excel.
*/
Vue.component('tt-table-pagination', {
props: {
pagination: {
type: Object,
required: true,
default: () => ({page: 1, per_page: 10, total_rows: 0, filtered_available: 0, total_pages: 1})
},
reverse: {type: Boolean, default: false}
},
computed: {
}, reverse: {type: Boolean, default: false}
}, computed: {
pagesToDisplay() {
const range = 2;
const start = Math.max(this.pagination.page - range, 1);
@@ -26,65 +48,61 @@ Vue.component('tt-table-pagination', {
pages.push(i);
}
return pages.length === 0 ? [1] : pages;
},
pageInfoText() {
}, pageInfoText() {
const start = Math.min(this.pagination.page * this.pagination.per_page - this.pagination.per_page + 1, this.pagination.total_rows);
const end = Math.min(this.pagination.page * this.pagination.per_page, this.pagination.total_rows);
const total = this.pagination.total_rows === this.pagination.filtered_available
? this.pagination.total_rows
: `${this.pagination.filtered_available} (${this.pagination.total_rows})`;
const total = this.pagination.total_rows === this.pagination.filtered_available ? this.pagination.total_rows : `${this.pagination.filtered_available} (${this.pagination.total_rows})`;
return `${start} bis ${end} von ${total}`;
}
},
methods: {
}, methods: {
fetchRows(page) {
this.$emit('fetch-rows', page);
}
},
template: `
<div class="tt-table-pagination-container">
<div v-if="pagination && typeof pagination.total_rows === 'number'" class="tt-table-pagination-wrapper">
}, template: `
<div class="tt-table-pagination-container">
<div v-if="pagination && typeof pagination.total_rows === 'number'" class="tt-table-pagination-wrapper">
<span class="tt-table-text-center" v-text="pageInfoText"
:style="{ 'grid-row': reverse ? 2 : 1, 'grid-column': 1 }"></span>
<ul class="pagination tt-table-pagination" :style="{ 'grid-row': reverse ? 1 : 2, 'grid-column': 1 }">
<li class="page-item tt-table-page-item" v-bind:class="{ disabled: pagination.page === 1 }">
<a class="page-link" href="#" v-on:click.prevent="fetchRows(1)" aria-label="First">
<span aria-hidden="true">&laquo;</span>
<span class="sr-only">First</span>
</a>
</li>
<li class="page-item tt-table-page-item" v-for="pageNumber in pagesToDisplay"
v-bind:class="{ 'active': pageNumber === pagination.page, 'disabled': pageNumber === pagination.page }">
<a class="page-link"
v-bind:class="{ 'active': pageNumber === pagination.page, 'disabled': pageNumber === pagination.page }"
href="#"
v-on:click.prevent="fetchRows(pageNumber)">{{ pageNumber }}</a>
</li>
<li class="page-item tt-table-page-item"
v-bind:class="{ disabled: pagination.page === pagination.total_pages }">
<a class="page-link" href="#" v-on:click.prevent="fetchRows(pagination.total_pages)"
aria-label="Last">
<span aria-hidden="true">&raquo;</span>
<span class="sr-only">Last</span>
</a>
</li>
</ul>
<ul class="pagination tt-table-pagination" :style="{ 'grid-row': reverse ? 1 : 2, 'grid-column': 1 }">
<li class="page-item tt-table-page-item" v-bind:class="{ disabled: pagination.page === 1 }">
<a class="page-link" href="#" v-on:click.prevent="fetchRows(1)" aria-label="First">
<span aria-hidden="true">&laquo;</span>
<span class="sr-only">First</span>
</a>
</li>
<li class="page-item tt-table-page-item" v-for="pageNumber in pagesToDisplay"
v-bind:class="{ 'active': pageNumber === pagination.page, 'disabled': pageNumber === pagination.page }">
<a class="page-link"
v-bind:class="{ 'active': pageNumber === pagination.page, 'disabled': pageNumber === pagination.page }"
href="#"
v-on:click.prevent="fetchRows(pageNumber)">{{ pageNumber }}</a>
</li>
<li class="page-item tt-table-page-item"
v-bind:class="{ disabled: pagination.page === pagination.total_pages }">
<a class="page-link" href="#" v-on:click.prevent="fetchRows(pagination.total_pages)"
aria-label="Last">
<span aria-hidden="true">&raquo;</span>
<span class="sr-only">Last</span>
</a>
</li>
</ul>
<span class="tt-table-text-center" :style="{ 'grid-row': reverse ? 2 : 1, 'grid-column': 2 }">Einträge pro Seite</span>
<span class="tt-table-text-center"
:style="{ 'grid-row': reverse ? 2 : 1, 'grid-column': 2 }">Einträge pro Seite</span>
<select v-model="pagination.per_page" v-on:change="fetchRows(1)"
class="form-control form-control-sm tt-table-select"
:style="{ 'grid-row': reverse ? 1 : 2, 'grid-column': 2 }">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
</select>
</div>
<select v-model="pagination.per_page" v-on:change="fetchRows(1)"
class="form-control form-control-sm tt-table-select"
:style="{ 'grid-row': reverse ? 1 : 2, 'grid-column': 2 }">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
</select>
</div>
</div>
`
})
@@ -92,7 +110,7 @@ Vue.component('tt-table-pagination', {
Vue.component('tt-table', {
template: `
<div class="card tt-table-card" v-if="columns && pagination">
<div class="card-body">
<div class="card-body" ref="tableContainer">
<!-- Top Buttons -->
<div
style="display:grid; grid-template-columns: auto auto auto auto auto; grid-gap: 8px; padding-bottom: 8px">
@@ -100,58 +118,30 @@ Vue.component('tt-table', {
</div>
<!-- Pagination Controls -->
<nav aria-label="Page navigation">
<div
style="display:grid; grid-template-columns: 1fr 1fr;padding-bottom: 8px;align-items:center; justify-content: space-between">
<div class="tt-table-top-pagination-container">
<!-- if excelExport is true, show the export button fontawesome icon excel -->
<div style="display:flex;align-items: center;">
<i v-if="!Object.values(columns).every(column => column.filter === false)" title="Filter zurücksetzen"
@click="resetTable" class="fa-solid fa-trash-undo"
style="font-size: 24px;margin-right: 8px;cursor: pointer; color: var(--orange)"></i>
<h4 style="margin: 0">{{ config.tableHeader }}</h4>
@click="resetTable" class="fas fa-times cursor-pointer text-danger"
style="font-size: 24px;margin-right: 6px;cursor: pointer; color: var(--orange)"></i>
<i v-if="excelExport" title="EXCEL Export" @click="exportToExcel" class="fa fa-file-excel"
style="font-size: 24px;margin-left: 8px;cursor: pointer; color: var(--success)"></i>
style="font-size: 24px;margin-right: 6px;cursor: pointer; color: var(--success)"></i>
<h4 style="margin: 0">{{ config.tableHeader }}</h4>
</div>
<div v-if="pagination && typeof pagination.total_rows === 'number'"
style="display:grid; grid-template-rows: auto auto; grid-template-columns: auto auto; grid-auto-flow: column; grid-gap: 4px; justify-content: end">
<ul class="pagination" style="margin: 0">
<li class="page-item" v-bind:class="{ disabled: pagination.page === 1 }">
<a class="page-link" href="#" v-on:click.prevent="fetchRows(1)" aria-label="First">
<span aria-hidden="true">&laquo;</span>
<span class="sr-only">First</span>
</a>
</li>
<li class="page-item" v-for="pageNumber in pagesToDisplay"
v-bind:class="{ 'active disabled': pageNumber === pagination.page }">
<a class="page-link" v-bind:class="{ 'active disabled': pageNumber === pagination.page }" href="#"
v-on:click.prevent="fetchRows(pageNumber)">{{ pageNumber }}</a>
</li>
<li class="page-item" v-bind:class="{ disabled: pagination.page === pagination.total_pages }">
<a class="page-link" href="#" v-on:click.prevent="fetchRows(pagination.total_pages)"
aria-label="Last">
<span aria-hidden="true">&raquo;</span>
<span class="sr-only">Last</span>
</a>
</li>
</ul>
<span class="text-center"
v-text="Math.min(pagination.page * pagination.per_page - pagination.per_page + 1, pagination.filtered_available)
+ ' bis ' + Math.min(pagination.page * pagination.per_page, pagination.filtered_available) + ' von ' + (pagination.total_rows === pagination.filtered_available ? pagination.total_rows : pagination.filtered_available + ' ('+pagination.total_rows+')')"></span>
<select v-model="pagination.per_page" v-on:change="fetchRows(1)" class="form-control form-control-sm">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
</select>
<span class="text-center">Einträge pro Seite</span>
</div>
<tt-table-pagination :pagination="pagination" @fetch-rows="fetchRows"
v-if="pagination"></tt-table-pagination>
</div>
</nav>
<!-- Table -->
<table
ref="table"
:class="['table','tt-table','table-condensed',{ 'loading': loading },{ 'table-striped': striped },{ 'table-bordered': bordered },{ 'table-hover': hover },{ 'table-sm': small }]">
<thead style="border-width: 2px">
<tr>
<th scope="col" v-for="column in columns"
<th scope="col" v-for="column in columns"
:ref="'table_header_'+column.key"
v-if="!hiddenColumns.includes(column.key)"
:style="'vertical-align: top; text-align: center;' + (column.filter === 'dateRange' ? 'min-width: 260px;' : '')">
<div style="text-align:center; white-space: nowrap;word-break: keep-all;"
:style="{ 'cursor': column.sortable ? 'pointer' : 'default' }"
@@ -169,6 +159,10 @@ Vue.component('tt-table', {
<tt-select v-else-if="column.filter === 'select'"
:options="[{text: 'Alle', value: undefined}, ...column.filterOptions]"
v-model="filters[column.key]"></tt-select>
<tt-autocomplete v-else-if="column.filter === 'autocomplete'"
:items="[{text: 'Alle', value: undefined}, ...column.filterOptions]"
v-model="filters[column.key]"> </tt-autocomplete>
<tt-date-picker v-else-if="column.filter === 'date'" v-model="filters[column.key]"></tt-date-picker>
</th>
</tr>
@@ -181,16 +175,18 @@ Vue.component('tt-table', {
style="height: 150px">
<td :colspan="Object.keys(columns).length" class="text-center">Laden...</td>
</tr>
<template v-for="(row) in (ssr === false ? computedRows : rows)">
<template v-for="(row, index) in (ssr === false ? computedRows : rows)">
<tr :class="typeof config.customRowClass === 'function' ? config.customRowClass(row) : ''"
@click="$emit('row-click', row)"
>
<template v-for="(column, key) in columns">
<template v-for="(column, key) in columns" v-if="!hiddenColumns.includes(column.key)">
<td :class="{ 'text-center': column.filter === 'iconSelect', [columns[key].class]: true }">
<!-- If td is first of row then check isExpanded and display fas.fa-chevron-right or fas.fa-chevron-down with cursor pointer -->
<i v-if="key === Object.keys(columns)[0] && $scopedSlots.expandedRow && (typeof config.expandCondition !== 'function' || config.expandCondition(row))"
@click.stop="toggleExpand(row.id)"
:class="isExpanded(row.id) ? 'fas fa-chevron-down' : 'fas fa-chevron-right'"
<i v-if="key === Object.keys(columns)[0] &&
($scopedSlots.expandedRow && (typeof config.expandCondition !== 'function' || config.expandCondition(row)) || hiddenColumns.length > 0)"
@click.stop="toggleExpand(index)"
:class="isExpanded(index) ? 'fas fa-chevron-down' : 'fas fa-chevron-right'"
style="cursor: pointer;font-size: 14px;padding-right: 8px;user-select: none"></i>
<slot :name="key.toLowerCase()" :value="row[key]" :row="row">
<span
@@ -199,13 +195,35 @@ Vue.component('tt-table', {
:title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"
:class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i>
<span v-else
v-html="row[key] === null || typeof row[key] === 'undefined' ? null : row[key]?.toString()?.replace('\\n', '<br>')"></span>
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )"></span>
</slot>
</td>
</template>
</tr>
<tr v-if="isExpanded(row.id) && $scopedSlots.expandedRow">
<tr v-if="isExpanded(index) && ($scopedSlots.expandedRow|| hiddenColumns.length > 0)">
<td :colspan="Object.keys(columns).length">
<!-- display ul with li for each column in hiddenColumns with slot name key.toLowerCase() or span with value of row[key] -->
<ul v-if="hiddenColumns.length > 0" style="list-style-type: none;padding: 0;margin: 0;">
<li v-for="(column, key) in columns" :key="'hiddenColumn-'+key">
<template v-if="hiddenColumns.includes(key)">
<strong>{{ column.text }}:</strong>
<slot :name="key.toLowerCase()" :value="row[key]" :row="row">
<span
v-if="column.filter === 'date'">{{ row[key] ? (moment.unix(row[key]).isValid() ? moment.unix(row[key]).format('DD.MM.YYYY HH:mm') : moment(row[key]).format('DD.MM.YYYY HH:mm')) : '' }}</span>
<i v-else-if="column.filter === 'iconSelect'"
:title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"
:class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i>
<span v-else
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )">
</span>
</slot>
</template>
</li>
</ul>
<slot name="expandedRow" :row="row"></slot>
</td>
</tr>
@@ -234,7 +252,6 @@ Vue.component('tt-table', {
return {
window: window,
moment: window.moment,
XLSX: window.XLSX,
loading: false,
rows: null,
rawRows: null,
@@ -244,11 +261,13 @@ Vue.component('tt-table', {
disableDebounce: false,
latestFetchTimestamp: null,
order: {
key: null,
order: 'asc' // default sort order
key: null, order: 'asc' // default sort order
},
expandedRows: {},
isInitialised: false
isInitialised: false,
hiddenColumns: [],
originalColumnWidths: {},
originalTableWidth: null
};
},
@@ -274,6 +293,8 @@ Vue.component('tt-table', {
* @async
*/
async fetchData(page = 0) {
this.expandedRows = {};
try {
if (this.ssr === false) {
const response = await axios.get(this.fetchUrl);
@@ -332,8 +353,7 @@ Vue.component('tt-table', {
} else {
await this.fetchData(page); // Directly call fetchData without debounce
}
},
saveSettingsToLocalStorage() {
}, saveSettingsToLocalStorage() {
if (this.isInitialised === false) return;
const filters = Object.entries(this.filters).reduce((acc, [key, value]) => {
@@ -353,21 +373,17 @@ Vue.component('tt-table', {
filters,
paginationPerPage: this.pagination.per_page,
order: this.order.key ? this.order : undefined,
expandedRows: this.expandedRows
}));
},
parseSettingsFromLocalStorage() {
}, parseSettingsFromLocalStorage() {
const settings = JSON.parse(localStorage.getItem(`tt-table-${this.config.key}`) || '{}');
if (settings) {
this.disableDebounce = true;
this.filters = settings.filters || {};
this.pagination.per_page = parseInt(settings.paginationPerPage) || this.config.defaultPageSize || 10;
this.order = settings.order || {key: null, order: 'asc'};
this.expandedRows = settings.expandedRows || {};
}
return !!settings;
},
setOrder(key) {
}, setOrder(key) {
if (this.order.key === key) {
// if current order is desc then set key to null
if (this.order.order === 'desc') {
@@ -382,16 +398,12 @@ Vue.component('tt-table', {
this.order.key = key;
this.order.order = 'asc';
}
},
getSortIconClass(key) {
}, getSortIconClass(key) {
if (this.order.key === key) {
return this.order.order === 'asc' ? 'fa fa-sort-asc' : 'fa fa-sort-desc';
}
return 'fa fa-sort'; // default icon when not sorted
},
async exportToExcel() {
// create script and await downloading: /plugins/xlsx/xlsx.min.js
}, async exportToExcel() {
await new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = '/plugins/xlsx/xlsx.min.js';
@@ -400,29 +412,41 @@ Vue.component('tt-table', {
document.head.appendChild(script);
})
function defaultExcelProcessor(rows) {
return rows.map(row => {
const parsedRow = {};
const wb = this.XLSX.utils.book_new();
for (const key in row) {
if (!this.columns[key]) continue;
let data = typeof this.config.customExcelProcessor === 'function' ? this.config.customExcelProcessor(this.rawRows) : JSON.parse(JSON.stringify(this.rawRows));
if (this.columns[key] && this.columns[key].filter === 'iconSelect') {
parsedRow[this.columns[key].text] = this.columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text;
} else if (this.columns[key] && this.columns[key].filter === 'date') {
parsedRow[this.columns[key].text] = this.moment(row[key]).format('DD.MM.YYYY HH:mm');
} else {
console.log(key)
parsedRow[this.columns[key].text] = row[key];
}
// convert all columns with date with momentjs
data = data.map(row => {
for (const key in row) {
if (this.columns[key].filter === 'date') {
row[key] = this.moment(row[key]).format('DD.MM.YYYY HH:mm');
}
}
return row;
});
return parsedRow;
});
}
const ws = this.XLSX.utils.json_to_sheet(data);
const wb = this.window.XLSX.utils.book_new();
let data = typeof this.config.customExcelProcessor === 'function' ?
this.config.customExcelProcessor(JSON.parse(JSON.stringify(this.rawRows))) :
defaultExcelProcessor.call(this, JSON.parse(JSON.stringify(this.rawRows)));
const ws = this.window.XLSX.utils.json_to_sheet(data);
for (const cell in ws) {
if (cell.startsWith('!')) continue; // Skip non-cell properties like '!ref'
const cellValue = ws[cell].v;
if (cellValue.toString().includes('\n')) {
console.log('Found newline in cell:', cell, cellValue);
// console.log('Found newline in cell:', cell, cellValue);
if (!ws[cell].s) ws[cell].s = {};
ws[cell].s.alignment = {wrapText: true, vertical: 'center'};
} else {
@@ -430,26 +454,54 @@ Vue.component('tt-table', {
}
}
this.XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
this.XLSX.writeFile(wb, 'export.xlsx');
},
resetTable() {
this.window.XLSX.utils.book_append_sheet(wb, ws, "Export");
this.window.XLSX.writeFile(wb, 'export.xlsx');
}, resetTable() {
this.$emit('reset-table');
this.filters = {};
this.order = {key: null, order: 'asc'};
this.expandedRows = {};
this.disableDebounce = true;
window.notify('success', 'Filter zurückgesetzt');
},
toggleExpand(index) {
}, toggleExpand(index) {
this.expandedRows[index] ? this.$delete(this.expandedRows, index) : this.$set(this.expandedRows, index, true);
},
isExpanded(index) {
}, isExpanded(index) {
return !!this.expandedRows[index];
},
async handleResponsiveColumns() {
const tableContainerComputedStyle = window.getComputedStyle(this.$refs.tableContainer);
let viewportWidth = this.$refs.tableContainer.offsetWidth -
parseInt(tableContainerComputedStyle.paddingLeft) -
parseInt(tableContainerComputedStyle.paddingRight);
this.hiddenColumns = []
await this.$nextTick()
// Initialize original column widths only if empty
if (Object.keys(this.originalColumnWidths).length === 0) {
for (let i = 0; i < Object.keys(this.columns).length; i++) {
const column = Object.keys(this.columns)[i]
this.originalColumnWidths[column] = this.$refs[`table_header_${column}`][0].offsetWidth
}
}
// Create an array of columns sorted by priority
const columns = Object.values(this.columns).sort((a, b) => b.priority - a.priority);
// Iterate over all columns and check if the viewport width is smaller than the column width and hide the column
for (let i = 0; i < columns.length; i++) {
viewportWidth -= this.originalColumnWidths[columns[i].key];
if (i === 0) continue;
else if (viewportWidth < 0) {
this.hiddenColumns.push(columns[i].key)
}
}
}
}, watch: {
filters: {
handler: function (newVal, oldVal) {
handler: function () {
if (!this.isInitialised) return;
if (this.ssr) {
@@ -457,17 +509,15 @@ Vue.component('tt-table', {
}
this.saveSettingsToLocalStorage();
}, deep: true
},
'pagination.per_page': {
}, 'pagination.per_page': {
handler: function (newVal, oldVal) {
if (!this.isInitialised) return;
if (newVal === oldVal) return
this.saveSettingsToLocalStorage();
}, deep: true
},
order: {
handler: function (newVal, oldVal) {
}, order: {
handler: function () {
if (!this.isInitialised) return;
if (this.ssr) {
@@ -475,13 +525,26 @@ Vue.component('tt-table', {
}
this.saveSettingsToLocalStorage();
}, deep: true
},
expandedRows: {
handler: function (newVal, oldVal) {
}, expandedRows: {
handler: function () {
if (!this.isInitialised) return;
this.saveSettingsToLocalStorage();
}, deep: true
},
rows: {
handler: async function () {
this.$nextTick(() => {
this.handleResponsiveColumns()
})
}, deep: true
},
computedRows: {
handler: async function () {
this.$nextTick(() => {
this.handleResponsiveColumns()
})
}, deep: true
}
}, computed: {
/**
@@ -489,18 +552,23 @@ Vue.component('tt-table', {
* @return {ttTableColumnConfig} The columns configuration.
*/
columns() {
let i = this.config.headers.length;
return this.config.headers.reduce((columns, column) => {
if (!column.key) {
console.warn('WARN: tt-table: Column text or key is not defined:', column);
// console.warn('WARN: tt-table: Column text or key is not defined:', column);
return columns; // Continue to the next iteration without modifying the accumulator
}
columns[column.key] = {
text: column.text,
key: column.key,
filter: column.filter !== undefined ? column.filter : 'search',
filterOptions: column.filterOptions || undefined,
sortable: column.sortable !== undefined ? column.sortable : true,
class: column.class !== undefined ? column.class : ''
class: column.class !== undefined ? column.class : '',
prefix: column.prefix || '',
suffix: column.suffix || '',
priority: column.priority + 100 || i--
};
return columns;
}, {});
@@ -526,10 +594,9 @@ Vue.component('tt-table', {
}
return pagesArray.length === 0 ? [1] : pagesArray;
},
computedRows() {
}, computedRows() {
if (!this.rawRows || this.ssr === true) return null;
console.time('Filtering and pagination');
// console.time('Filtering and pagination');
function handleRangeFilter(filter, value) {
if (filter[0] === '<') {
@@ -567,6 +634,7 @@ Vue.component('tt-table', {
const header = headers[filter];
const filterValue = filters[filter];
if (!filterValue) continue;
if (filterValue === '') continue;
if (filterValue === "!") continue;
@@ -596,7 +664,7 @@ Vue.component('tt-table', {
match = false;
break;
}
} else if (header.filter === 'select' || header.filter === 'iconSelect') {
} else if (header.filter === 'select' || header.filter === 'iconSelect' || header.filter === 'autocomplete') {
if (filterValue === '') continue;
if (filterValue !== row[header.key].toString()) {
match = false;
@@ -644,7 +712,7 @@ Vue.component('tt-table', {
}
console.timeEnd('Filtering and pagination');
// console.timeEnd('Filtering and pagination');
// Pagination and slice logic
this.pagination.total_pages = Math.ceil(output.length / this.pagination.per_page);
this.pagination.filtered_available = output.length;
@@ -656,14 +724,12 @@ Vue.component('tt-table', {
return this.rows;
}
},
async created() {
}, async created() {
if (this.config.hasOwnProperty('defaultPageSize') && this.config.defaultPageSize) {
this.pagination = {page: 1, per_page: this.config.defaultPageSize, total_rows: null, total_pages: 1};
}
this.parseSettingsFromLocalStorage()
if (!this.disableInitialFetch) {
@@ -675,8 +741,24 @@ Vue.component('tt-table', {
// if sticky is true then add style element to style thead sticky
if (this.sticky) {
const style = document.createElement('style');
style.innerHTML = `table thead th { position: sticky; top: 0; z-index: 1; background-color: white; }`;
// use header#topnav as top to stick to but if window is resized then check if header#topnav height is changed
const headerHeight = document.querySelector('header#topnav')?.offsetHeight || 0;
style.innerHTML = `table thead th { position: sticky; top: ${headerHeight}px; z-index: 1; background-color: white; }`;
style.id = 'tt-table-sticky-header';
window.addEventListener('resize', () => {
const headerHeight = document.querySelector('header#topnav')?.offsetHeight || 0;
style.innerHTML = `table thead th { position: sticky; top: ${headerHeight}px; z-index: 1; background-color: white; }`;
})
document.head.appendChild(style);
}
window.addEventListener('resize', this.debounce(this.handleResponsiveColumns, 100));
},
beforeDestroy() {
window.removeEventListener('resize', this.debounce(this.handleResponsiveColumns, 100));
}
})