Skip to content

Commit

Permalink
Development: Improve course overview component (#8971)
Browse files Browse the repository at this point in the history
  • Loading branch information
rabeatwork authored and icelord42 committed Jul 27, 2024
1 parent 5703731 commit 7ec82c2
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 147 deletions.
125 changes: 58 additions & 67 deletions src/main/webapp/app/overview/course-overview.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,43 +18,24 @@
[hidden]="isExamStarted"
>
<div class="sidebar-container d-flex h-100 justify-content-between flex-column" [ngClass]="{ collapsed: isNavbarCollapsed }">
<!-- NavItems -->
<div>
<!-- Course Icon + Title -->
<div id="container" class="d-flex p-3 align-items-center text-decoration-none" [title]="course?.title">
<div ngbDropdown container="body" class="d-flex">
@if (course && course.courseIcon) {
@if (course) {
@if (courses?.length) {
<div ngbDropdownToggle class="d-flex align-items-center justify-content-center pointer">
<jhi-secured-image [src]="course.courseIcon" />
<div ngbDropdownToggle class="pointer">
<ng-template *ngTemplateOutlet="courseImage; context: { $implicit: course.courseIcon, courseTitle: course.title }" />
</div>
} @else {
<div class="d-flex align-items-center justify-content-center">
<jhi-secured-image [src]="course.courseIcon" />
</div>
}
} @else {
@if (courses?.length) {
<div ngbDropdownToggle class="course-circle d-flex align-items-center justify-content-center pointer">
<span class="fs-4">{{ course?.title | slice: 0 : 1 }}</span>
</div>
} @else {
<div class="course-circle d-flex align-items-center justify-content-center">
<span class="fs-4">{{ course?.title | slice: 0 : 1 }}</span>
</div>
<ng-template *ngTemplateOutlet="courseImage; context: { $implicit: course.courseIcon, courseTitle: course.title }" />
}
}

<div ngbDropdownMenu class="dropdown-menu py-1 ms-n2">
@for (course of courses; track course) {
<button ngbDropdownItem (click)="switchCourse(course)" class="d-flex align-items-center py-1 px-2">
@if (course.courseIcon) {
<span class="d-flex align-items-center justify-content-center">
<jhi-secured-image [src]="course.courseIcon" />
</span>
} @else {
<div class="course-circle d-flex align-items-center justify-content-center">
<span class="fs-4">{{ course?.title | slice: 0 : 1 }}</span>
</div>
}
<ng-template *ngTemplateOutlet="courseImage; context: { $implicit: course.courseIcon, courseTitle: course.title }" />
<div class="h6 fw-normal mb-0 course-title text-wrap">{{ course?.title }}</div>
</button>
}
Expand All @@ -64,22 +45,21 @@
<div id="test-course-title" class="course-title h6 mb-0 fw-bold text-body auto-collapse">{{ course?.title }}</div>
}
</div>
<!-- NavItems -->
<div>
<hr class="mt-0" />
<ul class="navbar-nav justify-content-end flex-grow-1 text-decoration-none">
<ul ngbDropdown #itemsDrop="ngbDropdown" container="body" placement="right" class="navbar-nav justify-content-end flex-grow-1 text-decoration-none">
@for (sidebarItem of sidebarItems; track sidebarItem) {
<li class="nav-item" [hidden]="sidebarItem.hidden">
@if (sidebarItem.hasInOrionProperty && sidebarItem.showInOrionWindow !== undefined) {
<ng-template
*ngTemplateOutlet="navItemOrionFilter; context: { $implicit: sidebarItem, iconTextTemplate: navIconAndText, extraPadding: false }"
/>
<ng-template *ngTemplateOutlet="navItemOrionFilter; context: { $implicit: sidebarItem, iconTextTemplate: navIconAndText }" />
} @else {
<ng-template *ngTemplateOutlet="navItem; context: { $implicit: sidebarItem, iconTextTemplate: navIconAndText, extraPadding: false }" />
<ng-template *ngTemplateOutlet="navItem; context: { $implicit: sidebarItem, iconTextTemplate: navIconAndText }" />
}
</li>
}
<li class="nav-item">
<div [hidden]="!anyItemHidden" class="three-dots nav-link px-3" (click)="toggleDropdown()">
<li ngbDropdownToggle class="nav-item">
<div [hidden]="!anyItemHidden" class="three-dots nav-link px-3">
<fa-icon [fixedWidth]="true" [icon]="faEllipsis" class="ms-2 me-3" />
<span
class="more"
Expand All @@ -88,6 +68,31 @@
></span>
</div>
</li>
<div ngbDropdownMenu class="dropdown-content" [ngClass]="{ fixedContentSize: hiddenItems.length >= 4 }">
@for (hiddenItem of hiddenItems; track hiddenItem) {
<li class="nav-item">
@if (hiddenItem.hasInOrionProperty && hiddenItem.showInOrionWindow !== undefined) {
<ng-template *ngTemplateOutlet="navItemOrionFilter; context: { $implicit: hiddenItem, iconTextTemplate: navIconAndTextHidden }" />
} @else {
<ng-template *ngTemplateOutlet="navItem; context: { $implicit: hiddenItem, iconTextTemplate: navIconAndTextHidden }" />
}
</li>
}
@if (courseActionItems?.length && anyItemHidden) {
@for (courseActionItem of courseActionItems; track courseActionItem) {
<li class="nav-item">
<a
class="nav-link nav-link-sidebar px-3 py-2"
[ngClass]="{ collapsed: isNavbarCollapsed }"
(click)="courseActionItemClick(courseActionItem)"
[title]="courseActionItem.title"
>
<ng-template *ngTemplateOutlet="navIconAndTextHidden; context: { $implicit: courseActionItem }" />
</a>
</li>
}
}
</div>
</ul>
</div>
</div>
Expand Down Expand Up @@ -132,34 +137,6 @@
'exam-end-view': isExamEndView,
}"
>
<div class="dropdown-content" [hidden]="!dropdownOpen" [style.top.px]="dropdownOffset" [ngClass]="{ fixedContentSize: hiddenItems.length >= 3 }">
@for (hiddenItem of hiddenItems; track hiddenItem) {
<li class="dropdown-li">
@if (hiddenItem.hasInOrionProperty && hiddenItem.showInOrionWindow !== undefined) {
<ng-template
*ngTemplateOutlet="navItemOrionFilter; context: { $implicit: hiddenItem, iconTextTemplate: navIconAndTextHidden, extraPadding: true }"
/>
} @else {
<ng-template *ngTemplateOutlet="navItem; context: { $implicit: hiddenItem, iconTextTemplate: navIconAndTextHidden, extraPadding: true }" />
}
</li>
}
@if (courseActionItems?.length) {
@for (courseActionItem of courseActionItems; track courseActionItem) {
<li class="dropdown-li">
<a
class="nav-link nav-link-sidebar px-3 py-1"
[ngClass]="{ collapsed: isNavbarCollapsed }"
(click)="courseActionItemClick(courseActionItem)"
[title]="courseActionItem.title"
>
<ng-template *ngTemplateOutlet="navIconAndTextHidden; context: { $implicit: courseActionItem }" />
</a>
</li>
}
}
</div>

