menu app -> menu component; host menu component in cis app and teleport it to back to #cis-header in DOM; avoids rollup bundle seeing 2 imports and bundling it into 2 instances, this way rollup only ever creates the instance for Cis App and teleports it away; actually improves Menu component, since it is now a logical child of Cis app and retrieve provide/inject and possibly other reactives; keep Menu app as a menu component wrapper in case a legacy/content page is loaded -> switch inside Menu app not mounting if it detects a fhccontent element indicating cis4 native page; in CISVUE-Header.php duplicate props/data-attributes for either case; Link.js now checks for router availability since legacy pages dont have this.$router anymore and does not need to use CompatLinkHelpers, since the singleton router instance should be able to resolve all valid ones itself; Entry.js now always passes the URL to CisMenuLink so the first click on an expandable still toggles AND navigates;

This commit is contained in:
Johann Hoffmann
2026-08-14 16:08:23 +02:00
parent 4c1d3ceedf
commit 7121b46bf9
6 changed files with 224 additions and 214 deletions
+11 -7
View File
@@ -29,14 +29,18 @@ $this->load->view('templates/FHC-Header', $includesArray);
document.body.classList.add("in-frame");
</script>
<header id="cis-header" class="navbar-dark">
<cis-menu
root-url="<?= site_url(''); ?>"
logo-url="<?= base_url($this->config->item('theme_logo')); ?>"
avatar-url="<?= site_url('Cis/Pub/bild/person/' . getAuthPersonId()); ?>"
<!-- used by cis4 native pages, teleported menu needs data attributes-->
<header id="cis-header" class="navbar-dark"
data-root-url="<?= site_url(''); ?>"
data-logo-url="<?= base_url($this->config->item('theme_logo')); ?>"
data-avatar-url="<?= site_url('Cis/Pub/bild/person/' . getAuthPersonId()); ?>"
data-logout-url="<?= site_url('Cis/Auth/logout'); ?>">
<!-- used by legacy cis pages & content pages which use component props -->
<cis-menu
root-url="<?= site_url(''); ?>"
logo-url="<?= base_url($this->config->item('theme_logo')); ?>"
avatar-url="<?= site_url('Cis/Pub/bild/person/' . getAuthPersonId()); ?>"
logout-url="<?= site_url('Cis/Auth/logout'); ?>"
:searchbaroptions="searchbaroptions"
:searchfunction="searchfunction"
></cis-menu>
</header>
+16 -2
View File
@@ -8,9 +8,19 @@ import {capitalize} from "../../helpers/StringHelpers.js";
import ApiAuthinfo from "../../api/factory/authinfo.js";
import {router} from "../../routers/Cis/CisRouter.js";
import CisMenu from "../../components/Cis/Menu.js";
const app = Vue.createApp({
name: 'CisApp',
components: {
CisMenu,
},
template: `
<Teleport to="#cis-header">
<CisMenu />
</Teleport>
<router-view />
`,
data: () => ({
appSideMenuEntries: {},
windowWidth: 0,
@@ -18,12 +28,13 @@ const app = Vue.createApp({
isMitarbeiter: null,
}),
provide() {
return { // provide injectable & watchable language property
return {
language: Vue.computed(() => this.$p.user_language),
isNarrow: Vue.computed(() => this.windowWidth < 992),
isMobile: Vue.computed(() => this.isMobile),
isStudent: Vue.computed(() => this.isStudent),
isMitarbeiter: Vue.computed(() => this.isMitarbeiter)
}
}
},
computed: {
isMobile: function() {
@@ -103,6 +114,9 @@ app.directive('tooltip', primevue.tooltip);
app.use(PluginsPhrasen);
app.use(Theme);
app.directive('contrast', contrast);
// Clear the Teleport target before mounting (removes the static <cis-menu> tag from PHP)
document.getElementById('cis-header').innerHTML = '';
app.mount('#fhccontent');
router.afterEach((to, from, failure) => {
+16 -173
View File
@@ -1,191 +1,30 @@
import FhcSearchbar from "../../components/searchbar/searchbar.js";
import CisMenu from "../../components/Cis/Menu.js";
import PluginsPhrasen from '../../plugins/Phrasen.js';
import Theme from "../../plugins/Theme.js";
import ApiSearchbar from '../../api/factory/searchbar.js';
import ApiLvPlan from "../../api/factory/lvPlan.js";
import {router} from "../../routers/Cis/CisRouter.js";
const app = Vue.createApp({
name: 'CisMenuApp',
components: {
FhcSearchbar,
CisMenu
},
data: function() {
return {
searchbaroptions: {
origin: "cis",
cssclass: "",
calcheightonly: true,
types: {
employee: Vue.computed(() => this.$p.t("search/type_employee")),
student: Vue.computed(() => this.$p.t("search/type_student")),
room: Vue.computed(() => this.$p.t("search/type_room")),
organisationunit: Vue.computed(() => this.$p.t("search/type_organisationunit")),
cms: Vue.computed(() => this.$p.t("search/type_cms")),
dms: Vue.computed(() => this.$p.t("search/type_dms"))
},
actions: {
employee: {
defaultaction: {
type: "link",
action: function(data) {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router+
"/Cis/Profil/View/"+data.uid;
}
},
childactions: []
},
student: {
defaultaction: {
type: "link",
action: function (data) {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/Cis/Profil/View/" + data.uid;
}
},
childactions: []
},
room: {
defaultaction: {
type: "link",
renderif: function(data) {
if(data.content_id === null){
return false;
}
return true;
},
action: function(data) {
const link= FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/content/' + data.content_id;
return link;
}
},
childactions: [
{
label: "LV-Plan",
icon: "fas fa-bookmark",
type: "link",
action: function(data) {
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/getRoomInformation/' + data.ort_kurzbz;
return link;
}
},
{
label: "Rauminformation",
icon: "fas fa-info-circle",
type: "link",
renderif: function(data) {
if(data.content_id === null){
return false;
}
return true;
},
action: function(data) {
const link= FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/content/' + data.content_id;
return link;
}
},
]
},
organisationunit: {
defaultaction: {
type: "link",
renderif: function(data) {
if(data.mailgroup) {
return true;
}
return false;
},
action: function(data) {
const link = 'mailto:' + data.mailgroup;
return link;
}
},
childactions: []
},
cms: {
defaultaction: {
type: "link",
action: function (data) {
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/content/' + data.content_id;
return link;
}
},
childactions: []
},
dms: {
defaultaction: {
type: "link",
action: function (data) {
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root +
'cms/dms.php?id=' + data.dms_id;
return link;
}
},
childactions: []
}
}
},
windowWidth: 0,
};
},
name: 'CisMenuApp',
components: {
CisMenu,
},
data: () => ({
windowWidth: 0,
}),
provide() {
return {
isNarrow: Vue.computed(() => this.windowWidth < 992),
isMobile: Vue.computed(() => this.windowWidth < 767),
}
},
methods: {
searchfunction: function(searchsettings) {
return this.$api.call(ApiSearchbar.searchCis(searchsettings));
},
methods: {
handleWindowResize() {
this.windowWidth = window.innerWidth;
},
},
},
created() {
this.windowWidth = window.innerWidth;
},
async mounted() {
const openOtherLvPlanAction = {
label: Vue.computed(() => this.$p.t("lehre/stundenplan")),
icon: "fas fa-calendar-days",
type: "link",
action: function (data) {
const uid = JSON.parse(data.data).uid;
const link =
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/Cis/OtherLvPlan/" +
uid;
return link;
},
};
let checkPermissionOtherLvPlanResult = await this.$api.call(
ApiLvPlan.checkPermissionOtherLvPlan(),
);
if (
checkPermissionOtherLvPlanResult.meta.status === "success" &&
checkPermissionOtherLvPlanResult.data
) {
this.searchbaroptions.actions.employee.childactions.push(
openOtherLvPlanAction,
);
this.searchbaroptions.actions.student.childactions.push(
openOtherLvPlanAction,
);
}
mounted() {
window.addEventListener("resize", this.handleWindowResize);
},
beforeUnmount() {
@@ -195,7 +34,6 @@ const app = Vue.createApp({
FhcApps.makeExtendable(app);
app.use(router);
app.use(primevue.config.default, {
zIndex: {
overlay: 9000,
@@ -204,4 +42,9 @@ app.use(primevue.config.default, {
})
app.use(PluginsPhrasen);
app.use(Theme);
app.mount('#cis-header');
// On native CIS4 pages, CisApp handles the menu via Teleport.
// Menu.js only mounts on legacy pages that have no CisApp.
if (!document.getElementById('fhccontent')) {
app.mount('#cis-header');
}
+146 -9
View File
@@ -3,6 +3,8 @@ import FhcSearchbar from "../searchbar/searchbar.js";
import CisSprachen from "./Sprachen.js"
import ThemeSwitch from "./ThemeSwitch.js";
import ApiCisMenu from '../../api/factory/cis/menu.js';
import ApiSearchbar from '../../api/factory/searchbar.js';
import ApiLvPlan from "../../api/factory/lvPlan.js";
export default {
components: {
@@ -12,15 +14,24 @@ export default {
ThemeSwitch,
},
props: {
rootUrl: String,
logoUrl: String,
avatarUrl: String,
logoutUrl: String,
selectedtypes: Array,
searchbaroptions: Object,
searchfunction: Function
rootUrl: {
type: String,
default: () => document.getElementById('cis-header')?.dataset.rootUrl ?? ''
},
logoUrl: {
type: String,
default: () => document.getElementById('cis-header')?.dataset.logoUrl ?? ''
},
avatarUrl: {
type: String,
default: () => document.getElementById('cis-header')?.dataset.avatarUrl ?? ''
},
logoutUrl: {
type: String,
default: () => document.getElementById('cis-header')?.dataset.logoutUrl ?? ''
},
},
data: () => {
data: function() {
return {
entries: [],
activeEntry:null,
@@ -28,6 +39,112 @@ export default {
urlMatchRankings:[],
navUserDropdown:null,
menuOpen:true,
searchbaroptions: {
origin: "cis",
cssclass: "",
calcheightonly: true,
types: {
employee: Vue.computed(() => this.$p.t("search/type_employee")),
student: Vue.computed(() => this.$p.t("search/type_student")),
room: Vue.computed(() => this.$p.t("search/type_room")),
organisationunit: Vue.computed(() => this.$p.t("search/type_organisationunit")),
cms: Vue.computed(() => this.$p.t("search/type_cms")),
dms: Vue.computed(() => this.$p.t("search/type_dms"))
},
actions: {
employee: {
defaultaction: {
type: "link",
action: function(data) {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/Cis/Profil/View/" + data.uid;
}
},
childactions: []
},
student: {
defaultaction: {
type: "link",
action: function(data) {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/Cis/Profil/View/" + data.uid;
}
},
childactions: []
},
room: {
defaultaction: {
type: "link",
renderif: function(data) {
return data.content_id !== null;
},
action: function(data) {
return FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/content/' + data.content_id;
}
},
childactions: [
{
label: "LV-Plan",
icon: "fas fa-bookmark",
type: "link",
action: function(data) {
return FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/getRoomInformation/' + data.ort_kurzbz;
}
},
{
label: "Rauminformation",
icon: "fas fa-info-circle",
type: "link",
renderif: function(data) {
return data.content_id !== null;
},
action: function(data) {
return FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/content/' + data.content_id;
}
},
]
},
organisationunit: {
defaultaction: {
type: "link",
renderif: function(data) {
return !!data.mailgroup;
},
action: function(data) {
return 'mailto:' + data.mailgroup;
}
},
childactions: []
},
cms: {
defaultaction: {
type: "link",
action: function(data) {
return FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/content/' + data.content_id;
}
},
childactions: []
},
dms: {
defaultaction: {
type: "link",
action: function(data) {
return FHC_JS_DATA_STORAGE_OBJECT.app_root +
'cms/dms.php?id=' + data.dms_id;
}
},
childactions: []
}
}
},
};
},
inject: ["isNarrow", "isMobile"],
@@ -102,15 +219,35 @@ export default {
setActiveEntry(content_id){
this.activeEntry = content_id;
},
searchfunction(searchsettings) {
return this.$api.call(ApiSearchbar.searchCis(searchsettings));
},
},
created(){
this.fetchMenu();
},
mounted(){
async mounted(){
this.$p.loadCategory(['ui', 'global', 'profilUpdate'])
this.navUserDropdown = new bootstrap.Collapse(this.$refs.navUserDropdown,{
toggle: false
});
const openOtherLvPlanAction = {
label: Vue.computed(() => this.$p.t("lehre/stundenplan")),
icon: "fas fa-calendar-days",
type: "link",
action: function(data) {
const uid = JSON.parse(data.data).uid;
return FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/Cis/OtherLvPlan/" + uid;
},
};
let result = await this.$api.call(ApiLvPlan.checkPermissionOtherLvPlan());
if (result.meta.status === "success" && result.data) {
this.searchbaroptions.actions.employee.childactions.push(openOtherLvPlanAction);
this.searchbaroptions.actions.student.childactions.push(openOtherLvPlanAction);
}
},
template: /*html*/`
<div id="cis-header-bar" class="d-flex flex-row flex-grow-1">
+2 -2
View File
@@ -151,7 +151,7 @@ export default {
toggleCollapse(evt) {
if (this.active)
{
this.makeParentContentActive(this.entry.content_id);
this.makeParentContentActive(this.entry.content_id);
}
else
{
@@ -176,7 +176,7 @@ export default {
<template v-if="hasChilds">
<div class="btn-group w-100">
<cis-menu-link :target="target"
:href="(entry.menu_open && hasFullLink) ? entry.url : null"
:href="hasFullLink ? entry.url : null"
@click="toggleCollapse"
:class="{
'btn btn-default rounded-0 text-start': true,
+33 -21
View File
@@ -1,5 +1,3 @@
import {isCompatLink, calcCompatRouterLink} from '../../../helpers/CompatLinkHelpers.js';
export default {
name: 'CisMenuLink',
props: {
@@ -8,31 +6,45 @@ export default {
default: null
}
},
methods: {
isRouterHandled() {
let isrouterhandled = this.$route.matched.length > 0;
return isrouterhandled;
},
isCompatLink() {
if(this.href === null) {
return false;
computed: {
// Generaliserung von isRouterLink() aus CompatLinkHelpers.js:
// prüft ALLE bekannten Routen, nicht nur Compat-URLs.
resolvedRoute() {
// Legacy-Seiten haben keinen Router (Menu.js mountet ohne)
if (!this.href || !this.$router) return null;
let path = this.href;
// Menu-API liefert absolute URLs —> Pfad extrahieren, externe Links ignorieren
if (path.startsWith('http')) {
try {
const url = new URL(path);
if (url.origin !== window.location.origin) return null;
path = url.pathname + url.search;
} catch {
return null;
}
}
return isCompatLink(this.href);
},
calcCompatRouterLink() {
return calcCompatRouterLink(this.href);
// Nur Pfade unterhalb der Router-Base kommen in Frage
const base = this.$router.options.history.base;
if (!path.startsWith(base)) return null;
// Router die Route auflösen lassen
const route = path.substring(base.length) || '/';
const resolved = this.$router.resolve(route);
if (!resolved?.matched?.length || resolved.name === 'Fallback') return null;
return route;
}
},
template: `
<router-link v-if="this.isRouterHandled() && this.isCompatLink()"
:to="this.calcCompatRouterLink()"
>
<router-link v-if="resolvedRoute !== null" :to="resolvedRoute">
<slot></slot>
</router-link>
<a v-else
:href="this.href"
>
<a v-else :href="href">
<slot></slot>
</a>
`
};
};