mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-10-12 23:14:02 +00:00
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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user