Compare commits

...

10 Commits

Author SHA1 Message Date
adisposkofh b90dabeb2c minor fix 2026-04-21 11:23:17 +02:00
adisposkofh 2f1edfeeab minor fix 2026-04-21 11:21:50 +02:00
adisposkofh 414d8bd383 minor fix 2026-04-20 11:40:26 +02:00
adisposkofh 1a813e52ce minor fix 2026-04-20 09:50:41 +02:00
adisposkofh 16b238124a cleaned up search icon conditional render 2026-04-20 09:24:51 +02:00
adisposkofh 4def45907b reworked searchbar animation 2026-04-17 18:10:05 +02:00
adisposkofh 202e6e88d2 redid searchbar animation with handmade transitions 2026-04-17 17:44:23 +02:00
adisposkofh 3b2473039f code formatting 2026-04-17 16:37:58 +02:00
adisposkofh 59d1ca3409 animated searchbar display in header 2026-04-17 16:36:54 +02:00
adisposkofh 1d26303333 fixed mobile header/searchbar appearance 2026-04-17 15:59:32 +02:00
4 changed files with 412 additions and 325 deletions
+12
View File
@@ -136,6 +136,18 @@ const app = Vue.createApp({
} }
}; };
}, },
computed: {
isMobile() {
const smallScreen = window.matchMedia("(max-width: 767px)").matches;
const touchCapable = ("ontouchstart" in window) || navigator.maxTouchPoints > 0;
return smallScreen;// && touchCapable;
},
},
provide() {
return {
isMobile: this.isMobile
}
},
methods: { methods: {
searchfunction: function(searchsettings) { searchfunction: function(searchsettings) {
return this.$api.call(ApiSearchbar.searchCis(searchsettings)); return this.$api.call(ApiSearchbar.searchCis(searchsettings));
+2 -2
View File
@@ -238,13 +238,13 @@ const app = Vue.createApp({
const smallScreen = window.matchMedia("(max-width: 767px)").matches; const smallScreen = window.matchMedia("(max-width: 767px)").matches;
const touchCapable = ("ontouchstart" in window) || navigator.maxTouchPoints > 0; const touchCapable = ("ontouchstart" in window) || navigator.maxTouchPoints > 0;
return smallScreen;// && touchCapable; return smallScreen;// && touchCapable;
} },
}, },
provide() { provide() {
return { // provide injectable & watchable language property return { // provide injectable & watchable language property
language: Vue.computed(() => this.$p.user_language), language: Vue.computed(() => this.$p.user_language),
renderers: Vue.computed(() => this.renderers), renderers: Vue.computed(() => this.renderers),
isMobile: this.isMobile isMobile: this.isMobile,
} }
}, },
methods: { methods: {
+47 -20
View File
@@ -30,6 +30,7 @@ export default {
menuOpen:true, menuOpen:true,
}; };
}, },
inject: ["isMobile"],
provide(){ provide(){
return{ return{
setActiveEntry: this.setActiveEntry, setActiveEntry: this.setActiveEntry,
@@ -58,7 +59,7 @@ export default {
}, },
site_url(){ site_url(){
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router; return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
} },
}, },
methods: { methods: {
fetchMenu() { fetchMenu() {
@@ -112,10 +113,26 @@ export default {
}); });
}, },
template: /*html*/` template: /*html*/`
<button id="nav-main-btn" class="navbar-toggler rounded-0" type="button" data-bs-toggle="offcanvas" data-bs-target="#nav-main" aria-controls="nav-main" aria-expanded="false" aria-label="Toggle navigation"> <div
<span class="navbar-toggler-icon"></span> id="header-options-collapsible"
</button> class="collapse multi-collapse collapse-horizontal show"
<fhc-searchbar ref="searchbar" id="nav-search" class="fhc-searchbar w-100 py-1 py-lg-2" :searchoptions="searchbaroptions" :searchfunction="searchfunction"></fhc-searchbar> >
<div class="d-flex flex-row align-items-center gap-2 h-100" style="width: 79px">
<button id="nav-main-btn" class="navbar-toggler rounded-0 px-2 border-0" type="button" data-bs-toggle="offcanvas" data-bs-target="#nav-main" aria-controls="nav-main" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<span v-if="isMobile" class="d-flex flex-row align-items-center">
<theme-switch></theme-switch>
</span>
</div>
</div>
<fhc-searchbar
:searchoptions="searchbaroptions"
:searchfunction="searchfunction"
ref="searchbar"
id="nav-search"
class="fhc-searchbar w-100 py-1 py-lg-2"
></fhc-searchbar>
<div id="nav-logo" class="d-none d-lg-block"> <div id="nav-logo" class="d-none d-lg-block">
<div class="d-flex h-100 justify-content-between"> <div class="d-flex h-100 justify-content-between">
<a :href="rootUrl"> <a :href="rootUrl">
@@ -124,22 +141,32 @@ export default {
<theme-switch></theme-switch> <theme-switch></theme-switch>
</div> </div>
</div> </div>
<div id="nav-user">
<button id="nav-user-btn" class="btn btn-link rounded-0" type="button" data-bs-toggle="collapse" data-bs-target="#nav-user-menu" aria-expanded="false" aria-controls="nav-user-menu"> <div
<img :src="avatarUrl" :alt="$p.t('profilUpdate/profilBild')" class="bg-dark avatar rounded-circle border border-dark"/> id="header-usermenu-collapsible"
</button> class="collapse multi-collapse collapse-horizontal show"
<ul ref="navUserDropdown" >
@[\`shown.bs.collapse\`]="handleShowNavUser" <div
@[\`hide.bs.collapse\`]="handleHideNavUser" :style="!isMobile ? '' : 'width: 51px'"
id="nav-user-menu" class="top-100 end-0 collapse list-unstyled" aria-labelledby="nav-user-btn"> id="nav-user"
<li><a class="fhc-dark-bg btn rounded-0 d-block" :href="site_url + '/Cis/Profil'" id="menu-profil">Profil</a></li> >
<li > <button id="nav-user-btn" class="btn btn-link rounded-0" type="button" data-bs-toggle="collapse" data-bs-target="#nav-user-menu" aria-expanded="false" aria-controls="nav-user-menu">
<cis-sprachen @languageChanged="fetchMenu"></cis-sprachen> <img :src="avatarUrl" :alt="$p.t('profilUpdate/profilBild')" class="bg-dark avatar rounded-circle border border-dark"/>
</li> </button>
<li><hr class="dropdown-divider m-0 "></li> <ul ref="navUserDropdown"
<li ><a class="fhc-dark-bg btn rounded-0 d-block" :href="logoutUrl">Logout</a></li> @[\`shown.bs.collapse\`]="handleShowNavUser"
</ul> @[\`hide.bs.collapse\`]="handleHideNavUser"
id="nav-user-menu" class="top-100 end-0 collapse list-unstyled" aria-labelledby="nav-user-btn">
<li><a class="fhc-dark-bg btn rounded-0 d-block" :href="site_url + '/Cis/Profil'" id="menu-profil">Profil</a></li>
<li >
<cis-sprachen @languageChanged="fetchMenu"></cis-sprachen>
</li>
<li><hr class="dropdown-divider m-0 "></li>
<li ><a class="fhc-dark-bg btn rounded-0 d-block" :href="logoutUrl">Logout</a></li>
</ul>
</div>
</div> </div>
<nav id="nav-main" class="offcanvas offcanvas-start" tabindex="-1" aria-labelledby="nav-main-btn" data-bs-backdrop="false"> <nav id="nav-main" class="offcanvas offcanvas-start" tabindex="-1" aria-labelledby="nav-main-btn" data-bs-backdrop="false">
<div id="nav-main-sticky"> <div id="nav-main-sticky">
<div id="nav-main-toggle" class="position-static d-none d-lg-block "> <div id="nav-main-toggle" class="position-static d-none d-lg-block ">
+351 -303
View File
@@ -21,69 +21,71 @@ export default {
dms, dms,
cms, cms,
mergedStudent, mergedStudent,
mergedPerson mergedPerson,
}, },
props: { props: {
searchoptions: { searchoptions: {
type: Object, type: Object,
required: true required: true,
}, },
searchfunction: { searchfunction: {
type: Function, type: Function,
required: true required: true,
}, },
showBtnSubmit: Boolean showBtnSubmit: Boolean,
}, },
provide() { provide() {
return { return {
query: Vue.computed(() => this.lastQuery) query: Vue.computed(() => this.lastQuery),
}; };
}, },
data: function() { inject: ["isMobile"],
return { data: function () {
searchtimer: null, return {
hidetimer: null, searchtimer: null,
searchsettings: { hidetimer: null,
searchstr: this.getSearchStr(), searchsettings: {
types: this.getInitiallySelectedTypes(), searchstr: this.getSearchStr(),
}, types: this.getInitiallySelectedTypes(),
searchresult: [], },
searchmode: '', searchresult: [],
showresult: false, searchmode: "",
searching: false, showresult: false,
error: null, searching: false,
abortController: null, error: null,
abortController: null,
settingsDropdown: null, settingsDropdown: null,
lastQuery: '' lastQuery: "",
}; isSearchShownInMobileView: false,
}, };
},
computed: { computed: {
searchTypesPlaceholder() { searchTypesPlaceholder() {
if (!this.searchsettings.types.length) { if (!this.searchsettings.types.length) {
return Object.values(this.typeLabels).join(' / '); return Object.values(this.typeLabels).join(" / ");
} }
return this.searchsettings.types.map(type => this.typeLabels[type]).join(' / '); return this.searchsettings.types
.map((type) => this.typeLabels[type])
.join(" / ");
}, },
types() { types() {
if (!this.searchoptions.types) if (!this.searchoptions.types) return [];
return [];
if (Array.isArray(this.searchoptions.types)) if (Array.isArray(this.searchoptions.types))
return this.searchoptions.types; return this.searchoptions.types;
return Object.keys(this.searchoptions.types); return Object.keys(this.searchoptions.types);
}, },
typeLabels() { typeLabels() {
if (!this.searchoptions.types) if (!this.searchoptions.types) return {};
return {};
if (Array.isArray(this.searchoptions.types)) { if (Array.isArray(this.searchoptions.types)) {
return this.searchoptions.types.reduce((res, type) => { return this.searchoptions.types.reduce((res, type) => {
res[type] = type; res[type] = type;
return res return res;
}, {}); }, {});
} }
return this.searchoptions.types; return this.searchoptions.types;
} },
}, },
template: /*html*/` template: /*html*/ `
<form <form
ref="searchform" ref="searchform"
class="d-flex me-3" class="d-flex me-3"
@@ -92,80 +94,91 @@ export default {
@focusin="searchfocusin" @focusin="searchfocusin"
@focusout="searchfocusout" @focusout="searchfocusout"
> >
<div <span
ref="searchbox" v-if="isMobile"
class="h-100 input-group me-2 searchbar_searchbox" type="button"
:class="showresult ? 'open' : 'closed'" data-bs-toggle="collapse"
data-bs-target=".multi-collapse"
aria-controls="header-searchbar-collapsible header-options-collapsible header-usermenu-collapsible"
aria-expanded="false"
class="d-flex flex-row align-items-center ps-3 pe-1"
> >
<span class="input-group-text"> <i v-if="isSearchShownInMobileView" class="fa-solid fa-chevron-left"></i>
<i class="fa-solid fa-magnifying-glass"></i> <i v-else class="fa-solid fa-magnifying-glass"></i>
</span> </span>
<input
ref="input"
@keyup="search"
@focus="showsearchresult"
v-model="searchsettings.searchstr"
class="form-control searchbar_input"
type="search"
:placeholder="$p.t('search/input_search_label', { types: searchTypesPlaceholder })"
:aria-label="$p.t('search/input_search_label', { types: searchTypesPlaceholder })"
>
<button
v-if="searchsettings.searchstr"
type="button"
class="searchbar_input_clear btn btn-outline-secondary"
@click="clearInput"
@focusin.stop
>
<i class="fas fa-close"></i>
</button>
<button
v-if="showBtnSubmit"
type="submit"
class="btn btn-primary"
:title="$p.t('search/submit')"
:aria-label="$p.t('search/submit')"
>
<i class="fas fa-search"></i>
</button>
<button
data-bs-toggle="collapse"
data-bs-target="#searchSettings"
aria-expanded="false"
aria-controls="searchSettings"
ref="settingsbutton"
class="searchbar_setting_btn btn btn-secondary"
type="button"
:title="$p.t('search/button_filter_label')"
:aria-label="$p.t('search/button_filter_label')"
>
<i class="fas fa-cog"></i>
</button>
</div>
<div v-show="showresult" <div
class="searchbar_results" tabindex="-1"> :class="{'flex-grow-1': !isMobile, 'collapse multi-collapse collapse-horizontal': isMobile}"
<div class="searchbar_results_scroller" ref="result"> id="header-searchbar-collapsible"
<div class="searchbar_results_wrapper" ref="results"> @[\`show.bs.collapse\`]="isSearchShownInMobileView = true"
<div v-if="searching"> @[\`hidden.bs.collapse\`]="isSearchShownInMobileView = false"
<i class="fas fa-spinner fa-spin fa-2x"></i> >
</div> <div
<div v-else-if="this.error !== null">{{ error }}</div> :class="{open: showresult, closed: showresult, 'px-3': isMobile}"
<div v-else-if="searchresult.length < 1">{{ $p.t('search/error_no_results') }}</div> ref="searchbox"
<template v-else v-for="res in searchresult"> class="h-100 input-group me-2 searchbar_searchbox"
<component :style="isMobile ? 'width: ' + getMaxWidthOfSearchbarInMobileView() : ''"
v-if="isValidRenderer(res.renderer)" >
:is="res.renderer" <span class="input-group-text">
:mode="searchmode" <i class="fa-solid fa-magnifying-glass color-white"></i>
:res="res" </span>
:actions="getActions(res)" <input
@actionexecuted="hideresult" ref="input"
></component> @keyup="search"
<div v-else class="searchbar-result text-danger fw-bold">{{ $p.t('search/error_unknown_type', res) }}</div> @focus="showsearchresult"
</template> v-model="searchsettings.searchstr"
</div> class="form-control searchbar_input"
</div> type="search"
</div> :placeholder="$p.t('search/input_search_label', { types: searchTypesPlaceholder })"
:aria-label="$p.t('search/input_search_label', { types: searchTypesPlaceholder })"
>
<button
v-if="showBtnSubmit"
type="submit"
class="btn btn-primary"
:title="$p.t('search/submit')"
:aria-label="$p.t('search/submit')"
>
<i class="fas fa-search"></i>
</button>
<button
data-bs-toggle="collapse"
data-bs-target="#searchSettings"
aria-expanded="false"
aria-controls="searchSettings"
ref="settingsbutton"
class="searchbar_setting_btn btn btn-secondary"
type="button"
:title="$p.t('search/button_filter_label')"
:aria-label="$p.t('search/button_filter_label')"
>
<i class="fas fa-filter"></i>
</button>
</div>
<div v-show="showresult"
class="searchbar_results" tabindex="-1">
<div class="searchbar_results_scroller" ref="result">
<div class="searchbar_results_wrapper" ref="results">
<div v-if="searching">
<i class="fas fa-spinner fa-spin fa-2x"></i>
</div>
<div v-else-if="this.error !== null">{{ error }}</div>
<div v-else-if="searchresult.length < 1">{{ $p.t('search/error_no_results') }}</div>
<template v-else v-for="res in searchresult">
<component
v-if="isValidRenderer(res.renderer)"
:is="res.renderer"
:mode="searchmode"
:res="res"
:actions="getActions(res)"
@actionexecuted="hideresult"
></component>
<div v-else class="searchbar-result text-danger fw-bold">{{ $p.t('search/error_unknown_type', res) }}</div>
</template>
</div>
</div>
</div>
</div>
<div <div
id="searchSettings" id="searchSettings"
@@ -207,34 +220,42 @@ export default {
</div> </div>
</form> </form>
`, `,
watch:{ watch: {
'searchsettings.searchstr': function (newSearchValue) { "searchsettings.searchstr": function (newSearchValue) {
if(this.searchoptions.origin){ if (this.searchoptions.origin) {
sessionStorage.setItem(`${this.searchoptions.origin}_searchstr`,newSearchValue); sessionStorage.setItem(
`${this.searchoptions.origin}_searchstr`,
newSearchValue,
);
} }
}, },
'searchsettings.types'(newValue) { "searchsettings.types"(newValue) {
if (Array.isArray(newValue) && newValue.length === 0) { if (Array.isArray(newValue) && newValue.length === 0) {
this.searchsettings.types = [...this.types]; this.searchsettings.types = [...this.types];
} }
// stores the search types in the localstorage, only if the newValue is also an array // stores the search types in the localstorage, only if the newValue is also an array
if (Array.isArray(newValue) && this.searchoptions.origin) { if (Array.isArray(newValue) && this.searchoptions.origin) {
localStorage.setItem(`${this.searchoptions.origin}_searchtypes`, JSON.stringify(newValue)); localStorage.setItem(
`${this.searchoptions.origin}_searchtypes`,
JSON.stringify(newValue),
);
} }
this.search(); this.search();
} },
}, },
mounted(){ mounted() {
this.settingsDropdown = new bootstrap.Collapse(this.$refs.settings, { this.settingsDropdown = new bootstrap.Collapse(this.$refs.settings, {
toggle: false toggle: false,
}); });
if (!this.searchoptions.origin){ if (!this.searchoptions.origin) {
console.warn("No origin defined in the searchoptions for the searchbar, please define the origin property in the searchbaroptions to allow reliable storage of searchstr and searchtypes accross applications."); console.warn(
"No origin defined in the searchoptions for the searchbar, please define the origin property in the searchbaroptions to allow reliable storage of searchstr and searchtypes accross applications.",
);
} }
}, },
updated() { updated() {
if(this.showresult) { if (this.showresult) {
Vue.nextTick(() => { Vue.nextTick(() => {
this.calcSearchResultHeight(); this.calcSearchResultHeight();
}); });
@@ -249,32 +270,34 @@ export default {
getInitiallySelectedTypes() { getInitiallySelectedTypes() {
let result = false; let result = false;
if (this.searchoptions.origin) { if (this.searchoptions.origin) {
let localStorageValue = localStorage.getItem(`${this.searchoptions.origin}_searchtypes`); let localStorageValue = localStorage.getItem(
`${this.searchoptions.origin}_searchtypes`,
);
if (localStorageValue) { if (localStorageValue) {
result = JSON.parse(localStorageValue); result = JSON.parse(localStorageValue);
} }
} }
if (result) if (result) return result;
return result; if (!this.searchoptions.types) return [];
if (!this.searchoptions.types)
return [];
if (Array.isArray(this.searchoptions.types)) if (Array.isArray(this.searchoptions.types))
return [...this.searchoptions.types]; return [...this.searchoptions.types];
return Object.keys(this.searchoptions.types); return Object.keys(this.searchoptions.types);
}, },
getSearchStr: function(){ getSearchStr: function () {
if (!this.searchoptions.origin) if (!this.searchoptions.origin) return "";
return ''; return (
return sessionStorage.getItem(`${this.searchoptions.origin}_searchstr`) ?? ''; sessionStorage.getItem(
`${this.searchoptions.origin}_searchstr`,
) ?? ""
);
}, },
checkSettingsVisibility: function(event) { checkSettingsVisibility: function (event) {
// hides the settings collapsible if the user clicks somewhere else // hides the settings collapsible if the user clicks somewhere else
if (!this.$refs.settings.contains(event.target)) if (!this.$refs.settings.contains(event.target)) {
{
this.settingsDropdown.hide(); this.settingsDropdown.hide();
} }
}, },
handleShowSettings: function() { handleShowSettings: function () {
// adds the event listener checkSettingsVisibility only when the collapsible is shown // adds the event listener checkSettingsVisibility only when the collapsible is shown
document.addEventListener("click", this.checkSettingsVisibility); document.addEventListener("click", this.checkSettingsVisibility);
}, },
@@ -282,183 +305,208 @@ export default {
// removes the event listener checkSettingsVisibility when the collapsible is hidden // removes the event listener checkSettingsVisibility when the collapsible is hidden
document.removeEventListener("click", this.checkSettingsVisibility); document.removeEventListener("click", this.checkSettingsVisibility);
}, },
calcSearchResultHeight: function() { calcSearchResultHeight: function () {
const rect = this.$refs.results.getBoundingClientRect(); const rect = this.$refs.results.getBoundingClientRect();
if( rect.height > 0 && rect.height < (window.innerHeight * 0.8) ) { if (rect.height > 0 && rect.height < window.innerHeight * 0.8) {
this.$refs.result.style.height = Math.ceil(rect.height + 16) + 'px'; this.$refs.result.style.height =
Math.ceil(rect.height + 16) + "px";
} else { } else {
this.$refs.result.style.height = Math.floor(window.innerHeight * 0.8) + 'px'; this.$refs.result.style.height =
Math.floor(window.innerHeight * 0.8) + "px";
} }
}, },
calcSearchResultExtent: function() { calcSearchResultExtent: function () {
if(!this.showresult) { if (!this.showresult) {
return; return;
} }
if(this.searchoptions?.calcheightonly === undefined if (
|| this.searchoptions.calcheightonly === false) { this.searchoptions?.calcheightonly === undefined ||
this.searchoptions.calcheightonly === false
) {
var rect = this.$refs.searchbox.getBoundingClientRect(); var rect = this.$refs.searchbox.getBoundingClientRect();
this.$refs.result.style.top = Math.floor(rect.bottom + 3) + 'px'; this.$refs.result.style.top =
this.$refs.result.style.right = Math.floor(rect.right) + 'px'; Math.floor(rect.bottom + 3) + "px";
this.$refs.result.style.width = Math.floor(rect.width) + 'px'; this.$refs.result.style.right = Math.floor(rect.right) + "px";
this.$refs.result.style.width = Math.floor(rect.width) + "px";
} }
this.calcSearchResultHeight(); this.calcSearchResultHeight();
}, },
search: function() { search: function () {
if(this.searchoptions?.nolivesearch === true) return; if (this.searchoptions?.nolivesearch === true) return;
this.abort(); this.abort();
if( this.searchsettings.searchstr.length >= 2 ) { if (this.searchsettings.searchstr.length >= 2) {
this.calcSearchResultExtent();
this.searchtimer = setTimeout(
this.callsearchapi,
500
);
} else {
this.showresult = false;
}
},
abort() {
if (this.searchtimer !== null) {
clearTimeout(this.searchtimer);
}
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
this.searchresult = [];
},
callsearchapi: function() {
this.error = null;
this.searchresult.splice(0, this.searchresult.length);
this.searching = true;
this.showsearchresult();
if(this.searchsettings.types.length === 0) {
this.error = this.$p.t('search/error_missing_type');
this.searching = false;
return;
}
if (this.abortController)
this.abortController.abort();
this.abortController = new AbortController();
this.searchfunction(this.searchsettings, { timeout: 50000, signal: this.abortController.signal })
.then(response=>{
if (!response.data) {
this.error = this.$p.t('search/error_general');
} else {
let res = response.data.map(el => el.data ? {...el, ...JSON.parse(el.data)} : el);
this.lastQuery = response.meta.searchstring;
if (this.searchoptions.mergeResults) {
let counter = 0;
let mergeTypes = [];
let mergedType = 'merged-';
let mergeKey = '';
switch (this.searchoptions.mergeResults) {
case 'student':
mergeTypes = ['student', 'prestudent'];
mergedType += this.searchoptions.mergeResults;
mergeKey = 'uid';
break;
case 'person':
mergeTypes = ['person', 'employee', 'student', 'prestudent'];
mergedType += this.searchoptions.mergeResults;
mergeKey = 'person_id';
break;
}
if (mergeTypes.length) {
res = Object.values(res.reduce((a, c) => {
if (!mergeTypes.includes(c.renderer)) {
a['nomerge' + counter++] = c;
} else if (c[mergeKey] === null) {
a['nomerge' + counter++] = c;
} else if (a[c[mergeKey]] === undefined) {
a[c[mergeKey]] = {
rank: c.rank,
renderer: mergedType,
type: mergedType,
list: [c]
};
} else {
a[c[mergeKey]].list.push(c);
if (c.rank > a[c[mergeKey]].rank)
a[c[mergeKey]].rank = c.rank;
}
return a;
}, {})).sort((a, b) => b.rank - a.rank);
}
}
this.searchresult = res;
this.searchmode = response.meta.mode;
}
this.searching = false;
this.retry = 0;
})
.catch(error=> {
if (error.code == "ERR_CANCELED") {
return this.retry = 0;
}
if (error.code == "ECONNABORTED" && this.retry) {
this.retry--;
return this.callsearchapi();
}
this.error = this.$p.t('search/error_general', error);
this.searching = false;
this.retry = 0;
});
},
refreshsearch: function() {
this.search();
this.togglesettings();
},
hideresult: function() {
this.showresult = false;
window.removeEventListener('resize', this.calcSearchResultExtent);
},
showsearchresult: function() {
if(this.searchoptions?.nolivesearch === true) return;
if( this.searchsettings.searchstr.length >= 2 ) {
this.showresult = true;
window.addEventListener('resize', this.calcSearchResultExtent);
this.calcSearchResultExtent(); this.calcSearchResultExtent();
} this.searchtimer = setTimeout(this.callsearchapi, 500);
}, } else {
searchfocusin: function(e) { this.showresult = false;
e.preventDefault(); }
e.stopPropagation(); },
if( this.hidetimer !== null ) { abort() {
clearTimeout(this.hidetimer); if (this.searchtimer !== null) {
} clearTimeout(this.searchtimer);
if (this.searchsettings.searchstr.length >= 2 }
&& this.searchresult.length === 0) { if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
this.searchresult = [];
},
callsearchapi: function () {
this.error = null;
this.searchresult.splice(0, this.searchresult.length);
this.searching = true;
this.showsearchresult();
if (this.searchsettings.types.length === 0) {
this.error = this.$p.t("search/error_missing_type");
this.searching = false;
return;
}
if (this.abortController) this.abortController.abort();
this.abortController = new AbortController();
this.searchfunction(this.searchsettings, {
timeout: 50000,
signal: this.abortController.signal,
})
.then((response) => {
if (!response.data) {
this.error = this.$p.t("search/error_general");
} else {
let res = response.data.map((el) =>
el.data ? { ...el, ...JSON.parse(el.data) } : el,
);
this.lastQuery = response.meta.searchstring;
if (this.searchoptions.mergeResults) {
let counter = 0;
let mergeTypes = [];
let mergedType = "merged-";
let mergeKey = "";
switch (this.searchoptions.mergeResults) {
case "student":
mergeTypes = ["student", "prestudent"];
mergedType +=
this.searchoptions.mergeResults;
mergeKey = "uid";
break;
case "person":
mergeTypes = [
"person",
"employee",
"student",
"prestudent",
];
mergedType +=
this.searchoptions.mergeResults;
mergeKey = "person_id";
break;
}
if (mergeTypes.length) {
res = Object.values(
res.reduce((a, c) => {
if (!mergeTypes.includes(c.renderer)) {
a["nomerge" + counter++] = c;
} else if (c[mergeKey] === null) {
a["nomerge" + counter++] = c;
} else if (
a[c[mergeKey]] === undefined
) {
a[c[mergeKey]] = {
rank: c.rank,
renderer: mergedType,
type: mergedType,
list: [c],
};
} else {
a[c[mergeKey]].list.push(c);
if (c.rank > a[c[mergeKey]].rank)
a[c[mergeKey]].rank = c.rank;
}
return a;
}, {}),
).sort((a, b) => b.rank - a.rank);
}
}
this.searchresult = res;
this.searchmode = response.meta.mode;
}
this.searching = false;
this.retry = 0;
})
.catch((error) => {
if (error.code == "ERR_CANCELED") {
return (this.retry = 0);
}
if (error.code == "ECONNABORTED" && this.retry) {
this.retry--;
return this.callsearchapi();
}
this.error = this.$p.t("search/error_general", error);
this.searching = false;
this.retry = 0;
});
},
refreshsearch: function () {
this.search();
this.togglesettings();
},
hideresult: function () {
this.showresult = false;
window.removeEventListener("resize", this.calcSearchResultExtent);
},
showsearchresult: function () {
if (this.searchoptions?.nolivesearch === true) return;
if (this.searchsettings.searchstr.length >= 2) {
this.showresult = true;
window.addEventListener("resize", this.calcSearchResultExtent);
this.calcSearchResultExtent();
}
},
searchfocusin: function (e) {
e.preventDefault();
e.stopPropagation();
if (this.hidetimer !== null) {
clearTimeout(this.hidetimer);
}
if (
this.searchsettings.searchstr.length >= 2 &&
this.searchresult.length === 0
) {
this.search(); this.search();
} }
}, },
searchfocusout: function(e) { searchfocusout: function (e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.hidetimer = setTimeout( this.hidetimer = setTimeout(this.hideresult, 100);
this.hideresult, },
100 dash2camelCase(string) {
); return string.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
}, },
dash2camelCase(string) { isValidRenderer(renderer) {
return string.replace(/-([a-z])/g, g => g[1].toUpperCase()); const camelCaseRenderer = this.dash2camelCase(renderer);
}, return Object.keys(this.$.components).includes(camelCaseRenderer);
isValidRenderer(renderer) { },
const camelCaseRenderer = this.dash2camelCase(renderer);
return Object.keys(this.$.components).includes(camelCaseRenderer);
},
getActions(res) { getActions(res) {
let actions = this.searchoptions.actions[this.dash2camelCase(res.renderer)]; let actions =
this.searchoptions.actions[this.dash2camelCase(res.renderer)];
if (actions) { if (actions) {
return actions; return actions;
} }
return this.searchoptions.actions[res.type]; return this.searchoptions.actions[res.type];
} },
} getMaxWidthOfSearchbarInMobileView() {
// body width - hardcoded chevron width; necessary for accurate collapse transition transition
return (
document.querySelector("body").getBoundingClientRect().width -
27 +
"px"
);
},
},
}; };