Merge pull request #2747 from crazyserver/MOBILE-3745

Mobile 3745
main
Dani Palou 2021-05-10 08:28:35 +02:00 committed by GitHub
commit 6b4392d5de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
156 changed files with 1518 additions and 847 deletions

18
.github/workflows/migration.yml vendored 100644
View File

@ -0,0 +1,18 @@
name: Migration checks
on: workflow_dispatch
jobs:
checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '12.x'
- run: npm ci
- run: result=$(find src -type f -iname '*.html' -exec sh -c 'cat {} | tr "\n" " " | grep -Eo "class=\"[^\"]+\"[^>]+class=\"" ' \; | wc -l); test $result -eq 0
- run: npx tslint -c ionic-migration.json -p tsconfig.json

View File

@ -14,9 +14,7 @@ jobs:
with:
node-version: '12.x'
- run: npm ci
- run: result=$(find src -type f -iname '*.html' -exec sh -c 'cat {} | tr "\n" " " | grep -Eo "class=\"[^\"]+\"[^>]+class=\"" ' \; | wc -l); test $result -eq 0
- run: npm run lint
- run: npx tslint -c ionic-migration.json -p tsconfig.json
- run: npm run test:ci
- run: npm run build:prod
- run: result=$(npx check-es-compat www/*.js 2> /dev/null | grep -v -E "Array\.prototype\.includes|Promise\.prototype\.finally|String\.prototype\.(matchAll|trimRight)|globalThis" | grep -Po "(?<=error).*?(?=\s+ecmascript)" | wc -l); test $result -eq 0

View File

@ -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. -->

View File

@ -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);
}

View File

@ -1,4 +1,4 @@
<ion-item-divider>
<ion-item-divider sticky="true">
<ion-label>
<h2>{{ 'addon.block_recentlyaccessedcourses.pluginname' | translate }}</h2>
</ion-label>

View File

@ -1,4 +1,4 @@
<ion-item-divider>
<ion-item-divider sticky="true">
<ion-label>
<h2>{{ 'addon.block_sitemainmenu.pluginname' | translate }}</h2>
</ion-label>

View File

@ -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"

View File

@ -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) {

View File

@ -5,7 +5,7 @@
</ion-buttons>
<ion-title>{{ 'addon.calendar.calendarevents' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="openFilter($event)" [attr.aria-label]="'core.filter' | translate">
<ion-button fill="clear" (click)="openFilter($event)" [attr.aria-label]="'core.filter' | translate">
<ion-icon slot="icon-only" name="fas-filter" aria-hidden="true"></ion-icon>
</ion-button>
<core-context-menu>

View File

@ -34,7 +34,7 @@ import { CoreCategoryData, CoreCourses, CoreEnrolledCourseData } from '@features
import { CoreCoursesHelper } from '@features/courses/services/courses-helper';
import { AddonCalendarFilterPopoverComponent } from '../../components/filter/filter';
import moment from 'moment';
import { Network, NgZone, PopoverController } from '@singletons';
import { Network, NgZone } from '@singletons';
import { CoreNavigator } from '@services/navigator';
import { Params } from '@angular/router';
import { Subscription } from 'rxjs';
@ -535,7 +535,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
* @param event Event.
*/
async openFilter(event: MouseEvent): Promise<void> {
const popover = await PopoverController.create({
await CoreDomUtils.openPopover({
component: AddonCalendarFilterPopoverComponent,
componentProps: {
courses: this.courses,
@ -543,7 +543,6 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
},
event,
});
await popover.present();
}
/**

View File

@ -126,8 +126,8 @@
<ion-icon *ngIf="!advanced" name="fas-caret-right" slot="start" aria-hidden="true"></ion-icon>
<ion-icon *ngIf="advanced" name="fas-caret-down" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<span *ngIf="!advanced">{{ 'core.showmore' | translate }}</span>
<span *ngIf="advanced">{{ 'core.showless' | translate }}</span>
<h2 *ngIf="!advanced">{{ 'core.showmore' | translate }}</h2>
<h2 *ngIf="advanced">{{ 'core.showless' | translate }}</h2>
</ion-label>
</ion-item-divider>

View File

@ -5,7 +5,7 @@
</ion-buttons>
<ion-title>{{ (showCalendar ? 'addon.calendar.calendarevents' : 'addon.calendar.upcomingevents') | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="openFilter($event)" [attr.aria-label]="'core.filter' | translate">
<ion-button fill="clear" (click)="openFilter($event)" [attr.aria-label]="'core.filter' | translate">
<ion-icon slot="icon-only" name="fas-filter" aria-hidden="true"></ion-icon>
</ion-button>
<core-context-menu>

View File

@ -23,7 +23,7 @@ import { AddonCalendar, AddonCalendarProvider } from '../../services/calendar';
import { AddonCalendarOffline } from '../../services/calendar-offline';
import { AddonCalendarSync, AddonCalendarSyncProvider } from '../../services/calendar-sync';
import { AddonCalendarFilter, AddonCalendarHelper } from '../../services/calendar-helper';
import { Network, NgZone, PopoverController } from '@singletons';
import { Network, NgZone } from '@singletons';
import { Subscription } from 'rxjs';
import { CoreEnrolledCourseData } from '@features/courses/services/courses';
import { ActivatedRoute, Params } from '@angular/router';
@ -333,7 +333,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
* @param event Event.
*/
async openFilter(event: MouseEvent): Promise<void> {
const popover = await PopoverController.create({
await CoreDomUtils.openPopover({
component: AddonCalendarFilterPopoverComponent,
componentProps: {
courses: this.courses,
@ -341,7 +341,6 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
},
event,
});
await popover.present();
}
/**

View File

@ -5,7 +5,7 @@
</ion-buttons>
<ion-title>{{ 'addon.calendar.calendarevents' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="openFilter($event)" [attr.aria-label]="'core.filter' | translate">
<ion-button fill="clear" (click)="openFilter($event)" [attr.aria-label]="'core.filter' | translate">
<ion-icon slot="icon-only" name="fas-filter" aria-hidden="true"></ion-icon>
</ion-button>
<core-context-menu>
@ -40,7 +40,7 @@
<ion-list *ngIf="filteredEvents && filteredEvents.length" class="ion-no-margin">
<ng-container *ngFor="let event of filteredEvents">
<ion-item-divider *ngIf="event.showDate">
<ion-label>{{ event.timestart * 1000 | coreFormatDate: "strftimedayshort" }}</ion-label>
<ion-label><h2>{{ event.timestart * 1000 | coreFormatDate: "strftimedayshort" }}</h2></ion-label>
</ion-item-divider>
<ion-item class="addon-calendar-event ion-text-wrap" [attr.aria-label]="event.name" (click)="gotoEvent(event.id)"
[attr.aria-current]="event.id == eventId ? 'page' : 'false'"

View File

@ -35,7 +35,7 @@ import { CoreConstants } from '@/core/constants';
import { AddonCalendarFilterPopoverComponent } from '../../components/filter/filter';
import { Params } from '@angular/router';
import { Subscription } from 'rxjs';
import { Network, NgZone, PopoverController } from '@singletons';
import { Network, NgZone } from '@singletons';
import { CoreCoursesHelper } from '@features/courses/services/courses-helper';
import { CoreUtils } from '@services/utils/utils';
import { CoreNavigator } from '@services/navigator';
@ -616,7 +616,7 @@ export class AddonCalendarListPage implements OnInit, OnDestroy {
* @param event Event.
*/
async openFilter(event: MouseEvent): Promise<void> {
const popover = await PopoverController.create({
await CoreDomUtils.openPopover({
component: AddonCalendarFilterPopoverComponent,
componentProps: {
courses: this.courses,
@ -624,7 +624,6 @@ export class AddonCalendarListPage implements OnInit, OnDestroy {
},
event,
});
await popover.present();
}
/**

View File

@ -28,7 +28,7 @@
</ion-card>
<ion-card *ngIf="completion && tracked">
<ion-item-divider>
<ion-label>{{ 'addon.coursecompletion.requiredcriteria' | translate }}</ion-label>
<ion-label><h2>{{ 'addon.coursecompletion.requiredcriteria' | translate }}</h2></ion-label>
</ion-item-divider>
<ion-item class="ion-hide-md-up ion-text-wrap" *ngFor="let criteria of completion.completions">
<ion-label>
@ -71,7 +71,7 @@
</ion-card>
<ion-card *ngIf="showSelfComplete && tracked">
<ion-item-divider>
<ion-label>{{ 'addon.coursecompletion.manualselfcompletion' | translate }}</ion-label>
<ion-label><h2>{{ 'addon.coursecompletion.manualselfcompletion' | translate }}</h2></ion-label>
</ion-item-divider>
<ion-item>
<ion-label>

View File

@ -2,7 +2,7 @@
<ion-toolbar>
<ion-title>{{ 'addon.messages.groupinfo' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<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>
</ion-button>
</ion-buttons>

View File

@ -5,7 +5,7 @@
</ion-buttons>
<ion-title>{{ 'addon.messages.contacts' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="gotoSearch()" [attr.aria-label]="'addon.messages.searchcombined' | translate">
<ion-button fill="clear" (click)="gotoSearch()" [attr.aria-label]="'addon.messages.searchcombined' | translate">
<ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
<!-- Add an empty context menu so discussion page can add items in split view, otherwise the menu

View File

@ -93,7 +93,8 @@
[@coreSlideInOut]="message.useridfrom == currentUserId ? '' : 'fromLeft'">
<ion-label>
<!-- User data. -->
<h2 class="addon-message-user">
<h2 class="addon-message-user" [attr.aria-label]="message.useridfrom == currentUserId ?
('addon.messages.you' | translate) : members[message.useridfrom].fullname">
<core-user-avatar slot="start" [user]="members[message.useridfrom]" [linkProfile]="false"
*ngIf="message.showUserData"></core-user-avatar>
@ -101,7 +102,7 @@
<ion-note *ngIf="!message.pending">{{ message.timecreated | coreFormatDate: "strftimetime" }}</ion-note>
<ion-note *ngIf="message.pending">
<ion-icon name="fas-clock" [attr.aria-label]="'core.notsent' | translate "></ion-icon>
<ion-icon name="fas-clock" [attr.aria-label]="'core.notsent' | translate" role="status"></ion-icon>
</ion-note>
</h2>

View File

@ -39,7 +39,7 @@ import { Md5 } from 'ts-md5/dist/md5';
import moment from 'moment';
import { CoreAnimations } from '@components/animations';
import { CoreError } from '@classes/errors/error';
import { ModalController, Translate } from '@singletons';
import { Translate } from '@singletons';
import { CoreNavigator } from '@services/navigator';
import { CoreIonLoadingElement } from '@classes/ion-loading';
import { ActivatedRoute } from '@angular/router';
@ -1302,18 +1302,14 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
async viewInfo(): Promise<void> {
if (this.isGroup) {
// Display the group information.
const modal = await ModalController.create({
const userId = await CoreDomUtils.openModal<number>({
component: AddonMessagesConversationInfoComponent,
componentProps: {
conversationId: this.conversationId,
},
});
await modal.present();
const result = await modal.onDidDismiss();
if (typeof result.data != 'undefined') {
if (typeof userId != 'undefined') {
const splitViewLoaded = CoreNavigator.isCurrentPathInTablet('**/messages/**/discussion');
// Open user conversation.
@ -1321,12 +1317,12 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
// Notify the left pane to load it, this way the right conversation will be highlighted.
CoreEvents.trigger(
AddonMessagesProvider.OPEN_CONVERSATION_EVENT,
{ userId: result.data },
{ userId },
this.siteId,
);
} else {
// Open the discussion in a new view.
CoreNavigator.navigateToSitePath('/messages/discussion', { params: { userId: result.data.userId } });
CoreNavigator.navigateToSitePath('/messages/discussion', { params: { userId } });
}
}
} else {

View File

@ -20,11 +20,11 @@
}
.addon-messages-unreadfrom {
color: var(--core-color);
color: var(--ion-color-primary);
background-color: transparent;
margin-top: 6px;
ion-icon {
color: var(--core-color);
color: var(--ion-color-primary);
background-color: transparent;
}
}

View File

@ -5,7 +5,7 @@
</ion-buttons>
<ion-title>{{ 'addon.messages.messages' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="gotoSearch()" [attr.aria-label]="'addon.messages.searchcombined' | translate">
<ion-button fill="clear" (click)="gotoSearch()" [attr.aria-label]="'addon.messages.searchcombined' | translate">
<ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
<ion-button (click)="gotoSettings()" [attr.aria-label]="'addon.messages.messagepreferences' | translate">
@ -37,7 +37,7 @@
[attr.aria-expanded]="favourites.expanded" role="heading button">
<ion-icon *ngIf="!favourites.expanded" name="fas-caret-right" slot="start" aria-hidden="true"></ion-icon>
<ion-icon *ngIf="favourites.expanded" name="fas-caret-down" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'core.favourites' | translate }} ({{ favourites.count }})</ion-label>
<ion-label><h2>{{ 'core.favourites' | translate }} ({{ favourites.count }})</h2></ion-label>
<ion-badge slot="end" *ngIf="favourites.unread">{{ favourites.unread }}</ion-badge>
</ion-item-divider>
<div [hidden]="!favourites.conversations || !favourites.expanded || favourites.loading" #favlist>
@ -60,7 +60,7 @@
[attr.aria-expanded]="group.expanded" role="heading button">
<ion-icon *ngIf="!group.expanded" name="fas-caret-right" slot="start" aria-hidden="true"></ion-icon>
<ion-icon *ngIf="group.expanded" name="fas-caret-down" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'addon.messages.groupconversations' | translate }} ({{ group.count }})</ion-label>
<ion-label><h2>{{ 'addon.messages.groupconversations' | translate }} ({{ group.count }})</h2></ion-label>
<ion-badge slot="end" *ngIf="group.unread">{{ group.unread }}</ion-badge>
</ion-item-divider>
<div [hidden]="!group.conversations || !group.expanded || group.loading" #grouplist>
@ -82,7 +82,9 @@
[attr.aria-expanded]="individual.expanded" role="heading button">
<ion-icon *ngIf="!individual.expanded" name="fas-caret-right" slot="start" aria-hidden="true"></ion-icon>
<ion-icon *ngIf="individual.expanded" name="fas-caret-down" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'addon.messages.individualconversations' | translate }} ({{ individual.count }})</ion-label>
<ion-label>
<h2>{{ 'addon.messages.individualconversations' | translate }} ({{ individual.count }})</h2>
</ion-label>
<ion-badge slot="end" *ngIf="individual.unread">{{ individual.unread }}</ion-badge>
</ion-item-divider>
<div [hidden]="!individual.conversations || !individual.expanded || individual.loading" #indlist>

View File

@ -38,7 +38,7 @@
<ng-template #resultsTemplate let-item="item">
<ng-container *ngIf="item.results.length > 0">
<ion-item-divider class="ion-text-wrap">
<ion-label>{{ item.titleString | translate }}</ion-label>
<ion-label><h2>{{ item.titleString | translate }}</h2></ion-label>
</ion-item-divider>
<!-- List of results -->

View File

@ -15,7 +15,7 @@
import { Component, Input } from '@angular/core';
import { CoreCanceledError } from '@classes/errors/cancelederror';
import { CoreError } from '@classes/errors/error';
import { ModalController } from '@singletons';
import { CoreDomUtils } from '@services/utils/dom';
import { AddonModAssignEditFeedbackModalComponent } from '../components/edit-feedback-modal/edit-feedback-modal';
import { AddonModAssignFeedbackCommentsTextData } from '../feedback/comments/services/handler';
import { AddonModAssignAssign, AddonModAssignPlugin, AddonModAssignSubmission } from '../services/assign';
@ -47,7 +47,7 @@ export class AddonModAssignFeedbackPluginBaseComponent {
}
// Create the navigation modal.
const modal = await ModalController.create({
const modalData = await CoreDomUtils.openModal<AddonModAssignFeedbackCommentsTextData>({
component: AddonModAssignEditFeedbackModalComponent,
componentProps: {
assign: this.assign,
@ -57,15 +57,11 @@ export class AddonModAssignFeedbackPluginBaseComponent {
},
});
await modal.present();
const result = await modal.onDidDismiss();
if (typeof result.data == 'undefined') {
if (typeof modalData == 'undefined') {
throw new CoreCanceledError(); // User cancelled.
} else {
return result.data;
}
return modalData;
}
/**

View File

@ -2,7 +2,7 @@
<ion-toolbar>
<ion-title>{{ plugin.name }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<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>
</ion-button>
</ion-buttons>

View File

@ -28,7 +28,7 @@ import {
import { CoreTag, CoreTagItem } from '@features/tag/services/tag';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { ModalController, Translate } from '@singletons';
import { Translate } from '@singletons';
import { CoreUtils } from '@services/utils/utils';
import { CoreCourse } from '@features/course/services/course';
import { AddonModBookTocComponent } from '../toc/toc';
@ -84,7 +84,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
*/
async showToc(): Promise<void> {
// Create the toc modal.
const modal = await ModalController.create({
const modalData = await CoreDomUtils.openSideModal<number>({
component: AddonModBookTocComponent,
componentProps: {
moduleId: this.module.id,
@ -93,19 +93,10 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
courseId: this.courseId,
book: this.book,
},
cssClass: 'core-modal-lateral',
showBackdrop: true,
backdropDismiss: true,
// @todo enterAnimation: 'core-modal-lateral-transition',
// @todo leaveAnimation: 'core-modal-lateral-transition',
});
await modal.present();
const result = await modal.onDidDismiss();
if (result.data) {
this.changeChapter(result.data);
if (modalData) {
this.changeChapter(modalData);
}
}

View File

@ -2,7 +2,7 @@
<ion-toolbar>
<ion-title>{{ 'addon.mod_book.toc' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<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>
</ion-button>
</ion-buttons>

View File

@ -5,7 +5,7 @@
</ion-buttons>
<ion-title>{{ 'addon.mod_chat.currentusers' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<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>
</ion-button>
</ion-buttons>

View File

@ -8,7 +8,7 @@
</core-format-text>
</ion-title>
<ion-buttons slot="end">
<ion-button *ngIf="loaded" (click)="showChatUsers()" [attr.aria-label]="'core.users' | translate">
<ion-button fill="clear" *ngIf="loaded" (click)="showChatUsers()" [attr.aria-label]="'core.users' | translate">
<ion-icon name="fas-users" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-buttons>

View File

@ -21,7 +21,7 @@ import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { ModalController, Network, NgZone } from '@singletons';
import { Network, NgZone } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { Subscription } from 'rxjs';
import { AddonModChatUsersModalComponent, AddonModChatUsersModalResult } from '../../components/users-modal/users-modal';
@ -178,32 +178,23 @@ export class AddonModChatChatPage implements OnInit, OnDestroy {
*/
async showChatUsers(): Promise<void> {
// Create the toc modal.
const modal = await ModalController.create({
const modalData = await CoreDomUtils.openSideModal<AddonModChatUsersModalResult>({
component: AddonModChatUsersModalComponent,
componentProps: {
sessionId: this.sessionId,
cmId: this.cmId,
},
cssClass: 'core-modal-lateral',
showBackdrop: true,
backdropDismiss: true,
// @todo enterAnimation: 'core-modal-lateral-transition',
// @todo leaveAnimation: 'core-modal-lateral-transition',
});
await modal.present();
const result = await modal.onDidDismiss<AddonModChatUsersModalResult>();
if (result.data) {
if (result.data.talkTo) {
this.newMessage = `To ${result.data.talkTo}: ` + (this.sendMessageForm?.message || '');
if (modalData) {
if (modalData.talkTo) {
this.newMessage = `To ${modalData.talkTo}: ` + (this.sendMessageForm?.message || '');
}
if (result.data.beepTo) {
this.sendMessage('', result.data.beepTo);
if (modalData.beepTo) {
this.sendMessage('', modalData.beepTo);
}
this.users = result.data.users;
this.users = modalData.users;
}
}

View File

@ -29,7 +29,6 @@ import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils';
import { ModalController } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import {
AddonModDataProvider,
@ -376,7 +375,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
* Display the chat users modal.
*/
async showSearch(): Promise<void> {
const modal = await ModalController.create({
const modalData = await CoreDomUtils.openModal<AddonModDataSearchDataParams>({
component: AddonModDataSearchComponent,
componentProps: {
search: this.search,
@ -385,12 +384,9 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
},
});
await modal.present();
const result = await modal.onDidDismiss();
// Add data to search object.
if (result.data) {
this.search = result.data;
if (modalData) {
this.search = modalData;
this.searchEntries(0);
}
}

View File

@ -5,7 +5,7 @@
</ion-buttons>
<ion-title>{{ 'addon.mod_data.search' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<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>
</ion-button>
</ion-buttons>

View File

@ -41,7 +41,7 @@
}
&.has-focus {
--input-border-color: var(--core-color);
--input-border-color: var(--ion-color-primary);
}
&.has-focus.ion-valid {
--input-border-color: var(--success);

View File

@ -26,7 +26,7 @@
</ion-item>
<ion-item-divider>
<ion-label>{{ 'addon.mod_feedback.non_respondents_students' | translate : {$a: total } }}</ion-label>
<ion-label><h2>{{ 'addon.mod_feedback.non_respondents_students' | translate : {$a: total } }}</h2></ion-label>
</ion-item-divider>
<ng-container *ngIf="total > 0">
<ion-item *ngFor="let user of users" class="ion-text-wrap">

View File

@ -29,7 +29,7 @@
<ng-container *ngIf="responses.responses.total > 0">
<ion-item-divider>
<ion-label>
{{ 'addon.mod_feedback.non_anonymous_entries' | translate : {$a: responses.responses.total } }}
<h2>{{ 'addon.mod_feedback.non_anonymous_entries' | translate : {$a: responses.responses.total } }}</h2>
</ion-label>
</ion-item-divider>
<ion-item *ngFor="let attempt of responses.responses.attempts" class="ion-text-wrap" button detail="true"
@ -54,7 +54,7 @@
<ng-container *ngIf="responses.anonResponses.total > 0">
<ion-item-divider>
<ion-label>
{{ 'addon.mod_feedback.anonymous_entries' |translate : {$a: responses.anonResponses.total } }}
<h2>{{ 'addon.mod_feedback.anonymous_entries' |translate : {$a: responses.anonResponses.total } }}</h2>
</ion-label>
</ion-item-divider>
<ion-item *ngFor="let attempt of responses.anonResponses.attempts" class="ion-text-wrap" button detail="true"

View File

@ -2,7 +2,7 @@
<ion-toolbar>
<ion-title>{{ 'addon.mod_forum.yourreply' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<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>
</ion-button>
</ion-buttons>
@ -29,7 +29,7 @@
[attr.aria-expanded]="advanced" [attr.aria-label]="(advanced ? 'core.hideadvanced' : 'core.showadvanced') | translate">
<ion-icon *ngIf="!advanced" name="fa-caret-right" slot="start" aria-hidden="true"></ion-icon>
<ion-icon *ngIf="advanced" name="fa-caret-down" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'addon.mod_forum.advanced' | translate }}</ion-label>
<ion-label><h2>{{ 'addon.mod_forum.advanced' | translate }}</h2></ion-label>
</ion-item-divider>
<ng-container *ngIf="advanced">
<core-attachments *ngIf="forum.id && forum.maxattachments > 0"

View File

@ -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"

View File

@ -2,22 +2,8 @@
: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);
color: var(--core-star-color);
}
.addon-mod-forum-discussion.item {

View File

@ -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 { 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.
*
@ -660,7 +667,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
* @param discussion Discussion.
*/
async showOptionsMenu(event: Event, discussion: AddonModForumDiscussion): Promise<void> {
const popover = await PopoverController.create({
const popoverData = await CoreDomUtils.openPopover<{ action?: string; value: boolean }>({
component: AddonModForumDiscussionOptionsMenuComponent,
componentProps: {
discussion,
@ -670,20 +677,16 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
event,
});
await popover.present();
const result = await popover.onDidDismiss<{ action?: string; value: boolean }>();
if (result.data && result.data.action) {
switch (result.data.action) {
if (popoverData && popoverData.action) {
switch (popoverData.action) {
case 'lock':
discussion.locked = result.data.value;
discussion.locked = popoverData.value;
break;
case 'pin':
discussion.pinned = result.data.value;
discussion.pinned = popoverData.value;
break;
case 'star':
discussion.starred = result.data.value;
discussion.starred = popoverData.value;
break;
default:
break;

View File

@ -127,7 +127,7 @@
<ion-label>
<ion-icon *ngIf="!advanced" name="fa-caret-right" slot="start" aria-hidden="true"></ion-icon>
<ion-icon *ngIf="advanced" name="fa-caret-down" slot="start" aria-hidden="true"></ion-icon>
{{ 'addon.mod_forum.advanced' | translate }}
<h2>{{ 'addon.mod_forum.advanced' | translate }}</h2>
</ion-label>
</ion-item-divider>
<ng-container *ngIf="advanced">

View File

@ -5,7 +5,7 @@
border-bottom: 1px solid var(--addon-forum-border-color);
.addon-forum-star {
color: var(--core-color);
color: var(--core-star-color);
}
ion-card-header .item {

View File

@ -40,7 +40,7 @@ import {
AddonModForumUpdateDiscussionPostWSOptionsObject,
} from '../../services/forum';
import { CoreTag } from '@features/tag/services/tag';
import { ModalController, PopoverController, Translate } from '@singletons';
import { Translate } from '@singletons';
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
import { IonContent } from '@ionic/angular';
import { AddonModForumSync } from '../../services/forum-sync';
@ -218,7 +218,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
* @param event Click Event.
*/
async showOptionsMenu(event: Event): Promise<void> {
const popover = await PopoverController.create({
const popoverData = await CoreDomUtils.openPopover<{ action?: string }>({
component: AddonModForumPostOptionsMenuComponent,
componentProps: {
post: this.post,
@ -228,12 +228,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
event,
});
await popover.present();
const result = await popover.onDidDismiss<{ action?: string }>();
if (result.data && result.data.action) {
switch (result.data.action) {
if (popoverData && popoverData.action) {
switch (popoverData.action) {
case 'edit':
this.editPost();
break;
@ -254,7 +250,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
* Shows a form modal to edit an online post.
*/
async editPost(): Promise<void> {
const modal = await ModalController.create({
const modalData = await CoreDomUtils.openModal<AddonModForumReply>({
component: AddonModForumEditPostComponent,
componentProps: {
post: this.post,
@ -265,18 +261,13 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
backdropDismiss: false,
});
await modal.present();
const result = await modal.onDidDismiss<AddonModForumReply>();
const data = result.data;
if (!data) {
if (!modalData) {
return;
}
// Add some HTML to the message if needed.
const message = CoreTextUtils.formatHtmlLines(data.message!);
const files = data.files;
const message = CoreTextUtils.formatHtmlLines(modalData.message!);
const files = modalData.files;
const options: AddonModForumUpdateDiscussionPostWSOptionsObject = {};
const sendingModal = await CoreDomUtils.showModalLoading('core.sending', true);
@ -295,16 +286,16 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
}
// Try to send it to server.
const sent = await AddonModForum.updatePost(this.post.id, data.subject!, message, options);
const sent = await AddonModForum.updatePost(this.post.id, modalData.subject!, message, options);
if (sent && this.forum.id) {
// Data sent to server, delete stored files (if any).
AddonModForumHelper.deleteReplyStoredFiles(this.forum.id, this.post.id);
this.onPostChange.emit();
this.post.subject = data.subject!;
this.post.subject = modalData.subject!;
this.post.message = message;
this.post.attachments = data.files;
this.post.attachments = modalData.files;
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.couldnotupdate', true);

View File

@ -1,15 +1,15 @@
<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 (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<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>
</ion-button>
</ion-buttons>
</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"

View File

@ -35,7 +35,7 @@
[attr.aria-label]="(advanced ? 'core.hideadvanced' : 'core.showadvanced') |translate" role="heading button">
<ion-icon *ngIf="!advanced" name="fa-caret-right" slot="start" aria-hidden="true"></ion-icon>
<ion-icon *ngIf="advanced" name="fa-caret-down" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'addon.mod_forum.advanced' | translate }}</ion-label>
<ion-label><h2>{{ 'addon.mod_forum.advanced' | translate }}</h2></ion-label>
</ion-item-divider>
<ng-container *ngIf="advanced">
<ion-item *ngIf="showGroups && groupIds.length > 1 && accessInfo.cancanposttomygroups">

View File

@ -64,7 +64,7 @@
<ion-list *ngIf="!isSearch && entries.offlineEntries.length > 0">
<ion-item-divider>
<ion-label>{{ 'addon.mod_glossary.entriestobesynced' | translate }}</ion-label>
<ion-label><h2>{{ 'addon.mod_glossary.entriestobesynced' | translate }}</h2></ion-label>
</ion-item-divider>
<ion-item *ngFor="let entry of entries.offlineEntries" (click)="entries.select(entry)" detail="false" button
[attr.aria-current]="entries.getItemAriaCurrent(entry)">
@ -79,7 +79,7 @@
<ion-list *ngIf="entries.onlineEntries.length > 0">
<ng-container *ngFor="let entry of entries.onlineEntries; let index = index">
<ion-item-divider *ngIf="getDivider && showDivider(entry, entries.onlineEntries[index - 1])">
{{ getDivider!(entry) }}
<ion-label><h2>{{ getDivider!(entry) }}</h2></ion-label>
</ion-item-divider>
<ion-item button (click)="entries.select(entry)" [attr.aria-current]="entries.getItemAriaCurrent(entry)"

View File

@ -27,7 +27,7 @@ import { IonContent } from '@ionic/angular';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { PopoverController, Translate } from '@singletons';
import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import {
AddonModGlossary,
@ -405,7 +405,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
* @param event Event.
*/
async openModePicker(event: MouseEvent): Promise<void> {
const popover = await PopoverController.create({
const mode = await CoreDomUtils.openPopover<AddonModGlossaryFetchMode>({
component: AddonModGlossaryModePickerPopoverComponent,
componentProps: {
browseModes: this.glossary!.browsemodes,
@ -414,11 +414,6 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
event,
});
await popover.present();
const result = await popover.onDidDismiss<AddonModGlossaryFetchMode>();
const mode = result.data;
if (mode) {
if (mode !== this.fetchMode) {
this.changeFetchMode(mode);

View File

@ -46,14 +46,14 @@
</ion-textarea>
</ion-item>
<ion-item-divider>
<ion-label>{{ 'addon.mod_glossary.attachment' | translate }}</ion-label>
<ion-label><h2>{{ 'addon.mod_glossary.attachment' | translate }}</h2></ion-label>
</ion-item-divider>
<core-attachments [files]="attachments" [component]="component" [componentId]="glossary.coursemodule"
[allowOffline]="true">
</core-attachments>
<ng-container *ngIf="glossary.usedynalink">
<ion-item-divider>
<ion-label>{{ 'addon.mod_glossary.linking' | translate }}</ion-label>
<ion-label><h2>{{ 'addon.mod_glossary.linking' | translate }}</h2></ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap">
<ion-label>{{ 'addon.mod_glossary.entryusedynalink' | translate }}</ion-label>

View File

@ -21,7 +21,6 @@ import {
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { CoreDomUtils } from '@services/utils/dom';
import { ModalController } from '@singletons';
import { AddonModImscpProvider, AddonModImscp, AddonModImscpTocItem } from '../../services/imscp';
import { AddonModImscpTocComponent } from '../toc/toc';
@ -150,25 +149,16 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
*/
async showToc(): Promise<void> {
// Create the toc modal.
const modal = await ModalController.create({
const modalData = await CoreDomUtils.openSideModal<string>({
component: AddonModImscpTocComponent,
componentProps: {
items: this.items,
selected: this.currentItem,
},
cssClass: 'core-modal-lateral',
showBackdrop: true,
backdropDismiss: true,
// @todo enterAnimation: 'core-modal-lateral-transition',
// @todo leaveAnimation: 'core-modal-lateral-transition',
});
await modal.present();
const result = await modal.onDidDismiss();
if (result.data) {
this.loadItem(result.data);
if (modalData) {
this.loadItem(modalData);
}
}

View File

@ -2,7 +2,7 @@
<ion-toolbar>
<ion-title>{{ 'addon.mod_imscp.toc' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<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>
</ion-button>
</ion-buttons>

View File

@ -3,7 +3,7 @@
<ion-title>{{ pageInstance?.lesson?.name }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<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>
</ion-button>
</ion-buttons>

View File

@ -3,7 +3,7 @@
<ion-title>{{ 'core.login.password' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<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>
</ion-button>
</ion-buttons>

View File

@ -9,7 +9,7 @@
</core-format-text>
</ion-title>
<ion-buttons slot="end">
<ion-button *ngIf="displayMenu || mediaFile" [attr.aria-label]="'addon.mod_lesson.lessonmenu' | translate"
<ion-button fill="clear" *ngIf="displayMenu || mediaFile" [attr.aria-label]="'addon.mod_lesson.lessonmenu' | translate"
(click)="showMenu()">
<ion-icon name="fas-bookmark" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
@ -57,9 +57,13 @@
(ngSubmit)="submitQuestion($event)">
<ion-item-divider class="ion-text-wrap" *ngIf="pageContent">
<core-format-text [component]="component" [componentId]="lesson?.coursemodule" [text]="pageContent"
contextLevel="module" [contextInstanceId]="lesson.coursemodule" [courseId]="courseId">
</core-format-text>
<ion-label>
<h2>
<core-format-text [component]="component" [componentId]="lesson?.coursemodule" [text]="pageContent"
contextLevel="module" [contextInstanceId]="lesson.coursemodule" [courseId]="courseId">
</core-format-text>
</h2>
</ion-label>
</ion-item-divider>
<!-- Render a different input depending on the type of the question. -->

View File

@ -762,22 +762,13 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
async showMenu(): Promise<void> {
this.menuShown = true;
const menuModal = await ModalController.create({
await CoreDomUtils.openSideModal({
component: AddonModLessonMenuModalPage,
componentProps: {
pageInstance: this,
},
cssClass: 'core-modal-lateral',
showBackdrop: true,
backdropDismiss: true,
// @todo enterAnimation: 'core-modal-lateral-transition',
// leaveAnimation: 'core-modal-lateral-transition',
});
await menuModal.present();
await menuModal.onWillDismiss();
this.menuShown = false;
}

View File

@ -22,9 +22,10 @@ import { CoreFilepool } from '@services/filepool';
import { CoreGroups } from '@services/groups';
import { CoreFileSizeSum, CorePluginFileDelegate } from '@services/plugin-file-delegate';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSFile } from '@services/ws';
import { makeSingleton, ModalController, Translate } from '@singletons';
import { makeSingleton, Translate } from '@singletons';
import { AddonModLessonPasswordModalComponent } from '../../components/password-modal/password-modal';
import {
AddonModLesson,
@ -54,19 +55,15 @@ export class AddonModLessonPrefetchHandlerService extends CoreCourseActivityPref
*/
protected async askUserPassword(): Promise<string> {
// Create and show the modal.
const modal = await ModalController.create({
const modalData = await CoreDomUtils.openModal<string>({
component: AddonModLessonPasswordModalComponent,
});
await modal.present();
const result = await modal.onWillDismiss();
if (typeof result.data != 'string') {
if (typeof modalData != 'string') {
throw new CoreCanceledError();
}
return result.data;
return modalData;
}
/**

View File

@ -16,10 +16,10 @@ import { BehaviorSubject } from 'rxjs';
import { CoreQuestionHelper } from '@features/question/services/question-helper';
import { CoreQuestionsAnswers } from '@features/question/services/question';
import { PopoverController } from '@singletons';
import { CoreLogger } from '@singletons/logger';
import { AddonModQuizConnectionErrorComponent } from '../components/connection-error/connection-error';
import { AddonModQuiz, AddonModQuizAttemptWSData, AddonModQuizQuizWSData } from '../services/quiz';
import { CoreDomUtils } from '@services/utils/dom';
/**
* Class to support auto-save in quiz. Every certain seconds, it will check if there are changes in the current page answers
@ -197,13 +197,10 @@ export class AddonModQuizAutoSave {
};
this.popoverShown = true;
this.popover = await PopoverController.create({
this.popover = await CoreDomUtils.openPopover({
component: AddonModQuizConnectionErrorComponent,
event: <Event> event,
});
await this.popover.present();
await this.popover.onDidDismiss();
this.popoverShown = false;
}

View File

@ -3,7 +3,7 @@
<ion-title>{{ 'addon.mod_quiz.quiznavigation' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<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>
</ion-button>
</ion-buttons>

View File

@ -50,7 +50,7 @@ export class AddonModQuizNavigationModalComponent {
* @param slot Slot of the question to scroll to.
*/
loadPage(page: number, slot?: number): void {
ModalController.dismiss({
ModalController.dismiss(<AddonModQuizNavigationModalReturn>{
action: AddonModQuizNavigationModalComponent.CHANGE_PAGE,
page,
slot,
@ -61,7 +61,7 @@ export class AddonModQuizNavigationModalComponent {
* Switch mode in review.
*/
switchMode(): void {
ModalController.dismiss({
ModalController.dismiss(<AddonModQuizNavigationModalReturn>{
action: AddonModQuizNavigationModalComponent.SWITCH_MODE,
});
}
@ -74,3 +74,9 @@ export class AddonModQuizNavigationModalComponent {
export type AddonModQuizNavigationQuestion = CoreQuestionQuestionParsed & {
stateClass?: string;
};
export type AddonModQuizNavigationModalReturn = {
action: number;
page?: number;
slot?: number;
};

View File

@ -3,7 +3,7 @@
<ion-title>{{ title | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<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>
</ion-button>
</ion-buttons>

View File

@ -10,8 +10,9 @@
</ion-title>
<ion-buttons slot="end">
<ion-button id="addon-mod_quiz-connection-error-button" [hidden]="!autoSaveError" (click)="showConnectionError($event)"
[attr.aria-label]="'addon.mod_quiz.connectionerror' | translate" aria-haspopup="dialog">
<ion-button fill="clear" id="addon-mod_quiz-connection-error-button" [hidden]="!autoSaveError"
(click)="showConnectionError($event)" [attr.aria-label]="'addon.mod_quiz.connectionerror' | translate"
aria-haspopup="dialog">
<ion-icon name="fas-exclamation-circle" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
<ion-button *ngIf="navigation.length" [attr.aria-label]="'addon.mod_quiz.opentoc' | translate"
@ -30,7 +31,8 @@
</core-timer>
</ion-title>
<ion-buttons slot="end">
<ion-button *ngIf="previousPage >= 0" (click)="changePage(previousPage)" [attr.aria-label]="'core.previous' | translate">
<ion-button fill="clear" *ngIf="previousPage >= 0" (click)="changePage(previousPage)"
[attr.aria-label]="'core.previous' | translate">
<ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
<ion-button *ngIf="nextPage >= -1" (click)="changePage(nextPage)" [attr.aria-label]="'core.next' | translate">
@ -43,7 +45,7 @@
<!-- Navigation arrows if there's no timer. -->
<ion-toolbar *ngIf="!endTime && questions.length && !quizAborted && !showSummary" color="light">
<ion-buttons slot="end">
<ion-button *ngIf="previousPage >= 0" (click)="changePage(previousPage)"
<ion-button fill="clear" *ngIf="previousPage >= 0" (click)="changePage(previousPage)"
[attr.aria-label]="'core.previous' | translate">
<ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>

View File

@ -31,6 +31,7 @@ import { CoreEvents } from '@singletons/events';
import { AddonModQuizAutoSave } from '../../classes/auto-save';
import {
AddonModQuizNavigationModalComponent,
AddonModQuizNavigationModalReturn,
AddonModQuizNavigationQuestion,
} from '../../components/navigation-modal/navigation-modal';
import {
@ -581,7 +582,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
}
// Create the navigation modal.
const modal = await ModalController.create({
const modalData = await CoreDomUtils.openSideModal<AddonModQuizNavigationModalReturn>({
component: AddonModQuizNavigationModalComponent,
componentProps: {
navigation: this.navigation,
@ -589,19 +590,10 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
currentPage: this.attempt?.currentpage,
isReview: false,
},
cssClass: 'core-modal-lateral',
showBackdrop: true,
backdropDismiss: true,
// @todo enterAnimation: 'core-modal-lateral-transition',
// @todo leaveAnimation: 'core-modal-lateral-transition',
});
await modal.present();
const result = await modal.onWillDismiss();
if (result.data && result.data.action == AddonModQuizNavigationModalComponent.CHANGE_PAGE) {
this.changePage(result.data.page, true, result.data.slot);
if (modalData && modalData.action == AddonModQuizNavigationModalComponent.CHANGE_PAGE) {
this.changePage(modalData.page!, true, modalData.slot);
}
}

View File

@ -6,7 +6,7 @@
<ion-title>{{ 'addon.mod_quiz.review' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button *ngIf="navigation.length" [attr.aria-label]="'addon.mod_quiz.opentoc' | translate"
<ion-button fill="clear" *ngIf="navigation.length" [attr.aria-label]="'addon.mod_quiz.opentoc' | translate"
(click)="openNavigation()" aria-haspopup="true">
<ion-icon name="fas-bookmark" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>

View File

@ -21,9 +21,10 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils';
import { ModalController, Translate } from '@singletons';
import { Translate } from '@singletons';
import {
AddonModQuizNavigationModalComponent,
AddonModQuizNavigationModalReturn,
AddonModQuizNavigationQuestion,
} from '../../components/navigation-modal/navigation-modal';
import {
@ -325,7 +326,7 @@ export class AddonModQuizReviewPage implements OnInit {
async openNavigation(): Promise<void> {
// Create the navigation modal.
const modal = await ModalController.create({
const modalData = await CoreDomUtils.openSideModal<AddonModQuizNavigationModalReturn>({
component: AddonModQuizNavigationModalComponent,
componentProps: {
navigation: this.navigation,
@ -335,24 +336,15 @@ export class AddonModQuizReviewPage implements OnInit {
numPages: this.numPages,
showAll: this.showAll,
},
cssClass: 'core-modal-lateral',
showBackdrop: true,
backdropDismiss: true,
// @todo enterAnimation: 'core-modal-lateral-transition',
// @todo leaveAnimation: 'core-modal-lateral-transition',
});
await modal.present();
const result = await modal.onWillDismiss();
if (!result.data) {
if (!modalData) {
return;
}
if (result.data.action == AddonModQuizNavigationModalComponent.CHANGE_PAGE) {
this.changePage(result.data.page, true, result.data.slot);
} else if (result.data.action == AddonModQuizNavigationModalComponent.SWITCH_MODE) {
if (modalData.action == AddonModQuizNavigationModalComponent.CHANGE_PAGE) {
this.changePage(modalData.page!, true, modalData.slot);
} else if (modalData.action == AddonModQuizNavigationModalComponent.SWITCH_MODE) {
this.switchMode();
}
}

View File

@ -21,7 +21,7 @@ import { CoreNavigator } from '@services/navigator';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton, ModalController, Translate } from '@singletons';
import { makeSingleton, Translate } from '@singletons';
import { AddonModQuizPreflightModalComponent } from '../components/preflight-modal/preflight-modal';
import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate';
import { AddonModQuizModuleHandlerService } from './handlers/module';
@ -160,7 +160,7 @@ export class AddonModQuizHelperProvider {
}
// Create and show the modal.
const modal = await ModalController.create({
const modalData = await CoreDomUtils.openModal<Record<string, string>>({
component: AddonModQuizPreflightModalComponent,
componentProps: {
title: title,
@ -172,15 +172,11 @@ export class AddonModQuizHelperProvider {
},
});
await modal.present();
const result = await modal.onWillDismiss();
if (!result.data) {
if (!modalData) {
throw new CoreCanceledError();
}
return <Record<string, string>> result.data;
return modalData;
}
/**

View File

@ -2,7 +2,7 @@
<ion-toolbar>
<ion-title>{{ 'addon.mod_scorm.toc' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<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>
</ion-button>
</ion-buttons>

View File

@ -8,7 +8,7 @@
</core-format-text>
</ion-title>
<ion-buttons slot="end">
<ion-button *ngIf="showToc && !loadingToc && toc.length" (click)="openToc()"
<ion-button fill="clear" *ngIf="showToc && !loadingToc && toc.length" (click)="openToc()"
[attr.aria-label]="'addon.mod_scorm.toc' | translate" aria-haspopup="true">
<ion-icon name="fas-bookmark" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>

View File

@ -20,7 +20,6 @@ import { CoreSync } from '@services/sync';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils';
import { ModalController } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { AddonModScormDataModel12 } from '../../classes/data-model-12';
import { AddonModScormTocComponent } from '../../components/toc/toc';
@ -486,7 +485,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
* Show the TOC.
*/
async openToc(): Promise<void> {
const modal = await ModalController.create({
const modalData = await CoreDomUtils.openSideModal<AddonModScormScoWithData>({
component: AddonModScormTocComponent,
componentProps: {
toc: this.toc,
@ -497,19 +496,10 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
accessInfo: this.accessInfo,
mode: this.mode,
},
cssClass: 'core-modal-lateral',
showBackdrop: true,
backdropDismiss: true,
// @todo enterAnimation: 'core-modal-lateral-transition',
// leaveAnimation: 'core-modal-lateral-transition'
});
await modal.present();
const result = await modal.onDidDismiss();
if (result.data) {
this.loadSco(result.data);
if (modalData) {
this.loadSco(modalData);
}
}

View File

@ -27,7 +27,7 @@ import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { ModalController, PopoverController, Translate } from '@singletons';
import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { Md5 } from 'ts-md5';
import { AddonModWikiPageDBRecord } from '../../services/database/wiki';
@ -51,7 +51,7 @@ import {
AddonModWikiSyncWikiResult,
AddonModWikiSyncWikiSubwiki,
} from '../../services/wiki-sync';
import { AddonModWikiMapModalComponent } from '../map/map';
import { AddonModWikiMapModalComponent, AddonModWikiMapModalReturn } from '../map/map';
import { AddonModWikiSubwikiPickerComponent } from '../subwiki-picker/subwiki-picker';
/**
@ -576,7 +576,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
*/
async openMap(): Promise<void> {
// Create the toc modal.
const modal = await ModalController.create({
const modalData = await CoreDomUtils.openSideModal<AddonModWikiMapModalReturn>({
component: AddonModWikiMapModalComponent,
componentProps: {
pages: this.subwikiPages,
@ -585,23 +585,14 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
courseId: this.courseId,
selectedTitle: this.currentPageObj && this.currentPageObj.title,
},
cssClass: 'core-modal-lateral',
showBackdrop: true,
backdropDismiss: true,
// @todo enterAnimation: 'core-modal-lateral-transition',
// @todo leaveAnimation: 'core-modal-lateral-transition',
});
await modal.present();
const result = await modal.onDidDismiss();
if (result.data) {
if (result.data.type == 'home') {
if (modalData) {
if (modalData.home) {
// Go back to the initial page of the wiki.
CoreNavigator.navigateToSitePath(result.data.goto);
} else {
this.goToPage(result.data.goto);
CoreNavigator.navigateToSitePath(modalData.home);
} else if (modalData.page) {
this.goToPage(modalData.page);
}
}
@ -803,7 +794,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
* @param event Event.
*/
async showSubwikiPicker(event: MouseEvent): Promise<void> {
const popover = await PopoverController.create({
const popoverData = await CoreDomUtils.openPopover<AddonModWikiSubwiki>({
component: AddonModWikiSubwikiPickerComponent,
componentProps: {
subwikis: this.subwikiData.subwikis,
@ -812,12 +803,8 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
event,
});
await popover.present();
const result = await popover.onDidDismiss();
if (result.data) {
this.goToSubwiki(result.data.id, result.data.userid, result.data.groupid, result.data.canedit);
if (popoverData) {
this.goToSubwiki(popoverData.id, popoverData.userid, popoverData.groupid, popoverData.canedit);
}
}

View File

@ -2,7 +2,7 @@
<ion-toolbar>
<ion-title>{{ 'addon.mod_wiki.map' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="fas-times" aria-hidden="true"></ion-icon>
</ion-button>
</ion-buttons>
@ -18,7 +18,7 @@
</ion-item>
<ng-container *ngFor="let letter of map">
<ion-item-divider *ngIf="letter.label">
<ion-label>{{ letter.label }}</ion-label>
<ion-label><h2>{{ letter.label }}</h2></ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap" *ngFor="let page of letter.pages" (click)="goToPage(page)"
[attr.aria-current]="selectedTitle == page.title ? 'page' : 'false'" button>

View File

@ -47,14 +47,14 @@ export class AddonModWikiMapModalComponent implements OnInit {
* @param page Clicked page.
*/
goToPage(page: AddonModWikiSubwikiPage | AddonModWikiPageDBRecord): void {
ModalController.dismiss({ type: 'page', goto: page });
ModalController.dismiss(<AddonModWikiMapModalReturn>{ page });
}
/**
* Go back to the initial page of the wiki.
*/
goToWikiHome(): void {
ModalController.dismiss({ type: 'home', goto: this.homeView });
ModalController.dismiss(<AddonModWikiMapModalReturn>{ home: this.homeView });
}
/**
@ -103,3 +103,8 @@ type AddonModWikiPagesMapLetter = {
label: string;
pages: (AddonModWikiSubwikiPage | AddonModWikiPageDBRecord)[];
};
export type AddonModWikiMapModalReturn = {
page?: AddonModWikiSubwikiPage | AddonModWikiPageDBRecord;
home?: string;
};

View File

@ -1,7 +1,7 @@
<ion-list>
<ng-container *ngFor="let group of subwikis">
<ion-item-divider *ngIf="group.label">
<ion-label><strong>{{ group.label }}</strong></ion-label>
<ion-label><h2>{{ group.label }}</h2></ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap" *ngFor="let subwiki of group.subwikis" (click)="openSubwiki(subwiki)"
[attr.disabled]="!subwiki.canedit && subwiki.id <= 0"

View File

@ -20,8 +20,9 @@ import { CoreCourse } from '@features/course/services/course';
import { IonContent } from '@ionic/angular';
import { CoreGroupInfo, CoreGroups } from '@services/groups';
import { CoreNavigator } from '@services/navigator';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { ModalController, Platform } from '@singletons';
import { Platform } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { Subscription } from 'rxjs';
import { AddonModWorkshopModuleHandlerService } from '../../services/handlers/module';
@ -387,7 +388,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
*/
async viewPhaseInfo(): Promise<void> {
if (this.phases) {
const modal = await ModalController.create({
const modalData = await CoreDomUtils.openModal<boolean>({
component: AddonModWorkshopPhaseInfoComponent,
componentProps: {
phases: CoreUtils.objectToArray(this.phases),
@ -396,10 +397,8 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
showSubmit: this.showSubmit,
},
});
await modal.present();
const result = await modal.onDidDismiss();
if (result.data === true) {
if (modalData === true) {
this.gotoSubmit();
}
}

View File

@ -5,7 +5,7 @@
</ion-buttons>
<ion-title>{{ 'addon.mod_workshop.userplan' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<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>
</ion-button>
</ion-buttons>

View File

@ -2,7 +2,7 @@
<ion-toolbar>
<ion-title>{{ 'addon.notes.addnewnote' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<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>
</ion-button>
</ion-buttons>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { AddonNotes } from '@addons/notes/services/notes';
import { AddonNotes, AddonNotesPublishState } from '@addons/notes/services/notes';
import { Component, ViewChild, ElementRef, Input } from '@angular/core';
import { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites';
@ -32,7 +32,7 @@ export class AddonNotesAddComponent {
@Input() protected courseId!: number;
@Input() protected userId?: number;
@Input() type = 'personal';
@Input() type: AddonNotesPublishState = 'personal';
text = '';
processing = false;
@ -56,7 +56,7 @@ export class AddonNotesAddComponent {
CoreForms.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.getCurrentSiteId());
ModalController.dismiss({ type: this.type, sent: true }).finally(() => {
ModalController.dismiss(<AddonNotesAddModalReturn>{ type: this.type, sent: true }).finally(() => {
CoreDomUtils.showToast(sent ? 'addon.notes.eventnotecreated' : 'core.datastoredoffline', true, 3000);
});
} catch (error){
@ -73,7 +73,12 @@ export class AddonNotesAddComponent {
closeModal(): void {
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
ModalController.dismiss({ type: this.type });
ModalController.dismiss(<AddonNotesAddModalReturn>{ type: this.type });
}
}
export type AddonNotesAddModalReturn = {
type: AddonNotesPublishState;
sent?: boolean;
};

View File

@ -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>

View File

@ -13,8 +13,8 @@
// limitations under the License.
import { CoreConstants } from '@/core/constants';
import { AddonNotesAddComponent } from '@addons/notes/components/add/add-modal';
import { AddonNotes, AddonNotesNoteFormatted } from '@addons/notes/services/notes';
import { AddonNotesAddComponent, AddonNotesAddModalReturn } from '@addons/notes/components/add/add-modal';
import { AddonNotes, AddonNotesNoteFormatted, AddonNotesPublishState } from '@addons/notes/services/notes';
import { AddonNotesOffline } from '@addons/notes/services/notes-offline';
import { AddonNotesSync, AddonNotesSyncProvider } from '@addons/notes/services/notes-sync';
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
@ -26,7 +26,6 @@ import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { ModalController } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
/**
@ -43,7 +42,7 @@ export class AddonNotesListPage implements OnInit, OnDestroy {
courseId: number;
userId?: number;
type = 'course';
type: AddonNotesPublishState = 'course';
refreshIcon = CoreConstants.ICON_LOADING;
syncIcon = CoreConstants.ICON_LOADING;
notes: AddonNotesNoteFormatted[] = [];
@ -157,8 +156,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: AddonNotesPublishState): Promise<void> {
this.type = type;
this.notesLoaded = false;
this.refreshIcon = CoreConstants.ICON_LOADING;
this.syncIcon = CoreConstants.ICON_LOADING;
@ -176,7 +178,7 @@ export class AddonNotesListPage implements OnInit, OnDestroy {
e.preventDefault();
e.stopPropagation();
const modal = await ModalController.create({
const modalData = await CoreDomUtils.openModal<AddonNotesAddModalReturn>({
component: AddonNotesAddComponent,
componentProps: {
userId: this.userId,
@ -185,22 +187,17 @@ export class AddonNotesListPage implements OnInit, OnDestroy {
},
});
await modal.present();
if (typeof modalData != 'undefined') {
const result = await modal.onDidDismiss();
if (typeof result.data != 'undefined') {
if (result.data.sent && result.data.type) {
if (result.data.type != this.type) {
this.type = result.data.type;
if (modalData.sent && modalData.type) {
if (modalData.type != this.type) {
this.type = modalData.type;
this.notesLoaded = false;
}
this.refreshNotes(false);
} else if (result.data.type && result.data.type != this.type) {
this.type = result.data.type;
this.typeChanged();
} else if (modalData.type && modalData.type != this.type) {
this.typeChanged(modalData.type);
}
}
}

View File

@ -13,6 +13,7 @@
// limitations under the License.
import { CoreSiteSchema } from '@services/sites';
import { AddonNotesPublishState } from '../notes';
/**
* Database variables for AddonNotesOfflineProvider.
@ -83,7 +84,7 @@ export type AddonNotesDBRecord = {
content: string; // Primary key.
created: number; // Primary key.
courseid: number;
publishstate: string;
publishstate: AddonNotesPublishState;
format: number;
lastmodified: number;
};

View File

@ -17,6 +17,7 @@ import { CoreSites } from '@services/sites';
import { CoreTimeUtils } from '@services/utils/time';
import { makeSingleton } from '@singletons';
import { AddonNotesDBRecord, AddonNotesDeletedDBRecord, NOTES_DELETED_TABLE, NOTES_TABLE } from './database/notes';
import { AddonNotesPublishState } from './notes';
/**
* Service to handle offline notes.
@ -150,7 +151,7 @@ export class AddonNotesOfflineProvider {
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with notes.
*/
async getNotesWithPublishState(state: string, siteId?: string): Promise<AddonNotesDBRecord[]> {
async getNotesWithPublishState(state: AddonNotesPublishState, siteId?: string): Promise<AddonNotesDBRecord[]> {
const site = await CoreSites.getSite(siteId);
return await site.getDb().getRecords(NOTES_TABLE, { publishstate: state });
@ -189,7 +190,7 @@ export class AddonNotesOfflineProvider {
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with boolean: true if has offline notes, false otherwise.
*/
async hasNotesWithPublishState(state: string, siteId?: string): Promise<boolean> {
async hasNotesWithPublishState(state: AddonNotesPublishState, siteId?: string): Promise<boolean> {
const notes = await this.getNotesWithPublishState(state, siteId);
return !!notes.length;
@ -205,7 +206,13 @@ export class AddonNotesOfflineProvider {
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if stored, rejected if failure.
*/
async saveNote(userId: number, courseId: number, state: string, content: string, siteId?: string): Promise<void> {
async saveNote(
userId: number,
courseId: number,
state: AddonNotesPublishState,
content: string,
siteId?: string,
): Promise<void> {
const site = await CoreSites.getSite(siteId);
const now = CoreTimeUtils.timestamp();

View File

@ -42,7 +42,13 @@ export class AddonNotesProvider {
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with boolean: true if note was sent to server, false if stored in device.
*/
async addNote(userId: number, courseId: number, publishState: string, noteText: string, siteId?: string): Promise<boolean> {
async addNote(
userId: number,
courseId: number,
publishState: AddonNotesPublishState,
noteText: string,
siteId?: string,
): Promise<boolean> {
siteId = siteId || CoreSites.getCurrentSiteId();
// Convenience function to store a note to be synchronized later.
@ -82,7 +88,13 @@ export class AddonNotesProvider {
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when added, rejected otherwise.
*/
async addNoteOnline(userId: number, courseId: number, publishState: string, noteText: string, siteId?: string): Promise<void> {
async addNoteOnline(
userId: number,
courseId: number,
publishState: AddonNotesPublishState,
noteText: string,
siteId?: string,
): Promise<void> {
const notes: AddonNotesCreateNoteData[] = [
{
courseid: courseId,
@ -438,7 +450,7 @@ export type AddonNotesNote = {
created: number; // Time created (timestamp).
lastmodified: number; // Time of last modification (timestamp).
usermodified: number; // User id of the creator of this note.
publishstate: string; // State of the note (i.e. draft, public, site).
publishstate: AddonNotesPublishState; // State of the note (i.e. draft, public, site).
offline?: boolean;
};
@ -474,7 +486,7 @@ export type AddonNotesNoteFormatted = AddonNotesNote & {
export type AddonNotesCreateNoteData = {
userid: number; // Id of the user the note is about.
publishstate: string; // 'personal', 'course' or 'site'.
publishstate: AddonNotesPublishState; // 'personal', 'course' or 'site'.
courseid: number; // Course id of the note (in Moodle a note can only be created into a course,
// even for site and personal notes).
text: string; // The text of the message - text or HTML.
@ -504,3 +516,5 @@ export type AddonNotesCreateNotesWSResponse = {
type AddonNotesDeleteNotesWSParams = {
notes: number[]; // Array of Note Ids to be deleted.
};
export type AddonNotesPublishState = 'personal' | 'site' | 'course';

View File

@ -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">

View File

@ -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">

View File

@ -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;

View File

@ -0,0 +1,68 @@
// (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.
export abstract class CoreAriaRoleButton<T = unknown> {
componentInstance: T;
constructor(componentInstance: T) {
this.componentInstance = componentInstance;
}
/**
* A11y key functionallity that prevents keyDown events.
*
* @param event Event.
*/
keyDown(event: KeyboardEvent): void {
if ((event.key == ' ' || event.key == 'Enter') && this.isAllowed()) {
event.preventDefault();
event.stopPropagation();
}
}
/**
* A11y key functionallity that translates space and enter keys to click action.
*
* @param event Event.
*/
keyUp(event: KeyboardEvent): void {
if ((event.key == ' ' || event.key == 'Enter') && this.isAllowed()) {
event.preventDefault();
event.stopPropagation();
this.click(event);
}
}
/**
* A11y click functionallity.
*
* @param event Event.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
click(event?: Event): void {
// Nothing defined here.
}
/**
* Checks if action is allowed in class.
*
* @returns If allowed.
*/
isAllowed(): boolean {
return true;
}
}

View File

@ -0,0 +1,137 @@
// (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.
export class CoreAriaRoleTab<T = unknown> {
componentInstance: T;
constructor(componentInstance: T) {
this.componentInstance = componentInstance;
}
/**
* A11y key functionallity that prevents keyDown events.
*
* @param e Event.
*/
keyDown(e: KeyboardEvent): void {
if (e.key == ' ' ||
e.key == 'Enter' ||
e.key == 'Home' ||
e.key == 'End' ||
(this.isHorizontal() && (e.key == 'ArrowRight' || e.key == 'ArrowLeft')) ||
(!this.isHorizontal() && (e.key == 'ArrowUp' ||e.key == 'ArrowDown'))
) {
e.preventDefault();
e.stopPropagation();
}
}
/**
* A11y key functionallity.
*
* Enter or Space: When a tab has focus, activates the tab, causing its associated panel to be displayed.
* Right Arrow: When a tab has focus: Moves focus to the next tab. If focus is on the last tab, moves focus to the first tab.
* Left Arrow: When a tab has focus: Moves focus to the previous tab. If focus is on the first tab, moves focus to the last tab.
* Home: When a tab has focus, moves focus to the first tab.
* End: When a tab has focus, moves focus to the last tab.
* https://www.w3.org/TR/wai-aria-practices-1.1/examples/tabs/tabs-2/tabs.html
*
* @param tabFindIndex Tab finable index.
* @param e Event.
* @return Promise resolved when done.
*/
keyUp(tabFindIndex: string, e: KeyboardEvent): void {
if (e.key == ' ' || e.key == 'Enter') {
this.selectTab(tabFindIndex, e);
return;
}
e.preventDefault();
e.stopPropagation();
const tabs = this.getSelectableTabs();
let index = tabs.findIndex((tab) => tabFindIndex == tab.findIndex);
const previousKey = this.isHorizontal() ? 'ArrowLeft' : 'ArrowUp';
const nextKey = this.isHorizontal() ? 'ArrowRight' : 'ArrowDown';
switch (e.key) {
case nextKey:
index++;
if (index >= tabs.length) {
index = 0;
}
break;
case 'Home':
index = 0;
break;
case previousKey:
index--;
if (index < 0) {
index = tabs.length - 1;
}
break;
case 'End':
index = tabs.length - 1;
break;
default:
return;
}
const tabId = tabs[index].id;
// @todo Pages should match aria-controls id.
const tabElement = document.querySelector<HTMLIonTabButtonElement>(`ion-tab-button[aria-controls=${tabId}]`);
tabElement?.focus();
}
/**
* Selects the tab.
*
* @param tabId Tab identifier.
* @param e Event.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
selectTab(tabId: string, e: Event): void {
//
}
/**
* Return all the selectable tabs.
*
* @returns all the selectable tabs.
*/
getSelectableTabs(): CoreAriaRoleTabFindable[] {
return [];
}
/**
* Returns if tabs are displayed horizontal or not.
*
* @returns Where the tabs are displayed horizontal.
*/
isHorizontal(): boolean {
return true;
}
}
export type CoreAriaRoleTabFindable = {
id: string;
findIndex: string;
};

View File

@ -31,6 +31,7 @@ import { Subscription } from 'rxjs';
import { Platform, Translate } from '@singletons';
import { CoreSettingsHelper } from '@features/settings/services/settings-helper';
import { CoreAriaRoleTab, CoreAriaRoleTabFindable } from './aria-role-tab';
/**
* Class to abstract some common code for tabs.
@ -89,10 +90,13 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
protected slidesSwiperLoaded = false;
protected scrollElements: Record<string | number, HTMLElement> = {}; // Scroll elements for each loaded tab.
tabAction: CoreTabsRoleTab<T>;
constructor(
protected element: ElementRef,
) {
this.backButtonFunction = this.backButtonClicked.bind(this);
this.tabAction = new CoreTabsRoleTab(this);
}
/**
@ -632,6 +636,30 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
}
/**
* Helper class to manage rol tab.
*/
class CoreTabsRoleTab<T extends CoreTabBase> extends CoreAriaRoleTab<CoreTabsBaseComponent<T>> {
/**
* @inheritdoc
*/
selectTab(tabId: string, e: Event): void {
this.componentInstance.selectTab(tabId, e);
}
/**
* @inheritdoc
*/
getSelectableTabs(): CoreAriaRoleTabFindable[] {
return this.componentInstance.tabs.filter((tab) => tab.enabled).map((tab) => ({
id: tab.id!,
findIndex: tab.id!,
}));
}
}
/**
* Data for each tab.
*/

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -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 {}

View File

@ -17,7 +17,7 @@ import { Subject } from 'rxjs';
import { auditTime } from 'rxjs/operators';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { PopoverController, Translate } from '@singletons';
import { Translate } from '@singletons';
import { CoreContextMenuItemComponent } from './context-menu-item';
import { CoreContextMenuPopoverComponent } from './context-menu-popover';
@ -176,26 +176,23 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy {
*/
async showContextMenu(event: MouseEvent): Promise<void> {
if (!this.expanded) {
const popover = await PopoverController.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<CoreContextMenuItemComponent>();
const popoverData = await CoreDomUtils.openPopover<CoreContextMenuItemComponent>({
event,
component: CoreContextMenuPopoverComponent,
componentProps: {
title: this.title,
items: this.items,
},
showBackdrop: true,
id: this.uniqueId,
});
this.expanded = false;
if (data.data) {
data.data.onClosed?.emit();
if (popoverData) {
popoverData.onClosed?.emit();
}
}

View File

@ -1,5 +1,6 @@
:host {
display: flex;
width: 100%;
.core-progress-text {
line-height: 40px;
@ -16,7 +17,8 @@
margin: 16px 0;
padding: 0;
display: block;
width: calc(100% - 55px);
width: 100%;
flex: 1;
&[value]::-webkit-progress-bar {
background-color: var(--background);

View File

@ -3,7 +3,7 @@
<ion-title>{{ 'core.login.security_question' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<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>
</ion-button>
</ion-buttons>

View File

@ -42,7 +42,7 @@ export class CoreRecaptchaModalComponent implements OnDestroy {
* Close modal.
*/
closeModal(): void {
ModalController.dismiss({
ModalController.dismiss(<CoreRecaptchaModalReturn>{
expired: this.expired,
value: this.value,
});
@ -119,3 +119,8 @@ export class CoreRecaptchaModalComponent implements OnDestroy {
}
}
export type CoreRecaptchaModalReturn = {
expired: boolean;
value: string;
};

View File

@ -16,9 +16,9 @@ import { Component, Input, OnInit } from '@angular/core';
import { CoreLang } from '@services/lang';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { ModalController } from '@singletons';
import { CoreRecaptchaModalComponent } from './recaptcha-modal';
import { CoreRecaptchaModalComponent, CoreRecaptchaModalReturn } from './recaptcha-modal';
/**
* Component that allows answering a recaptcha.
@ -66,7 +66,7 @@ export class CoreRecaptchaComponent implements OnInit {
// Modal to answer the recaptcha.
// This is because the size of the recaptcha is dynamic, so it could cause problems if it was displayed inline.
const modal = await ModalController.create({
const modalData = await CoreDomUtils.openModal<CoreRecaptchaModalReturn>({
component: CoreRecaptchaModalComponent,
cssClass: 'core-modal-fullscreen',
componentProps: {
@ -74,12 +74,10 @@ export class CoreRecaptchaComponent implements OnInit {
},
});
await modal.present();
const result = await modal.onWillDismiss();
this.expired = result.data.expired;
this.model![this.modelValueName] = result.data.value;
if (modalData) {
this.expired = modalData.expired;
this.model![this.modelValueName] = modalData.value;
}
}
}

View File

@ -1,11 +1,9 @@
<form #messageForm>
<textarea class="core-send-message-input" [autofocus]="showKeyboard" [placeholder]="placeholder" rows="1" core-auto-rows
[(ngModel)]="message" name="message" (onResize)="textareaResized()" (keydown.enter)="enterClicked($event)"
(keydown.control.enter)="enterClicked($event, 'control')" (keydown.meta.enter)="enterClicked($event, 'meta')"></textarea>
<ion-buttons>
<ion-button fill="clear" type="submit" [disabled]="!message || sendDisabled"
[attr.aria-label]="'core.send' | translate" [core-suppress-events] (onClick)="submitForm($event)">
<ion-icon name="send" color="dark" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-buttons>
[(ngModel)]="message" name="message" (onResize)="textareaResized()" (keyup.enter)="enterClicked($event)"
(keyup.control.enter)="enterClicked($event, 'control')" (keyup.meta.enter)="enterClicked($event, 'meta')"></textarea>
<ion-button fill="clear" size="large" type="submit" [disabled]="!message || sendDisabled"
[attr.aria-label]="'core.send' | translate" [core-suppress-events] (click)="submitForm($event)">
<ion-icon name="send" color="dark" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</form>

View File

@ -6,28 +6,19 @@
display: flex;
align-items: center;
width: 100%;
margin-top: 5px;
margin-bottom: 5px;
}
.core-send-message-input {
appearance: none;
display: block;
width: 100%;
min-height: 28px;
border: 0;
font-family: inherit;
background: var(--core-send-message-input-background);
color: var(--core-send-message-input-color);
border-radius: 5px;
margin: 0 5px;
}
.core-send-message-button {
margin: 0;
padding: 0;
display: none;
min-height: 0;
align-self: self-end;
border-radius: 21px;
line-height: 20px;
padding: 9px 12px 11px;
margin: 5px 10px;
}
}

View File

@ -7,15 +7,28 @@
</ion-col>
<ion-col class="ion-no-padding" size="10">
<ion-slides (ionSlideDidChange)="slideChanged()" [options]="slidesOpts" [dir]="direction" role="tablist"
[attr.aria-label]="description" aria-hidden="false">
[attr.aria-label]="description">
<ng-container *ngFor="let tab of tabs">
<ion-slide [hidden]="!hideUntil" [attr.aria-selected]="selected == tab.id" class="tab-slide" role="tab"
[attr.aria-label]="tab.title | translate" [attr.aria-controls]="tab.id" [id]="tab.id! + '-tab'"
[tabindex]="selected == tab.id ? null : -1">
<ion-tab-button (ionTabButtonClick)="selectTab(tab.id, $event)" [tab]="tab.page" [layout]="layout"
class="{{tab.class}}" [attr.aria-label]="tab.title | translate">
<ion-icon *ngIf="tab.icon" aria-hidden="true"></ion-icon>
<ion-label aria-hidden="true">{{ tab.title | translate}}</ion-label>
<ion-slide
role="presentation"
[hidden]="!hideUntil"
[id]="tab.id! + '-tab'"
class="tab-slide"
[class.selected]="selected == tab.id">
<ion-tab-button
(ionTabButtonClick)="selectTab(tab.id, $event)"
(keydown)="tabAction.keyDown($event)"
(keyup)="tabAction.keyUp(tab.id, $event)"
[tab]="tab.page"
[layout]="layout"
class="{{tab.class}}"
role="tab"
[attr.aria-controls]="tab.id"
[attr.aria-selected]="selected == tab.id"
[tabindex]="selected == tab.id ? 0 : -1"
>
<ion-icon *ngIf="tab.icon" [name]="tab.icon" aria-hidden="true"></ion-icon>
<ion-label>{{ tab.title | translate}}</ion-label>
<ion-badge *ngIf="tab.badge">{{ tab.badge }}</ion-badge>
</ion-tab-button>
</ion-slide>

View File

@ -45,7 +45,8 @@ import { CoreTabBase, CoreTabsBaseComponent } from '@classes/tabs';
* Tab contents will only be shown if that tab is selected.
*
* @todo: Test RTL and tab history.
* @todo: This should behave like the split-view in relation to routing (maybe we could reuse some code from CoreItemsListManager).
* @todo: This should behave like the split-view in relation to routing (maybe we could reuse some code from
* CorePageItemsListManager).
*/
@Component({
selector: 'core-tabs-outlet',

View File

@ -6,15 +6,30 @@
</ion-col>
<ion-col class="ion-no-padding" size="10">
<ion-slides (ionSlideDidChange)="slideChanged()" [options]="slidesOpts" [dir]="direction" role="tablist"
[attr.aria-label]="description" aria-hidden="false">
[attr.aria-label]="description">
<ng-container *ngFor="let tab of tabs">
<ion-slide *ngIf="tab.enabled" [hidden]="!hideUntil" [attr.aria-selected]="selected == tab.id"
class="tab-slide {{tab.class}}" role="tab" [attr.aria-label]="tab.title | translate"
[attr.aria-controls]="tab.id" [id]="tab.id! + '-tab'" [tabindex]="selected == tab.id ? null : -1"
(click)="selectTab(tab.id, $event)" [attr.aria-label]="tab.title | translate">
<ion-icon *ngIf="tab.icon" [name]="tab.icon" aria-hidden="true"></ion-icon>
<ion-label aria-hidden="true">{{ tab.title | translate}}</ion-label>
<ion-badge *ngIf="tab.badge">{{ tab.badge }}</ion-badge>
<ion-slide
*ngIf="tab.enabled"
role="presentation"
[hidden]="!hideUntil"
class="tab-slide"
[id]="tab.id! + '-tab'"
[class.selected]="selected == tab.id">
<ion-tab-button
(click)="selectTab(tab.id, $event)"
(keydown)="tabAction.keyDown($event)"
(keyup)="tabAction.keyUp(tab.id, $event)"
class="{{tab.class}}"
[layout]="layout"
role="tab"
[attr.aria-controls]="tab.id"
[attr.aria-selected]="selected == tab.id"
[tabindex]="selected == tab.id ? 0 : -1"
>
<ion-icon *ngIf="tab.icon" [name]="tab.icon" aria-hidden="true"></ion-icon>
<ion-label>{{ tab.title | translate}}</ion-label>
<ion-badge *ngIf="tab.badge">{{ tab.badge }}</ion-badge>
</ion-tab-button>
</ion-slide>
</ng-container>
</ion-slides>

View File

@ -84,6 +84,7 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase {
this.element.setAttribute('role', 'tabpanel');
this.element.setAttribute('tabindex', '0');
this.element.setAttribute('aria-hidden', 'true');
}
/**
@ -113,6 +114,7 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase {
this.tabElement = this.tabElement || document.getElementById(this.id + '-tab');
this.tabElement?.setAttribute('aria-selected', 'true');
this.element.setAttribute('aria-hidden', 'false');
this.loaded = true;
this.ionSelect.emit(this);
@ -128,6 +130,8 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase {
unselectTab(): void {
this.tabElement?.setAttribute('aria-selected', 'false');
this.element.classList.remove('selected');
this.element.setAttribute('aria-hidden', 'true');
this.showHideNavBarButtons(false);
}

Some files were not shown because too many files have changed in this diff Show More