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
10 changed files with 435 additions and 382 deletions
+1 -1
View File
@@ -8,4 +8,4 @@ if (! defined('BASEPATH')) exit('No direct script access allowed');
$config['theme_name']='default';
$config['theme_css']= "public/css/theme/default.css";
$config['theme_logo']= "public/images/logo_fh-complete_300x46.png";
$config['theme_modes']=['light','dark','contrast'];
$config['theme_modes']=['light','dark'];
@@ -594,10 +594,7 @@ class Studiengang_model extends DB_Model
$this->addSelect('p.prestudent_id');
$this->addSelect('pers.vorname');
$this->addSelect('pers.nachname');
$this->addSelect("CONCAT(UPPER(pers.nachname), ' ', pers.vorname, ' (', "
. $this->dbTable . ".bezeichnung, ', ', "
. "UPPER(" . $this->dbTable . ".typ), "
. "UPPER(" . $this->dbTable . ".kurzbz),')') AS name");
$this->addSelect("CONCAT(UPPER(pers.nachname), ' ', pers.vorname, ' (', " . $this->dbTable . ".bezeichnung, ')') AS name");
$this->addJoin('public.tbl_prestudent p', 'studiengang_kz');
$this->addJoin(
+17 -35
View File
@@ -41,32 +41,28 @@ html {
color: var(--fhc-primary-highlight) !important;
}
.fhc-primary-text{
color: var(--fhc-light) !important;
.fhc-body-bg{
background-color:var(--fhc-background) !important;
}
.fhc-body-color{
color: var(--fhc-text) !important;
}
.fhc-secondary-bg{
background-color: var(--fhc-secondary) !important;
}
.fhc-secondary-color {
color: var(--fhc-secondary) !important;
}
.fhc-secondary-text{
color: var(--fhc-secondary-text) !important;
}
.fhc-tertiary-bg{
background-color: var(--fhc-tertiary) !important;
}
.fhc-tertiary-color {
.fhc-secondary-color {
color: var(--fhc-tertiary) !important;
}
.fhc-tertiary-text{
color: var(--fhc-tertiary-text) !important;
.fhc-tertiary-bg{
background-color: var(--fhc-secondary) !important;
}
.fhc-tertiary-color {
color: var(--fhc-secondary) !important;
}
.fhc-fourth-bg {
@@ -77,10 +73,6 @@ html {
color: var(--fhc-fourth) !important;
}
.fhc-fourth-text {
color: var(--fhc-fourth-text) !important;
}
.fhc-fifth-bg {
background-color: var(--fhc-fifth) !important;
}
@@ -89,22 +81,12 @@ html {
color: var(--fhc-fifth) !important;
}
.fhc-fifth-text {
color: var(--fhc-fifth-text) !important;
}
.fhc-body{
color:var(--fhc-text);
background-color: var(--fhc-background);
border-color: var(--fhc-border);
}
.fhc-body-highlight {
color: var(--fhc-text);
background-color: var(--fhc-background-highlight);
border-color: var(--fhc-border);
}
.fhc-secondary {
color: var(--fhc-text);
background-color: var(--fhc-tertiary);
@@ -145,22 +127,22 @@ html {
--fhc-cis-menu-lvl-1-color-hover: var(--fhc-light);
--fhc-cis-menu-lvl-2-bg: var(--fhc-secondary);
--fhc-cis-menu-lvl-2-color: var(--fhc-secondary-text);
--fhc-cis-menu-lvl-2-color: var(--fhc-text);
--fhc-cis-menu-lvl-2-bg-hover: var(--fhc-secondary-highlight);
--fhc-cis-menu-lvl-2-color-hover: var(--fhc-text);
--fhc-cis-menu-lvl-3-bg: var(--fhc-tertiary);
--fhc-cis-menu-lvl-3-color: var(--fhc-secondary-text);
--fhc-cis-menu-lvl-3-color: var(--fhc-text);
--fhc-cis-menu-lvl-3-bg-hover: var(--fhc-tertiary-highlight);
--fhc-cis-menu-lvl-3-color-hover: var(--fhc-text);
--fhc-cis-menu-lvl-4-bg: var(--fhc-fourth);
--fhc-cis-menu-lvl-4-color: var(--fhc-secondary-text);
--fhc-cis-menu-lvl-4-color: var(--fhc-text);
--fhc-cis-menu-lvl-4-bg-hover: var(--fhc-fourth-highlight);
--fhc-cis-menu-lvl-4-color-hover: var(--fhc-text);
--fhc-cis-menu-lvl-5-bg: var(--fhc-fifth);
--fhc-cis-menu-lvl-5-color: var(--fhc-secondary-text);
--fhc-cis-menu-lvl-5-color: var(--fhc-text);
--fhc-cis-menu-lvl-5-bg-hover: var(--fhc-fifth-highlight);
--fhc-cis-menu-lvl-5-color-hover: var(--fhc-text);
--fhc-cis-grade-positive: var(--fhc-success);
+1 -1
View File
@@ -14,7 +14,7 @@
--fhc-calendar-border-event: var(--bs-secondary, #6c757d);
--fhc-calendar-radius-event: var(--bs-border-radius, .375rem);
--fhc-calendar-bg-markings-past: var(--fhc-outdated, rgba(245, 233, 215, 0.5));
--fhc-calendar-bg-markings-past: var(--fhc-beige-10, rgba(245, 233, 215, 0.5));
--fhc-calendar-border-markings-past: var(--fhc-calendar-border);
--fhc-calendar-bg-markings-past-label: var(--fhc-calendar-bg);
--fhc-calendar-border-markings-past-label: var(--fhc-calendar-border);
+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: {
searchfunction: function(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 touchCapable = ("ontouchstart" in window) || navigator.maxTouchPoints > 0;
return smallScreen;// && touchCapable;
}
},
},
provide() {
return { // provide injectable & watchable language property
language: Vue.computed(() => this.$p.user_language),
renderers: Vue.computed(() => this.renderers),
isMobile: this.isMobile
isMobile: this.isMobile,
}
},
methods: {
+1 -1
View File
@@ -103,7 +103,7 @@ export default {
<div class="row">
<div class="col" v-html="content">
</div>
<div class="col-auto " style="align-self:flex-start;">
<div class="col-auto">
<div style="width:15rem">
<studiengang-information></studiengang-information>
</div>
+49 -34
View File
@@ -11,18 +11,6 @@ export default {
CisSprachen,
ThemeSwitch,
},
directives:{
navToggleButton:{
updated(el, binding, vnode, prevVnode){
const { menuOpen, themeName } = binding.value;
if(menuOpen && themeName === "contrast"){
el.querySelector('i').style.setProperty('color', 'white', 'important')
} else if (!menuOpen && themeName === "contrast"){
el.querySelector('i').style.setProperty('color', 'black', 'important')
}
}
}
},
props: {
rootUrl: String,
logoUrl: String,
@@ -42,6 +30,7 @@ export default {
menuOpen:true,
};
},
inject: ["isMobile"],
provide(){
return{
setActiveEntry: this.setActiveEntry,
@@ -70,7 +59,7 @@ export default {
},
site_url(){
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
}
},
},
methods: {
fetchMenu() {
@@ -124,10 +113,26 @@ export default {
});
},
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">
<span class="navbar-toggler-icon"></span>
</button>
<fhc-searchbar ref="searchbar" id="nav-search" class="fhc-searchbar w-100 py-1 py-lg-2" :searchoptions="searchbaroptions" :searchfunction="searchfunction"></fhc-searchbar>
<div
id="header-options-collapsible"
class="collapse multi-collapse collapse-horizontal show"
>
<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 class="d-flex h-100 justify-content-between">
<a :href="rootUrl">
@@ -136,27 +141,37 @@ export default {
<theme-switch></theme-switch>
</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">
<img :src="avatarUrl" :alt="$p.t('profilUpdate/profilBild')" class="bg-dark avatar rounded-circle border border-dark"/>
</button>
<ul ref="navUserDropdown"
@[\`shown.bs.collapse\`]="handleShowNavUser"
@[\`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
id="header-usermenu-collapsible"
class="collapse multi-collapse collapse-horizontal show"
>
<div
:style="!isMobile ? '' : 'width: 51px'"
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">
<img :src="avatarUrl" :alt="$p.t('profilUpdate/profilBild')" class="bg-dark avatar rounded-circle border border-dark"/>
</button>
<ul ref="navUserDropdown"
@[\`shown.bs.collapse\`]="handleShowNavUser"
@[\`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>
<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-toggle" class="position-static d-none d-lg-block ">
<button v-navToggleButton="{menuOpen, themeName: $theme.theme_name.value}" :aria-label="menuCollapseAriaLabel" :title="menuCollapseAriaLabel" type="button" @click="menuOpen = !menuOpen" class="btn text-light rounded-0 p-1 d-flex align-items-center" data-bs-toggle="collapse" data-bs-target=".nav-menu-collapse" aria-expanded="true" aria-controls="nav-sprachen nav-main-menu">
<i aria-hidden="true" class="fa fa-arrow-circle-left fhc-secondary-text"></i>
<button :aria-label="menuCollapseAriaLabel" type="button" @click="menuOpen = !menuOpen" class="btn text-light rounded-0 p-1 d-flex align-items-center" data-bs-toggle="collapse" data-bs-target=".nav-menu-collapse" aria-expanded="true" aria-controls="nav-sprachen nav-main-menu">
<i aria-hidden="true" class="fa fa-arrow-circle-left fhc-text"></i>
</button>
</div>
<div class="offcanvas-body p-0">
-1
View File
@@ -27,7 +27,6 @@ export default {
<button id="themeSwitch" :aria-label="$p.t('global','switchTheme',[nextTheme])" @click="switchTheme(nextTheme)" class="fhc-primary-highlight-bg align-self-center btn btn-secondary rounded-5">
<i v-if="theme == 'light'" class="fa-solid fa-sun " aria-hidden="true"></i>
<i v-else-if="theme == 'dark'" class="fa-solid fa-moon " aria-hidden="true"></i>
<i v-else-if="theme == 'contrast'" class="fa-solid fa-circle-half-stroke " aria-hidden="true"></i>
<!--<i v-else-if="theme == 'purple'" class="fa-solid fa-wine-bottle" aria-hidden="true"></i>-->
</button>
`
+351 -303
View File
@@ -21,69 +21,71 @@ export default {
dms,
cms,
mergedStudent,
mergedPerson
mergedPerson,
},
props: {
searchoptions: {
type: Object,
required: true
},
searchfunction: {
type: Function,
required: true
},
showBtnSubmit: Boolean
},
provide() {
return {
query: Vue.computed(() => this.lastQuery)
};
},
data: function() {
return {
searchtimer: null,
hidetimer: null,
searchsettings: {
searchstr: this.getSearchStr(),
types: this.getInitiallySelectedTypes(),
},
searchresult: [],
searchmode: '',
showresult: false,
searching: false,
error: null,
abortController: null,
props: {
searchoptions: {
type: Object,
required: true,
},
searchfunction: {
type: Function,
required: true,
},
showBtnSubmit: Boolean,
},
provide() {
return {
query: Vue.computed(() => this.lastQuery),
};
},
inject: ["isMobile"],
data: function () {
return {
searchtimer: null,
hidetimer: null,
searchsettings: {
searchstr: this.getSearchStr(),
types: this.getInitiallySelectedTypes(),
},
searchresult: [],
searchmode: "",
showresult: false,
searching: false,
error: null,
abortController: null,
settingsDropdown: null,
lastQuery: ''
};
},
lastQuery: "",
isSearchShownInMobileView: false,
};
},
computed: {
searchTypesPlaceholder() {
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() {
if (!this.searchoptions.types)
return [];
if (!this.searchoptions.types) return [];
if (Array.isArray(this.searchoptions.types))
return this.searchoptions.types;
return Object.keys(this.searchoptions.types);
},
typeLabels() {
if (!this.searchoptions.types)
return {};
if (!this.searchoptions.types) return {};
if (Array.isArray(this.searchoptions.types)) {
return this.searchoptions.types.reduce((res, type) => {
res[type] = type;
return res
return res;
}, {});
}
return this.searchoptions.types;
}
},
},
template: /*html*/`
template: /*html*/ `
<form
ref="searchform"
class="d-flex me-3"
@@ -92,80 +94,91 @@ export default {
@focusin="searchfocusin"
@focusout="searchfocusout"
>
<div
ref="searchbox"
class="h-100 input-group me-2 searchbar_searchbox"
:class="showresult ? 'open' : 'closed'"
<span
v-if="isMobile"
type="button"
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" type="button" @click="$refs.input.focus()">
<i class="fa-solid fa-magnifying-glass"></i>
</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>
<i v-if="isSearchShownInMobileView" class="fa-solid fa-chevron-left"></i>
<i v-else class="fa-solid fa-magnifying-glass"></i>
</span>
<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
:class="{'flex-grow-1': !isMobile, 'collapse multi-collapse collapse-horizontal': isMobile}"
id="header-searchbar-collapsible"
@[\`show.bs.collapse\`]="isSearchShownInMobileView = true"
@[\`hidden.bs.collapse\`]="isSearchShownInMobileView = false"
>
<div
:class="{open: showresult, closed: showresult, 'px-3': isMobile}"
ref="searchbox"
class="h-100 input-group me-2 searchbar_searchbox"
:style="isMobile ? 'width: ' + getMaxWidthOfSearchbarInMobileView() : ''"
>
<span class="input-group-text">
<i class="fa-solid fa-magnifying-glass color-white"></i>
</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="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
id="searchSettings"
@@ -207,34 +220,42 @@ export default {
</div>
</form>
`,
watch:{
'searchsettings.searchstr': function (newSearchValue) {
if(this.searchoptions.origin){
sessionStorage.setItem(`${this.searchoptions.origin}_searchstr`,newSearchValue);
watch: {
"searchsettings.searchstr": function (newSearchValue) {
if (this.searchoptions.origin) {
sessionStorage.setItem(
`${this.searchoptions.origin}_searchstr`,
newSearchValue,
);
}
},
'searchsettings.types'(newValue) {
"searchsettings.types"(newValue) {
if (Array.isArray(newValue) && newValue.length === 0) {
this.searchsettings.types = [...this.types];
}
// stores the search types in the localstorage, only if the newValue is also an array
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();
}
},
mounted(){
},
},
mounted() {
this.settingsDropdown = new bootstrap.Collapse(this.$refs.settings, {
toggle: false
toggle: false,
});
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.");
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.",
);
}
},
updated() {
if(this.showresult) {
if (this.showresult) {
Vue.nextTick(() => {
this.calcSearchResultHeight();
});
@@ -249,32 +270,34 @@ export default {
getInitiallySelectedTypes() {
let result = false;
if (this.searchoptions.origin) {
let localStorageValue = localStorage.getItem(`${this.searchoptions.origin}_searchtypes`);
let localStorageValue = localStorage.getItem(
`${this.searchoptions.origin}_searchtypes`,
);
if (localStorageValue) {
result = JSON.parse(localStorageValue);
}
}
if (result)
return result;
if (!this.searchoptions.types)
return [];
if (result) return result;
if (!this.searchoptions.types) return [];
if (Array.isArray(this.searchoptions.types))
return [...this.searchoptions.types];
return Object.keys(this.searchoptions.types);
},
getSearchStr: function(){
if (!this.searchoptions.origin)
return '';
return sessionStorage.getItem(`${this.searchoptions.origin}_searchstr`) ?? '';
getSearchStr: function () {
if (!this.searchoptions.origin) return "";
return (
sessionStorage.getItem(
`${this.searchoptions.origin}_searchstr`,
) ?? ""
);
},
checkSettingsVisibility: function(event) {
checkSettingsVisibility: function (event) {
// 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();
}
},
handleShowSettings: function() {
handleShowSettings: function () {
// adds the event listener checkSettingsVisibility only when the collapsible is shown
document.addEventListener("click", this.checkSettingsVisibility);
},
@@ -282,183 +305,208 @@ export default {
// removes the event listener checkSettingsVisibility when the collapsible is hidden
document.removeEventListener("click", this.checkSettingsVisibility);
},
calcSearchResultHeight: function() {
calcSearchResultHeight: function () {
const rect = this.$refs.results.getBoundingClientRect();
if( rect.height > 0 && rect.height < (window.innerHeight * 0.8) ) {
this.$refs.result.style.height = Math.ceil(rect.height + 16) + 'px';
if (rect.height > 0 && rect.height < window.innerHeight * 0.8) {
this.$refs.result.style.height =
Math.ceil(rect.height + 16) + "px";
} 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() {
if(!this.showresult) {
calcSearchResultExtent: function () {
if (!this.showresult) {
return;
}
if(this.searchoptions?.calcheightonly === undefined
|| this.searchoptions.calcheightonly === false) {
if (
this.searchoptions?.calcheightonly === undefined ||
this.searchoptions.calcheightonly === false
) {
var rect = this.$refs.searchbox.getBoundingClientRect();
this.$refs.result.style.top = Math.floor(rect.bottom + 3) + 'px';
this.$refs.result.style.right = Math.floor(rect.right) + 'px';
this.$refs.result.style.width = Math.floor(rect.width) + 'px';
this.$refs.result.style.top =
Math.floor(rect.bottom + 3) + "px";
this.$refs.result.style.right = Math.floor(rect.right) + "px";
this.$refs.result.style.width = Math.floor(rect.width) + "px";
}
this.calcSearchResultHeight();
},
search: function() {
if(this.searchoptions?.nolivesearch === true) return;
this.calcSearchResultHeight();
},
search: function () {
if (this.searchoptions?.nolivesearch === true) return;
this.abort();
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.abort();
if (this.searchsettings.searchstr.length >= 2) {
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.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();
}
},
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();
}
},
searchfocusout: function(e) {
e.preventDefault();
e.stopPropagation();
this.hidetimer = setTimeout(
this.hideresult,
100
);
},
dash2camelCase(string) {
return string.replace(/-([a-z])/g, g => g[1].toUpperCase());
},
isValidRenderer(renderer) {
const camelCaseRenderer = this.dash2camelCase(renderer);
return Object.keys(this.$.components).includes(camelCaseRenderer);
},
},
searchfocusout: function (e) {
e.preventDefault();
e.stopPropagation();
this.hidetimer = setTimeout(this.hideresult, 100);
},
dash2camelCase(string) {
return string.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
},
isValidRenderer(renderer) {
const camelCaseRenderer = this.dash2camelCase(renderer);
return Object.keys(this.$.components).includes(camelCaseRenderer);
},
getActions(res) {
let actions = this.searchoptions.actions[this.dash2camelCase(res.renderer)];
let actions =
this.searchoptions.actions[this.dash2camelCase(res.renderer)];
if (actions) {
return actions;
}
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"
);
},
},
};