improved performance of vue modules
This commit is contained in:
@@ -19,12 +19,6 @@ if (is_dir($vueViewPath)) {
|
||||
|
||||
$additionalCSS = [
|
||||
...$additionalCSS,
|
||||
'plugins/daterangepicker/daterangepicker.css',
|
||||
'plugins/vue/tt-components/css/tt-table.css',
|
||||
'plugins/vue/tt-components/css/tt-tooltip.css',
|
||||
'plugins/vue/tt-components/css/tt-loader.css',
|
||||
'plugins/vue/tt-components/css/tt-file-gallery.css',
|
||||
'plugins/vue/tt-components/css/tt-position-manager.css',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
<?php endif; ?>
|
||||
|
||||
<script type="text/javascript">
|
||||
if (typeof jQuery === 'undefined') {
|
||||
throw new Error('jQuery not found. Please include jQuery before this script.');
|
||||
}
|
||||
|
||||
if ($(".selectpicker").length) {
|
||||
$(".selectpicker").selectpicker({
|
||||
iconBase: "fas",
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
<?php /** @var TYPE_NAME $git_merge_ts */ ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta charset="utf-8">
|
||||
<title><?= MFAPPNAME_FULL ?></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<!-- App favicon -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="shortcut icon" href="<?= self::getResourcePath() ?>assets/images/favicon.ico">
|
||||
|
||||
<link href="<?=self::getResourcePath()?>fontawesome/css/all.min.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
|
||||
<link href="<?=self::getResourcePath()?>fontawesome/css/sharp-solid.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
|
||||
<link href="<?=self::getResourcePath()?>fontawesome/css/sharp-regular.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
|
||||
<link href="<?=self::getResourcePath()?>fontawesome/css/sharp-light.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
|
||||
<link href="<?=self::getResourcePath()?>fontawesome/css/sharp-thin.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
|
||||
<link href="<?=self::getResourcePath()?>assets/css/bootstrap.min.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
|
||||
<link href="<?=self::getResourcePath()?>assets/css/app.min.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
|
||||
<link href="<?=self::getResourcePath()?>plugins/notification/notify.min.css" rel="stylesheet" type="text/css" />
|
||||
<link href="<?=self::getResourcePath()?>assets/css/thetool.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
|
||||
<link href="<?= self::getResourcePath() ?>cssbundler.php?<?= $git_merge_ts ?>" rel="stylesheet">
|
||||
<link href="<?= self::getResourcePath() ?>fontawesome/css/fontawesome.min.css?<?= $git_merge_ts ?>"
|
||||
rel="stylesheet">
|
||||
<link href="<?= self::getResourcePath() ?>fontawesome/css/solid.min.css?<?= $git_merge_ts ?>" rel="stylesheet">
|
||||
<link href="<?= self::getResourcePath() ?>fontawesome/css/regular.min.css?<?= $git_merge_ts ?>" rel="stylesheet">
|
||||
<link href="<?= self::getResourcePath() ?>fontawesome/css/duotone.min.css?<?= $git_merge_ts ?>" rel="stylesheet">
|
||||
<link href="<?= self::getResourcePath() ?>fontawesome/css/sharp-light.min.css?<?= $git_merge_ts ?>"
|
||||
rel="stylesheet">
|
||||
|
||||
<?php if (!empty($additionalCSS)):
|
||||
foreach ($additionalCSS as $css): ?>
|
||||
<link rel="stylesheet" href="<?= self::getResourcePath() ?><?= $css ?>?<?= $git_merge_ts ?>">
|
||||
<?php endforeach;
|
||||
endif;
|
||||
|
||||
if (!empty($additionalHead)):
|
||||
foreach ($additionalHead as $head):
|
||||
echo $head;
|
||||
endforeach;
|
||||
endif; ?>
|
||||
|
||||
<?php if(isset($additionalCSS) && is_array($additionalCSS) && count($additionalCSS)): ?>
|
||||
<?php foreach($additionalCSS as $css): ?>
|
||||
<link rel="stylesheet" href="<?=self::getResourcePath()?><?=$css?>?<?=$git_merge_ts?>" />
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.mfNotify = <?=isset($mfNotify) ? json_encode($mfNotify) : "null"; ?>;
|
||||
<script>
|
||||
window.mfNotify = <?=json_encode($mfNotify ?? null)?>;
|
||||
window.TT_CONFIG = {};
|
||||
|
||||
<?php
|
||||
if(isset($JSGlobals) && is_array($JSGlobals) && count($JSGlobals)):
|
||||
foreach($JSGlobals as $key => $value): ?>
|
||||
@@ -38,48 +40,36 @@
|
||||
<?php endforeach; endif;?>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="<?=self::getResourcePath()?>js/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="<?=self::getResourcePath()?>js/popper.min.js" defer></script>
|
||||
<script type="text/javascript" src="<?=self::getResourcePath()?>js/bootstrap.min.js" defer></script>
|
||||
<script type="text/javascript" src="<?=self::getResourcePath()?>plugins/notification/notify.js" defer></script>
|
||||
<script type="text/javascript" src="<?=self::getResourcePath()?>plugins/bookstack/bookstackIntegration.js" defer></script>
|
||||
<script src="<?= self::getResourcePath() ?>plugins/notification/notify.js" defer></script>
|
||||
<script src="<?= self::getResourcePath() ?>plugins/bookstack/bookstackIntegration.js" defer></script>
|
||||
|
||||
<?php if(isset($additionalJS) && is_array($additionalJS) && count($additionalJS)): ?>
|
||||
<?php foreach($additionalJS as $js): ?>
|
||||
<?php if (!empty($additionalJS)):
|
||||
foreach ($additionalJS as $js): ?>
|
||||
<script src="<?= self::getResourcePath() ?><?= $js ?>?<?= $git_merge_ts ?>"></script>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if(isset($additionalHead) && is_array($additionalHead) && count($additionalHead)):
|
||||
foreach($additionalHead as $head): ?>
|
||||
<?=$head?>
|
||||
<?php endforeach; endif;?>
|
||||
<?php endforeach;
|
||||
endif; ?>
|
||||
|
||||
<style>
|
||||
<?php if(MFAPPNAME == "devthetool"): ?>
|
||||
body {
|
||||
border-left: 8px dashed #f672a7;
|
||||
}
|
||||
<?php endif; ?>
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
<?php if (MFAPPNAME === "devthetool"): ?>
|
||||
body {
|
||||
border-left: 8px dashed #f672a7;
|
||||
}
|
||||
|
||||
<?php endif; ?>
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
baseurl = '<?=self::getResourcePath()?>';
|
||||
</script>
|
||||
|
||||
<!-- Navigation Bar-->
|
||||
<header id="topnav">
|
||||
<?php include(realpath(dirname(__FILE__)."/")."/topbar.php"); ?>
|
||||
<?php include(realpath(dirname(__FILE__)."/")."/menu.php"); ?>
|
||||
<?php
|
||||
include(__DIR__ . "/topbar.php");
|
||||
include(__DIR__ . "/menu.php");
|
||||
?>
|
||||
</header>
|
||||
<!-- End Navigation Bar-->
|
||||
|
||||
|
||||
<div class="wrapper pl-0 pl-lg-1 pr-0 pr-lg-1">
|
||||
<div class="container-fluid">
|
||||
@@ -30,7 +30,6 @@ class WarehouseShippingNoteController extends TTCrud {
|
||||
protected array $defaultOrder = ['key' => 'create', 'order' => 'DESC'];
|
||||
|
||||
protected array $additionalJSVariables = ['WAREHOUSE_ADMIN' => true];
|
||||
protected array $additionalJS = ['js/pages/WarehouseArticle/WarehouseArticleModal.js'];
|
||||
protected array $additionalHead = ['<link rel="manifest" href="/assets/pwa/shipping-note-manifest.json">'];
|
||||
|
||||
protected array $infoMessages = ['create' => 'Lieferschein wurde erstellt.',
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"phpseclib/phpseclib": "^3.0",
|
||||
"stomp-php/stomp-php": "^5",
|
||||
"phpmailer/phpmailer": "^6.9",
|
||||
"pear2/net_routeros": "dev-develop@dev"
|
||||
"pear2/net_routeros": "dev-develop@dev",
|
||||
"matthiasmullie/minify": "^1.3"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/assets/images/the-tool-logo-top.max.png
Normal file
BIN
public/assets/images/the-tool-logo-top.max.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 2.5 KiB |
53
public/cssbundler.php
Normal file
53
public/cssbundler.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
require '../vendor/autoload.php';
|
||||
|
||||
// Set the content type to CSS
|
||||
header('Content-Type: text/css');
|
||||
header('Cache-Control: max-age=31536000, public'); // Cache for 1 year
|
||||
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT'); // Expires in 1 year
|
||||
|
||||
/**
|
||||
* Combines and minifies an array of CSS files.
|
||||
*
|
||||
* @param array $files An array of CSS file paths.
|
||||
* @return string The combined and minified CSS content.
|
||||
*/
|
||||
function combineAndMinifyCSS($files) {
|
||||
// Create a new Minify\CSS instance
|
||||
$minifier = new \MatthiasMullie\Minify\CSS();
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (file_exists($file)) {
|
||||
// Add the file's content to the minifier
|
||||
$minifier->add($file);
|
||||
} else {
|
||||
// Handle missing files gracefully
|
||||
header("HTTP/1.1 404 Not Found");
|
||||
echo "/* File not found: $file */";
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the minified content
|
||||
return $minifier->minify();
|
||||
}
|
||||
|
||||
// Define the array of CSS files to be combined and minified
|
||||
$cssFiles = [
|
||||
'plugins/daterangepicker/daterangepicker.css',
|
||||
'assets/css/bootstrap.min.css',
|
||||
'assets/css/app.min.css',
|
||||
'plugins/notification/notify.min.css',
|
||||
'assets/css/thetool.css',
|
||||
'plugins/vue/tt-components/css/tt-table.css',
|
||||
'plugins/vue/tt-components/css/tt-tooltip.css',
|
||||
'plugins/vue/tt-components/css/tt-loader.css',
|
||||
'plugins/vue/tt-components/css/tt-file-gallery.css',
|
||||
'plugins/vue/tt-components/css/tt-position-manager.css',
|
||||
];
|
||||
|
||||
// Output the combined and minified CSS
|
||||
echo combineAndMinifyCSS($cssFiles);
|
||||
|
||||
?>
|
||||
File diff suppressed because one or more lines are too long
@@ -1,72 +1,57 @@
|
||||
//TODO: if autocomplete is focus and the input has not a single character still show "Bitte mindestens 3 Zeichen eingeben"
|
||||
//TODO: fix the fetchSuggestions function to not show a loading spinner when the input gets cleared
|
||||
//TODO: fix so we can use arrow keys to navigate the suggestions
|
||||
// TODO: Implement giving the option without the need of an API || need to use computed property to filter the items
|
||||
// TODO: Fix the weirdness with timeout and selecting the suggestion
|
||||
|
||||
Vue.component('tt-autocomplete', {
|
||||
template: `
|
||||
<div class="form-group" :class="{'row': row}"
|
||||
:data-api-url="apiUrl"
|
||||
>
|
||||
<div class="form-group" :class="{'row': row}">
|
||||
<slot name="prepend"></slot>
|
||||
<label :class="{'col-form-label': row, 'col-sm-4': row, 'col-form-label-sm': sm && row}"
|
||||
v-if="label" :for="label">{{ label }}</label>
|
||||
<label v-if="label" :for="label" :class="{'col-form-label': row, 'col-sm-4': row, 'col-form-label-sm': sm && row}">{{ label }}</label>
|
||||
<div class="autocomplete position-relative" :class="{'col-sm-8 p-0': row}">
|
||||
<input
|
||||
type="text"
|
||||
:id="label"
|
||||
class="form-control tt-autocomplete"
|
||||
:class="{'form-control-sm': sm}"
|
||||
v-model="displayValue"
|
||||
:placeholder="placeholder"
|
||||
:class="{'form-control-sm': sm}"
|
||||
:style="{'padding-right': $slots.append ? '30px' : '0'}"
|
||||
type="text"
|
||||
class="form-control tt-autocomplete"
|
||||
autocomplete="off"
|
||||
@input="onInput"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
autocomplete="off"
|
||||
:style="{'padding-right': $slots.append ? '30px' : '0'}"
|
||||
@keydown.esc="showSuggestions = false"
|
||||
@keydown.down.prevent="navigate(1)"
|
||||
@keydown.up.prevent="navigate(-1)"
|
||||
@keydown.enter.prevent="selectOnEnter"
|
||||
/>
|
||||
<slot name="append"></slot>
|
||||
<button v-show="displayValue.length > 0" @click="displayValue = ''; $emit('input', '');" tabindex="-1" type="button"
|
||||
class="btn btn-link position-absolute"
|
||||
style="right: -5px; top: 50%; transform: translateY(-50%);">
|
||||
<button v-show="displayValue.length > 0" @click="clear" tabindex="-1" type="button" class="btn btn-link position-absolute" style="right: -5px; top: 50%; transform: translateY(-50%);">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
|
||||
<ul v-show="showSuggestions && displayValue.length > 0 || isLoading"
|
||||
class="dropdown-menu show dropdown-shadow">
|
||||
|
||||
<li class="dropdown-item" v-show="isLoading">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<ul v-if="showSuggestions" class="dropdown-menu show dropdown-shadow">
|
||||
<li v-if="isLoading" class="dropdown-item">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status"><span class="sr-only">Loading...</span></div>
|
||||
Einträge werden geladen...
|
||||
</li>
|
||||
|
||||
<template v-show="showSuggestions && displayingItems.length && isLoading !== true">
|
||||
<template v-if="!isLoading">
|
||||
<li v-if="displayValue.length < 3" class="dropdown-item disabled">
|
||||
Bitte mindestens 3 Zeichen eingeben
|
||||
</li>
|
||||
<li v-if="displayValue.length >= 3 && !displayingItems.length" class="dropdown-item disabled">
|
||||
Keine Suchergebnisse vorhanden.
|
||||
</li>
|
||||
<li
|
||||
v-for="(item) in displayingItems.slice(0, 10)"
|
||||
v-for="(item, index) in displayingItems.slice(0, 10)"
|
||||
:key="item.value"
|
||||
:class="{'active': value === item.value}"
|
||||
:class="{'active': cursor === index}"
|
||||
class="dropdown-item"
|
||||
@click.prevent="selectSuggestion(item)"
|
||||
@mousedown.prevent="selectSuggestion(item)"
|
||||
style="cursor: pointer;"
|
||||
@mousedown.prevent="selectSuggestion(item)"
|
||||
>
|
||||
{{ item.text }}
|
||||
</li>
|
||||
<!-- display more search results available define it more precisely in German -->
|
||||
<li v-show="displayingItems.length > 10" class="dropdown-item disabled">
|
||||
<li v-if="displayingItems.length > 10" class="dropdown-item disabled">
|
||||
Mehr Suchergebnisse vorhanden. Bitte genauer eingeben
|
||||
</li>
|
||||
</template>
|
||||
<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
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,132 +65,117 @@ Vue.component('tt-autocomplete', {
|
||||
sm: {type: Boolean, default: true},
|
||||
row: {type: Boolean, default: false},
|
||||
returnText: {type: Boolean, default: false},
|
||||
emitDisplayValue: {type: Boolean, default: false},
|
||||
caseInsensitive: {type: Boolean, default: false},
|
||||
}, data() {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
window,
|
||||
displayingItems: [],
|
||||
oldDisplayValue: '',
|
||||
displayValue: '',
|
||||
displayingItems: [],
|
||||
isLoading: false,
|
||||
showSuggestions: false,
|
||||
cursor: -1,
|
||||
fetchSuggestionsDebounceTimer: null,
|
||||
disableIDFetch: false
|
||||
disableValueWatcher: false,
|
||||
cursor: -1,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.updateDisplayValue().then();
|
||||
watch: {
|
||||
value: {
|
||||
handler(newValue) {
|
||||
if (this.disableValueWatcher) return;
|
||||
this.updateDisplayValue(newValue);
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
apiUrl: {
|
||||
handler: 'fetchSuggestions',
|
||||
immediate: true
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setOldDisplayValue(newValue, oldValue) {
|
||||
if (this.emitDisplayValue && newValue && typeof this.value !== 'number') {
|
||||
this.$emit('displayValue', newValue);
|
||||
}
|
||||
this.oldDisplayValue = oldValue;
|
||||
},
|
||||
async updateDisplayValue(newValue, oldValue) {
|
||||
if (this.disableIDFetch) {
|
||||
this.disableIDFetch = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue) {
|
||||
this.$emit('input', newValue);
|
||||
this.value = newValue;
|
||||
}
|
||||
if (oldValue && !newValue) {
|
||||
this.$emit('input', '');
|
||||
async updateDisplayValue(id) {
|
||||
if (!id) {
|
||||
this.displayValue = '';
|
||||
}
|
||||
|
||||
|
||||
if (this.value && this.apiUrl) {
|
||||
if (this.returnText && isNaN(this.value)) {
|
||||
this.displayValue = this.value;
|
||||
return;
|
||||
}
|
||||
const response = await axios.get(`${this.apiUrl}${this.apiUrl.includes('?') ? '&' : '?'}autocomplete=1&searchedID=${this.value}`);
|
||||
this.displayValue = response.data[0].text;
|
||||
} else if (this.value) {
|
||||
const selectedItem = this.caseInsensitive ?
|
||||
this.items.find(item => item.value.toLowerCase() === this.value.toLowerCase()) : this.items.find(item => item.value === this.value)
|
||||
;
|
||||
this.displayValue = selectedItem ? selectedItem.text : this.displayValue;
|
||||
|
||||
if (this.returnText && isNaN(id)) {
|
||||
this.displayValue = id;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.apiUrl) {
|
||||
const response = await axios.get(`${this.apiUrl}${this.apiUrl.includes('?') ? '&' : '?'}autocomplete=1&searchedID=${id}`);
|
||||
if (response.data[0]) this.displayValue = response.data[0].text;
|
||||
} else {
|
||||
if (this.returnText === false && !(typeof this.value === 'undefined' || this.value === '')) this.$emit('input', '');
|
||||
this.displayValue = this.displayValue.replace(this.oldDisplayValue, '');
|
||||
const selectedItem = this.items.find(item =>
|
||||
this.caseInsensitive ? String(item.value).toLowerCase() === String(id).toLowerCase() : item.value === id
|
||||
);
|
||||
if (selectedItem) this.displayValue = selectedItem.text;
|
||||
}
|
||||
},
|
||||
onInput(event) {
|
||||
this.displayValue = event.target.value;
|
||||
onInput() {
|
||||
if (this.returnText) this.$emit('input', this.displayValue);
|
||||
this.fetchSuggestions();
|
||||
}, onFocus() {
|
||||
},
|
||||
onFocus() {
|
||||
this.showSuggestions = true;
|
||||
}, onBlur() {
|
||||
setTimeout(() => {
|
||||
this.showSuggestions = false;
|
||||
}, 200);
|
||||
}, fetchSuggestions() {
|
||||
this.$set(this, 'displayingItems', []);
|
||||
if (this.displayValue.length < 3) return;
|
||||
|
||||
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;
|
||||
this.fetchSuggestions();
|
||||
},
|
||||
onBlur() {
|
||||
setTimeout(() => this.showSuggestions = false, 200);
|
||||
},
|
||||
fetchSuggestions() {
|
||||
clearTimeout(this.fetchSuggestionsDebounceTimer);
|
||||
|
||||
this.fetchSuggestionsDebounceTimer = setTimeout(() => {
|
||||
setTimeout(async () => {
|
||||
if (this.displayValue.length < 3) {
|
||||
this.displayingItems = [];
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.get(`${this.apiUrl}${this.apiUrl.includes('?') ? '&' : '?'}autocomplete=1&q=${encodeURIComponent(this.displayValue)}`);
|
||||
if (response.data?.status === 'error') {
|
||||
this.displayingItems = [];
|
||||
} else {
|
||||
this.displayingItems = response.data
|
||||
if (response.data.length === 1 && response.data[0].text === this.displayValue) this.selectSuggestion(response.data[0]);
|
||||
}
|
||||
this.isLoading = false;
|
||||
this.fetchSuggestionsDebounceTimer = setTimeout(() => {
|
||||
this.isLoading = true;
|
||||
this.cursor = -1;
|
||||
|
||||
this.fetchSuggestionsDebounceTimer = null;
|
||||
}, 100);
|
||||
}, 300); // Adjust the 300ms debounce time as needed
|
||||
}, selectSuggestion(item) {
|
||||
this.disableIDFetch = true;
|
||||
this.$emit('input', item.value);
|
||||
if (!this.apiUrl) {
|
||||
const searchTerms = this.displayValue.toLowerCase().split(' ').filter(s => s);
|
||||
this.displayingItems = this.items.filter(item => {
|
||||
const itemText = item.text.toLowerCase();
|
||||
return searchTerms.every(term => itemText.includes(term));
|
||||
});
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
axios.get(`${this.apiUrl}${this.apiUrl.includes('?') ? '&' : '?'}autocomplete=1&q=${encodeURIComponent(this.displayValue)}`)
|
||||
.then(response => {
|
||||
this.displayingItems = response.data?.status === 'error' ? [] : response.data;
|
||||
if (this.displayingItems.length === 1 && this.displayingItems[0].text === this.displayValue) {
|
||||
this.selectSuggestion(this.displayingItems[0]);
|
||||
}
|
||||
})
|
||||
.finally(() => this.isLoading = false);
|
||||
}, 300);
|
||||
},
|
||||
selectSuggestion(item) {
|
||||
this.disableValueWatcher = true;
|
||||
this.displayValue = item.text;
|
||||
this.$emit('input', item.value);
|
||||
this.showSuggestions = false;
|
||||
}, clear() {
|
||||
this.$nextTick(() => this.disableValueWatcher = false);
|
||||
},
|
||||
selectOnEnter() {
|
||||
if (this.cursor >= 0 && this.displayingItems[this.cursor])
|
||||
this.selectSuggestion(this.displayingItems[this.cursor]);
|
||||
},
|
||||
navigate(direction) {
|
||||
const itemCount = this.displayingItems.slice(0, 10).length;
|
||||
if (!itemCount) return;
|
||||
this.cursor = (this.cursor + direction + itemCount) % itemCount;
|
||||
},
|
||||
clear() {
|
||||
this.displayValue = '';
|
||||
this.$emit('input', '');
|
||||
this.displayingItems = [];
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
displayValue: {handler: 'setOldDisplayValue', immediate: true},
|
||||
value: {handler: 'updateDisplayValue', immediate: true},
|
||||
apiUrl: {handler: 'fetchSuggestions', immediate: true},
|
||||
}
|
||||
});
|
||||
@@ -1,25 +1,18 @@
|
||||
// noinspection JSCheckFunctionSignatures
|
||||
Vue.component('tt-button', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<a v-if="href" :href="href" class="btn" :class="buttonClasses" @click="handleClick">
|
||||
<template v-if="loading">
|
||||
<span class="spinner"></span>
|
||||
</template>
|
||||
<component :is="href ? 'a' : 'button'"
|
||||
:href="href"
|
||||
:disabled="!href && loading"
|
||||
:class="buttonClasses"
|
||||
class="btn"
|
||||
@click="handleClick">
|
||||
<span v-if="loading" class="spinner"></span>
|
||||
<template v-else>
|
||||
<i v-if="icon" :class="icon"></i>
|
||||
{{ text }}
|
||||
</template>
|
||||
</a>
|
||||
<button v-else @click="handleClick" class="btn" :class="buttonClasses" :disabled="loading">
|
||||
<template v-if="loading">
|
||||
<span class="spinner"></span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i v-if="icon" :class="icon"></i>
|
||||
{{text}}
|
||||
</template>
|
||||
</button>
|
||||
</component>
|
||||
`,
|
||||
props: {
|
||||
sm: {type: Boolean, default: false},
|
||||
@@ -32,26 +25,20 @@ Vue.component('tt-button', {
|
||||
},
|
||||
computed: {
|
||||
buttonClasses() {
|
||||
const classes = {
|
||||
'btn-sm': this.sm,
|
||||
'btn-loading': this.loading
|
||||
}
|
||||
|
||||
if (this.additionalClass) {
|
||||
this.additionalClass.split(' ').forEach(className => {
|
||||
classes[className] = true
|
||||
})
|
||||
}
|
||||
|
||||
return classes
|
||||
const classes = [];
|
||||
if (this.sm) classes.push('btn-sm');
|
||||
if (this.loading) classes.push('btn-loading');
|
||||
if (this.additionalClass) classes.push(...this.additionalClass.split(' '));
|
||||
return classes;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick(event) {
|
||||
if (this.loading) return event.preventDefault();
|
||||
if (typeof this.confirmText === 'string' && !confirm(this.confirmText)) return event.preventDefault();
|
||||
|
||||
this.$emit('click', event)
|
||||
if (this.loading || (this.confirmText && !confirm(this.confirmText))) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
this.$emit('click', event);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -14,139 +14,109 @@ Vue.component('tt-date-picker', {
|
||||
template: `
|
||||
<div class="form-group" :class="{'row': row}">
|
||||
<slot name="prepend"></slot>
|
||||
<label
|
||||
:class="{'col-form-label': row, 'col-sm-4': row, 'col-form-label-sm': sm && row}"
|
||||
v-if="label"
|
||||
:for="label">{{ label }}</label>
|
||||
<input type="text"
|
||||
:id="label"
|
||||
:name="label"
|
||||
class="form-control"
|
||||
:class="{'form-control-sm': sm, 'col-sm-8': row}"
|
||||
:placeholder="placeholder"
|
||||
:required="required"
|
||||
:disabled="disabled"
|
||||
v-bind="additionalProps"
|
||||
ref="input"
|
||||
style="cursor: pointer;"
|
||||
>
|
||||
<label v-if="label" :for="label" :class="{'col-form-label': row, 'col-sm-4': row, 'col-form-label-sm': sm && row}">{{ label }}</label>
|
||||
<input type="text" ref="input" style="cursor: pointer;"
|
||||
:id="label" :name="label" :placeholder="placeholder" :required="required" :disabled="disabled"
|
||||
class="form-control" :class="{'form-control-sm': sm, 'col-sm-8': row}" v-bind="additionalProps">
|
||||
<small v-if="hint" class="form-text text-muted">{{ hint }}</small>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
inputValue: '',
|
||||
isInitialized: false,
|
||||
moment: window.moment,
|
||||
locale: {
|
||||
// ... (keep your existing locale object)
|
||||
format: 'DD.MM.YYYY HH:mm',
|
||||
separator: ' - ',
|
||||
applyLabel: 'Übernehmen',
|
||||
cancelLabel: 'Abbrechen',
|
||||
fromLabel: 'Von',
|
||||
toLabel: 'Bis',
|
||||
customRangeLabel: 'Benutzerdef.',
|
||||
weekLabel: 'W',
|
||||
daysOfWeek: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
|
||||
monthNames: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
|
||||
firstDay: 1
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.isInitialized = true;
|
||||
const datePickerOptions = {
|
||||
autoUpdateInput: true,
|
||||
async mounted() {
|
||||
const loadScript = (src, check) => new Promise((resolve, reject) => {
|
||||
if (check()) return resolve();
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.onload = resolve;
|
||||
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
await loadScript('/js/jquery.min.js', () => typeof jQuery !== 'undefined');
|
||||
await loadScript('/plugins/daterangepicker/daterangepicker.js', () => typeof $?.fn?.daterangepicker !== 'undefined');
|
||||
|
||||
const pickerOptions = {
|
||||
autoUpdateInput: false,
|
||||
singleDatePicker: !this.dateRange,
|
||||
timePicker: true,
|
||||
timePicker24Hour: true,
|
||||
locale: this.locale,
|
||||
startDate: this.dateRange ? (this.value?.from ? this.moment.unix(this.value.from) : this.moment()) : (this.value ? this.moment.unix(this.value) : this.moment()),
|
||||
endDate: this.dateRange ? (this.value?.to ? this.moment.unix(this.value.to) : this.moment()) : (this.value ? this.moment.unix(this.value) : this.moment()),
|
||||
};
|
||||
|
||||
if (this.dateRange) {
|
||||
datePickerOptions.startDate = this.value?.from ? this.moment.unix(this.value.from) : undefined;
|
||||
datePickerOptions.endDate = this.value?.to ? this.moment.unix(this.value.to) : undefined;
|
||||
} else {
|
||||
datePickerOptions.startDate = this.value ? this.moment.unix(this.value) : undefined;
|
||||
}
|
||||
const $input = $(this.$refs.input);
|
||||
$input.daterangepicker(pickerOptions);
|
||||
this.updateInputValue(this.value);
|
||||
|
||||
$(this.$refs.input).daterangepicker(datePickerOptions);
|
||||
|
||||
const _this = this;
|
||||
$(this.$refs.input).on('apply.daterangepicker', function (ev, picker) {
|
||||
if (_this.dateRange) {
|
||||
_this.$emit('input', {
|
||||
from: picker.startDate.unix(),
|
||||
to: picker.endDate.unix()
|
||||
});
|
||||
$(_this.$refs.input).val(picker.startDate.format(_this.locale.format) + _this.locale.separator + picker.endDate.format(_this.locale.format));
|
||||
} else {
|
||||
_this.$emit('input', picker.startDate.unix());
|
||||
$(_this.$refs.input).val(picker.startDate.format(_this.locale.format));
|
||||
}
|
||||
$input.on('apply.daterangepicker', (ev, picker) => {
|
||||
const val = this.dateRange
|
||||
? { from: picker.startDate.unix(), to: picker.endDate.unix() }
|
||||
: picker.startDate.unix();
|
||||
this.$emit('input', val);
|
||||
this.updateInputValue(val);
|
||||
});
|
||||
|
||||
function checkIfAppliedElseClear() {
|
||||
if (_this.dateRange) {
|
||||
if (_this.value && _this.value.from && _this.value.to) return;
|
||||
} else {
|
||||
if (_this.value) return;
|
||||
}
|
||||
$(_this.$refs.input).val('');
|
||||
}
|
||||
$input.on('cancel.daterangepicker', () => {
|
||||
this.$emit('input', undefined);
|
||||
this.updateInputValue(undefined);
|
||||
});
|
||||
|
||||
function clearIfCancelled() {
|
||||
_this.$emit('input', _this.dateRange ? {from: null, to: null} : null);
|
||||
$(_this.$refs.input).val('');
|
||||
}
|
||||
|
||||
$(this.$refs.input).on('cancel.daterangepicker', clearIfCancelled);
|
||||
$(this.$refs.input).on('hide.daterangepicker', checkIfAppliedElseClear.bind(this));
|
||||
|
||||
// Clear input field if value is not set
|
||||
if (_this.dateRange) {
|
||||
if (!this.value || this.value.from === null || this.value.to === null) {
|
||||
$(_this.$refs.input).val('');
|
||||
}
|
||||
} else {
|
||||
if (!this.value) {
|
||||
$(_this.$refs.input).val('');
|
||||
}
|
||||
}
|
||||
|
||||
// Add click event listener to the document
|
||||
document.addEventListener('click', this.handleOutsideClick);
|
||||
this.isInitialized = true;
|
||||
},
|
||||
methods: {
|
||||
handleOutsideClick(event) {
|
||||
const datepicker = document.querySelector('.daterangepicker');
|
||||
const input = this.$refs.input;
|
||||
|
||||
if (datepicker && !datepicker.contains(event.target) && event.target !== input) {
|
||||
if (input.parentElement.tagName.toLowerCase() === 'div' && input.parentElement.classList.contains('form-group')) {
|
||||
return;
|
||||
}
|
||||
$(this.$refs.input).data('daterangepicker').hide();
|
||||
updateInputValue(val) {
|
||||
const $input = $(this.$refs.input);
|
||||
if (this.dateRange) {
|
||||
if (val?.from && val?.to) {
|
||||
const from = this.moment.unix(val.from).format(this.locale.format);
|
||||
const to = this.moment.unix(val.to).format(this.locale.format);
|
||||
$input.val(from + this.locale.separator + to);
|
||||
} else $input.val('');
|
||||
} else {
|
||||
if (val) $input.val(this.moment.unix(val).format(this.locale.format));
|
||||
else $input.val('');
|
||||
}
|
||||
},
|
||||
setStartDate(date) {
|
||||
$(this.$refs.input).data('daterangepicker').setStartDate(date);
|
||||
}
|
||||
setStartDate: (date) => $(this.$refs.input).data('daterangepicker').setStartDate(date)
|
||||
},
|
||||
watch: {
|
||||
value: function (newVal) {
|
||||
value(newVal) {
|
||||
if (!this.isInitialized) return;
|
||||
|
||||
this.updateInputValue(newVal);
|
||||
|
||||
const datePicker = $(this.$refs.input).data('daterangepicker');
|
||||
if (this.dateRange) {
|
||||
if (!newVal || newVal.from === null || newVal.to === null) {
|
||||
$(this.$refs.input).val('');
|
||||
datePicker.setStartDate(newVal?.from ? this.moment.unix(newVal.from) : this.moment());
|
||||
datePicker.setEndDate(newVal?.to ? this.moment.unix(newVal.to) : this.moment());
|
||||
} else {
|
||||
datePicker.setStartDate(this.moment.unix(newVal.from));
|
||||
datePicker.setEndDate(this.moment.unix(newVal.to));
|
||||
}
|
||||
} else {
|
||||
if (!newVal) {
|
||||
$(this.$refs.input).val('');
|
||||
} else {
|
||||
datePicker.setStartDate(this.moment.unix(newVal));
|
||||
datePicker.setEndDate(this.moment.unix(newVal));
|
||||
}
|
||||
const date = newVal ? this.moment.unix(newVal) : this.moment();
|
||||
datePicker.setStartDate(date);
|
||||
datePicker.setEndDate(date);
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
$(this.$refs.input).off('apply.daterangepicker');
|
||||
document.removeEventListener('click', this.handleOutsideClick);
|
||||
const picker = $(this.$refs.input).data('daterangepicker');
|
||||
if (picker) picker.remove();
|
||||
},
|
||||
})
|
||||
});
|
||||
@@ -201,9 +201,9 @@ Vue.component('tt-table', {
|
||||
.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>-->
|
||||
<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-if="column.filter === 'autocomplete'">
|
||||
{{ autoCompleteData[key] && autoCompleteData[key][row[key]] ? autoCompleteData[key][row[key]]['title'] : row[key] }}
|
||||
|
||||
@@ -236,9 +236,9 @@ Vue.component('tt-table', {
|
||||
}}</span>
|
||||
<span v-else-if="column.filter === 'select'">{{ columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text }}</span>
|
||||
<span v-else-if="key === 'create'">{{ window.moment(row[key] * 1000).format('DD.MM.YYYY HH:mm:ss') }}</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>-->
|
||||
<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-if="column.filter === 'autocomplete'">
|
||||
{{ autoCompleteData[key] && autoCompleteData[key][row[key]] ? autoCompleteData[key][row[key]]['title'] : row[key] }}</span>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user