MOBILE-3745 a11y: Add new combobox component
parent
e7b61672f1
commit
fa22325fb4
|
@ -34,10 +34,9 @@
|
|||
</core-context-menu>
|
||||
</ion-item-divider>
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
<div class="ion-padding safe-padding-horizontal" [hidden]="showFilter || !showSelectorFilter">
|
||||
<div class="safe-padding-horizontal" [hidden]="showFilter || !showSelectorFilter">
|
||||
<!-- "Time" selector. -->
|
||||
<ion-select class="core-button-select ion-text-start" [title]="'core.show' | translate" [(ngModel)]="selectedFilter"
|
||||
(ngModelChange)="selectedChanged()" interface="popover">
|
||||
<core-combobox [label]="'core.show' | translate" [selection]="selectedFilter" (onChange)="selectedChanged($event)">
|
||||
<ion-select-option value="allincludinghidden" *ngIf="showFilters.allincludinghidden != 'hidden'">
|
||||
{{ 'addon.block_myoverview.allincludinghidden' | translate }}
|
||||
</ion-select-option>
|
||||
|
@ -66,7 +65,7 @@
|
|||
<ion-select-option value="hidden" *ngIf="showFilters.hidden != 'hidden'" [disabled]="showFilters.hidden == 'disabled'">
|
||||
{{ 'addon.block_myoverview.hiddencourses' | translate }}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</core-combobox>
|
||||
</div>
|
||||
|
||||
<!-- Filter courses. -->
|
||||
|
|
|
@ -414,8 +414,11 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
|||
|
||||
/**
|
||||
* The selected courses filter have changed.
|
||||
*
|
||||
* @param filter New filter
|
||||
*/
|
||||
selectedChanged(): void {
|
||||
selectedChanged(filter: string): void {
|
||||
this.selectedFilter = filter;
|
||||
this.setCourseFilter(this.selectedFilter);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,9 +10,8 @@
|
|||
</core-context-menu>
|
||||
</ion-item-divider>
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
<div class="ion-padding safe-padding-horizontal">
|
||||
<ion-select class="ion-text-start core-button-select" [(ngModel)]="filter" (ngModelChange)="switchFilter()"
|
||||
interface="popover">
|
||||
<div class="safe-padding-horizontal">
|
||||
<core-combobox [selection]="filter" (onChange)="switchFilter($event)">
|
||||
<ion-select-option value="all">{{ 'core.all' | translate }}</ion-select-option>
|
||||
<ion-select-option value="overdue">{{ 'addon.block_timeline.overdue' | translate }}</ion-select-option>
|
||||
<ion-select-option disabled value="disabled">{{ 'addon.block_timeline.duedate' | translate }}</ion-select-option>
|
||||
|
@ -20,7 +19,7 @@
|
|||
<ion-select-option value="next30days">{{ 'addon.block_timeline.next30days' | translate }}</ion-select-option>
|
||||
<ion-select-option value="next3months">{{ 'addon.block_timeline.next3months' | translate }}</ion-select-option>
|
||||
<ion-select-option value="next6months">{{ 'addon.block_timeline.next6months' | translate }}</ion-select-option>
|
||||
</ion-select>
|
||||
</core-combobox>
|
||||
</div>
|
||||
<core-loading [hideUntil]="timeline.loaded" [hidden]="sort != 'sortbydates'" class="core-loading-center">
|
||||
<addon-block-timeline-events [events]="timeline.events" showCourse="true" [canLoadMore]="timeline.canLoadMore"
|
||||
|
|
|
@ -72,7 +72,7 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
|
|||
this.currentSite = CoreSites.getCurrentSite();
|
||||
|
||||
this.filter = await this.currentSite!.getLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
|
||||
this.switchFilter();
|
||||
this.switchFilter(this.filter);
|
||||
|
||||
this.sort = await this.currentSite!.getLocalSiteConfig('AddonBlockTimelineSort', this.sort);
|
||||
|
||||
|
@ -183,8 +183,11 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
|
|||
|
||||
/**
|
||||
* Change timeline filter being viewed.
|
||||
*
|
||||
* @param filter New filter.
|
||||
*/
|
||||
switchFilter(): void {
|
||||
switchFilter(filter: string): void {
|
||||
this.filter = filter;
|
||||
this.currentSite?.setLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
|
||||
|
||||
switch (this.filter) {
|
||||
|
|
|
@ -67,15 +67,15 @@
|
|||
<core-empty-box *ngIf="discussions.empty" icon="chatbubbles" [message]="'addon.mod_forum.forumnodiscussionsyet' | translate">
|
||||
</core-empty-box>
|
||||
|
||||
<div *ngIf="!discussions.empty && sortingAvailable && selectedSortOrder" class="ion-text-wrap addon-forum-sorting-select">
|
||||
<ion-button *ngIf="sortingAvailable" id="addon-mod-forum-sort-order-button"
|
||||
class="core-button-select button-no-uppercase"
|
||||
aria-haspopup="true" aria-controls="addon-mod-forum-sort-order-selector"
|
||||
[attr.aria-label]="('core.sort' | translate)"
|
||||
(click)="showSortOrderSelector()">
|
||||
<span class="core-button-select-text">{{ selectedSortOrder.label | translate }}</span>
|
||||
<div class="select-icon" slot="end"><div class="select-icon-inner"></div></div>
|
||||
</ion-button>
|
||||
<div *ngIf="!discussions.empty && sortingAvailable && selectedSortOrder" class="ion-text-wrap">
|
||||
<core-combobox
|
||||
[modalOptions]="sortOrderSelectorModalOptions"
|
||||
listboxId="addon-mod-forum-sort-selector"
|
||||
[label]="('core.sort' | translate)"
|
||||
(onChange)="setSortOrder($event)"
|
||||
[selection]="selectedSortOrder.label | translate"
|
||||
interface="modal">
|
||||
</core-combobox>
|
||||
</div>
|
||||
|
||||
<ion-item *ngFor="let discussion of discussions.items"
|
||||
|
|
|
@ -2,20 +2,6 @@
|
|||
|
||||
:host {
|
||||
|
||||
.addon-forum-sorting-select {
|
||||
display: flex;
|
||||
|
||||
.core-button-select {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.core-button-select-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.addon-forum-star {
|
||||
color: var(--core-color);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
import { Component, Optional, OnInit, OnDestroy, ViewChild, AfterViewInit } from '@angular/core';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
import { ModalOptions } from '@ionic/core';
|
||||
|
||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||
import {
|
||||
AddonModForum,
|
||||
|
@ -26,7 +28,7 @@ import {
|
|||
AddonModForumReplyDiscussionData,
|
||||
} from '@addons/mod/forum/services/forum';
|
||||
import { AddonModForumOffline, AddonModForumOfflineDiscussion } from '@addons/mod/forum/services/forum-offline';
|
||||
import { ModalController, PopoverController, Translate } from '@singletons';
|
||||
import { PopoverController, Translate } from '@singletons';
|
||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||
import { AddonModForumHelper } from '@addons/mod/forum/services/forum-helper';
|
||||
import { CoreGroups, CoreGroupsProvider } from '@services/groups';
|
||||
|
@ -80,18 +82,20 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
sortOrders: AddonModForumSortOrder[] = [];
|
||||
selectedSortOrder: AddonModForumSortOrder | null = null;
|
||||
canPin = false;
|
||||
trackPosts = false;
|
||||
hasOfflineRatings = false;
|
||||
sortOrderSelectorModalOptions: ModalOptions = {
|
||||
component: AddonModForumSortOrderSelectorComponent,
|
||||
};
|
||||
|
||||
protected syncEventName = AddonModForumSyncProvider.AUTO_SYNCED;
|
||||
protected page = 0;
|
||||
trackPosts = false;
|
||||
protected usesGroups = false;
|
||||
protected syncManualObserver?: CoreEventObserver; // It will observe the sync manual event.
|
||||
protected replyObserver?: CoreEventObserver;
|
||||
protected newDiscObserver?: CoreEventObserver;
|
||||
protected viewDiscObserver?: CoreEventObserver;
|
||||
protected changeDiscObserver?: CoreEventObserver;
|
||||
|
||||
hasOfflineRatings = false;
|
||||
protected ratingOfflineObserver?: CoreEventObserver;
|
||||
protected ratingSyncObserver?: CoreEventObserver;
|
||||
|
||||
|
@ -117,6 +121,10 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
this.sortingAvailable = AddonModForum.isDiscussionListSortingAvailable();
|
||||
this.sortOrders = AddonModForum.getAvailableSortOrders();
|
||||
|
||||
this.sortOrderSelectorModalOptions.componentProps = {
|
||||
sortOrders: this.sortOrders,
|
||||
};
|
||||
|
||||
await super.ngOnInit();
|
||||
|
||||
// Refresh data if this forum discussion is synchronized from discussions list.
|
||||
|
@ -516,6 +524,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
const value = await getSortOrder();
|
||||
|
||||
this.selectedSortOrder = this.sortOrders.find(sortOrder => sortOrder.value === value) || this.sortOrders[0];
|
||||
this.sortOrderSelectorModalOptions.componentProps!.selected = this.selectedSortOrder.value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -621,31 +630,18 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
}
|
||||
|
||||
/**
|
||||
* Display the sort order selector modal.
|
||||
* Changes the sort order.
|
||||
*
|
||||
* @param sortOrder Sort order new data.
|
||||
*/
|
||||
async showSortOrderSelector(): Promise<void> {
|
||||
if (!this.sortingAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = await ModalController.create({
|
||||
component: AddonModForumSortOrderSelectorComponent,
|
||||
componentProps: {
|
||||
sortOrders: this.sortOrders,
|
||||
selected: this.selectedSortOrder!.value,
|
||||
},
|
||||
});
|
||||
|
||||
await modal.present();
|
||||
|
||||
const result = await modal.onDidDismiss<AddonModForumSortOrder>();
|
||||
|
||||
if (result.data && result.data.value != this.selectedSortOrder?.value) {
|
||||
this.selectedSortOrder = result.data;
|
||||
async setSortOrder(sortOrder: AddonModForumSortOrder): Promise<void> {
|
||||
if (sortOrder.value != this.selectedSortOrder?.value) {
|
||||
this.selectedSortOrder = sortOrder;
|
||||
this.sortOrderSelectorModalOptions.componentProps!.selected = this.selectedSortOrder.value;
|
||||
this.page = 0;
|
||||
|
||||
try {
|
||||
await CoreUser.setUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER, result.data.value.toFixed(0));
|
||||
await CoreUser.setUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER, sortOrder.value.toFixed(0));
|
||||
await this.showLoadingAndFetch();
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error updating preference.');
|
||||
|
@ -653,6 +649,17 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the sort order selector modal.
|
||||
*/
|
||||
async showSortOrderSelector(): Promise<void> {
|
||||
const modalData = await CoreDomUtils.openModal<AddonModForumSortOrder>(this.sortOrderSelectorModalOptions);
|
||||
|
||||
if (modalData) {
|
||||
this.setSortOrder(modalData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the context menu.
|
||||
*
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>{{ 'core.sort' | translate }}</ion-title>
|
||||
<ion-title id="addon-mod-forum-sort-order-label">{{ 'core.sort' | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon name="fas-times" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
|
@ -9,7 +9,7 @@
|
|||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list id="addon-mod-forum-sort-selector" role="menu" aria-labelledby="addon-mod-forum-sort-order-button">
|
||||
<ion-list id="addon-mod-forum-sort-selector" role="listbox" aria-labelledby="addon-mod-forum-sort-order-label">
|
||||
<ng-container *ngFor="let sortOrder of sortOrders">
|
||||
<ion-item class="ion-text-wrap" detail="false" role="combobox"
|
||||
[attr.aria-current]="selected == sortOrder.value ? 'page' : 'false'" [attr.aria-label]="sortOrder.label | translate"
|
||||
|
|
|
@ -35,13 +35,11 @@
|
|||
<ion-label><h2>{{user!.fullname}}</h2></ion-label>
|
||||
</ion-item>
|
||||
|
||||
<div class="ion-padding">
|
||||
<ion-select [(ngModel)]="type" (ngModelChange)="typeChanged()" interface="popover" class="core-button-select">
|
||||
<ion-select-option value="site">{{ 'addon.notes.sitenotes' | translate }}</ion-select-option>
|
||||
<ion-select-option value="course">{{ 'addon.notes.coursenotes' | translate }}</ion-select-option>
|
||||
<ion-select-option value="personal">{{ 'addon.notes.personalnotes' | translate }}</ion-select-option>
|
||||
</ion-select>
|
||||
</div>
|
||||
<core-combobox [selection]="type" (onChange)="typeChanged($event)">
|
||||
<ion-select-option value="site">{{ 'addon.notes.sitenotes' | translate }}</ion-select-option>
|
||||
<ion-select-option value="course">{{ 'addon.notes.coursenotes' | translate }}</ion-select-option>
|
||||
<ion-select-option value="personal">{{ 'addon.notes.personalnotes' | translate }}</ion-select-option>
|
||||
</core-combobox>
|
||||
|
||||
<ion-card class="core-warning-card" *ngIf="hasOffline">
|
||||
<ion-item>
|
||||
|
|
|
@ -157,8 +157,11 @@ export class AddonNotesListPage implements OnInit, OnDestroy {
|
|||
|
||||
/**
|
||||
* Function called when the type has changed.
|
||||
*
|
||||
* @param type New type.
|
||||
*/
|
||||
async typeChanged(): Promise<void> {
|
||||
async typeChanged(type: string): Promise<void> {
|
||||
this.type = type;
|
||||
this.notesLoaded = false;
|
||||
this.refreshIcon = CoreConstants.ICON_LOADING;
|
||||
this.syncIcon = CoreConstants.ICON_LOADING;
|
||||
|
@ -199,8 +202,7 @@ export class AddonNotesListPage implements OnInit, OnDestroy {
|
|||
|
||||
this.refreshNotes(false);
|
||||
} else if (result.data.type && result.data.type != this.type) {
|
||||
this.type = result.data.type;
|
||||
this.typeChanged();
|
||||
this.typeChanged(result.data.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,13 +41,12 @@
|
|||
</ion-card>
|
||||
|
||||
<!-- Show processor selector. -->
|
||||
<ion-select *ngIf="preferences && preferences.processors && preferences.processors.length > 0"
|
||||
[ngModel]="currentProcessor!.name" (ngModelChange)="changeProcessor($event)" interface="action-sheet"
|
||||
class="core-button-select">
|
||||
<core-combobox *ngIf="preferences && preferences.processors && preferences.processors.length > 0"
|
||||
[selection]="currentProcessor!.name" (onChange)="changeProcessor($event)">
|
||||
<ion-select-option *ngFor="let processor of preferences.processors" [value]="processor.name">
|
||||
{{ processor.displayname }}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</core-combobox>
|
||||
|
||||
<ion-card list *ngFor="let component of components" class="ion-margin-top">
|
||||
<ion-item-divider class="ion-text-wrap">
|
||||
|
|
|
@ -14,12 +14,10 @@
|
|||
|
||||
<core-loading [hideUntil]="filesLoaded" *ngIf="showPrivateFiles || showSiteFiles">
|
||||
<!-- Allow selecting the files to see: private or site. -->
|
||||
<div class="ion-padding" *ngIf="showPrivateFiles && showSiteFiles && !path">
|
||||
<ion-select [(ngModel)]="root" (ngModelChange)="rootChanged()" interface="popover" class="core-button-select">
|
||||
<ion-select-option value="my">{{ 'addon.privatefiles.privatefiles' | translate }}</ion-select-option>
|
||||
<ion-select-option value="site">{{ 'addon.privatefiles.sitefiles' | translate }}</ion-select-option>
|
||||
</ion-select>
|
||||
</div>
|
||||
<core-combobox [selection]="root" (onChange)="rootChanged($event)" *ngIf="showPrivateFiles && showSiteFiles && !path">
|
||||
<ion-select-option value="my">{{ 'addon.privatefiles.privatefiles' | translate }}</ion-select-option>
|
||||
<ion-select-option value="site">{{ 'addon.privatefiles.sitefiles' | translate }}</ion-select-option>
|
||||
</core-combobox>
|
||||
|
||||
<!-- Display info about space used and space left. -->
|
||||
<ion-card class="core-info-card" *ngIf="userQuota && filesInfo && filesInfo.filecount > 0">
|
||||
|
|
|
@ -99,7 +99,7 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
if (this.root) {
|
||||
this.rootChanged();
|
||||
this.rootChanged(this.root);
|
||||
} else {
|
||||
this.filesLoaded = true;
|
||||
}
|
||||
|
@ -127,8 +127,12 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
|
|||
|
||||
/**
|
||||
* Function called when the root has changed.
|
||||
*
|
||||
* @param root New root.
|
||||
*/
|
||||
rootChanged(): void {
|
||||
rootChanged(root: 'my' | 'site'): void {
|
||||
this.root = root;
|
||||
|
||||
this.filesLoaded = false;
|
||||
this.component = this.root == 'my' ? AddonPrivateFilesProvider.PRIVATE_FILES_COMPONENT :
|
||||
AddonPrivateFilesProvider.SITE_FILES_COMPONENT;
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
@import "~theme/globals";
|
||||
|
||||
:host {
|
||||
ion-select,
|
||||
ion-button {
|
||||
--icon-margin: 0 8px;
|
||||
--background: var(--core-combobox-background);
|
||||
--background-hover: #000000;
|
||||
--background-activated: #000000;
|
||||
--background-focused: #000000;
|
||||
--background-hover-opacity: .04;
|
||||
|
||||
--color: var(--core-combobox-color);
|
||||
--color-activated: var(--core-combobox-color);
|
||||
--color-focused: currentcolor;
|
||||
--color-hover: currentcolor;
|
||||
|
||||
--padding-top: 10px;
|
||||
--padding-end: 10px;
|
||||
--padding-bottom: 10px;
|
||||
--padding-start: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.md) {
|
||||
--background-activated-opacity: 0;
|
||||
--background-focused-opacity: .12;
|
||||
}
|
||||
|
||||
:host-context(.ios) {
|
||||
--background-activated-opacity: .12;
|
||||
--background-focused-opacity: .15;
|
||||
}
|
||||
|
||||
ion-select,
|
||||
ion-button {
|
||||
background: var(--background);
|
||||
color: var(--color);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-height: 25px;
|
||||
overflow: hidden;
|
||||
margin: 8px;
|
||||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12);
|
||||
}
|
||||
|
||||
ion-select {
|
||||
&::part(icon) {
|
||||
margin: var(--icon-margin);
|
||||
}
|
||||
|
||||
&::after {
|
||||
@include button-state();
|
||||
transition: var(--transition);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
color: var(--color-hover);
|
||||
background: var(--background-hover);
|
||||
opacity: var(--background-hover-opacity);
|
||||
}
|
||||
|
||||
&:focus::after,
|
||||
&:focus-within::after {
|
||||
color: var(--color-focused);
|
||||
background: var(--background-focused);
|
||||
opacity: var(--background-focused-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
ion-button {
|
||||
--border-radius: 0;
|
||||
flex: 1;
|
||||
min-height: 45px;
|
||||
|
||||
&::part(native) {
|
||||
text-transform: none;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.select-text {
|
||||
margin-inline-end: auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&.ion-activated {
|
||||
--color: var(--color-activated);
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
margin: var(--icon-margin);
|
||||
}
|
||||
|
||||
.select-icon {
|
||||
margin: var(--icon-margin);
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
position: relative;
|
||||
opacity: 0.33;
|
||||
|
||||
.select-icon-inner {
|
||||
left: 5px;
|
||||
top: 50%;
|
||||
margin-top: -2px;
|
||||
position: absolute;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
color: currentcolor;
|
||||
pointer-events: none;
|
||||
border-top: 5px solid;
|
||||
border-right: 5px solid transparent;
|
||||
border-left: 5px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
|
||||
import { Translate } from '@singletons';
|
||||
import { ModalOptions } from '@ionic/core';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
|
||||
/**
|
||||
* Component that show a combo select button (combobox).
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* Example using modal:
|
||||
*
|
||||
* <core-combobox interface="modal" (onChange)="selectedChanged($event)" [modalOptions]="modalOptions"
|
||||
* icon="fas-folder" [label]="'core.course.section' | translate">
|
||||
* <span slot="text">selection</span>
|
||||
* </core-combobox>
|
||||
*
|
||||
* Example using popover:
|
||||
*
|
||||
* <core-combobox [label]="'core.show' | translate" [selection]="selectedFilter" (onChange)="selectedChanged()">
|
||||
* <ion-select-option value="1">1</ion-select-option>
|
||||
* </core-combobox>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-combobox',
|
||||
templateUrl: 'core-combobox.html',
|
||||
styleUrls: ['combobox.scss'],
|
||||
encapsulation: ViewEncapsulation.ShadowDom,
|
||||
})
|
||||
export class CoreComboboxComponent {
|
||||
|
||||
@Input() interface: 'popover' | 'modal' = 'popover';
|
||||
@Input() label = Translate.instant('core.show'); // Aria label.
|
||||
@Input() disabled = false;
|
||||
@Input() selection = '';
|
||||
@Output() onChange = new EventEmitter<unknown>(); // Will emit an event the value changed.
|
||||
|
||||
// Additional options when interface modal is selected.
|
||||
@Input() icon?: string; // Icon for modal interface.
|
||||
@Input() protected modalOptions?: ModalOptions; // Will emit an event the value changed.
|
||||
@Input() listboxId = '';
|
||||
|
||||
expanded = false;
|
||||
|
||||
async showModal(): Promise<void> {
|
||||
if (this.expanded || !this.modalOptions) {
|
||||
return;
|
||||
}
|
||||
this.expanded = true;
|
||||
|
||||
if (this.listboxId) {
|
||||
this.modalOptions.id = this.listboxId;
|
||||
}
|
||||
|
||||
const data = await CoreDomUtils.openModal(this.modalOptions);
|
||||
this.expanded = false;
|
||||
|
||||
if (data) {
|
||||
this.onChange.emit(data);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<ion-select
|
||||
*ngIf="interface != 'modal'"
|
||||
class="ion-text-start"
|
||||
[(ngModel)]="selection"
|
||||
(ngModelChange)="onChange.emit(selection)"
|
||||
[interface]="interface"
|
||||
[attr.aria-label]="label + ': ' + selection"
|
||||
[disabled]="disabled"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
</ion-select>
|
||||
|
||||
<ion-button
|
||||
*ngIf="interface == 'modal'"
|
||||
id="addon-mod-forum-sort-order-button"
|
||||
aria-haspopup="listbox"
|
||||
aria-controls="addon-mod-forum-sort-order-selector"
|
||||
[attr.aria-owns]="listboxId"
|
||||
[attr.aria-expanded]="expanded"
|
||||
(click)="showModal()"
|
||||
[disabled]="disabled"
|
||||
expand="block"
|
||||
role="combobox"
|
||||
>
|
||||
<ion-icon *ngIf="icon" [name]="icon" slot="start" aria-hidden="true"></ion-icon>
|
||||
<span class="sr-only" *ngIf="label">{{ label }}:</span>
|
||||
<div class="select-text">
|
||||
<slot name="text">{{selection}}</slot>
|
||||
</div>
|
||||
<div class="select-icon" role="presentation" aria-hidden="true">
|
||||
<div class="select-icon-inner"></div>
|
||||
</div>
|
||||
</ion-button>
|
|
@ -55,6 +55,7 @@ import { CoreTabsComponent } from './tabs/tabs';
|
|||
import { CoreTabsOutletComponent } from './tabs-outlet/tabs-outlet';
|
||||
import { CoreTimerComponent } from './timer/timer';
|
||||
import { CoreUserAvatarComponent } from './user-avatar/user-avatar';
|
||||
import { CoreComboboxComponent } from './combobox/combobox';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -92,6 +93,7 @@ import { CoreUserAvatarComponent } from './user-avatar/user-avatar';
|
|||
CoreTabsOutletComponent,
|
||||
CoreTimerComponent,
|
||||
CoreUserAvatarComponent,
|
||||
CoreComboboxComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -136,6 +138,7 @@ import { CoreUserAvatarComponent } from './user-avatar/user-avatar';
|
|||
CoreTabsOutletComponent,
|
||||
CoreTimerComponent,
|
||||
CoreUserAvatarComponent,
|
||||
CoreComboboxComponent,
|
||||
],
|
||||
})
|
||||
export class CoreComponentsModule {}
|
||||
|
|
|
@ -19,18 +19,15 @@
|
|||
<div *ngIf="displaySectionSelector && sections && hasSeveralSections"
|
||||
class="ion-text-wrap clearfix ion-justify-content-between core-button-selector-row"
|
||||
[class.core-section-download]="downloadEnabled">
|
||||
<ion-button class="core-button-select button-no-uppercase" (click)="showSectionSelector()"
|
||||
aria-haspopup="listbox" id="core-course-section-button" expand="block">
|
||||
<ion-icon name="fas-folder" slot="start" aria-hidden="true"></ion-icon>
|
||||
<span class="sr-only" *ngIf="selectedSection">{{ 'core.course.sections' | translate }}:</span>
|
||||
<span class="core-button-select-text">
|
||||
<core-combobox [modalOptions]="sectionSelectorModalOptions" interface="modal" listboxId="core-course-section-button"
|
||||
icon="fas-folder" [label]="'core.course.section' | translate" (onChange)="sectionChanged($event)">
|
||||
<span slot="text">
|
||||
<core-format-text *ngIf="selectedSection" [text]="selectedSection.name" contextLevel="course"
|
||||
[contextInstanceId]="course?.id" [clean]="true" [singleLine]="true">
|
||||
</core-format-text>
|
||||
<span *ngIf="!selectedSection">{{ 'core.course.sections' | translate }}</span>
|
||||
<ng-container *ngIf="!selectedSection">{{ 'core.course.sections' | translate }}</ng-container>
|
||||
</span>
|
||||
<div class="select-icon" slot="end"><div class="select-icon-inner"></div></div>
|
||||
</ion-button>
|
||||
</core-combobox>
|
||||
<!-- Section download. -->
|
||||
<ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: selectedSection}"></ng-container>
|
||||
</div>
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
ViewChild,
|
||||
ElementRef,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ModalOptions } from '@ionic/core';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
|
||||
|
@ -49,7 +49,6 @@ import { IonContent, IonRefresher } from '@ionic/angular';
|
|||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
|
||||
import { CoreBlockCourseBlocksComponent } from '@features/block/components/course-blocks/course-blocks';
|
||||
import { ModalController } from '@singletons';
|
||||
import { CoreCourseSectionSelectorComponent } from '../section-selector/section-selector';
|
||||
|
||||
/**
|
||||
|
@ -104,6 +103,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
hasSeveralSections?: boolean;
|
||||
imageThumb?: string;
|
||||
progress?: number;
|
||||
sectionSelectorModalOptions: ModalOptions = {
|
||||
component: CoreCourseSectionSelectorComponent,
|
||||
};
|
||||
|
||||
protected sectionStatusObserver?: CoreEventObserver;
|
||||
protected selectTabObserver?: CoreEventObserver;
|
||||
|
@ -122,6 +124,12 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
|
||||
this.sectionSelectorModalOptions.componentProps = {
|
||||
course: this.course,
|
||||
sections: this.sections,
|
||||
};
|
||||
|
||||
// Listen for section status changes.
|
||||
this.sectionStatusObserver = CoreEvents.on(
|
||||
CoreEvents.SECTION_STATUS_CHANGED,
|
||||
|
@ -194,6 +202,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
|
||||
if (changes.sections && this.sections) {
|
||||
this.sectionSelectorModalOptions.componentProps!.sections = this.sections;
|
||||
this.treatSections(this.sections);
|
||||
}
|
||||
|
||||
|
@ -342,21 +351,11 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
|
||||
this.sectionSelectorExpanded = true;
|
||||
|
||||
const modal = await ModalController.create({
|
||||
component: CoreCourseSectionSelectorComponent,
|
||||
componentProps: {
|
||||
course: this.course,
|
||||
sections: this.sections,
|
||||
selected: this.selectedSection,
|
||||
},
|
||||
});
|
||||
await modal.present();
|
||||
|
||||
const result = await modal.onWillDismiss();
|
||||
const data = await CoreDomUtils.openModal<CoreCourseSection>(this.sectionSelectorModalOptions);
|
||||
|
||||
this.sectionSelectorExpanded = false;
|
||||
if (result?.data) {
|
||||
this.sectionChanged(result?.data);
|
||||
if (data) {
|
||||
this.sectionChanged(data);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -368,6 +367,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
sectionChanged(newSection: CoreCourseSection): void {
|
||||
const previousValue = this.selectedSection;
|
||||
this.selectedSection = newSection;
|
||||
this.sectionSelectorModalOptions.componentProps!.selected = this.selectedSection;
|
||||
this.data.section = this.selectedSection;
|
||||
|
||||
if (newSection.id != this.allSectionsId) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>{{ 'core.course.sections' | translate }}</ion-title>
|
||||
<ion-title id="core-course-section-selector-label">{{ 'core.course.sections' | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon slot="icon-only" name="fas-times" aria-hidden="true"></ion-icon>
|
||||
|
@ -9,7 +9,7 @@
|
|||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list id="core-course-section-selector">
|
||||
<ion-list id="core-course-section-selector" role="listbox" aria-labelledby="core-course-section-selector-label">
|
||||
<ng-container *ngFor="let section of sections">
|
||||
<ion-item *ngIf="!section.hiddenbynumsections && section.id != stealthModulesSectionId" class="ion-text-wrap"
|
||||
(click)="selectSection(section)" [attr.aria-current]="selected?.id == section.id ? 'page' : 'false'"
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"nocontentavailable": "No content available at the moment.",
|
||||
"overriddennotice": "Your final grade from this activity was manually adjusted.",
|
||||
"refreshcourse": "Refresh course",
|
||||
"section": "Section",
|
||||
"sections": "Sections",
|
||||
"useactivityonbrowser": "You can still use it using your device's web browser.",
|
||||
"warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.",
|
||||
|
|
|
@ -18,12 +18,11 @@
|
|||
searchArea="CoreTag"></core-search-box>
|
||||
</ion-col>
|
||||
<ion-col size="12" size-sm="6" *ngIf="collections && collections.length > 1">
|
||||
<ion-select class="core-button-select ion-text-start" [(ngModel)]="collectionId" (ngModelChange)="searchTags(query)"
|
||||
[disabled]="searching" interface="popover">
|
||||
<core-combobox [selection]="collectionId" (onChange)="searchTags($event)" [disabled]="searching">
|
||||
<ion-select-option [value]="0">{{ 'core.tag.inalltagcoll' | translate }}</ion-select-option>
|
||||
<ion-select-option *ngFor="let collection of collections" [value]="collection.id">
|
||||
{{ collection.name }}</ion-select-option>
|
||||
</ion-select>
|
||||
</core-combobox>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
import { Injectable, SimpleChange, ElementRef, KeyValueChanges } from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
import { AlertOptions, AlertButton, TextFieldTypes } from '@ionic/core';
|
||||
import { ModalOptions, AlertOptions, AlertButton, TextFieldTypes } from '@ionic/core';
|
||||
import { Md5 } from 'ts-md5';
|
||||
|
||||
import { CoreApp } from '@services/app';
|
||||
|
@ -1681,6 +1681,26 @@ export class CoreDomUtilsProvider {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a Modal.
|
||||
*
|
||||
* @param opts Modal Options.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async openModal<T = any>(
|
||||
opts: ModalOptions,
|
||||
): Promise<T | undefined> {
|
||||
|
||||
const modal = await ModalController.create(opts);
|
||||
|
||||
await modal.present();
|
||||
|
||||
const result = await modal.onWillDismiss<T>();
|
||||
if (result?.data) {
|
||||
return result?.data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View an image in a modal.
|
||||
*
|
||||
|
|
|
@ -382,62 +382,6 @@ ion-select::part(text) {
|
|||
white-space: normal;
|
||||
}
|
||||
|
||||
ion-select.core-button-select,
|
||||
.core-button-select {
|
||||
--background: var(--core-button-select-background);
|
||||
background: var(--background);
|
||||
--color: var(--ion-color-primary);
|
||||
color: var(--color);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-height: 45px;
|
||||
overflow: hidden;
|
||||
margin: 8px;
|
||||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12);
|
||||
&::part(icon) {
|
||||
margin: 0 8px;
|
||||
}
|
||||
.core-button-select-text {
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
|
||||
&.ion-activated {
|
||||
--color: var(--ion-color-primary-contrast);
|
||||
}
|
||||
|
||||
}
|
||||
ion-button.core-button-select {
|
||||
--border-radius: 0;
|
||||
&::part(native) {
|
||||
text-transform: none;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
}
|
||||
ion-icon {
|
||||
margin: 0 8px;
|
||||
}
|
||||
.select-icon {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
position: relative;
|
||||
opacity: 0.33;
|
||||
|
||||
.select-icon-inner {
|
||||
left: 5px;
|
||||
top: 50%;
|
||||
margin-top: -2px;
|
||||
position: absolute;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
color: currentcolor;
|
||||
pointer-events: none;
|
||||
border-top: 5px solid;
|
||||
border-right: 5px solid transparent;
|
||||
border-left: 5px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// File uploader.
|
||||
.action-sheet-button input.core-fileuploader-file-handler-input {
|
||||
position: absolute;
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
--color: var(--white);
|
||||
}
|
||||
|
||||
--core-button-select-background: var(--custom-button-select-background, #3a3a3a);
|
||||
--core-combobox-background: var(--custom-combobox-background, #3a3a3a);
|
||||
|
||||
--core-login-background: var(--custom-login-background, #3a3a3a);
|
||||
--core-login-text-color: var(--custom-login-text-color, white);
|
||||
|
|
|
@ -158,7 +158,8 @@
|
|||
--color: inherit;
|
||||
}
|
||||
|
||||
--core-button-select-background: var(--custom-button-select-background, var(--ion-color-primary-contrast));
|
||||
--core-combobox-background: var(--custom-combobox-background, var(--ion-item-background));
|
||||
--core-combobox-color: var(--custom-combobox-color, var(--ion-color-primary));
|
||||
|
||||
--selected-item-color: var(--custom-selected-item-color, var(--core-color));
|
||||
--selected-item-border-width: var(--custom-selected-item-border-width, 5px);
|
||||
|
|
Loading…
Reference in New Issue