Compare commits

...
Author SHA1 Message Date
adisposkofh 99445d2372 Merge branch 'bugfix-83277_menu-nodes-subnodes-behavior' of github.com:FH-Complete/FHC-Core into bugfix-83277_menu-nodes-subnodes-behavior 2026-09-14 16:49:49 +02:00
adisposkofh 0ab6e3d00d opening active sidenav menu node's submenu on page load 2026-09-14 16:44:25 +02:00
adisposkofh 385b262be8 naming change for clarity 2026-09-14 16:08:50 +02:00
adisposkofh b4206e1e48 removed redundant comments 2026-09-14 15:25:25 +02:00
adisposkofh e09555908a code formatting 2026-09-14 15:25:15 +02:00
adisposkofh 38060c60fb cleaned up repetitive code, code formatting 2026-09-14 15:14:17 +02:00
adisposkofh 40516b34a2 moved repetitive sidenav menu node style into computed property 2026-09-14 14:21:33 +02:00
adisposkofh ea97e3a192 removed redundant comment 2026-09-14 14:18:36 +02:00
adisposkofh f34481ae60 reworked sidenav menu collapse logic 2026-09-14 14:18:01 +02:00
adisposkofh fcf3cbac6c progressively indenting sidenav submenus for hierarchical clarity 2026-09-14 11:31:53 +02:00
adisposkofh 40b28b88b9 fixed issue where click on content-less sidenav menu node didn't close open submenu 2026-09-14 09:26:33 +02:00
Harald Bamberger 11f2f0ad75 Merge branch 'master' into bugfix-83277_menu-nodes-subnodes-behavior 2026-09-10 10:48:31 +02:00
Harald Bamberger 4abe6f0d24 Merge branch 'bugfix_missing-studvw-filter-clear-action-button' 2026-09-09 16:15:39 +02:00
adisposkofh 9fbbd6b855 moved studvw 'clear filter' action button into 'actions' slot in the filter component 2026-09-09 15:14:20 +02:00
adisposkofh 60b2c89c66 fixed issue with menu node collapsing instead of activating if subnode is active 2026-09-09 13:21:53 +02:00
3 changed files with 131 additions and 137 deletions
+13 -15
View File
@@ -28,6 +28,7 @@ export default {
urlMatchRankings:[],
navUserDropdown:null,
menuOpen:true,
openMenuHierarchy: [],
};
},
inject: ["isNarrow", "isMobile"],
@@ -35,7 +36,7 @@ export default {
return{
setActiveEntry: this.setActiveEntry,
addUrlCount: this.addUrlCount,
makeParentContentActive: this.makeParentContentActive,
setOpenMenuHierarchy: this.setOpenMenuHierarchy,
}
},
computed:{
@@ -82,19 +83,6 @@ export default {
handleHideNavUser(){
document.removeEventListener("click", this.checkSettingsVisibility);
},
makeParentContentActive(content_id, collection=this.entries, parent=null){
if(!collection) return;
if (typeof collection == 'object' && !Array.isArray(collection) && Object.entries(collection).length > 0) {
collection = Object.values(collection);
}
for(let entry of collection){
if(entry.content_id == content_id){
this.activeEntry = parent;
}
this.makeParentContentActive(content_id, entry.childs, entry.content_id);
}
},
addUrlCount(count){
this.urlMatchRankings.push(count);
},
@@ -102,6 +90,9 @@ export default {
setActiveEntry(content_id){
this.activeEntry = content_id;
},
setOpenMenuHierarchy(openMenuHierarchy) {
this.openMenuHierarchy = openMenuHierarchy;
},
},
created(){
this.fetchMenu();
@@ -192,7 +183,14 @@ export default {
<div class="offcanvas-body p-0">
<div id="nav-main-menu" class="nav-menu-collapse collapse collapse-horizontal show">
<div class="flex-grow-1">
<cis-menu-entry :highestMatchingUrlCount="highestMatchingUrlCount" :activeContent="activeEntry" v-for="entry in entries" :key="entry.content_id" :entry="entry" />
<cis-menu-entry
v-for="entry in entries"
:key="entry.content_id"
:highestMatchingUrlCount="highestMatchingUrlCount"
:activeContent="activeEntry"
:entry="entry"
:openMenuHierarchy="openMenuHierarchy"
/>
</div>
</div>
</div>
+90 -94
View File
@@ -8,6 +8,11 @@ export default {
},
activeContent: [String, Number],
highestMatchingUrlCount: Number,
openMenuHierarchy: Array,
precedingMenuHierarchy: {
type: Array,
default: [],
},
},
data: () => {
return {
@@ -15,7 +20,7 @@ export default {
urlCount:0,
}
},
inject: ['makeParentContentActive', 'setActiveEntry','addUrlCount'],
inject: ['setActiveEntry', 'setOpenMenuHierarchy', 'addUrlCount'],
watch:{
highestMatchingUrlCount: function(newValue)
{
@@ -25,49 +30,27 @@ export default {
this.setActiveEntry(this.entry.content_id);
}
},
activeContent: function(newValue){
if(newValue == this.entry.content_id){
// wenn der Menupunkt nicht bereits offen ist
if (!this.entry.menu_open){
this.entry.menu_open = true;
}
}else{
if (this.searchRecursiveChild(this.entry, 'content_id',newValue)) {
this.entry.menu_open = true;
} else {
this.entry.menu_open = false;
}
openMenuHierarchy() {
if (!this.hasChilds) return;
if (this.$props.openMenuHierarchy.length && this.$props.openMenuHierarchy[0] === this.$props.entry.content_id) {
this.$props.entry.menu_open = true;
} else {
this.$props.entry.menu_open = false;
}
},
'entry.menu_open': function (newValue,oldValue) {
if (newValue)
{
// only invokes .show if this.collapse is not null
this.collapse && this.collapse.show();
}
else
{
// only invokes .hide if this.collapse is not null
this.collapse && this.collapse.hide();
if (this.activeContent == this.entry.content_id)
{
this.makeParentContentActive(this.entry.content_id);
}
'entry.menu_open': function (isMenuOpen) {
if (!this.collapse)
return;
if (isMenuOpen) {
this.collapse.show();
} else {
this.collapse.hide();
}
},
},
computed: {
active: function () {
if (this.entry.menu_open){
return true;
}
else if (this.activeContent) {
return this.activeContent == this.entry.content_id;
} else {
return false;
}
},
hasFullLink() {
return this.entry.url.startsWith(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router)
},
@@ -89,25 +72,34 @@ export default {
},
hasChilds() {
return this.entry.childs && this.entry.childs.length !== 0;
}
},
menuHierarchy() {
return [...this.$props.precedingMenuHierarchy, this.$props.entry.content_id];
},
menuNodeHref() {
if (this.hasChilds) {
return this.$props.entry.menu_open && this.hasFullLink
? this.$props.entry.url
: null;
} else {
return this.$props.entry.url;
}
},
},
methods: {
getUrlMatchPoints(url,link){
let splitted_link = link.split('/');
let splitted_url = url.href.split('/');
let count = 0;
for(let part_url of splitted_url)
{
for (let part_link of splitted_link)
{
if(part_url == part_link)
{
for (let part_url of splitted_url) {
for (let part_link of splitted_link) {
if (part_url === part_link) {
count++;
}
}
}
this.urlCount = count;
this.addUrlCount(count);
},
@@ -119,14 +111,11 @@ export default {
let url_hash = url.hash;
url_hash = url_hash.replace(url_hash_spaceSymbol_regex, " ").replace(url_hash_sharpSymbol_regex,"");
// if the url hash contains the titel of the menu
// or if the url equals the link of a menu
// then set the menu active
if (url_hash == this.entry.titel || url.href == this.entry.url) {
this.setActiveEntry(this.entry.content_id);
this.setOpenMenuHierarchy(this.menuHierarchy);
}
},
// searches the childs of an entry recursively based on the value of a property
searchRecursiveChild(entry,property,value){
if (typeof entry.childs == 'object' && !Array.isArray(entry.childs) && Object.entries(entry.childs).length > 0){
entry.childs = Object.values(entry.childs);
@@ -143,16 +132,23 @@ export default {
}
return false;
},
toggleCollapse(evt) {
if (this.active)
{
this.makeParentContentActive(this.entry.content_id);
toggleCollapse() {
if (!this.$props.entry.menu_open) {
this.setOpenMenuHierarchy(this.menuHierarchy);
} else {
this.setOpenMenuHierarchy(this.precedingMenuHierarchy);
}
else
{
this.setActiveEntry(this.entry.content_id);
},
handleClickOnMenuNode() {
if (this.hasFullLink) {
this.setActiveEntry(this.$props.entry.content_id);
if (this.hasChilds) {
this.setOpenMenuHierarchy(this.menuHierarchy);
}
} else if (this.hasChilds) {
this.toggleCollapse();
}
}
},
},
mounted() {
if (this.$refs.children) {
@@ -168,41 +164,41 @@ export default {
INCLUDE
</div>
<template v-else>
<template v-if="hasChilds">
<div class="btn-group w-100">
<a :target="target"
:href="(entry.menu_open && hasFullLink) ? entry.url : null"
@click="toggleCollapse"
:class="{
'btn btn-default rounded-0 text-start': true,
['btn-level-' + level]: true,
'fw-bold':active
}">
{{ entry.titel }}
</a>
<button @click.prevent="toggleCollapse" :aria-expanded="entry.menu_open"
:class="{
'btn btn-default rounded-0 dropdown-toggle dropdown-toggle-split flex-grow-0': true,
collapsed: !entry.menu_open
}">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
</div>
<ul ref="children"
class="nav w-100 collapse">
<cis-menu-entry :highestMatchingUrlCount="highestMatchingUrlCount" :activeContent="activeContent" v-for="child in entry.childs" :key="child" :entry="child" :level="level + 1"/>
</ul>
</template>
<a v-else
:href="entry.url"
:target="target"
:class="{
'btn btn-default rounded-0 w-100 text-start': true,
['btn-level-' + level]: true,
'fw-bold':active
}"
@mouseup="setActiveEntry(entry.content_id)">
{{ entry.titel }}
</a>
<div class="btn-group w-100">
<a
@click="handleClickOnMenuNode($event)"
:href="menuNodeHref"
:target="target"
class="btn btn-default rounded-0 text-start"
:class="{
['btn-level-' + level]: true,
'fw-bold': $props.activeContent === $props.entry.content_id
}"
:style="'padding-left: calc(var(--bs-btn-padding-x) * ' + $props.level + ');'"
>
{{ entry.titel }}
</a>
<button
v-if="hasChilds"
@click.prevent="toggleCollapse()"
:aria-expanded="entry.menu_open"
class="btn btn-default rounded-0 dropdown-toggle dropdown-toggle-split flex-grow-0"
:class="{ collapsed: !entry.menu_open }"
>
<span class="visually-hidden">Toggle Dropdown</span>
</button>
</div>
<ul v-if="hasChilds" ref="children" class="nav w-100 collapse" >
<cis-menu-entry
v-for="child in entry.childs"
:key="child"
:highestMatchingUrlCount="highestMatchingUrlCount"
:activeContent="activeContent"
:entry="child"
:level="level + 1"
:openMenuHierarchy="$props.openMenuHierarchy.slice(1)"
:precedingMenuHierarchy="menuHierarchy"
/>
</ul>
</template>`
};
@@ -822,18 +822,33 @@ export default {
@headerFilterOn="handleHeaderFilter"
>
<!--
<template #actions>
<div>
<button
class="btn btn-outline-success sm mb-1"
:title="'Export ' + selectedPrestudents.length + ' prestudent(s) to Excel'"
>
<i class="fas fa-file-excel fa-xl"></i>
</button>
</div>
</template>
-->
<template #actions>
<!-- <div>
<button
class="btn btn-outline-success sm mb-1"
:title="'Export ' + selectedPrestudents.length + ' prestudent(s) to Excel'"
>
<i class="fas fa-file-excel fa-xl"></i>
</button>
</div> -->
<template v-if="filter.length || headerFilterActive">
<div class="d-flex justify-content-center align-items-center gap-2 ps-4 position-absolute start-50 translate-middle-x">
<p class="text-danger mb-0">
<strong>{{$p.t('filter','filterActive')}}</strong>
</p>
<button
class="btn btn-outline-danger sm"
:title="$p.t('filter/filterDelete')"
@click="resetFilter"
>
<span class="fa-solid fa-filter-circle-xmark"></span>
</button>
</div>
</template>
</template>
<template #additional>
<div class="pe-1">
@@ -873,22 +888,7 @@ export default {
<i class="fa-solid fa-refresh pe-1"></i> {{selected.length}}
</button>
</div>
</template>
<template v-if="filter.length || headerFilterActive">
<div class="d-flex justify-content-center align-items-center gap-2 ps-4 position-absolute start-50 translate-middle-x">
<p class="text-danger mb-0">
<strong>{{$p.t('filter','filterActive')}}</strong>
</p>
<button
class="btn btn-outline-danger sm"
:title="$p.t('filter/filterDelete')"
@click="resetFilter"
>
<span class="fa-solid fa-filter-circle-xmark"></span>
</button>
</div>
</template>
<template #filter>