frontend/src/app/core/main-menu/main-menu-toggle.service.ts
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2024 the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See COPYRIGHT and LICENSE files for more details.
//++
import { Injectable, Injector } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
import { DeviceService } from 'core-app/core/browser/device.service';
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
@Injectable({ providedIn: 'root' })
export class MainMenuToggleService {
public toggleTitle:string;
private elementWidth:number;
private elementMinWidth = 11;
private readonly defaultWidth:number = 230;
private readonly localStorageKey:string = 'openProject-mainMenuWidth';
private readonly localStorageStateKey:string = 'openProject-mainMenuCollapsed';
@InjectField() currentProject:CurrentProjectService;
private global = (window as any);
private htmlNode = document.getElementsByTagName('html')[0];
private mainMenu = jQuery('#main-menu')[0]; // main menu, containing sidebar and resizer
private hideElements = jQuery('.can-hide-navigation');
// Title needs to be sync in main-menu-toggle.component.ts and main-menu-resizer.component.ts
private titleData = new BehaviorSubject<string>('');
public titleData$ = this.titleData.asObservable();
// Notes all changes of the menu size (currently needed in wp-resizer.component.ts)
private changeData = new BehaviorSubject<any>({});
public changeData$ = this.changeData.asObservable();
constructor(
protected I18n:I18nService,
public injector:Injector,
readonly deviceService:DeviceService,
) {
}
public initializeMenu():void {
if (!this.mainMenu) {
return;
}
this.elementWidth = parseInt(window.OpenProject.guardedLocalStorage(this.localStorageKey) as string);
const menuCollapsed = window.OpenProject.guardedLocalStorage(this.localStorageStateKey) as string;
if (!this.elementWidth) {
this.saveWidth(this.mainMenu.offsetWidth);
} else if (menuCollapsed && JSON.parse(menuCollapsed)) {
this.closeMenu();
} else {
this.setWidth();
}
const currentProject:CurrentProjectService = this.injector.get(CurrentProjectService);
if (jQuery(document.body).hasClass('controller-my') && this.elementWidth === 0 || currentProject.id === null) {
this.saveWidth(this.defaultWidth);
}
// small desktop version default: hide menu on initialization
this.closeWhenOnSmallDesktop();
}
// click on arrow or hamburger icon
public toggleNavigation(event?:JQuery.TriggeredEvent|Event):void {
if (event) {
event.stopPropagation();
event.preventDefault();
}
if (!this.showNavigation) { // sidebar is hidden -> show menu
if (this.deviceService.isSmallDesktop) { // small desktop version
this.setWidth(window.innerWidth);
} else { // desktop version
const savedWidth = parseInt(window.OpenProject.guardedLocalStorage(this.localStorageKey) as string);
const widthToSave = savedWidth >= this.elementMinWidth ? savedWidth : this.defaultWidth;
this.saveWidth(widthToSave);
}
} else { // sidebar is expanded -> close menu
this.closeMenu();
}
// Set focus on first visible main menu item.
// This needs to be called after AngularJS has rendered the menu, which happens some when after(!) we leave this
// method here. So we need to set the focus after a timeout.
setTimeout(() => {
jQuery('#main-menu [class*="-menu-item"]:visible').first().focus();
}, 500);
}
public closeMenu():void {
this.setWidth(0);
window.OpenProject.guardedLocalStorage(this.localStorageStateKey, 'true');
jQuery('.searchable-menu--search-input').blur();
}
public closeWhenOnSmallDesktop():void {
if (this.deviceService.isSmallDesktop) {
this.closeMenu();
window.OpenProject.guardedLocalStorage(this.localStorageStateKey, 'false');
}
}
public saveWidth(width?:number):void {
this.setWidth(width);
window.OpenProject.guardedLocalStorage(this.localStorageKey, String(this.elementWidth));
window.OpenProject.guardedLocalStorage(this.localStorageStateKey, String(this.elementWidth === 0));
}
public setWidth(width?:any):void {
if (width !== undefined) {
// Leave a minimum amount of space for space for the content
const maxMenuWidth = this.deviceService.isSmallDesktop ? window.innerWidth - 120 : window.innerWidth - 520;
if (width > maxMenuWidth) {
this.elementWidth = maxMenuWidth;
} else {
this.elementWidth = width as number;
}
}
this.snapBack();
this.setToggleTitle();
this.toggleClassHidden();
this.global.showNavigation = this.showNavigation;
this.htmlNode.style.setProperty('--main-menu-width', `${this.elementWidth}px`);
// Send change event when size of menu is changing (menu toggled or resized)
const changeEvent = jQuery.Event('change');
this.changeData.next(changeEvent);
}
public get showNavigation():boolean {
return (this.elementWidth >= this.elementMinWidth);
}
private snapBack():void {
if (this.elementWidth < this.elementMinWidth) {
this.elementWidth = 0;
}
}
private setToggleTitle():void {
if (this.showNavigation) {
this.toggleTitle = this.I18n.t('js.label_hide_project_menu');
} else {
this.toggleTitle = this.I18n.t('js.label_expand_project_menu');
}
this.titleData.next(this.toggleTitle);
}
private toggleClassHidden():void {
this.hideElements.toggleClass('hidden-navigation', !this.showNavigation);
}
}