@if (course) {
<div
id="course-title-bar-test"
Expand Down Expand Up @@ -217,6 +194,20 @@
</div>
</div>

<!-- TEMPLATES -->

<ng-template #courseImage let-courseImage let-courseTitle="courseTitle">
@if (courseImage) {
<div class="d-flex align-items-center justify-content-center">
<jhi-secured-image [src]="courseImage" />
</div>
} @else {
<div class="course-circle d-flex align-items-center justify-content-center">
<span class="fs-4">{{ courseTitle | slice: 0 : 1 }}</span>
</div>
}
</ng-template>

<ng-template #navIconAndText let-sidebarItem>
@if (sidebarItem.icon) {
<fa-icon [fixedWidth]="true" [icon]="sidebarItem.icon" class="ms-2 me-3" />
Expand All @@ -233,41 +224,41 @@
}
</ng-template>

<ng-template #navItemOrionFilter let-sidebarItem let-iconTextTemplate="iconTextTemplate" let-extraPadding="extraPadding">
<ng-template #navItemOrionFilter let-sidebarItem let-iconTextTemplate="iconTextTemplate">
<a
class="nav-link nav-link-sidebar px-3"
class="nav-link nav-link-sidebar px-3 py-2"
[id]="sidebarItem.testId ?? ''"
[ngClass]="{
'guided-tour': sidebarItem.guidedTour,
newMessage: !communicationRouteLoaded && hasUnreadMessages && sidebarItem.title === 'Communication',
collapsed: isNavbarCollapsed,
'py-1': extraPadding,
}"
jhiOrionFilter
[showInOrionWindow]="sidebarItem.showInOrionWindow"
[routerLink]="sidebarItem.routerLink"
routerLinkActive="active"
[jhiFeatureToggleHide]="sidebarItem.featureToggle"
[title]="sidebarItem.title"
(click)="itemsDrop.close()"
>
<ng-template *ngTemplateOutlet="iconTextTemplate; context: { $implicit: sidebarItem }" />
</a>
</ng-template>

