diff --git a/src/core/components/components.module.ts b/src/core/components/components.module.ts
index aeef5fea2..feb716d63 100644
--- a/src/core/components/components.module.ts
+++ b/src/core/components/components.module.ts
@@ -32,9 +32,13 @@ import { CoreEmptyBoxComponent } from './empty-box/empty-box';
import { CoreTabsComponent } from './tabs/tabs';
import { CoreInfiniteLoadingComponent } from './infinite-loading/infinite-loading';
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
+import { CoreContextMenuComponent } from './context-menu/context-menu';
+import { CoreContextMenuItemComponent } from './context-menu/context-menu-item';
+import { CoreContextMenuPopoverComponent } from './context-menu/context-menu-popover';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
+import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
@NgModule({
declarations: [
@@ -53,6 +57,10 @@ import { CorePipesModule } from '@pipes/pipes.module';
CoreTabsComponent,
CoreInfiniteLoadingComponent,
CoreProgressBarComponent,
+ CoreContextMenuComponent,
+ CoreContextMenuItemComponent,
+ CoreContextMenuPopoverComponent,
+ CoreNavBarButtonsComponent,
],
imports: [
CommonModule,
@@ -77,6 +85,10 @@ import { CorePipesModule } from '@pipes/pipes.module';
CoreTabsComponent,
CoreInfiniteLoadingComponent,
CoreProgressBarComponent,
+ CoreContextMenuComponent,
+ CoreContextMenuItemComponent,
+ CoreContextMenuPopoverComponent,
+ CoreNavBarButtonsComponent,
],
})
export class CoreComponentsModule {}
diff --git a/src/core/components/context-menu/context-menu-item.ts b/src/core/components/context-menu/context-menu-item.ts
new file mode 100644
index 000000000..0fb926e2a
--- /dev/null
+++ b/src/core/components/context-menu/context-menu-item.ts
@@ -0,0 +1,106 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Input, Output, OnInit, OnDestroy, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
+import { CoreContextMenuComponent } from './context-menu';
+
+/**
+ * This directive adds a item to the Context Menu popover.
+ *
+ * @description
+ * This directive defines and item to be added to the popover generated in CoreContextMenu.
+ *
+ * It is required to place this tag inside a core-context-menu tag.
+ *
+ *
+ *
+ *
+ */
+@Component({
+ selector: 'core-context-menu-item',
+ template: '',
+})
+export class CoreContextMenuItemComponent implements OnInit, OnDestroy, OnChanges {
+
+ @Input() content?: string; // Content of the item.
+ @Input() iconDescription?: string; // Name of the icon to be shown on the left side of the item.
+ @Input() iconAction?: string; // Name of the icon to show on the right side of the item. Represents the action to do on click.
+ // If is "spinner" an spinner will be shown.
+ // If no icon or spinner is selected, no action or link will work.
+ // If href but no iconAction is provided arrow-right will be used.
+ @Input() iconSlash?: boolean; // Display a red slash over the icon.
+ @Input() ariaDescription?: string; // Aria label to add to iconDescription.
+ @Input() ariaAction?: string; // Aria label to add to iconAction. If not set, it will be equal to content.
+ @Input() href?: string; // Link to go if no action provided.
+ @Input() captureLink?: boolean | string; // Whether the link needs to be captured by the app.
+ @Input() autoLogin?: string; // Whether the link needs to be opened using auto-login.
+ @Input() closeOnClick = true; // Whether to close the popover when the item is clicked.
+ @Input() priority?: number; // Used to sort items. The highest priority, the highest position.
+ @Input() badge?: string; // A badge to show in the item.
+ @Input() badgeClass?: number; // A class to set in the badge.
+ @Input() hidden?: boolean; // Whether the item should be hidden.
+ @Output() action?: EventEmitter<() => void>; // Will emit an event when the item clicked.
+ @Output() onClosed?: EventEmitter<() => void>; // Will emit an event when the popover is closed because the item was clicked.
+
+ protected hasAction = false;
+ protected destroyed = false;
+
+ constructor(
+ protected ctxtMenu: CoreContextMenuComponent,
+ ) {
+ this.action = new EventEmitter();
+ this.onClosed = new EventEmitter();
+ }
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit(): void {
+ // Initialize values.
+ this.priority = this.priority || 1;
+ this.hasAction = !!this.action && this.action.observers.length > 0;
+ this.ariaAction = this.ariaAction || this.content;
+
+ if (this.hasAction) {
+ this.href = '';
+ }
+
+ // Navigation help if href provided.
+ this.captureLink = this.href && this.captureLink ? this.captureLink : false;
+ this.autoLogin = this.autoLogin || 'check';
+
+ if (!this.destroyed) {
+ this.ctxtMenu.addItem(this);
+ }
+ }
+
+ /**
+ * Component destroyed.
+ */
+ ngOnDestroy(): void {
+ this.destroyed = true;
+ this.ctxtMenu.removeItem(this);
+ }
+
+ /**
+ * Detect changes on input properties.
+ */
+ ngOnChanges(changes: { [name: string]: SimpleChange }): void {
+ if (changes.hidden && !changes.hidden.firstChange) {
+ this.ctxtMenu.itemsChanged();
+ }
+ }
+
+}
diff --git a/src/core/components/context-menu/context-menu-popover.scss b/src/core/components/context-menu/context-menu-popover.scss
new file mode 100644
index 000000000..4efe68f89
--- /dev/null
+++ b/src/core/components/context-menu/context-menu-popover.scss
@@ -0,0 +1,5 @@
+:host {
+ ion-list {
+ padding: 0;
+ }
+}
diff --git a/src/core/components/context-menu/context-menu-popover.ts b/src/core/components/context-menu/context-menu-popover.ts
new file mode 100644
index 000000000..89e3861df
--- /dev/null
+++ b/src/core/components/context-menu/context-menu-popover.ts
@@ -0,0 +1,77 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component } from '@angular/core';
+import { NavParams, PopoverController } from '@ionic/angular';
+import { CoreContextMenuItemComponent } from './context-menu-item';
+
+/**
+ * Component to display a list of items received by param in a popover.
+ */
+@Component({
+ selector: 'core-context-menu-popover',
+ templateUrl: 'core-context-menu-popover.html',
+ styleUrls: ['context-menu-popover.scss'],
+})
+export class CoreContextMenuPopoverComponent {
+
+ title: string;
+ uniqueId: string;
+ items: CoreContextMenuItemComponent[];
+
+ constructor(
+ navParams: NavParams,
+ protected popoverCtrl: PopoverController,
+ ) {
+ this.title = navParams.get('title');
+ this.items = navParams.get('items') || [];
+ this.uniqueId = navParams.get('id');
+ }
+
+ /**
+ * Close the popover.
+ */
+ closeMenu(item?: CoreContextMenuItemComponent): void {
+ this.popoverCtrl.dismiss(item);
+ }
+
+ /**
+ * Function called when an item is clicked.
+ *
+ * @param event Click event.
+ * @param item Item clicked.
+ * @return Return true if success, false if error.
+ */
+ itemClicked(event: Event, item: CoreContextMenuItemComponent): boolean {
+ if (!!item.action && item.action.observers.length > 0) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (item.iconAction == 'spinner') {
+ return false;
+ }
+
+ if (item.closeOnClick) {
+ this.closeMenu(item);
+ }
+
+ item.action.emit(this.closeMenu.bind(this, item));
+ } else if (item.closeOnClick && (item.href || (!!item.onClosed && item.onClosed.observers.length > 0))) {
+ this.closeMenu(item);
+ }
+
+ return true;
+ }
+
+}
diff --git a/src/core/components/context-menu/context-menu.ts b/src/core/components/context-menu/context-menu.ts
new file mode 100644
index 000000000..86c5940bd
--- /dev/null
+++ b/src/core/components/context-menu/context-menu.ts
@@ -0,0 +1,213 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Input, OnInit, OnDestroy, ElementRef } from '@angular/core';
+import { Subject } from 'rxjs';
+import { auditTime } from 'rxjs/operators';
+import { PopoverController } from '@ionic/angular';
+import { CoreDomUtils } from '@services/utils/dom';
+import { CoreUtils } from '@services/utils/utils';
+import { Translate } from '@singletons/core.singletons';
+import { CoreContextMenuItemComponent } from './context-menu-item';
+import { CoreContextMenuPopoverComponent } from './context-menu-popover';
+
+/**
+ * This component adds a button (usually in the navigation bar) that displays a context menu popover.
+ */
+@Component({
+ selector: 'core-context-menu',
+ templateUrl: 'core-context-menu.html',
+})
+export class CoreContextMenuComponent implements OnInit, OnDestroy {
+
+ @Input() icon?: string; // Icon to be shown on the navigation bar. Default: Kebab menu icon.
+ @Input() title?: string; // Text to be shown on the top of the popover.
+ @Input('aria-label') ariaLabel?: string; // Aria label to be shown on the top of the popover.
+
+ hideMenu = true; // It will be unhidden when items are added.
+ expanded = false;
+ protected items: CoreContextMenuItemComponent[] = [];
+ protected itemsMovedToParent: CoreContextMenuItemComponent[] = [];
+ protected itemsChangedStream: Subject; // Stream to update the hideMenu boolean when items change.
+ protected instanceId: string;
+ protected parentContextMenu?: CoreContextMenuComponent;
+ protected uniqueId: string;
+
+ constructor(
+ protected popoverCtrl: PopoverController,
+ elementRef: ElementRef,
+ ) {
+ // Create the stream and subscribe to it. We ignore successive changes during 250ms.
+ this.itemsChangedStream = new Subject();
+ this.itemsChangedStream.pipe(auditTime(250));
+ this.itemsChangedStream.subscribe(() => {
+ // Hide the menu if all items are hidden.
+ this.hideMenu = !this.items.some((item) => !item.hidden);
+
+ // Sort the items by priority.
+ this.items.sort((a, b) => (a.priority || 0) <= (b.priority || 0) ? 1 : -1);
+ });
+
+ // Calculate the unique ID.
+ this.uniqueId = 'core-context-menu-' + CoreUtils.instance.getUniqueId('CoreContextMenuComponent');
+
+ this.instanceId = CoreDomUtils.instance.storeInstanceByElement(elementRef.nativeElement, this);
+ }
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit(): void {
+ this.icon = this.icon || 'ellipsis-vertical';
+ this.ariaLabel = this.ariaLabel || this.title || Translate.instance.instant('core.displayoptions');
+ }
+
+ /**
+ * Add a context menu item.
+ *
+ * @param item The item to add.
+ */
+ addItem(item: CoreContextMenuItemComponent): void {
+ if (this.parentContextMenu) {
+ // All items were moved to the "parent" menu. Add the item in there.
+ this.parentContextMenu.addItem(item);
+
+ if (this.itemsMovedToParent.indexOf(item) == -1) {
+ this.itemsMovedToParent.push(item);
+ }
+ } else if (this.items.indexOf(item) == -1) {
+ this.items.push(item);
+ this.itemsChanged();
+ }
+ }
+
+ /**
+ * Function called when the items change.
+ */
+ itemsChanged(): void {
+ if (this.parentContextMenu) {
+ // All items were moved to the "parent" menu, call the function in there.
+ this.parentContextMenu.itemsChanged();
+ } else {
+ this.itemsChangedStream.next();
+ }
+ }
+
+ /**
+ * Merge the current context menu with the one passed as parameter. All the items in this menu will be moved to the
+ * one passed as parameter.
+ *
+ * @param contextMenu The context menu where to move the items.
+ */
+ mergeContextMenus(contextMenu: CoreContextMenuComponent): void {
+ this.parentContextMenu = contextMenu;
+
+ // Add all the items to the other menu.
+ for (let i = 0; i < this.items.length; i++) {
+ const item = this.items[i];
+ contextMenu.addItem(item);
+ this.itemsMovedToParent.push(item);
+ }
+
+ // Remove all items from the current menu.
+ this.items = [];
+ this.itemsChanged();
+ }
+
+ /**
+ * Remove an item from the context menu.
+ *
+ * @param item The item to remove.
+ */
+ removeItem(item: CoreContextMenuItemComponent): void {
+ if (this.parentContextMenu) {
+ // All items were moved to the "parent" menu. Remove the item from there.
+ this.parentContextMenu.removeItem(item);
+
+ const index = this.itemsMovedToParent.indexOf(item);
+ if (index >= 0) {
+ this.itemsMovedToParent.splice(index, 1);
+ }
+ } else {
+ const index = this.items.indexOf(item);
+ if (index >= 0) {
+ this.items.splice(index, 1);
+ }
+ this.itemsChanged();
+ }
+ }
+
+ /**
+ * Remove the items that were merged to a parent context menu.
+ */
+ removeMergedItems(): void {
+ if (this.parentContextMenu) {
+ for (let i = 0; i < this.itemsMovedToParent.length; i++) {
+ this.parentContextMenu.removeItem(this.itemsMovedToParent[i]);
+ }
+ }
+ }
+
+ /**
+ * Restore the items that were merged to a parent context menu.
+ */
+ restoreMergedItems(): void {
+ if (this.parentContextMenu) {
+ for (let i = 0; i < this.itemsMovedToParent.length; i++) {
+ this.parentContextMenu.addItem(this.itemsMovedToParent[i]);
+ }
+ }
+ }
+
+ /**
+ * Show the context menu.
+ *
+ * @param event Event.
+ */
+ async showContextMenu(event: MouseEvent): Promise {
+ if (!this.expanded) {
+ const popover = await this.popoverCtrl.create(
+ {
+ event,
+ component: CoreContextMenuPopoverComponent,
+ componentProps: {
+ title: this.title,
+ items: this.items,
+ },
+ showBackdrop: true,
+ id: this.uniqueId,
+ },
+ );
+ await popover.present();
+ this.expanded = true;
+
+ const data = await popover.onDidDismiss();
+ this.expanded = false;
+
+ if (data.data) {
+ data.data.onClosed?.emit();
+ }
+
+ }
+ }
+
+ /**
+ * Component destroyed.
+ */
+ ngOnDestroy(): void {
+ CoreDomUtils.instance.removeInstanceById(this.instanceId);
+ this.removeMergedItems();
+ }
+
+}
diff --git a/src/core/components/context-menu/core-context-menu-popover.html b/src/core/components/context-menu/core-context-menu-popover.html
new file mode 100644
index 000000000..36cfcacdd
--- /dev/null
+++ b/src/core/components/context-menu/core-context-menu-popover.html
@@ -0,0 +1,15 @@
+
+ {{title}}
+
+
+
+
+
+
+
+ {{item.badge}}
+
+
diff --git a/src/core/components/context-menu/core-context-menu.html b/src/core/components/context-menu/core-context-menu.html
new file mode 100644
index 000000000..adac51104
--- /dev/null
+++ b/src/core/components/context-menu/core-context-menu.html
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/src/core/components/navbar-buttons/navbar-buttons.scss b/src/core/components/navbar-buttons/navbar-buttons.scss
new file mode 100644
index 000000000..5d9e00932
--- /dev/null
+++ b/src/core/components/navbar-buttons/navbar-buttons.scss
@@ -0,0 +1,3 @@
+:host {
+ display: none !important;
+}
diff --git a/src/core/components/navbar-buttons/navbar-buttons.ts b/src/core/components/navbar-buttons/navbar-buttons.ts
new file mode 100644
index 000000000..7697224a4
--- /dev/null
+++ b/src/core/components/navbar-buttons/navbar-buttons.ts
@@ -0,0 +1,269 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Input, OnInit, OnDestroy, ElementRef } from '@angular/core';
+import { CoreLogger } from '@singletons/logger';
+import { CoreDomUtils } from '@services/utils/dom';
+import { CoreContextMenuComponent } from '../context-menu/context-menu';
+
+const BUTTON_HIDDEN_CLASS = 'core-navbar-button-hidden';
+
+/**
+ * Component to add buttons to the app's header without having to place them inside the header itself. This is meant for
+ * pages that are loaded inside a sub ion-nav, so they don't have a header.
+ *
+ * If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that
+ * position. If no start/end is specified, then the buttons will be added to the first found in the header.
+ *
+ * If this component has a "prepend" attribute, the buttons will be added before other existing buttons in the header.
+ *
+ * You can use the [hidden] input to hide all the inner buttons if a certain condition is met.
+ *
+ * IMPORTANT: Do not use *ngIf in the buttons inside this component, it can cause problems. Please use [hidden] instead.
+ *
+ * Example usage:
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+@Component({
+ selector: 'core-navbar-buttons',
+ template: '',
+ styleUrls: ['navbar-buttons.scss'],
+})
+export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
+
+ // If the hidden input is true, hide all buttons.
+ // eslint-disable-next-line @angular-eslint/no-input-rename
+ @Input('hidden') set hidden(value: boolean) {
+ if (typeof value == 'string' && value == '') {
+ value = true;
+ }
+ this.allButtonsHidden = value;
+ this.showHideAllElements();
+ }
+
+ protected element: HTMLElement;
+ protected allButtonsHidden = false;
+ protected forceHidden = false;
+ protected logger: CoreLogger;
+ protected movedChildren?: Node[];
+ protected instanceId: string;
+ protected mergedContextMenu?: CoreContextMenuComponent;
+
+ constructor(element: ElementRef) {
+ this.element = element.nativeElement;
+ this.logger = CoreLogger.getInstance('CoreNavBarButtonsComponent');
+ this.instanceId = CoreDomUtils.instance.storeInstanceByElement(this.element, this);
+ }
+
+ /**
+ * Component being initialized.
+ */
+ async ngOnInit(): Promise {
+ try {
+ const header = await this.searchHeader();
+ if (header) {
+ // Search the right buttons container (start, end or any).
+ let selector = 'ion-buttons';
+
+ let slot = this.element.getAttribute('slot');
+ // Take the slot from the parent if it has.
+ if (!slot && this.element.parentElement) {
+ slot = this.element.parentElement.getAttribute('slot');
+ }
+ if (slot) {
+ selector += '[slot="' + slot + '"]';
+ }
+
+ const buttonsContainer = header.querySelector(selector);
+ if (buttonsContainer) {
+ this.mergeContextMenus(buttonsContainer);
+
+ const prepend = this.element.hasAttribute('prepend');
+
+ this.movedChildren = CoreDomUtils.instance.moveChildren(this.element, buttonsContainer, prepend);
+ this.showHideAllElements();
+
+ } else {
+ this.logger.warn('The header was found, but it didn\'t have the right ion-buttons.', selector);
+ }
+ }
+ } catch (error) {
+ // Header not found.
+ this.logger.warn(error);
+ }
+ }
+
+ /**
+ * Force or unforce hiding all buttons. If this is true, it will override the "hidden" input.
+ *
+ * @param value The value to set.
+ */
+ forceHide(value: boolean): void {
+ this.forceHidden = value;
+
+ this.showHideAllElements();
+ }
+
+ /**
+ * If both button containers have a context menu, merge them into a single one.
+ *
+ * @param buttonsContainer The container where the buttons will be moved.
+ * @todo
+ */
+ protected mergeContextMenus(buttonsContainer: HTMLElement): void {
+ // Check if both button containers have a context menu.
+ const mainContextMenu = buttonsContainer.querySelector('core-context-menu');
+ if (!mainContextMenu) {
+ return;
+ }
+
+ const secondaryContextMenu = this.element.querySelector('core-context-menu');
+ if (!secondaryContextMenu) {
+ return;
+ }
+
+ // Both containers have a context menu. Merge them to prevent having 2 menus at the same time.
+ const mainContextMenuInstance: CoreContextMenuComponent = CoreDomUtils.instance.getInstanceByElement(mainContextMenu);
+ const secondaryContextMenuInstance: CoreContextMenuComponent =
+ CoreDomUtils.instance.getInstanceByElement(secondaryContextMenu);
+
+ // Check that both context menus belong to the same core-tab. We shouldn't merge menus from different tabs.
+ if (mainContextMenuInstance && secondaryContextMenuInstance) {
+ this.mergedContextMenu = secondaryContextMenuInstance;
+
+ this.mergedContextMenu.mergeContextMenus(mainContextMenuInstance);
+
+ // Remove the empty context menu from the DOM.
+ secondaryContextMenu.parentElement?.removeChild(secondaryContextMenu);
+ }
+ }
+
+ /**
+ * Search the ion-header where the buttons should be added.
+ *
+ * @param retries Number of retries so far.
+ * @return Promise resolved with the header element.
+ */
+ protected async searchHeader(retries: number = 0): Promise {
+ let parentPage: HTMLElement = this.element;
+
+ while (parentPage) {
+ if (!parentPage.parentElement) {
+ // No parent, stop.
+ break;
+ }
+
+ // Get the next parent page.
+ parentPage = CoreDomUtils.instance.closest(parentPage.parentElement, '.ion-page');
+ if (parentPage) {
+ // Check if the page has a header. If it doesn't, search the next parent page.
+ const header = this.searchHeaderInPage(parentPage);
+ if (header && getComputedStyle(header, null).display != 'none') {
+ return header;
+ }
+ }
+ }
+
+ // Header not found.
+ if (retries < 5) {
+ // If the component or any of its parent is inside a ng-content or similar it can be detached when it's initialized.
+ // Try again after a while.
+ return new Promise((resolve, reject): void => {
+ setTimeout(() => {
+ // eslint-disable-next-line promise/catch-or-return
+ this.searchHeader(retries + 1).then(resolve, reject);
+ }, 200);
+ });
+ }
+
+ // We've waited enough time, reject.
+ throw Error('Header not found.');
+ }
+
+ /**
+ * Search ion-header inside a page. The header should be a direct child.
+ *
+ * @param page Page to search in.
+ * @return Header element. Undefined if not found.
+ */
+ protected searchHeaderInPage(page: HTMLElement): HTMLElement | undefined {
+ for (let i = 0; i < page.children.length; i++) {
+ const child = page.children[i];
+ if (child.tagName == 'ION-HEADER') {
+ return child;
+ }
+ }
+ }
+
+ /**
+ * Show or hide all the elements.
+ */
+ protected showHideAllElements(): void {
+ // Show or hide all moved children.
+ if (this.movedChildren) {
+ this.movedChildren.forEach((child: Node) => {
+ this.showHideElement(child);
+ });
+ }
+
+ // Show or hide all the context menu items that were merged to another context menu.
+ if (this.mergedContextMenu) {
+ if (this.forceHidden || this.allButtonsHidden) {
+ this.mergedContextMenu.removeMergedItems();
+ } else {
+ this.mergedContextMenu.restoreMergedItems();
+ }
+ }
+ }
+
+ /**
+ * Show or hide an element.
+ *
+ * @param element Element to show or hide.
+ */
+ protected showHideElement(element: Node): void {
+ // Check if it's an HTML Element
+ if (element instanceof Element) {
+ element.classList.toggle(BUTTON_HIDDEN_CLASS, !!this.forceHidden || !!this.allButtonsHidden);
+ }
+ }
+
+ /**
+ * Component destroyed.
+ */
+ ngOnDestroy(): void {
+ CoreDomUtils.instance.removeInstanceById(this.instanceId);
+
+ // This component was destroyed, remove all the buttons that were moved.
+ // The buttons can be moved outside of the current page, that's why we need to manually destroy them.
+ // There's no need to destroy context menu items that were merged because they weren't moved from their DOM position.
+ if (this.movedChildren) {
+ this.movedChildren.forEach((child) => {
+ if (child.parentElement) {
+ child.parentElement.removeChild(child);
+ }
+ });
+ }
+
+ if (this.mergedContextMenu) {
+ this.mergedContextMenu.removeMergedItems();
+ }
+ }
+
+}
diff --git a/src/core/features/mainmenu/pages/home/home.html b/src/core/features/mainmenu/pages/home/home.html
index 71b4ffdad..d3248c03a 100644
--- a/src/core/features/mainmenu/pages/home/home.html
+++ b/src/core/features/mainmenu/pages/home/home.html
@@ -8,9 +8,15 @@
-
-
+
+
+
+
+
+
diff --git a/src/core/features/mainmenu/pages/home/home.page.ts b/src/core/features/mainmenu/pages/home/home.page.ts
index 4a9d1c418..a8a1887f7 100644
--- a/src/core/features/mainmenu/pages/home/home.page.ts
+++ b/src/core/features/mainmenu/pages/home/home.page.ts
@@ -12,10 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { CoreSites } from '@services/sites';
import { Component, OnInit } from '@angular/core';
+import { NavController } from '@ionic/angular';
import { Subscription } from 'rxjs';
+
+import { CoreSites } from '@services/sites';
import { CoreHomeDelegate, CoreHomeHandlerToDisplay } from '../../services/home.delegate';
+import { CoreCourses } from '@features/courses/services/courses';
+import { CoreEventObserver, CoreEvents } from '@singletons/events';
/**
* Page that displays the Home.
@@ -31,11 +35,16 @@ export class CoreHomePage implements OnInit {
tabs: CoreHomeHandlerToDisplay[] = [];
loaded = false;
selectedTab?: number;
+ searchEnabled = false;
+ downloadCourseEnabled = false;
+ downloadCoursesEnabled = false;
protected subscription?: Subscription;
+ protected updateSiteObserver?: CoreEventObserver;
constructor(
protected homeDelegate: CoreHomeDelegate,
+ protected navCtrl: NavController,
) {
this.loadSiteName();
}
@@ -44,9 +53,22 @@ export class CoreHomePage implements OnInit {
* Initialize the component.
*/
ngOnInit(): void {
+ this.searchEnabled = !CoreCourses.instance.isSearchCoursesDisabledInSite();
+ this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite();
+ this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite();
+
this.subscription = this.homeDelegate.getHandlersObservable().subscribe((handlers) => {
handlers && this.initHandlers(handlers);
});
+
+ // Refresh the enabled flags if site is updated.
+ this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
+ this.searchEnabled = !CoreCourses.instance.isSearchCoursesDisabledInSite();
+ this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite();
+ this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite();
+
+ this.loadSiteName();
+ }, CoreSites.instance.getCurrentSiteId());
}
/**
@@ -91,4 +113,18 @@ export class CoreHomePage implements OnInit {
this.siteName = CoreSites.instance.getCurrentSite()!.getSiteName();
}
+ /**
+ * Open page to manage courses storage.
+ */
+ manageCoursesStorage(): void {
+ // @todo this.navCtrl.navigateForward(['/courses/storage']);
+ }
+
+ /**
+ * Go to search courses.
+ */
+ openSearch(): void {
+ this.navCtrl.navigateForward(['/courses/search']);
+ }
+
}
diff --git a/src/core/features/settings/pages/space-usage/space-usage.html b/src/core/features/settings/pages/space-usage/space-usage.html
index ad08d1c8d..8d9294c40 100644
--- a/src/core/features/settings/pages/space-usage/space-usage.html
+++ b/src/core/features/settings/pages/space-usage/space-usage.html
@@ -5,10 +5,11 @@
{{ 'core.settings.spaceusage' | translate }}
-
-
-
-
+
+
+
+
+
diff --git a/src/core/features/settings/pages/synchronization/synchronization.html b/src/core/features/settings/pages/synchronization/synchronization.html
index 587cf2b4a..3ac2cd268 100644
--- a/src/core/features/settings/pages/synchronization/synchronization.html
+++ b/src/core/features/settings/pages/synchronization/synchronization.html
@@ -5,10 +5,11 @@
{{ 'core.settings.synchronization' | translate }}
-
-
-
-
+
+
+
+
+
diff --git a/src/theme/app.scss b/src/theme/app.scss
index 45ee03c21..f48f74a42 100644
--- a/src/theme/app.scss
+++ b/src/theme/app.scss
@@ -10,6 +10,10 @@ ion-toolbar .in-toolbar.button-clear {
--color: var(--ion-color-primary-contrast);
}
+ion-toolbar .core-navbar-button-hidden {
+ display: none !important;
+}
+
// Ionic icon.
ion-icon {
&.icon-slash::after,