<ng-template #navItem let-sidebarItem let-iconTextTemplate="iconTextTemplate" let-extraPadding="extraPadding">
<ng-template #navItem let-sidebarItem let-iconTextTemplate="iconTextTemplate">
<a
class="nav-link nav-link-sidebar px-3"
class="nav-link nav-link-sidebar px-3 py-2"
[id]="sidebarItem.testId ?? ''"
[ngClass]="{
'guided-tour': sidebarItem.guidedTour,
newMessage: !communicationRouteLoaded && hasUnreadMessages && sidebarItem.title === 'Communication',
collapsed: isNavbarCollapsed,
'py-1': extraPadding,
}"
[routerLink]="sidebarItem.routerLink"
routerLinkActive="active"
[jhiFeatureToggleHide]="sidebarItem.featureToggle"
[title]="sidebarItem.title"
(click)="itemsDrop.close()"
>
<ng-template *ngTemplateOutlet="iconTextTemplate; context: { $implicit: sidebarItem }" />
</a>
Expand Down
9 changes: 5 additions & 4 deletions src/main/webapp/app/overview/course-overview.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,12 @@ a:not(.btn):not(.tab-link):hover {
font-size: xx-small;
}

.newMessage:after {
.newMessage:after,
.dropdown-content > .nav-item > .newMessage:after {
@extend %message-block;
margin-left: 0.25rem;
}

.collapsed.newMessage:after {
@extend %message-block;
margin-left: -0.9rem;
Expand Down Expand Up @@ -287,11 +289,10 @@ jhi-secured-image {
position: absolute;
background-color: var(--dropdown-bg);
border: 1px solid var(--border-color);
z-index: 1000000;
z-index: 3000;
border-radius: 4px;
&.fixedContentSize {
bottom: 0px;
max-height: 91px; // To avoid cut offs in the dropdown menu content
max-height: 171px; // To avoid cut offs in the dropdown menu content (4 items)
}
}

Expand Down
69 changes: 21 additions & 48 deletions src/main/webapp/app/overview/course-overview.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
faTimes,
faWrench,
} from '@fortawesome/free-solid-svg-icons';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { NgbDropdown, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { AlertService, AlertType } from 'app/core/util/alert.service';
import { JhiWebsocketService } from 'app/core/websocket/websocket.service';
import { CourseAccessStorageService } from 'app/course/course-access-storage.service';
Expand Down Expand Up @@ -157,6 +157,8 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit
// Using a list query to be able to listen for changes (late mount); need both as this only returns native nodes
@ViewChildren('controlsViewContainer') controlsViewContainerAsList: QueryList<ViewContainerRef>;

@ViewChild('itemsDrop', { static: true }) itemsDrop: NgbDropdown;

// Icons
faTimes = faTimes;
faEye = faEye;
Expand Down Expand Up @@ -229,71 +231,42 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit
await this.initAfterCourseLoad();
this.sidebarItems = this.getSidebarItems();
this.courseActionItems = this.getCourseActionItems();
this.updateVisibility(window.innerHeight);
this.updateMenuPosition();
this.updateVisibleNavbarItems(window.innerHeight);
await this.updateRecentlyAccessedCourses();
}

/** Listen window resize event by height */
@HostListener('window: resize', ['$event'])
onResize() {
this.dropdownOpen = false;
this.dropdownClickNumber = 0;
this.updateVisibility(window.innerHeight);
this.updateMenuPosition();
}

/** Listen click event whether on outside of the menu or one of the items in the menu to close the dropdown menu */
@HostListener('document: click', ['$event'])
onClickCloseDropdownMenu() {
if (this.dropdownOpen) {
this.dropdownClickNumber += 1;
if (this.dropdownClickNumber === 2) {
this.dropdownOpen = false;
this.dropdownClickNumber = 0;
}
}
this.updateVisibleNavbarItems(window.innerHeight);
if (!this.anyItemHidden) this.itemsDrop.close();
}

/** Update sidebar item's hidden property based on the window height to display three-dots */
updateVisibility(height: number) {
let thresholdLevelForCurrentSidebar = this.calculateThreshold();
updateVisibleNavbarItems(height: number) {
const threshold = this.calculateThreshold();
this.applyThreshold(threshold, height);
}

/** Applies the visibility threshold to sidebar items, determining which items should be hidden.*/
private applyThreshold(threshold: number, height: number) {
this.anyItemHidden = false;
this.hiddenItems = [];

for (let i = 0; i < this.sidebarItems.length - 1; i++) {
this.thresholdsForEachSidebarItem.unshift(thresholdLevelForCurrentSidebar);
thresholdLevelForCurrentSidebar -= this.ITEM_HEIGHT;
}
this.thresholdsForEachSidebarItem.unshift(0);

this.sidebarItems.forEach((item, index) => {
item.hidden = height <= this.thresholdsForEachSidebarItem[index];
// Reverse the sidebar items to remove items starting from the bottom
const reversedSidebarItems = [...this.sidebarItems].reverse();
reversedSidebarItems.forEach((item, index) => {
const currentThreshold = threshold - index * this.ITEM_HEIGHT;
item.hidden = height <= currentThreshold;
if (item.hidden) {
this.anyItemHidden = true;
this.hiddenItems.push(item);
this.hiddenItems.unshift(item);
}
});
}

/** Calculate dropdown-menu position based on the number of entries in the sidebar */
updateMenuPosition() {
const leftSidebarItems: number = this.sidebarItems.length - this.hiddenItems.length;
this.dropdownOffset = leftSidebarItems * this.ITEM_HEIGHT + this.BREADCRUMB_AND_NAVBAR_HEIGHT;
}

/** Calculate threshold levels based on the number of entries in the sidebar */
calculateThreshold(): number {
const numberOfSidebarItems: number = this.sidebarItems.length;
return numberOfSidebarItems * this.ITEM_HEIGHT + this.WINDOW_OFFSET;
}

toggleDropdown() {
this.dropdownOpen = !this.dropdownOpen;
// Refresh click numbers after toggle
if (!this.dropdownOpen) {
this.dropdownClickNumber = 0;
}
return this.sidebarItems.length * this.ITEM_HEIGHT + this.WINDOW_OFFSET;
}

/** initialize courses attribute by retrieving all courses from the server */
Expand Down Expand Up @@ -343,7 +316,7 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit
const examsItem: SidebarItem = this.getExamsItems();
sidebarItems.unshift(examsItem);
}
if (isMessagingEnabled(this.course) || isCommunicationEnabled(this.course)) {
if (isCommunicationEnabled(this.course)) {
const communicationsItem: SidebarItem = this.getCommunicationsItems();
sidebarItems.push(communicationsItem);
}
Expand Down
Loading

0 comments on commit 7ec82c2

Please sign in to comment.