Merge pull request #3878 from crazyserver/MOBILE-3947

Mobile 3947
main
Dani Palou 2023-12-14 10:57:04 +01:00 committed by GitHub
commit a2f49c5356
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
97 changed files with 852 additions and 847 deletions

24
package-lock.json generated
View File

@ -40,7 +40,7 @@
"@awesome-cordova-plugins/sqlite": "^6.3.0",
"@awesome-cordova-plugins/status-bar": "^6.3.0",
"@awesome-cordova-plugins/web-intent": "^6.3.0",
"@ionic/angular": "^7.0.0",
"@ionic/angular": "^7.6.1",
"@ionic/cordova-builders": "^10.0.0",
"@moodlehq/cordova-plugin-advanced-http": "3.3.1-moodle.1",
"@moodlehq/cordova-plugin-camera": "6.0.0-moodle.2",
@ -3449,11 +3449,11 @@
"dev": true
},
"node_modules/@ionic/angular": {
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-7.5.6.tgz",
"integrity": "sha512-RJDQgGiVRps/04HBfx23E8tiGCvzE2d5NpWB1Mi1CDmc0ENTSc6odb2XI45YhFxmGvQsWZ8k+H1N/8emAHPraw==",
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-7.6.1.tgz",
"integrity": "sha512-Eh//g/bAL9se4PD6C19NqymgQbqKp4W+Ffbjo8Qnqwk02jGMs/jcMP0WVEcLNiEws2m67kIiWItrUhJjb8pplA==",
"dependencies": {
"@ionic/core": "7.5.6",
"@ionic/core": "7.6.1",
"ionicons": "^7.0.0",
"jsonc-parser": "^3.0.0",
"tslib": "^2.3.0"
@ -3967,11 +3967,11 @@
}
},
"node_modules/@ionic/core": {
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.5.6.tgz",
"integrity": "sha512-bYQp2twwm61uA0Q31ToVIpQWsiQ9so1dRoWZPD+l+y4fVuFmOCLYeS6XTLTm73jVBq40JfEcsac7eYC4DxoemQ==",
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.6.1.tgz",
"integrity": "sha512-o4PSRxokfRB5H3E5DAM7xivG8XFXaXD3+U/tha0QKemiMSntqgPqy0FYX0pNEwIrV3llRzFbAGNqyvB1+BG97Q==",
"dependencies": {
"@stencil/core": "^4.7.2",
"@stencil/core": "^4.8.2",
"ionicons": "^7.2.1",
"tslib": "^2.1.0"
}
@ -5990,9 +5990,9 @@
}
},
"node_modules/@stencil/core": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.7.2.tgz",
"integrity": "sha512-sPPDYrXiTbfeUF5CCyfqysXK/yfTHC4xYR1+nHzGkS2vhRSBOLp0oPuB+xkJLKA+K2ZqDJUxpOnDxy1CLWwBXA==",
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.8.2.tgz",
"integrity": "sha512-KdZEAtz9VnqMtXOkf51+8mphyRt0fN/LYgtj5M8gnveGspG8KzoyTDzlWt0wsstWIsJJ21RA1yd3AgMMZiu3MA==",
"bin": {
"stencil": "bin/stencil"
},

View File

@ -75,7 +75,7 @@
"@awesome-cordova-plugins/sqlite": "^6.3.0",
"@awesome-cordova-plugins/status-bar": "^6.3.0",
"@awesome-cordova-plugins/web-intent": "^6.3.0",
"@ionic/angular": "^7.0.0",
"@ionic/angular": "^7.6.1",
"@ionic/cordova-builders": "^10.0.0",
"@moodlehq/cordova-plugin-advanced-http": "3.3.1-moodle.1",
"@moodlehq/cordova-plugin-camera": "6.0.0-moodle.2",

View File

@ -17,8 +17,9 @@
</ion-refresher>
<core-loading [hideUntil]="loaded">
<ion-item *ngIf="showMyEntriesToggle">
<ion-label>{{ 'addon.blog.showonlyyourentries' | translate }}</ion-label>
<ion-toggle [(ngModel)]="onlyMyEntries" (ionChange)="onlyMyEntriesToggleChanged(onlyMyEntries)" slot="end" />
<ion-toggle [(ngModel)]="onlyMyEntries" (ionChange)="onlyMyEntriesToggleChanged(onlyMyEntries)">
{{ 'addon.blog.showonlyyourentries' | translate }}
</ion-toggle>
</ion-item>
<core-empty-box *ngIf="entries && entries.length === 0" icon="far-newspaper" [message]="'addon.blog.noentriesyet' | translate" />
<ng-container *ngFor="let entry of entries">

View File

@ -11,17 +11,17 @@
<ion-list>
<ion-item *ngFor="let type of types" class="addon-calendar-event" [ngClass]="['addon-calendar-eventtype-'+type]">
<ion-icon [name]="typeIcons[type]" slot="start" aria-hidden="true" />
<ion-label>{{ 'addon.calendar.' + type + 'events' | translate}}</ion-label>
<ion-toggle [(ngModel)]="filter[type]" (ionChange)="onChange()" slot="end" />
<ion-toggle [(ngModel)]="filter[type]" (ionChange)="onChange()">
{{ 'addon.calendar.' + type + 'events' | translate}}
</ion-toggle>
</ion-item>
<core-spacer *ngIf="filter.course || filter.category || filter.group" />
<ng-container *ngIf="filter.course || filter.category || filter.group">
<ion-radio-group [(ngModel)]="courseId" (ionChange)="onChange()">
<ion-item class="ion-text-wrap" *ngFor="let course of sortedCourses">
<ion-label>
<ion-radio [value]="course.id">
<core-format-text [text]="course.shortname" />
</ion-label>
<ion-radio slot="end" [value]="course.id" />
</ion-radio>
</ion-item>
</ion-radio-group>
</ng-container>

View File

@ -17,11 +17,11 @@
<form [formGroup]="form" *ngIf="!error" #editEventForm>
<!-- Event name. -->
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<p class="item-heading" [core-mark-required]="true">{{ 'addon.calendar.eventname' | translate }}</p>
</ion-label>
<ion-input type="text" name="name" [placeholder]="'addon.calendar.eventname' | translate" formControlName="name" />
<core-input-errors [control]="form.controls.name" [errorMessages]="errors" />
<ion-input labelPlacement="stacked" type="text" name="name" [placeholder]="'addon.calendar.eventname' | translate"
formControlName="name">
<div slot="label" [core-mark-required]="true">{{ 'addon.calendar.eventname' | translate }}</div>
</ion-input>
<core-input-errors [control]="form.controls.name" />
</ion-item>
<!-- Date. -->
@ -37,17 +37,18 @@
</ion-datetime>
</ng-template>
</ion-modal>
<core-input-errors [control]="form.controls.timestart" [errorMessages]="errors" />
<core-input-errors [control]="form.controls.timestart" />
</ion-item>
<!-- Type. -->
<ion-item class="ion-text-wrap addon-calendar-eventtype-container">
<ion-label>
<ion-label *ngIf="eventTypes.length === 1">
<p class="item-heading" [core-mark-required]="true">{{ 'addon.calendar.eventkind' | translate }}</p>
</ion-label>
<p *ngIf="eventTypes.length === 1" slot="end">{{eventTypes[0].name | translate }}</p>
<ion-select *ngIf="eventTypes.length > 1" formControlName="eventtype" interface="action-sheet"
[cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'addon.calendar.eventkind' | translate}">
<div [core-mark-required]="true" slot="label">{{ 'addon.calendar.eventkind' | translate }}</div>
<ion-select-option *ngFor="let type of eventTypes" [value]="type.value">
{{ type.name | translate }}
</ion-select-option>
@ -56,11 +57,9 @@
<!-- Category. -->
<ion-item class="ion-text-wrap" *ngIf="typeControl.value === 'category'">
<ion-label>
<p class="item-heading" [core-mark-required]="true">{{ 'core.category' | translate }}</p>
</ion-label>
<ion-select formControlName="categoryid" interface="action-sheet" [placeholder]="'core.noselection' | translate"
[cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'core.category' | translate}">
<p [core-mark-required]="true" slot="label">{{ 'core.category' | translate }}</p>
<ion-select-option *ngFor="let category of categories" [value]="category.id">
{{ category.name }}
</ion-select-option>
@ -69,11 +68,9 @@
<!-- Course. -->
<ion-item class="ion-text-wrap" *ngIf="typeControl.value === 'course'">
<ion-label>
<p class="item-heading" [core-mark-required]="true">{{ 'core.course' | translate }}</p>
</ion-label>
<ion-select formControlName="courseid" interface="action-sheet" [placeholder]="'core.noselection' | translate"
[cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'core.course' | translate}">
<p [core-mark-required]="true" slot="label">{{ 'core.course' | translate }}</p>
<ion-select-option *ngFor="let course of courses" [value]="course.id">{{ course.fullname }}</ion-select-option>
</ion-select>
</ion-item>
@ -82,12 +79,10 @@
<ng-container *ngIf="typeControl.value === 'group'">
<!-- Select the course. -->
<ion-item class="ion-text-wrap">
<ion-label>
<p class="item-heading" [core-mark-required]="true">{{ 'core.course' | translate }}</p>
</ion-label>
<ion-select formControlName="groupcourseid" interface="action-sheet" [placeholder]="'core.noselection' | translate"
[cancelText]="'core.cancel' | translate" (ionChange)="groupCourseSelected()"
[interfaceOptions]="{header: 'core.course' | translate}">
<p [core-mark-required]="true" slot="label">{{ 'core.course' | translate }}</p>
<ion-select-option *ngFor="let course of courses" [value]="course.id">
{{ course.fullname }}
</ion-select-option>
@ -101,11 +96,9 @@
</ion-item>
<!-- Select the group. -->
<ion-item class="ion-text-wrap core-edit-set-group" *ngIf="!loadingGroups && groups.length > 0">
<ion-label>
<p class="item-heading" [core-mark-required]="true">{{ 'core.group' | translate }}</p>
</ion-label>
<ion-select formControlName="groupid" interface="action-sheet" [placeholder]="'core.noselection' | translate"
[cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'core.group' | translate}">
<p [core-mark-required]="true" slot="label">{{ 'core.group' | translate }}</p>
<ion-select-option *ngFor="let group of groups" [value]="group.id">{{ group.name }}</ion-select-option>
</ion-select>
</ion-item>
@ -147,16 +140,14 @@
</ion-label>
</ion-item-divider>
<ion-item>
<ion-label>
<ion-radio [value]="0">
<p>{{ 'addon.calendar.durationnone' | translate }}</p>
</ion-label>
<ion-radio slot="end" [value]="0" />
</ion-radio>
</ion-item>
<ion-item>
<ion-label>
<ion-radio [value]="1">
<p>{{ 'addon.calendar.durationuntil' | translate }}</p>
</ion-label>
<ion-radio slot="end" [value]="1" />
</ion-radio>
</ion-item>
<ion-item *ngIf="form.controls.duration.value === 1">
<ion-label position="stacked" />
@ -171,14 +162,12 @@
</ion-modal>
</ion-item>
<ion-item>
<ion-label>
<p>{{ 'addon.calendar.durationminutes' | translate }}</p>
</ion-label>
<ion-radio slot="end" [value]="2" />
<ion-radio [value]="2">
<p id="durationinminutes">{{ 'addon.calendar.durationminutes' | translate }}</p>
</ion-radio>
</ion-item>
<ion-item *ngIf="form.controls.duration.value === 2">
<ion-label class="sr-only">{{ 'addon.calendar.durationminutes' | translate }}</ion-label>
<ion-input type="number" name="timedurationminutes" slot="end"
<ion-input type="number" name="timedurationminutes" labelPlacement="start" aria-labelledby="durationinminutes"
[placeholder]="'addon.calendar.durationminutes' | translate" formControlName="timedurationminutes" />
</ion-item>
</ion-radio-group>
@ -187,16 +176,13 @@
<!-- Repeat (for new events). -->
<ng-container *ngIf="!eventId || eventId < 0">
<ion-item class="ion-text-wrap divider">
<ion-label>
<ion-checkbox labelPlacement="start" formControlName="repeat">
<p class="item-heading">{{ 'addon.calendar.repeatevent' | translate }}</p>
</ion-label>
<ion-checkbox slot="end" formControlName="repeat" />
</ion-checkbox>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<p class="item-heading">{{ 'addon.calendar.repeatweeksl' | translate }}</p>
</ion-label>
<ion-input type="number" name="repeats" formControlName="repeats" [disabled]="!form.controls.repeat.value" />
<ion-input labelPlacement="stacked" [label]="'addon.calendar.repeatweeksl' | translate" type="number" name="repeats"
formControlName="repeats" [disabled]="!form.controls.repeat.value" />
</ion-item>
</ng-container>
@ -209,16 +195,14 @@
</ion-label>
</ion-item-divider>
<ion-item>
<ion-label>
<ion-radio value="1">
<p>{{ 'addon.calendar.repeateditall' | translate:{$a: otherEventsCount} }}</p>
</ion-label>
<ion-radio slot="end" value="1" />
</ion-radio>
</ion-item>
<ion-item>
<ion-label>
<ion-radio value="0">
<p>{{ 'addon.calendar.repeateditthis' | translate }}</p>
</ion-label>
<ion-radio slot="end" value="0" />
</ion-radio>
</ion-item>
</ion-radio-group>
</div>
@ -235,10 +219,8 @@
<!-- Location. -->
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<p class="item-heading">{{ 'core.location' | translate }}</p>
</ion-label>
<ion-input type="text" name="location" [placeholder]="'core.location' | translate" formControlName="location" />
<ion-input type="text" name="location" [placeholder]="'core.location' | translate" [label]="'core.location' | translate"
labelPlacement="stacked" formControlName="location" />
</ion-item>
</form>
<div collapsible-footer appearOnBottom *ngIf="loaded && !error" slot="fixed">

View File

@ -69,7 +69,6 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
groups: CoreGroup[] = [];
loadingGroups = false;
courseGroupSet = false;
errors: Record<string, string>;
error = false;
eventRepeatId?: number;
otherEventsCount = 0;
@ -100,9 +99,6 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
) {
this.currentSite = CoreSites.getRequiredCurrentSite();
this.remindersEnabled = CoreReminders.isEnabled();
this.errors = {
required: Translate.instant('core.required'),
};
this.form = new FormGroup({});

View File

@ -11,8 +11,9 @@
<ion-content>
<ion-list>
<ion-item *ngIf="defaultTimeLabel">
<ion-label>{{ 'addon.calendar.defaultnotificationtime' | translate }}</ion-label>
<ion-select [(ngModel)]="defaultTimeLabel" (click)="changeDefaultTime($event)">
<ion-select [(ngModel)]="defaultTimeLabel" (click)="changeDefaultTime($event)"
[label]="'addon.calendar.defaultnotificationtime' | translate">
<ion-select-option [value]="defaultTimeLabel">{{ defaultTimeLabel }}</ion-select-option>
</ion-select>
</ion-item>

View File

@ -23,7 +23,7 @@
<ion-list>
<ion-item class="ion-text-wrap" *ngFor="let device of platform.devices" [class.item-current]="device.current">
<ion-label>
<p class="item-heading">
<p class="item-heading" id="device-{{device.id}}">
<strong>{{ device.name }} {{ device.model }}</strong> ({{platform.platform}} {{ device.version }})
</p>
<p *ngIf="device.current"><strong>{{ 'core.currentdevice' | translate }}</strong></p>
@ -33,7 +33,8 @@
</p>
</ion-label>
<core-button-with-spinner [loading]="device.updating" slot="end">
<ion-toggle [(ngModel)]="device.enable" (ngModelChange)="enableDevice(device, device.enable)" />
<ion-toggle [(ngModel)]="device.enable" (ngModelChange)="enableDevice(device, device.enable)"
[attr.aria-labelledby]="'device-'+ device.id " />
</core-button-with-spinner>
</ion-item>
</ion-list>

View File

@ -161,7 +161,8 @@
<div class="flex-row ion-justify-content-between">
<p class="item-heading">
<core-format-text [text]="conversation.name" contextLevel="system" [contextInstanceId]="0" />
<ion-icon name="fas-user-slash" *ngIf="conversation.isblocked" [title]="'addon.messages.contactblocked' | translate" />
<ion-icon name="fas-user-slash" *ngIf="conversation.isblocked"
[attr.aria-label]="'addon.messages.contactblocked' | translate" />
<ion-icon *ngIf="conversation.ismuted" name="fas-volume-xmark"
[title]="'addon.messages.mutedconversation' | translate" />
</p>

View File

@ -22,21 +22,19 @@
</ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap">
<ion-label>
<p>{{ 'addon.messages.useentertosend' | translate }}</p>
</ion-label>
<ion-toggle [(ngModel)]="sendOnEnter" (ngModelChange)="sendOnEnterChanged()" slot="end" />
<ion-toggle [(ngModel)]="sendOnEnter" (ngModelChange)="sendOnEnterChanged()">
{{ 'addon.messages.useentertosend' | translate }}
</ion-toggle>
</ion-item>
</ion-list>
</ion-card>
<!-- Contactable privacy. -->
<ion-card>
<ion-item *ngIf="!advancedContactable">
<ion-label class="ion-text-wrap">
<p>{{ 'addon.messages.blocknoncontacts' | translate }}</p>
</ion-label>
<ion-toggle [(ngModel)]="contactablePrivacy" (ngModelChange)="saveContactablePrivacy(contactablePrivacy)" slot="end" />
<ion-item *ngIf="!advancedContactable" class="ion-text-wrap">
<ion-toggle [(ngModel)]="contactablePrivacy" (ngModelChange)="saveContactablePrivacy(contactablePrivacy)">
{{ 'addon.messages.blocknoncontacts' | translate }}
</ion-toggle>
</ion-item>
<ion-list *ngIf="advancedContactable">
@ -47,22 +45,19 @@
</ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap">
<ion-label>
<p>{{ 'addon.messages.contactableprivacy_onlycontacts' | translate }}</p>
</ion-label>
<ion-radio slot="start" [value]="onlyContactsValue" />
<ion-radio labelPlacement="end" justify="start" [value]="onlyContactsValue">
{{ 'addon.messages.contactableprivacy_onlycontacts' | translate }}
</ion-radio>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<p>{{ 'addon.messages.contactableprivacy_coursemember' | translate }}</p>
</ion-label>
<ion-radio slot="start" [value]="courseMemberValue" />
<ion-radio labelPlacement="end" justify="start" [value]="courseMemberValue">
{{ 'addon.messages.contactableprivacy_coursemember' | translate }}
</ion-radio>
</ion-item>
<ion-item *ngIf="allowSiteMessaging" class="ion-text-wrap">
<ion-label>
<p>{{ 'addon.messages.contactableprivacy_site' | translate }}</p>
</ion-label>
<ion-radio slot="start" [value]="siteValue" />
<ion-radio labelPlacement="end" justify="start" [value]="siteValue">
{{ 'addon.messages.contactableprivacy_site' | translate }}
</ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>

View File

@ -226,10 +226,9 @@
<!-- Submit for grading form. -->
<ng-container *ngIf="canSubmit">
<ion-item class="ion-text-wrap" *ngIf="submissionStatement">
<ion-label>
<ion-checkbox name="submissionstatement" [(ngModel)]="acceptStatement">
<core-format-text [text]="submissionStatement" [filter]="false" />
</ion-label>
<ion-checkbox slot="end" name="submissionstatement" [(ngModel)]="acceptStatement" />
</ion-checkbox>
</ion-item>
<!-- Submit button. -->
<ion-item class="ion-text-wrap" *ngIf="!showErrorStatementSubmit">
@ -277,22 +276,18 @@
<!-- Numeric grade.
Use a text input because otherwise we cannot readthe value if it has an invalid character. -->
<ion-item class="ion-text-wrap" *ngIf="grade.method === 'simple' && !grade.scale">
<ion-label position="stacked">
<p class="item-heading">{{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo!.grade} }}</p>
</ion-label>
<ion-input *ngIf="!grade.disabled" type="text" [(ngModel)]="grade.grade" min="0" [max]="gradeInfo!.grade"
[lang]="grade.lang" />
<p *ngIf="grade.disabled">{{ 'addon.mod_assign.gradelocked' | translate }}</p>
[lang]="grade.lang" [label]="'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo!.grade}"
labelPlacement="stacked"
[helperText]="grade.disabled ? ('addon.mod_assign.gradelocked' | translate) : null" />
</ion-item>
<!-- Grade using a scale. -->
<ion-item class="ion-text-wrap" *ngIf="grade.method === 'simple' && grade.scale">
<ion-label>
<p class="item-heading">{{ 'addon.mod_assign.grade' | translate }}</p>
</ion-label>
<ion-select [(ngModel)]="grade.grade" interface="action-sheet" [disabled]="grade.disabled"
[cancelText]="'core.cancel' | translate"
[interfaceOptions]="{header: 'addon.mod_assign.grade' | translate}">
<p class="item-heading" slot="label">{{ 'addon.mod_assign.grade' | translate }}</p>
<ion-select-option *ngFor="let grade of grade.scale" [value]="grade.value">
{{grade.label}}
</ion-select-option>
@ -301,12 +296,10 @@
<!-- Outcomes. -->
<ion-item class="ion-text-wrap" *ngFor="let outcome of gradeInfo!.outcomes">
<ion-label>
<p class="item-heading">{{ outcome.name }}</p>
</ion-label>
<ion-select *ngIf="canSaveGrades && outcome.itemNumber" [(ngModel)]="outcome.selectedId"
interface="action-sheet" [disabled]="gradeInfo!.disabled" [cancelText]="'core.cancel' | translate"
[interfaceOptions]="{header: outcome.name }">
<p class="item-heading" slot="label">{{ outcome.name }}</p>
<ion-select-option *ngFor="let grade of outcome.options" [value]="grade.value">
{{grade.label}}
</ion-select-option>
@ -353,11 +346,10 @@
<!--- Apply grade to all team members. -->
<ion-item class="ion-text-wrap" *ngIf="assign!.teamsubmission && canSaveGrades">
<ion-label>
<ion-toggle [(ngModel)]="grade.applyToAll">
<p class="item-heading">{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</p>
<p>{{ 'addon.mod_assign.applytoteam' | translate }}</p>
</ion-label>
<ion-toggle [(ngModel)]="grade.applyToAll" slot="end" />
</ion-toggle>
</ion-item>
<!-- Attempt status. -->
@ -380,18 +372,19 @@
</ion-label>
</ion-item>
<ion-item *ngIf="canSaveGrades && allowAddAttempt">
<ion-label>{{ 'addon.mod_assign.addattempt' | translate }}</ion-label>
<ion-toggle [(ngModel)]="grade.addAttempt" slot="end" />
<ion-toggle [(ngModel)]="grade.addAttempt">
<p>{{ 'addon.mod_assign.addattempt' | translate }}</p>
</ion-toggle>
</ion-item>
</ng-container>
<!-- Data about the grader (teacher who graded). -->
<ion-item class="ion-text-wrap" *ngIf="grader" core-user-link [userId]="grader!.id" [courseId]="courseId"
[attr.aria-label]="grader!.fullname" [detail]="true">
<ion-item class="ion-text-wrap" *ngIf="grader" core-user-link [userId]="grader.id" [courseId]="courseId"
[attr.aria-label]="grader.fullname" [detail]="true">
<core-user-avatar [user]="grader" slot="start" [linkProfile]="false" />
<ion-label>
<p class="item-heading">{{ 'addon.mod_assign.gradedby' | translate }}</p>
<p class="item-heading">{{ grader!.fullname }}</p>
<p class="item-heading">{{ grader.fullname }}</p>
<p *ngIf="feedback!.gradeddate">{{ feedback!.gradeddate * 1000 | coreFormatDate }}</p>
</ion-label>
</ion-item>

View File

@ -1124,9 +1124,15 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
return [];
}
// Receved submission statement should not be undefined. It would mean that the WS is not returning the value.
const submissionStatementMissing = !!this.assign.requiresubmissionstatement &&
this.assign.submissionstatement === undefined;
// If received submission statement is empty, then it's not required.
if(!this.assign.submissionstatement && this.assign.submissionstatement !== undefined) {
this.assign.requiresubmissionstatement = 0;
}
this.canSubmit = !this.isSubmittedForGrading && !this.submittedOffline && (lastAttempt.cansubmit ||
(this.hasOffline && AddonModAssign.canSubmitOffline(this.assign, submissionStatus)));

View File

@ -38,10 +38,9 @@
<form name="addon-mod_assign-edit-form" #editSubmissionForm>
<!-- Submission statement. -->
<ion-item class="ion-text-wrap" *ngIf="submissionStatement">
<ion-label>
<ion-checkbox name="submissionstatement" [(ngModel)]="submissionStatementAccepted">
<core-format-text [text]="submissionStatement" [filter]="false" />
</ion-label>
<ion-checkbox slot="end" name="submissionstatement" [(ngModel)]="submissionStatementAccepted" />
</ion-checkbox>
<!-- ion-checkbox doesn't use an input. Create a hidden input to hold the value. -->
<input type="hidden" [ngModel]="submissionStatementAccepted" name="submissionstatement">
</ion-item>

View File

@ -205,6 +205,12 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
this.introAttachments = submissionStatus.assignmentdata?.attachments?.intro ?? this.assign.introattachments;
this.allowOffline = true; // If offline isn't allowed we shouldn't have reached this point.
// If received submission statement is empty, then it's not required.
if(!this.assign.submissionstatement && this.assign.submissionstatement !== undefined) {
this.assign.requiresubmissionstatement = 0;
}
// Only show submission statement if we are editing our own submission.
if (this.assign.requiresubmissionstatement && !this.assign.submissiondrafts && this.userId == currentUserId) {
this.submissionStatement = this.assign.submissionstatement;

View File

@ -17,8 +17,9 @@
<core-group-selector [groupInfo]="groupInfo" [(selected)]="groupId" (selectedChange)="reloadSessions()" [courseId]="courseId" />
<ion-item>
<ion-label>{{ 'addon.mod_chat.showincompletesessions' | translate }}</ion-label>
<ion-toggle [(ngModel)]="showAll" (ionChange)="reloadSessions()" slot="end" />
<ion-toggle [(ngModel)]="showAll" (ionChange)="reloadSessions()">
{{ 'addon.mod_chat.showincompletesessions' | translate }}
</ion-toggle>
</ion-item>
<ion-card *ngFor="let session of sessions.items" (click)="sessions.select(session)" button

View File

@ -48,18 +48,16 @@
<ion-card *ngIf="options.length && choice">
<ng-container *ngIf="choice.allowmultiple">
<ion-item class="ion-text-wrap" *ngFor="let option of options">
<ion-label>
<ion-checkbox [(ngModel)]="option.checked" [disabled]="option.disabled || !canEdit">
<ng-container *ngTemplateOutlet="optionLabelTemplate; context: {option: option}" />
</ion-label>
<ion-checkbox slot="end" [(ngModel)]="option.checked" [disabled]="option.disabled || !canEdit" />
</ion-checkbox>
</ion-item>
</ng-container>
<ion-radio-group *ngIf="!choice.allowmultiple" [(ngModel)]="selectedOption.id">
<ion-item class="ion-text-wrap" *ngFor="let option of options">
<ion-label>
<ion-radio [value]="option.id" [disabled]="option.disabled || !canEdit">
<ng-container *ngTemplateOutlet="optionLabelTemplate; context: {option: option}" />
</ion-label>
<ion-radio slot="end" [value]="option.id" [disabled]="option.disabled || !canEdit" />
</ion-radio>
</ion-item>
</ion-radio-group>
</ion-card>
@ -147,7 +145,7 @@
<!-- Template to render a choice option label. -->
<ng-template #optionLabelTemplate let-option="option">
<p>
<p class="item-heading">
<core-format-text [text]="option.text" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId" />
<span *ngIf="choice!.limitanswers && option.countanswers >= option.maxanswers">
{{ 'addon.mod_choice.full' | translate }}

View File

@ -12,20 +12,20 @@
</ion-header>
<ion-content>
<ion-item>
<ion-label>{{ 'addon.mod_data.advancedsearch' | translate }}</ion-label>
<ion-toggle [(ngModel)]="search.searchingAdvanced" slot="end" />
<ion-toggle [(ngModel)]="search.searchingAdvanced">
{{ 'addon.mod_data.advancedsearch' | translate }}
</ion-toggle>
</ion-item>
<form (ngSubmit)="searchEntries($event)" [formGroup]="searchForm" #searchFormEl>
<ion-list class="ion-no-margin">
<ion-item [hidden]="search.searchingAdvanced">
<ion-label class="sr-only">{{ 'addon.mod_data.search' | translate}}</ion-label>
<ion-input type="text" placeholder="{{ 'addon.mod_data.search' | translate}}" [(ngModel)]="search.text" name="text"
formControlName="text" />
<ion-input type="text" [attr.aria-label]="'addon.mod_data.search' | translate"
placeholder="{{ 'addon.mod_data.search' | translate}}" name="text" formControlName="text" />
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">{{ 'core.sortby' | translate }}</ion-label>
<ion-select interface="action-sheet" name="sortBy" formControlName="sortBy" [placeholder]="'core.sortby' | translate"
[cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'core.sortby' | translate}">
<ion-select labelPlacement="stacked" interface="action-sheet" name="sortBy" formControlName="sortBy"
[placeholder]="'core.sortby' | translate" [cancelText]="'core.cancel' | translate"
[interfaceOptions]="{header: 'core.sortby' | translate}" [label]="'core.sortby' | translate">
<optgroup *ngIf="fieldsArray.length" label="{{ 'addon.mod_data.fields' | translate }}">
<ion-select-option *ngFor="let field of fieldsArray" [value]="field.id">{{field.name}}</ion-select-option>
</optgroup>
@ -41,14 +41,16 @@
</ion-select>
</ion-item>
<ion-list>
<ion-radio-group [(ngModel)]="search.sortDirection" name="sortDirection" formControlName="sortDirection">
<ion-radio-group name="sortDirection" formControlName="sortDirection">
<ion-item>
<ion-label>{{ 'addon.mod_data.ascending' | translate }}</ion-label>
<ion-radio slot="start" value="ASC" />
<ion-radio value="ASC" labelPlacement="end" justify="start">
{{ 'addon.mod_data.ascending' | translate }}
</ion-radio>
</ion-item>
<ion-item>
<ion-label>{{ 'addon.mod_data.descending' | translate }}</ion-label>
<ion-radio slot="start" value="DESC" />
<ion-radio value="DESC" labelPlacement="end" justify="start">
{{'addon.mod_data.descending' | translate}}
</ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>

View File

@ -16,6 +16,12 @@
.addon-data-latlong {
display: flex;
.input-units {
flex-grow: 1;
white-space: nowrap;
align-self: center;
}
}
}

View File

@ -3,13 +3,17 @@
<ion-select [formControlName]="'f_'+field.id" multiple="true" [placeholder]="'addon.mod_data.menuchoose' | translate"
[cancelText]="'core.cancel' | translate" [okText]="'core.ok' | translate" [interfaceOptions]="{header: field.name}"
interface="alert">
<ion-select-option *ngFor="let option of options" [value]="option.value">{{option.key}}</ion-select-option>
<ion-select-option *ngFor="let option of options" [value]="option.value">
<core-format-text [text]="option.key" contextLevel="module" [contextInstanceId]="database?.coursemodule"
[courseId]="database?.course" [wsNotFiltered]="true" />
</ion-select-option>
</ion-select>
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error" />
<ion-item *ngIf="searchMode">
<ion-label>{{ 'addon.mod_data.selectedrequired' | translate }}</ion-label>
<ion-checkbox slot="end" [formControlName]="'f_'+field.id+'_allreq'" [(ngModel)]="searchFields!['f_'+field.id+'_allreq']" />
<ion-item *ngIf="searchMode" class="ion-text-wrap">
<ion-checkbox [formControlName]="'f_'+field.id+'_allreq'" [(ngModel)]="searchFields!['f_'+field.id+'_allreq']">
{{ 'addon.mod_data.selectedrequired' | translate }}
</ion-checkbox>
</ion-item>
</span>

View File

@ -10,9 +10,10 @@
</ion-modal>
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error" />
<ion-item *ngIf="searchMode">
<ion-label>{{ 'addon.mod_data.usedate' | translate }}</ion-label>
<ion-checkbox slot="end" [formControlName]="'f_'+field.id+'_z'" [(ngModel)]="searchFields['f_'+field.id+'_z']" />
<ion-item *ngIf="searchMode" class="ion-text-wrap">
<ion-checkbox [formControlName]="'f_'+field.id+'_z'" [(ngModel)]="searchFields['f_'+field.id+'_z']">
{{ 'addon.mod_data.usedate' | translate }}
</ion-checkbox>
</ion-item>
</span>

View File

@ -3,13 +3,13 @@
<ng-container *ngIf="editMode">
<span [core-mark-required]="field.required" class="core-mark-required"></span>
<div class="addon-data-latlong">
<div class="addon-data-latlong flex-row">
<ion-input type="text" [formControlName]="'f_'+field.id+'_0'" maxlength="10" />
<span class="placeholder-icon" item-right>°N</span>
<div class="input-units">°N</div>
</div>
<div class="addon-data-latlong">
<div class="addon-data-latlong flex-row">
<ion-input type="text" [formControlName]="'f_'+field.id+'_1'" maxlength="10" />
<span class="placeholder-icon" item-right>°E</span>
<div class="input-units">°E</div>
</div>
<div class="addon-data-latlong" *ngIf="locationServicesEnabled">
<ion-button (click)="getLocation($event)">

View File

@ -3,7 +3,10 @@
<ion-select [formControlName]="'f_'+field.id" [placeholder]="'addon.mod_data.menuchoose' | translate"
[cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: field.name}" interface="action-sheet">
<ion-select-option value="">{{ 'addon.mod_data.menuchoose' | translate }}</ion-select-option>
<ion-select-option *ngFor="let option of options" [value]="option">{{option}}</ion-select-option>
<ion-select-option *ngFor="let option of options" [value]="option">
<core-format-text [text]="option" contextLevel="module" [contextInstanceId]="database?.coursemodule"
[courseId]="database?.course" [wsNotFiltered]="true" />
</ion-select-option>
</ion-select>
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error" />
</span>

View File

@ -3,14 +3,18 @@
<ion-select [formControlName]="'f_'+field.id" multiple="true" [placeholder]="'addon.mod_data.menuchoose' | translate"
[cancelText]="'core.cancel' | translate" [okText]="'core.ok' | translate" [interfaceOptions]="{header: field.name}"
interface="alert">
<ion-select-option *ngFor="let option of options" [value]="option.value">{{option.key}}</ion-select-option>
<ion-select-option *ngFor="let option of options" [value]="option.value">
<core-format-text [text]="option.key" contextLevel="module" [contextInstanceId]="database?.coursemodule"
[courseId]="database?.course" [wsNotFiltered]="true" />
</ion-select-option>
</ion-select>
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error" />
<ion-item *ngIf="searchMode">
<ion-label>{{ 'addon.mod_data.selectedrequired' | translate }}</ion-label>
<ion-checkbox slot="end" [formControlName]="'f_'+field.id+'_allreq'" [(ngModel)]="searchFields!['f_'+field.id+'_allreq']" />
<ion-item *ngIf="searchMode" class="ion-text-wrap">
<ion-checkbox [formControlName]="'f_'+field.id+'_allreq'" [(ngModel)]="searchFields!['f_'+field.id+'_allreq']">
{{ 'addon.mod_data.selectedrequired' | translate }}
</ion-checkbox>
</ion-item>
</span>

View File

@ -4,8 +4,8 @@
[allowOffline]="true" acceptedTypes="image" [courseId]="database?.course" />
<core-input-errors *ngIf="error" [errorText]="error" />
<ion-label position="stacked">{{ 'addon.mod_data.alttext' | translate }}</ion-label>
<ion-input type="text" [formControlName]="'f_'+field.id+'_alttext'" [placeholder]=" 'addon.mod_data.alttext' | translate" />
<ion-input [label]="'addon.mod_data.alttext' | translate" labelPlacement="stacked" type="text"
[formControlName]="'f_'+field.id+'_alttext'" [placeholder]=" 'addon.mod_data.alttext' | translate" />
</span>
<span *ngIf="searchMode && form" [formGroup]="form">

View File

@ -4,7 +4,10 @@
[cancelText]="'core.cancel' | translate" [okText]="'core.ok' | translate" [interfaceOptions]="{header: field.name}"
interface="alert">
<ion-select-option value="">{{ 'addon.mod_data.menuchoose' | translate }}</ion-select-option>
<ion-select-option *ngFor="let option of options" [value]="option">{{option}}</ion-select-option>
<ion-select-option *ngFor="let option of options" [value]="option">
<core-format-text [text]="option" contextLevel="module" [contextInstanceId]="database?.coursemodule"
[courseId]="database?.course" [wsNotFiltered]="true" />
</ion-select-option>
</ion-select>
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error" />
</span>

View File

@ -26,37 +26,40 @@
<ng-container *ngIf="item.typ !== 'pagebreak'">
<ion-item class="ion-text-wrap addon-mod_feedback-item" [color]="item.dependitem > 0 ? 'light' : ''"
[class.core-danger-item]="item.isEmpty || item.hasError">
<ion-label [position]="item.hasTextInput ? 'stacked' : undefined">
<p *ngIf="item.name" [core-mark-required]="item.required">
<span *ngIf="feedback!.autonumbering && item.itemnumber">{{item.itemnumber}}. </span>
<core-format-text [component]="component" [componentId]="cmId" [text]="item.name" contextLevel="module"
[contextInstanceId]="cmId" [courseId]="courseId" [wsNotFiltered]="true" />
<span *ngIf="item.postfix" class="addon-mod_feedback-postfix">{{item.postfix}}</span>
</p>
<ion-label *ngIf="!item.slottedLabel">
<ng-container *ngTemplateOutlet="label; context: {item: item}" />
<p *ngIf="item.templateName === 'label'">
<core-format-text [component]="component" [componentId]="cmId" [text]="item.presentation"
contextLevel="module" [contextInstanceId]="cmId" [wsNotFiltered]="true" [courseId]="courseId" />
</p>
</ion-label>
<ion-input *ngIf="item.templateName === 'textfield'" type="text" [(ngModel)]="item.value" autocorrect="off"
name="{{item.typ}}_{{item.id}}" maxlength="{{item.length}}" [required]="item.required" />
<ion-input labelPlacement="stacked" *ngIf="item.templateName === 'textfield'" type="text"
[(ngModel)]="item.value" autocorrect="off" name="{{item.typ}}_{{item.id}}" maxlength="{{item.length}}"
[required]="item.required">
<ng-container *ngTemplateOutlet="label; context: {item: item}" />
</ion-input>
<ng-container *ngIf="item.templateName === 'numeric'">
<ion-input type="number" [(ngModel)]="item.value" name="{{item.typ}}_{{item.id}}"
[required]="item.required" />
<ion-input labelPlacement="stacked" type="number" [(ngModel)]="item.value" name="{{item.typ}}_{{item.id}}"
[required]="item.required">
<ng-container *ngTemplateOutlet="label; context: {item: item}" />
</ion-input>
<ion-text *ngIf="item.hasError" color="danger" class="addon-mod_feedback-item-error">
{{ 'addon.mod_feedback.numberoutofrange' | translate }} [{{item.rangefrom}}
<span *ngIf="item.rangefrom && item.rangeto">, </span>{{item.rangeto}}]
</ion-text>
</ng-container>
<ion-textarea *ngIf="item.templateName === 'textarea'" [required]="item.required"
name="{{item.typ}}_{{item.id}}" [(ngModel)]="item.value" />
<ion-textarea labelPlacement="stacked" *ngIf="item.templateName === 'textarea'" [required]="item.required"
name="{{item.typ}}_{{item.id}}" [(ngModel)]="item.value">
<ng-container *ngTemplateOutlet="label; context: {item: item}" />
</ion-textarea>
<ion-select *ngIf="item.templateName === 'multichoice-d'" [required]="item.required"
<ion-select labelPlacement="stacked" *ngIf="item.templateName === 'multichoice-d'" [required]="item.required"
name="{{item.typ}}_{{item.id}}" [(ngModel)]="item.value" interface="action-sheet"
[cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: item.name}">
<ng-container *ngTemplateOutlet="label; context: {item: item}" />
<ion-select-option *ngFor="let option of item.choices" [value]="option.value">
<core-format-text [component]="component" [componentId]="cmId" [text]="option.label"
contextLevel="module" [contextInstanceId]="cmId" [wsNotFiltered]="true" [courseId]="courseId" />
@ -66,23 +69,21 @@
<ion-radio-group *ngIf="item.templateName === 'multichoice-r'" [(ngModel)]="item.value" [required]="item.required"
name="{{item.typ}}_{{item.id}}">
<ion-item *ngFor="let option of item.choices">
<ion-label>
<ion-item *ngFor="let option of item.choices" class="ion-text-wrap">
<ion-radio [value]="option.value">
<core-format-text [component]="component" [componentId]="cmId" [text]="option.label"
contextLevel="module" [contextInstanceId]="cmId" [wsNotFiltered]="true" [courseId]="courseId" />
</ion-label>
<ion-radio slot="start" [value]="option.value" />
</ion-radio>
</ion-item>
</ion-radio-group>
<ng-container *ngIf="item.templateName === 'multichoice-c'">
<ion-item *ngFor="let option of item.choices">
<ion-label>
<ion-checkbox [required]="item.required" name="{{item.typ}}_{{item.id}}" [(ngModel)]="option.checked"
value="option.value">
<core-format-text [component]="component" [componentId]="cmId" [text]="option.label"
contextLevel="module" [contextInstanceId]="cmId" [wsNotFiltered]="true" [courseId]="courseId" />
</ion-label>
<ion-checkbox [required]="item.required" name="{{item.typ}}_{{item.id}}" [(ngModel)]="option.checked"
value="option.value" />
</ion-checkbox>
</ion-item>
</ng-container>
@ -152,3 +153,13 @@
</div>
</core-loading>
</ion-content>
<ng-template #label let-item="item">
<p *ngIf="item.name" [core-mark-required]="item.required" [slot]="item.slottedLabel ? 'label' : undefined">
<span *ngIf="feedback!.autonumbering && item.itemnumber">{{item.itemnumber}}. </span>
<core-format-text [component]="component" [componentId]="cmId" [text]="item.name" contextLevel="module" [contextInstanceId]="cmId"
[courseId]="courseId" [wsNotFiltered]="true" />
<span *ngIf="item.postfix" class="addon-mod_feedback-postfix">{{item.postfix}}</span>
</p>
</ng-template>

View File

@ -251,7 +251,7 @@ export class AddonModFeedbackHelperProvider {
return Object.assign(item, {
templateName: 'label',
value: '',
hasTextInput: false,
slottedLabel: false,
});
}
@ -265,7 +265,7 @@ export class AddonModFeedbackHelperProvider {
const formItem: AddonModFeedbackFormBasicItem = Object.assign(item, {
templateName: 'label',
value: '',
hasTextInput: false,
slottedLabel: false,
});
const type = parseInt(formItem.presentation, 10);
@ -304,7 +304,7 @@ export class AddonModFeedbackHelperProvider {
value: item.rawValue !== undefined ? Number(item.rawValue) : '',
rangefrom: typeof rangeFrom == 'number' && !isNaN(rangeFrom) ? range[0] : '',
rangeto: typeof rangeTo == 'number' && !isNaN(rangeTo) ? rangeTo : '',
hasTextInput: true,
slottedLabel: true,
});
formItem.postfix = this.getNumericBoundariesForDisplay(formItem.rangefrom, formItem.rangeto);
@ -322,7 +322,7 @@ export class AddonModFeedbackHelperProvider {
templateName: 'textfield',
length: Number(item.presentation.split(AddonModFeedbackProvider.LINE_SEP)[1]) || 255,
value: item.rawValue !== undefined ? item.rawValue : '',
hasTextInput: true,
slottedLabel: true,
});
}
@ -336,7 +336,7 @@ export class AddonModFeedbackHelperProvider {
return Object.assign(item, {
templateName: 'textarea',
value: item.rawValue !== undefined ? item.rawValue : '',
hasTextInput: true,
slottedLabel: true,
});
}
@ -356,7 +356,7 @@ export class AddonModFeedbackHelperProvider {
subtype: subType,
value: '',
choices: [],
hasTextInput: false,
slottedLabel: subType === 'd',
});
formItem.presentation = parts.length > 1 ? parts[1] : '';
@ -411,7 +411,7 @@ export class AddonModFeedbackHelperProvider {
const formItem: AddonModFeedbackCaptchaItem = Object.assign(item, {
templateName: 'captcha',
value: '',
hasTextInput: false,
slottedLabel: false,
});
const data = <string[]> CoreTextUtils.parseJSON(item.otherdata);
@ -549,7 +549,7 @@ export type AddonModFeedbackFormItem =
export type AddonModFeedbackFormBasicItem = AddonModFeedbackItem & {
templateName: string;
value: AddonModFeedbackResponseValue;
hasTextInput: boolean;
slottedLabel: boolean;
isEmpty?: boolean;
hasError?: boolean;
};

View File

@ -94,9 +94,9 @@
</ng-container>
<form *ngIf="showForm" [id]="'addon-forum-reply-edit-form-' + uniqueId" #replyFormEl>
<ion-item>
<ion-label position="stacked">{{ 'addon.mod_forum.subject' | translate }}</ion-label>
<ion-input type="text" [placeholder]="'addon.mod_forum.subject' | translate" [(ngModel)]="formData.subject" name="subject" />
<ion-item class="ion-text-wrap">
<ion-input labelPlacement="stacked" type="text" [placeholder]="'addon.mod_forum.subject' | translate"
[(ngModel)]="formData.subject" name="subject" [label]="'addon.mod_forum.subject' | translate" />
</ion-item>
<ion-item>
<ion-label position="stacked">{{ 'addon.mod_forum.message' | translate }}</ion-label>
@ -106,8 +106,9 @@
[draftExtraParams]="{reply: post.id}" (contentChanged)="onMessageChange($event)" />
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="accessInfo.canpostprivatereply">
<ion-label>{{ 'addon.mod_forum.privatereply' | translate }}</ion-label>
<ion-checkbox slot="end" [(ngModel)]="formData.isprivatereply" name="isprivatereply" />
<ion-checkbox [(ngModel)]="formData.isprivatereply" name="isprivatereply">
{{ 'addon.mod_forum.privatereply' | translate }}
</ion-checkbox>
</ion-item>
<ng-container *ngIf="forum.id && forum.maxattachments > 0">
<ion-item button class="divider ion-text-wrap" (click)="toggleAdvanced()" [detail]="false" [attr.aria-expanded]="advanced"

View File

@ -16,9 +16,10 @@
<core-loading [hideUntil]="groupsLoaded">
<form *ngIf="showForm" #newDiscFormEl>
<ion-item>
<ion-label position="stacked">{{ 'addon.mod_forum.subject' | translate }}</ion-label>
<ion-input [(ngModel)]="newDiscussion.subject" type="text" [placeholder]="'addon.mod_forum.subject' | translate"
name="subject" />
<ion-input labelPlacement="stacked" [(ngModel)]="newDiscussion.subject" type="text"
[placeholder]="'addon.mod_forum.subject' | translate" name="subject">
<p>{{ 'addon.mod_forum.subject' | translate }}</p>
</ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">{{ 'addon.mod_forum.message' | translate }}</ion-label>
@ -38,27 +39,30 @@
</ion-item>
<div *ngIf="advanced" id="addon-mod-forum-new-discussion-advanced">
<ion-item *ngIf="showGroups && groupIds.length > 1 && accessInfo.cancanposttomygroups">
<ion-label>{{ 'addon.mod_forum.posttomygroups' | translate }}</ion-label>
<ion-toggle [(ngModel)]="newDiscussion.postToAllGroups" name="postallgroups" slot="end" />
<ion-toggle [(ngModel)]="newDiscussion.postToAllGroups" name="postallgroups">
{{ 'addon.mod_forum.posttomygroups' | translate }}
</ion-toggle>
</ion-item>
<ion-item *ngIf="showGroups" class="core-edit-set-group">
<ion-label>{{ 'addon.mod_forum.group' | translate }}</ion-label>
<ion-item *ngIf="showGroups" class="core-edit-set-group ion-text-wrap">
<ion-select [(ngModel)]="newDiscussion.groupId" [disabled]="newDiscussion.postToAllGroups" interface="action-sheet"
name="groupid" [interfaceOptions]="{header: 'addon.mod_forum.group' | translate}"
[cancelText]="'core.cancel' | translate" (ionChange)="calculateGroupName()">
<p class="item-heading" slot="label">{{ 'addon.mod_forum.group' | translate }}</p>
<ion-select-option *ngFor="let group of groups" [value]="group.id">
<core-format-text [text]="group.name" contextLevel="course" [contextInstanceId]="courseId"
[wsNotFiltered]="true" />
</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label>{{ 'addon.mod_forum.discussionsubscription' | translate }}</ion-label>
<ion-toggle [(ngModel)]="newDiscussion.subscribe" name="subscribe" slot="end" />
<ion-item class="ion-text-wrap">
<ion-toggle [(ngModel)]="newDiscussion.subscribe" name="subscribe">
{{ 'addon.mod_forum.discussionsubscription' | translate }}
</ion-toggle>
</ion-item>
<ion-item *ngIf="canPin">
<ion-label>{{ 'addon.mod_forum.discussionpinned' | translate }}</ion-label>
<ion-toggle [(ngModel)]="newDiscussion.pin" name="pin" slot="end" />
<ion-item *ngIf="canPin" class="ion-text-wrap">
<ion-toggle [(ngModel)]="newDiscussion.pin" name="pin">
{{ 'addon.mod_forum.discussionpinned' | translate }}
</ion-toggle>
</ion-item>
<core-attachments *ngIf="canCreateAttachments && forum && forum.maxattachments > 0" [files]="newDiscussion.files"
[maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component" [componentId]="forum.cmid"

View File

@ -12,7 +12,7 @@
<ion-content class="limited-width">
<div>
<ion-card class="core-danger-card" *ngIf="searchBanner">
<ion-item>
<ion-item class="ion-text-wrap">
<ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true" />
<ion-label>
<core-format-text [text]="searchBanner" />

View File

@ -1,8 +1,9 @@
<ion-content>
<ion-radio-group [(ngModel)]="selectedMode" (ionChange)="modePicked()">
<ion-item class="ion-text-wrap" *ngFor="let mode of modes">
<ion-label>{{ mode.langkey | translate }}</ion-label>
<ion-radio slot="end" [value]="mode.key" />
<ion-radio [value]="mode.key">
{{ mode.langkey | translate }}
</ion-radio>
</ion-item>
</ion-radio-group>
</ion-content>

View File

@ -14,8 +14,8 @@
<core-loading [hideUntil]="loaded">
<form #editFormEl *ngIf="glossary">
<ion-item>
<ion-label position="stacked">{{ 'addon.mod_glossary.concept' | translate }}</ion-label>
<ion-input type="text" [placeholder]="'addon.mod_glossary.concept' | translate" [(ngModel)]="data.concept" name="concept" />
<ion-input labelPlacement="stacked" type="text" [placeholder]="'addon.mod_glossary.concept' | translate"
[(ngModel)]="data.concept" name="concept" [label]="'addon.mod_glossary.concept' | translate" />
</ion-item>
<ion-item>
<ion-label position="stacked">{{ 'addon.mod_glossary.definition' | translate }}</ion-label>
@ -25,22 +25,20 @@
[draftExtraParams]="editorExtraParams" />
</ion-item>
<ion-item *ngIf="categories.length > 0">
<ion-label position="stacked">
{{ 'addon.mod_glossary.categories' | translate }}
</ion-label>
<ion-select [(ngModel)]="data.categories" multiple="true" interface="action-sheet"
<ion-select labelPlacement="stacked" [(ngModel)]="data.categories" multiple="true" interface="action-sheet"
[placeholder]="'addon.mod_glossary.categories' | translate" name="categories" [cancelText]="'core.cancel' | translate"
[interfaceOptions]="{header: 'addon.mod_glossary.categories' | translate}">
[interfaceOptions]="{header: 'addon.mod_glossary.categories' | translate}"
[label]="'addon.mod_glossary.categories' | translate">
<ion-select-option *ngFor="let category of categories" [value]="category.id">
{{ category.name }}
</ion-select-option>
</ion-select>
</ion-item>
<ion-item *ngIf="showAliases">
<ion-label position="stacked">
{{ 'addon.mod_glossary.aliases' | translate }}
</ion-label>
<ion-textarea [(ngModel)]="data.aliases" rows="1" [core-auto-rows]="data.aliases" name="aliases" />
<ion-textarea labelPlacement="stacked" [(ngModel)]="data.aliases" rows="1" [core-auto-rows]="data.aliases" name="aliases"
[label]="'addon.mod_glossary.aliases' | translate" />
</ion-item>
<ion-item-divider>
<ion-label>
@ -56,16 +54,19 @@
</ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap">
<ion-label>{{ 'addon.mod_glossary.entryusedynalink' | translate }}</ion-label>
<ion-toggle [(ngModel)]="data.usedynalink" name="usedynalink" slot="end" />
<ion-toggle [(ngModel)]="data.usedynalink" name="usedynalink">
{{ 'addon.mod_glossary.entryusedynalink' | translate }}
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>{{ 'addon.mod_glossary.casesensitive' | translate }}</ion-label>
<ion-toggle [disabled]="!data.usedynalink" [(ngModel)]="data.casesensitive" name="casesensitive" slot="end" />
<ion-toggle [disabled]="!data.usedynalink" [(ngModel)]="data.casesensitive" name="casesensitive">
{{ 'addon.mod_glossary.casesensitive' | translate }}
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>{{ 'addon.mod_glossary.fullmatch' | translate }}</ion-label>
<ion-toggle [disabled]="!data.usedynalink" [(ngModel)]="data.fullmatch" name="fullmatch" slot="end" />
<ion-toggle [disabled]="!data.usedynalink" [(ngModel)]="data.fullmatch" name="fullmatch">
{{ 'addon.mod_glossary.fullmatch' | translate }}
</ion-toggle>
</ion-item>
</ng-container>
<ion-button class="ion-margin" expand="block" [disabled]="!data.concept || !data.definition" (click)="save()">

View File

@ -28,11 +28,11 @@
<ion-card *ngIf="askPassword">
<form (ngSubmit)="submitPassword($event, passwordinput)" #passwordForm>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label>
<core-show-password name="password">
<ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}"
core-auto-focus #passwordinput [clearOnEdit]="false" />
</core-show-password>
<ion-input labelPlacement="stacked" name="password" type="password"
placeholder="{{ 'core.login.password' | translate }}" core-auto-focus #passwordinput [clearOnEdit]="false"
[label]="'addon.mod_lesson.enterpassword' | translate">
<core-show-password slot="end" />
</ion-input>
</ion-item>
<ion-button expand="block" type="submit">
{{ 'addon.mod_lesson.continue' | translate }}

View File

@ -71,10 +71,9 @@
<!-- Short answer. -->
<ion-item class="ion-text-wrap" *ngSwitchCase="'shortanswer'">
<ion-label class="sr-only" stacked />
<ion-input [type]="question.input!.type" placeholder="{{ 'addon.mod_lesson.youranswer' | translate }}"
[id]="question.input!.id" [formControlName]="question.input!.name" autocorrect="off"
[maxlength]="question.input!.maxlength" />
[attr.aria-label]="'addon.mod_lesson.youranswer' | translate" [id]="question.input!.id"
[formControlName]="question.input!.name" autocorrect="off" [maxlength]="question.input!.maxlength" />
</ion-item>
<!-- Essay. -->
@ -103,22 +102,20 @@
<!-- Single choice. -->
<ion-radio-group *ngIf="!question.multi" [formControlName]="question.controlName">
<ion-item class="ion-text-wrap" *ngFor="let option of question.options">
<ion-label>
<ion-radio [id]="option.id" [value]="option.value" [disabled]="option.disabled">
<core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="option.text"
contextLevel="module" [contextInstanceId]="lesson.coursemodule" [courseId]="courseId" />
</ion-label>
<ion-radio slot="end" [id]="option.id" [value]="option.value" [disabled]="option.disabled" />
</ion-radio>
</ion-item>
</ion-radio-group>
<!-- Multiple choice. -->
<ng-container *ngIf="question.multi">
<ion-item class="ion-text-wrap" *ngFor="let option of question.options">
<ion-label>
<ion-checkbox [id]="option.id" [formControlName]="option.name">
<core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="option.text"
contextLevel="module" [contextInstanceId]="lesson.coursemodule" [courseId]="courseId" />
</ion-label>
<ion-checkbox [id]="option.id" [formControlName]="option.name" slot="end" />
</ion-checkbox>
</ion-item>
</ng-container>
</ng-container>
@ -126,14 +123,11 @@
<!-- Matching. -->
<ng-container *ngSwitchCase="'matching'">
<ion-item class="ion-text-wrap" *ngFor="let row of question.rows">
<ion-label>
<p>
<core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="row.text"
contextLevel="module" [contextInstanceId]="lesson.coursemodule" [courseId]="courseId" />
</p>
</ion-label>
<ion-select [id]="row.id" [formControlName]="row.name" [cancelText]="'core.cancel' | translate"
interface="action-sheet">
<core-format-text slot="label" [component]="component" [componentId]="lesson.coursemodule"
[text]="row.text" contextLevel="module" [contextInstanceId]="lesson.coursemodule"
[courseId]="courseId" />
<ion-select-option *ngFor="let option of row.options" [value]="option.value">
{{option.label}}
</ion-select-option>

View File

@ -26,10 +26,10 @@
<!-- Retake selector if there is more than one retake. -->
<ion-item class="ion-text-wrap" *ngIf="student.attempts && student.attempts.length > 1">
<ion-label>{{ 'addon.mod_lesson.attemptheader' | translate }}</ion-label>
<ion-select [(ngModel)]="selectedRetake" (ionChange)="changeRetake(selectedRetake!)"
[cancelText]="'core.cancel' | translate" interface="action-sheet"
[interfaceOptions]="{header: 'addon.mod_lesson.attemptheader' | translate}">
<p slot="label" class="item-heading">{{ 'addon.mod_lesson.attemptheader' | translate }}</p>
<ion-select-option *ngFor="let retake of student.attempts" [value]="retake.try">
{{retake.label}}
</ion-select-option>
@ -129,7 +129,7 @@
<!-- Truefalse or multichoice. -->
<ion-item class="ion-text-wrap" *ngIf="answer[0].isCheckbox"
[ngClass]="{'addon-mod_lesson-highlight': answer[0].highlight}">
<ion-label>
<ion-checkbox [attr.name]="answer[0].name" [ngModel]="answer[0].checked" [disabled]="true">
<p>
<core-format-text [component]="component" [componentId]="lesson?.coursemodule"
[text]="answer[0].content" contextLevel="module" [contextInstanceId]="lesson?.coursemodule"
@ -143,8 +143,7 @@
<ion-badge *ngIf="answer[0].successBadge" color="success" class="addon-mod_lesson-answer-success">
{{ answer[0].successBadge }}
</ion-badge>
</ion-label>
<ion-checkbox [attr.name]="answer[0].name" [ngModel]="answer[0].checked" [disabled]="true" slot="end" />
</ion-checkbox>
</ion-item>
<!-- Short answer or numeric. -->

View File

@ -5,9 +5,9 @@
</ion-label>
</ion-item>
<ion-item [formGroup]="form">
<ion-label class="sr-only">{{ 'addon.mod_quiz.quizpassword' | translate }}</ion-label>
<core-show-password [name]="'quizpassword'">
<ion-input id="addon-mod_quiz-accessrule-password-input" name="quizpassword" type="password"
placeholder="{{ 'addon.mod_quiz.quizpassword' | translate }}" [formControlName]="'quizpassword'" [clearOnEdit]="false" />
</core-show-password>
<ion-input id="addon-mod_quiz-accessrule-password-input" name="quizpassword" type="password"
placeholder="{{ 'addon.mod_quiz.quizpassword' | translate }}" [formControlName]="'quizpassword'" [clearOnEdit]="false"
[attr.aria-label]="'addon.mod_quiz.quizpassword' | translate">
<core-show-password slot="end" />
</ion-input>
</ion-item>

View File

@ -110,10 +110,10 @@
</ion-card-header>
<ion-list>
<ion-item class="ion-text-wrap" *ngIf="organizations.length > 1">
<ion-label>{{ 'addon.mod_scorm.organizations' | translate }}</ion-label>
<ion-select [(ngModel)]="currentOrganization.identifier" (ionChange)="loadOrganization()"
[cancelText]="'core.cancel' | translate" interface="action-sheet"
[interfaceOptions]="{header: 'addon.mod_scorm.organizations' | translate}">
<p class="item-heading" slot="label">{{ 'addon.mod_scorm.organizations' | translate }}</p>
<ion-select-option *ngFor="let org of organizations" [value]="org.identifier">
{{ org.title }}
</ion-select-option>
@ -182,8 +182,9 @@
<ng-container *ngIf="!downloading && !skip">
<!-- Create new attempt -->
<ion-item class="ion-text-wrap" *ngIf="!scorm.forcenewattempt && numAttempts > 0 && !incomplete && attemptsLeft > 0">
<ion-label>{{ 'addon.mod_scorm.newattempt' | translate }}</ion-label>
<ion-checkbox slot="end" name="newAttempt" [(ngModel)]="startNewAttempt" />
<ion-checkbox name="newAttempt" [(ngModel)]="startNewAttempt">
{{ 'addon.mod_scorm.newattempt' | translate }}
</ion-checkbox>
</ion-item>
<ion-item *ngIf="statusMessage">

View File

@ -101,12 +101,12 @@
</ion-row>
<ion-item *ngIf="question.type === 0" class="ion-text-wrap" [class.even]="isEven">
<ion-label position="floating">
<span [core-mark-required]="question.required">
<ion-textarea labelPlacement="floating" [(ngModel)]="answers[question.name]" [name]="question.name"
[required]="question.required">
<p slot="label" [core-mark-required]="question.required">
<strong>{{question.num}}.</strong> {{ question.text }}
</span>
</ion-label>
<ion-textarea [(ngModel)]="answers[question.name]" [name]="question.name" [required]="question.required" />
</p>
</ion-textarea>
</ion-item>
</ng-container>

View File

@ -20,8 +20,8 @@
<core-loading [hideUntil]="loaded">
<form [formGroup]="pageForm" #editPageForm *ngIf="loaded">
<ion-item class="ion-text-wrap" *ngIf="canEditTitle">
<ion-label class="sr-only">{{ 'addon.mod_wiki.newpagetitle' | translate }}</ion-label>
<ion-input name="title" type="text" [placeholder]="'addon.mod_wiki.newpagetitle' | translate" formControlName="title" />
<ion-input [attr.aria-label]="'addon.mod_wiki.newpagetitle' | translate" name="title" type="text"
[placeholder]="'addon.mod_wiki.newpagetitle' | translate" formControlName="title" />
</ion-item>
<ion-item>

View File

@ -7,14 +7,12 @@
</ion-label>
</ion-item>
<ion-item *ngIf="edit && field.grades">
<ion-label position="stacked">
<span [core-mark-required]="true">
<ion-select labelPlacement="stacked" [(ngModel)]="selectedValues[n].grade" [cancelText]="'core.cancel' | translate"
interface="action-sheet" [interfaceOptions]="{header: 'addon.mod_workshop_assessment_accumulative.dimensiongradefor' |
translate : {'$a': field.dimtitle }}">
<div [core-mark-required]="true" slot="label">
{{ 'addon.mod_workshop_assessment_accumulative.dimensiongradefor' | translate : {'$a': field.dimtitle } }}
</span>
</ion-label>
<ion-select [interfaceOptions]="{header: 'addon.mod_workshop_assessment_accumulative.dimensiongradefor' |
translate : {'$a': field.dimtitle }}" [(ngModel)]="selectedValues[n].grade" [cancelText]="'core.cancel' | translate"
interface="action-sheet">
</div>
<ion-select-option *ngFor="let grade of field.grades" [value]="grade.value">{{grade.label}}</ion-select-option>
</ion-select>
<core-input-errors *ngIf="fieldErrors['grade_' + n]" [errorText]="fieldErrors['grade_' + n]" />
@ -29,10 +27,9 @@
</ion-label>
</ion-item>
<ion-item *ngIf="edit">
<ion-label position="stacked">
{{ 'addon.mod_workshop_assessment_accumulative.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}
</ion-label>
<ion-textarea [(ngModel)]="selectedValues[n].peercomment" [core-auto-rows]="selectedValues[n].peercomment" />
<ion-textarea labelPlacement="stacked" [(ngModel)]="selectedValues[n].peercomment"
[core-auto-rows]="selectedValues[n].peercomment"
[label]=" 'addon.mod_workshop_assessment_accumulative.dimensioncommentfor' | translate : {'$a': field.dimtitle }" />
</ion-item>
<ion-item *ngIf="!edit" class="ion-text-wrap">
<ion-label>

View File

@ -7,12 +7,12 @@
</ion-label>
</ion-item>
<ion-item *ngIf="edit">
<ion-label position="stacked">
<span [core-mark-required]="true">
<ion-textarea labelPlacement="stacked" [(ngModel)]="selectedValues[n].peercomment"
[core-auto-rows]="selectedValues[n].peercomment">
<div [core-mark-required]="true" slot="label">
{{ 'addon.mod_workshop_assessment_comments.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}
</span>
</ion-label>
<ion-textarea [(ngModel)]="selectedValues[n].peercomment" [core-auto-rows]="selectedValues[n].peercomment" />
</div>
</ion-textarea>
<core-input-errors *ngIf="fieldErrors['peercomment_' + n]" [errorText]="fieldErrors['peercomment_' + n]" />
</ion-item>
<ion-item *ngIf="!edit" class="ion-text-wrap">

View File

@ -10,32 +10,28 @@
<ion-radio-group [(ngModel)]="selectedValues[n].grade" [name]="'grade_' + n">
<ion-item>
<ion-label position="stacked">
<span [core-mark-required]="edit">
<p [core-mark-required]="edit">
{{ 'addon.mod_workshop.yourassessmentfor' | translate : {'$a': field.dimtitle } }}
</span>
</p>
</ion-label>
<core-input-errors *ngIf="edit && fieldErrors['grade_' + n]" [errorText]="fieldErrors['grade_' + n]" />
</ion-item>
<ion-item>
<ion-label>
<ion-radio [value]="-1" [disabled]="!edit" labelPlacement="end" justify="start">
<core-format-text [text]="field.grade0" [filter]="false" />
</ion-label>
<ion-radio slot="start" [value]="-1" [disabled]="!edit" />
</ion-radio>
</ion-item>
<ion-item>
<ion-label>
<ion-radio [value]="1" [disabled]="!edit" labelPlacement="end" justify="start">
<core-format-text [text]="field.grade1" [filter]="false" />
</ion-label>
<ion-radio slot="start" [value]="1" [disabled]="!edit" />
</ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>
<ion-item *ngIf="edit">
<ion-label position="stacked">
{{ 'addon.mod_workshop_assessment_numerrors.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}
</ion-label>
<ion-textarea [(ngModel)]="selectedValues[n].peercomment" [name]="'peercomment_' + n"
[core-auto-rows]="selectedValues[n].peercomment" />
<ion-textarea labelPlacement="stacked" [(ngModel)]="selectedValues[n].peercomment" [name]="'peercomment_' + n"
[core-auto-rows]="selectedValues[n].peercomment"
[label]="'addon.mod_workshop_assessment_numerrors.dimensioncommentfor' | translate : {'$a': field.dimtitle }" />
</ion-item>
<ion-item *ngIf="!edit" class="ion-text-wrap">
<ion-label>

View File

@ -10,13 +10,12 @@
<ion-list>
<ion-radio-group [(ngModel)]="selectedValues[n].chosenlevelid" [name]="'chosenlevelid_' + n">
<ion-item *ngFor="let subfield of field.fields">
<ion-label>
<ion-radio [value]="subfield.levelid" [disabled]="!edit" labelPlacement="end" justify="start">
<p>
<core-format-text [text]="subfield.definition" contextLevel="module" [contextInstanceId]="moduleId"
[courseId]="courseId" />
</p>
</ion-label>
<ion-radio slot="start" [value]="subfield.levelid" [disabled]="!edit" />
</ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>

View File

@ -21,7 +21,7 @@
<h3 class="item-heading">{{ 'addon.mod_workshop.overallfeedback' | translate }}</h3>
</ion-label>
</ion-item>
<ion-item position="stacked" *ngIf="edit">
<ion-item *ngIf="edit">
<ion-label position="stacked">
<span [core-mark-required]="overallFeedkbackRequired">
{{ 'addon.mod_workshop.feedbackauthor' | translate }}
@ -37,13 +37,12 @@
[maxSize]="workshop.overallfeedbackmaxbytes" [maxSubmissions]="workshop.overallfeedbackfiles" [component]="component"
[componentId]="componentId" [allowOffline]="true" [courseId]="workshop.course" />
<ion-item *ngIf="edit && access && access.canallocate">
<ion-label position="stacked">
<span [core-mark-required]="true">
{{ 'addon.mod_workshop.assessmentweight' | translate }}
</span>
</ion-label>
<ion-select [(ngModel)]="weight" interface="action-sheet" name="weight" [cancelText]="'core.cancel' | translate"
<ion-select labelPlacement="stacked" [(ngModel)]="weight" interface="action-sheet" name="weight"
[cancelText]="'core.cancel' | translate"
[interfaceOptions]="{header: 'addon.mod_workshop.assessmentweight' | translate}">
<div [core-mark-required]="true" slot="label">
{{ 'addon.mod_workshop.assessmentweight' | translate }}
</div>
<ion-select-option *ngFor="let w of weights" [value]="w">{{w}}</ion-select-option>
</ion-select>
</ion-item>

View File

@ -58,13 +58,12 @@
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="access?.canallocate">
<ion-label position="stacked">
<span core-mark-required="true">
{{ 'addon.mod_workshop.assessmentweight' | translate }}
</span>
</ion-label>
<ion-select formControlName="weight" required="true" interface="action-sheet" [cancelText]="'core.cancel' | translate"
<ion-select labelPlacement="stacked" formControlName="weight" required="true" interface="action-sheet"
[cancelText]="'core.cancel' | translate"
[interfaceOptions]="{header: 'addon.mod_workshop.assessmentweight' | translate}">
<div [core-mark-required]="true" slot="label">
{{ 'addon.mod_workshop.assessmentweight' | translate }}
</div>
<ion-select-option *ngFor="let w of weights" [value]="w">{{ w }}</ion-select-option>
</ion-select>
</ion-item>
@ -75,9 +74,10 @@
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="access?.canoverridegrades">
<ion-label position="stacked">{{ 'addon.mod_workshop.gradinggradeover' | translate }}</ion-label>
<ion-select formControlName="grade" interface="action-sheet" [cancelText]="'core.cancel' | translate"
[interfaceOptions]="{header: 'addon.mod_workshop.gradinggradeover' | translate}">
<ion-select labelPlacement="stacked" formControlName="grade" interface="action-sheet"
[cancelText]="'core.cancel' | translate"
[interfaceOptions]="{header: 'addon.mod_workshop.gradinggradeover' | translate}"
[label]="'addon.mod_workshop.gradinggradeover' | translate">
<ion-select-option *ngFor="let grade of evaluationGrades" [value]="grade.value">
{{grade.label}}
</ion-select-option>

View File

@ -17,20 +17,19 @@
<core-loading [hideUntil]="loaded">
<form [formGroup]="editForm" *ngIf="workshop" #editFormEl>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<span core-mark-required="true">
<ion-input labelPlacement="stacked" name="title" type="text"
[placeholder]="'addon.mod_workshop.submissiontitle' | translate" formControlName="title">
<div [core-mark-required]="true" slot="label">
{{ 'addon.mod_workshop.submissiontitle' | translate }}
</span>
</ion-label>
<ion-input name="title" type="text" [placeholder]="'addon.mod_workshop.submissiontitle' | translate"
formControlName="title" />
</div>
</ion-input>
</ion-item>
<ion-item *ngIf="textAvailable">
<ion-label position="stacked">
<span [core-mark-required]="textRequired">
<div [core-mark-required]="textRequired">
{{ 'addon.mod_workshop.submissioncontent' | translate }}
</span>
</div>
</ion-label>
<core-rich-text-editor [control]="editForm.controls['content']" name="content"
[placeholder]="'addon.mod_workshop.submissioncontent' | translate" [component]="component" [componentId]="componentId"

View File

@ -100,11 +100,10 @@
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="access.canpublishsubmissions">
<ion-label>
<ion-toggle formControlName="published">
<p class="item-heading">{{ 'addon.mod_workshop.publishsubmission' | translate }}</p>
<p>{{ 'addon.mod_workshop.publishsubmission_help' | translate }}</p>
</ion-label>
<ion-toggle formControlName="published" slot="end" />
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap">
@ -114,9 +113,9 @@
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">{{ 'addon.mod_workshop.gradeover' | translate }}</ion-label>
<ion-select formControlName="grade" interface="action-sheet" [cancelText]="'core.cancel' | translate"
[interfaceOptions]="{header: 'addon.mod_workshop.gradeover' | translate}">
<ion-select labelPlacement="stacked" formControlName="grade" interface="action-sheet"
[cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'addon.mod_workshop.gradeover' | translate}"
[label]="'addon.mod_workshop.gradeover' | translate">
<ion-select-option *ngFor="let grade of evaluationGrades" [value]="grade.value">
{{grade.label}}
</ion-select-option>

View File

@ -13,16 +13,15 @@
<ion-content>
<form name="itemEdit" (ngSubmit)="addNote($event)" #itemEdit>
<ion-item>
<ion-label>{{ 'addon.notes.publishstate' | translate }}</ion-label>
<ion-select [(ngModel)]="type" name="publishState" interface="popover">
<ion-select-option value="personal">{{ 'addon.notes.personalnotes' | translate }}</ion-select-option>
<ion-select [(ngModel)]="type" name="publishState" interface="popover" [label]="'addon.notes.publishstate' | translate">
<ion-select-option value=" personal">{{ 'addon.notes.personalnotes' | translate }}</ion-select-option>
<ion-select-option value="course">{{ 'addon.notes.coursenotes' | translate }}</ion-select-option>
<ion-select-option value="site">{{ 'addon.notes.sitenotes' | translate }}</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label class="sr-only">{{ 'addon.notes.note' | translate }}</ion-label>
<ion-textarea placeholder="{{ 'addon.notes.note' | translate }}" rows="5" [(ngModel)]="text" name="text" required="required" />
<ion-textarea [attr.aria-label]="'addon.notes.note' | translate" placeholder="{{ 'addon.notes.note' | translate }}" rows="5"
[(ngModel)]="text" name="text" required="required" />
</ion-item>
<div class="ion-padding">
<ion-button expand="block" type="submit" [disabled]="processing || text.length < 2">

View File

@ -22,26 +22,22 @@
<core-loading [hideUntil]="preferencesLoaded">
<ion-card>
<ion-item class="ion-text-wrap" *ngIf="preferences">
<ion-label>
<ion-toggle [(ngModel)]="preferences.enableall" (ngModelChange)="enableAll(preferences.enableall)">
<p class="item-heading">{{ 'addon.notifications.allownotifications' | translate }}</p>
</ion-label>
<ion-toggle [(ngModel)]="preferences.enableall" (ngModelChange)="enableAll(preferences.enableall)" slot="end" />
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="canChangeSound">
<ion-label>
<ion-toggle [(ngModel)]="notificationSound" (ngModelChange)="changeNotificationSound(notificationSound)">
<p class="item-heading">{{ 'addon.notifications.playsound' | translate }}</p>
</ion-label>
<ion-toggle [(ngModel)]="notificationSound" (ngModelChange)="changeNotificationSound(notificationSound)" slot="end" />
</ion-toggle>
</ion-item>
</ion-card>
<ion-card>
<ion-item class="ion-text-wrap only-links" *ngIf="preferences?.processors?.length" lines="none" [button]="false">
<ion-label class="addon-notification-type-form">
<p class="item-heading">{{ 'addon.notifications.typeofnotification' | translate }}</p>
</ion-label>
<ion-item class="ion-text-wrap addon-notification-type-form" *ngIf="preferences?.processors?.length" lines="none">
<!-- Show processor selector. -->
<ion-select [(ngModel)]="currentProcessorName" (ionChange)="changeProcessor($event)" interface="popover">
<p class="item-heading" slot="label">{{ 'addon.notifications.typeofnotification' | translate }}</p>
<ion-select-option class="ion-text-wrap" *ngFor="let processor of preferences?.processors" [value]="processor.name">
{{ processor.displayname }}
</ion-select-option>

View File

@ -7,8 +7,9 @@
<ion-radio-group [(ngModel)]="question.behaviourCertaintySelected" [name]="question.behaviourCertaintyOptions[0].name">
<ion-item class="ion-text-wrap" *ngFor="let option of question.behaviourCertaintyOptions">
<ion-label>{{ option.text }}</ion-label>
<ion-radio slot="end" id="{{option.id}}" [value]="option.value" [disabled]="option.disabled" />
<ion-radio id="{{option.id}}" [value]="option.value" [disabled]="option.disabled">
{{ option.text }}
</ion-radio>
</ion-item>
</ion-radio-group>

View File

@ -12,18 +12,16 @@
</ng-container>
<ion-item *ngIf="question.input" class="ion-text-wrap core-{{question.input.correctIconColor}}-item">
<ion-label position="stacked">{{ 'addon.mod_quiz.answercolon' | translate }}</ion-label>
<div class="flex-row">
<div class="flex-row ion-align-items-end">
<!-- Display unit select before the answer input. -->
<ng-container *ngIf="question.select && question.selectFirst">
<ng-container *ngTemplateOutlet="selectUnits" />
</ng-container>
<!-- Input to enter the answer. -->
<ion-input type="text" [attr.name]="question.input.name"
<ion-input labelPlacement="stacked" type="text" [attr.name]="question.input.name"
[placeholder]="question.input.readOnly ? '' : 'core.question.answer' | translate" [value]="question.input.value"
[disabled]="question.input.readOnly" autocorrect="off" />
[disabled]="question.input.readOnly" autocorrect="off" [label]="'addon.mod_quiz.answercolon' | translate" />
<!-- Display unit select after the answer input. -->
<ng-container *ngIf="question.select && !question.selectFirst">
@ -58,9 +56,10 @@
<ng-template #radioUnits>
<ion-radio-group [(ngModel)]="question!.unit" [name]="question!.optionsName">
<ion-item class="ion-text-wrap" *ngFor="let option of question!.options">
<ion-label>{{ option.text }}</ion-label>
<ion-radio slot="end" [value]="option.value" [disabled]="option.disabled || question!.input?.readOnly"
[color]="question!.input?.correctIconColor" />
<ion-radio [value]="option.value" [disabled]="option.disabled || question!.input?.readOnly"
[color]="question!.input?.correctIconColor">
{{ option.text }}
</ion-radio>
</ion-item>
<!-- ion-radio doesn't use an input. Create a hidden input to hold the selected value. -->

View File

@ -0,0 +1,3 @@
.flex-row {
width: 100%;
}

View File

@ -22,6 +22,7 @@ import { AddonModQuizCalculatedQuestion, CoreQuestionBaseComponent } from '@feat
@Component({
selector: 'addon-qtype-calculated',
templateUrl: 'addon-qtype-calculated.html',
styleUrls: ['calculated.scss'],
})
export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent<AddonModQuizCalculatedQuestion> {

View File

@ -11,19 +11,18 @@
<ng-container *ngIf="!review">
<!-- Textarea. -->
<ion-item *ngIf="question.textarea && (!question.hasDraftFiles || uploadFilesSupported)">
<ion-label class="sr-only">{{ 'core.question.answer' | translate }}</ion-label>
<!-- "Format" and draftid hidden inputs -->
<input *ngIf="question.formatInput" type="hidden" [name]="question.formatInput.name" [value]="question.formatInput.value">
<input *ngIf="question.answerDraftIdInput" type="hidden" [name]="question.answerDraftIdInput.name"
[value]="question.answerDraftIdInput.value">
<!-- Plain text textarea. -->
<ion-textarea *ngIf="question.isPlainText" class="core-question-textarea" [ngClass]='{"core-monospaced": question.isMonospaced}'
placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.textarea.name"
[ngModel]="question.textarea.text" />
<ion-textarea *ngIf="question.isPlainText" [attr.aria-label]="'core.question.answer' | translate" class="core-question-textarea"
[ngClass]='{"core-monospaced": question.isMonospaced}' placeholder="{{ 'core.question.answer' | translate }}"
[attr.name]="question.textarea.name" [ngModel]="question.textarea.text" />
<!-- Rich text editor. -->
<core-rich-text-editor *ngIf="!question.isPlainText" placeholder="{{ 'core.question.answer' | translate }}"
[control]="formControl" [name]="question.textarea.name" [component]="component" [componentId]="componentId"
[autoSave]="false" />
<core-rich-text-editor *ngIf="!question.isPlainText" [attr.aria-label]="'core.question.answer' | translate"
placeholder="{{ 'core.question.answer' | translate }}" [control]="formControl" [name]="question.textarea.name"
[component]="component" [componentId]="componentId" [autoSave]="false" />
</ion-item>
<!-- Draft files not supported. -->

View File

@ -6,17 +6,17 @@
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngFor="let row of question.rows">
<ion-label>
<core-format-text id="addon-qtype-match-question-{{row.id}}" [component]="component" [componentId]="componentId"
[text]="row.text" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" />
<label class="accesshide" for="{{row.id}}" *ngIf="row.accessibilityLabel">
{{ row.accessibilityLabel }}
</label>
</ion-label>
<ion-select id="{{row.id}}" [name]="row.name" [(ngModel)]="row.selected" interface="action-sheet"
[attr.aria-labelledby]="'addon-qtype-match-question-' + row.id" [disabled]="row.disabled"
<ion-select id="{{row.id}}" [name]="row.name" [(ngModel)]="row.selected" interface="action-sheet" [disabled]="row.disabled"
[cancelText]="'core.cancel' | translate"
[ngClass]="{'addon-qtype-match-correct': row.isCorrect === 1,'addon-qtype-match-incorrect': row.isCorrect === 0}">
<div slot="label">
<core-format-text [component]="component" [componentId]="componentId" [text]="row.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId" />
<label class="accesshide" for="{{row.id}}" *ngIf="row.accessibilityLabel">
{{ row.accessibilityLabel }}
</label>
</div>
<ion-select-option *ngFor="let option of row.options" [value]="option.value">
{{option.label}}
</ion-select-option>

View File

@ -16,21 +16,21 @@
<!-- Checkbox for multiple choice. -->
<ng-container *ngIf="question.multi">
<ion-item class="ion-text-wrap answer" *ngFor="let option of question.options">
<ion-label [color]='(option.isCorrect === 1 ? "success": "") + (option.isCorrect === 0 ? "danger": "")' [class]="option.class">
<core-format-text [component]="component" [componentId]="componentId" [text]="option.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId" />
<div *ngIf="option.feedback" class="specificfeedback">
<core-format-text [component]="component" [componentId]="componentId" [text]="option.feedback"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" />
<ion-checkbox [attr.name]="option.name" [(ngModel)]="option.checked" [disabled]="option.disabled"
[color]='(option.isCorrect === 1 ? "success": "") + (option.isCorrect === 0 ? "danger": "")'>
<div [class]="option.class">
<core-format-text [component]="component" [componentId]="componentId" [text]="option.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId" />
<div *ngIf="option.feedback" class="specificfeedback">
<core-format-text [component]="component" [componentId]="componentId" [text]="option.feedback"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" />
</div>
</div>
</ion-label>
</ion-checkbox>
<ion-checkbox slot="end" [attr.name]="option.name" [(ngModel)]="option.checked" [disabled]="option.disabled"
[color]='(option.isCorrect === 1 ? "success": "") + (option.isCorrect === 0 ? "danger": "")' />
<ion-icon *ngIf="option.isCorrect === 1" class="core-correct-icon" name="fas-check" color="success"
<ion-icon slot="end" *ngIf="option.isCorrect === 1" class="core-correct-icon" name="fas-check" color="success"
[attr.aria-label]="'core.question.correct' | translate" />
<ion-icon *ngIf="option.isCorrect === 0" class="core-correct-icon" name="fas-xmark" color="danger"
<ion-icon slot="end" *ngIf="option.isCorrect === 0" class="core-correct-icon" name="fas-xmark" color="danger"
[attr.aria-label]="'core.question.incorrect' | translate" />
<!-- ion-checkbox doesn't use an input. Create a hidden input to hold the value. -->
@ -41,21 +41,21 @@
<!-- Radio buttons for single choice. -->
<ion-radio-group *ngIf="!question.multi" [(ngModel)]="question.singleChoiceModel" [name]="question.optionsName">
<ion-item class="ion-text-wrap answer" *ngFor="let option of question.options">
<ion-label [class]="option.class">
<core-format-text [component]="component" [componentId]="componentId" [text]="option.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId" />
<div *ngIf="option.feedback" class="specificfeedback">
<core-format-text [component]="component" [componentId]="componentId" [text]="option.feedback"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" />
<ion-radio [value]="option.value" [disabled]="option.disabled"
[color]='(option.isCorrect === 1 ? "success": "") + (option.isCorrect === 0 ? "danger": "")'>
<div [class]="option.class">
<core-format-text [component]="component" [componentId]="componentId" [text]="option.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId" />
<div *ngIf="option.feedback" class="specificfeedback">
<core-format-text [component]="component" [componentId]="componentId" [text]="option.feedback"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" />
</div>
</div>
</ion-label>
</ion-radio>
<ion-radio [value]="option.value" [disabled]="option.disabled" slot="end"
[color]='(option.isCorrect === 1 ? "success": "") + (option.isCorrect === 0 ? "danger": "")' />
<ion-icon *ngIf="option.isCorrect === 1" class="core-correct-icon" name="fas-check" color="success"
<ion-icon slot="end" *ngIf="option.isCorrect === 1" class="core-correct-icon" name="fas-check" color="success"
[attr.aria-label]="'core.question.correct' | translate" />
<ion-icon *ngIf="option.isCorrect === 0" class="core-correct-icon" name="fas-xmark" color="danger"
<ion-icon slot="end" *ngIf="option.isCorrect === 0" class="core-correct-icon" name="fas-xmark" color="danger"
[attr.aria-label]="'core.question.incorrect' | translate" />
</ion-item>
<ion-button *ngIf="!question.disabled" class="ion-text-wrap ion-margin-top" expand="block" fill="outline"

View File

@ -7,9 +7,9 @@
</ion-item>
<ion-item *ngIf="question.input && !question.input.isInline"
class="ion-text-wrap addon-qtype-shortanswer-input core-{{question.input.correctIconColor}}-item">
<ion-label position="stacked">{{ 'addon.mod_quiz.answercolon' | translate }}</ion-label>
<ion-input type="text" [placeholder]="question.input.readOnly ? '' : 'core.question.answer' | translate"
[attr.name]="question.input.name" [value]="question.input.value" autocorrect="off" [disabled]="question.input.readOnly" />
<ion-input labelPlacement="stacked" type="text" [placeholder]="question.input.readOnly ? '' : 'core.question.answer' | translate"
[attr.name]="question.input.name" [value]="question.input.value" autocorrect="off" [disabled]="question.input.readOnly"
[label]="'addon.mod_quiz.answercolon' | translate" />
<ion-icon *ngIf="question.input.correctIcon" class="core-correct-icon" slot="end" [name]="question.input.correctIcon"
[color]="[question.input.correctIconColor]" />
</ion-item>

View File

@ -16,12 +16,11 @@
<!-- Edit. -->
<ion-item *ngIf="edit && field && field.shortname && form" [formGroup]="form">
<ion-label>
<span class="label-text" [core-mark-required]="required">
<ion-checkbox [formControlName]="modelName" labelPlacement="start" justify="space-between">
<span [core-mark-required]="required">
<core-format-text [text]="field.name" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId"
[courseId]="courseId" [wsNotFiltered]="true" />
</span>
<core-input-errors [control]="form.controls[modelName]" />
</ion-label>
<ion-checkbox item-end [formControlName]="modelName" />
</ion-checkbox>
<core-input-errors [control]="form.controls[modelName]" />
</ion-item>

View File

@ -14,14 +14,12 @@
<!-- Edit. -->
<ion-item *ngIf="edit && field && field.shortname && form" class="ion-text-wrap" [formGroup]="form">
<ion-label position="stacked">
<span [core-mark-required]="required">
<ion-select labelPlacement="stacked" [formControlName]="modelName" [placeholder]="'core.choosedots' | translate"
interface="action-sheet" [cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: field.name}">
<div [core-mark-required]="required" slot="label">
<core-format-text [text]="field.name" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId"
[courseId]="courseId" [wsNotFiltered]="true" />
</span>
</ion-label>
<ion-select [formControlName]="modelName" [placeholder]="'core.choosedots' | translate" interface="action-sheet"
[cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: field.name}">
</div>
<ion-select-option value="">{{ 'core.choosedots' | translate }}</ion-select-option>
<ion-select-option *ngFor="let option of options" [value]="option">{{option}}</ion-select-option>
</ion-select>

View File

@ -14,12 +14,11 @@
<!-- Edit. -->
<ion-item *ngIf="edit && field && field.shortname && form" class="ion-text-wrap" [formGroup]="form">
<ion-label position="stacked">
<span [core-mark-required]="required">
<ion-input labelPlacement="stacked" type="text" [formControlName]="modelName" [placeholder]="field.name">
<div [core-mark-required]="required" slot="label">
<core-format-text [text]="field.name" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId"
[courseId]="courseId" [wsNotFiltered]="true" />
</span>
</ion-label>
<ion-input type="text" [formControlName]="modelName" [placeholder]="field.name" />
</div>
</ion-input>
<core-input-errors [control]="form.controls[modelName]" />
</ion-item>

View File

@ -14,12 +14,12 @@
<!-- Edit. -->
<ion-item *ngIf="edit && field && field.shortname && form" class="ion-text-wrap" [formGroup]="form">
<ion-label position="stacked">
<span [core-mark-required]="required">
<ion-input labelPlacement="stacked" [type]="inputType" [formControlName]="modelName" [placeholder]="field.name"
maxlength="{{maxLength}}">
<div [core-mark-required]="required" slot="label">
<core-format-text [text]="field.name" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId"
[courseId]="courseId" [wsNotFiltered]="true" />
</span>
</ion-label>
<ion-input [type]="inputType" [formControlName]="modelName" [placeholder]="field.name" maxlength="{{maxLength}}" />
</div>
</ion-input>
<core-input-errors [control]="form.controls[modelName]" />
</ion-item>

View File

@ -19,8 +19,8 @@
<core-format-text [text]="field.name" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId"
[courseId]="courseId" [wsNotFiltered]="true" />
</span>
<core-input-errors [control]="control" />
</ion-label>
<core-rich-text-editor [control]="control" [placeholder]="field.name" [autoSave]="true" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [elementId]="modelName" />
<core-input-errors [control]="control" />
</ion-item>

View File

@ -59,7 +59,8 @@ export class AppComponent implements OnInit, AfterViewInit {
ngOnInit(): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const win = <any> window;
CoreDomUtils.toggleModeClass('ionic5', true, { includeLegacy: true });
CoreDomUtils.toggleModeClass('ionic7', true, { includeLegacy: true });
CoreDomUtils.toggleModeClass('development', CoreConstants.BUILD.isDevelopment);
this.addVersionClass(MOODLEAPP_VERSION_PREFIX, CoreConstants.CONFIG.versionname.replace('-dev', ''));
CoreEvents.on(CoreEvents.LOGOUT, async () => {

View File

@ -51,7 +51,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
})
export class CoreComboboxComponent implements ControlValueAccessor {
@ViewChild(IonSelect) select!: IonSelect;
@ViewChild(IonSelect) select?: IonSelect;
@Input() interface: 'popover' | 'modal' = 'popover';
@Input() label = Translate.instant('core.show'); // Aria label.
@ -118,7 +118,7 @@ export class CoreComboboxComponent implements ControlValueAccessor {
async openSelect(event?: UIEvent): Promise<void> {
this.touch();
if (this.interface == 'modal') {
if (this.interface === 'modal') {
if (this.expanded || !this.modalOptions) {
return;
}

View File

@ -7,12 +7,9 @@
</ion-card>
<ion-item class="ion-text-wrap core-group-selector">
<ion-label>
<ng-container *ngIf="groupInfo.separateGroups">{{'core.groupsseparate' | translate }}</ng-container>
<ng-container *ngIf="groupInfo.visibleGroups">{{'core.groupsvisible' | translate }}</ng-container>
</ion-label>
<ion-select [(ngModel)]="selected" (ionChange)="selectedChange.emit(selected)" interface="action-sheet"
[cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'core.group' | translate}">
<ion-select [label]="(groupInfo.separateGroups ? 'core.groupsseparate': 'core.groupsvisible') | translate" [(ngModel)]=" selected"
(ionChange)="selectedChange.emit(selected)" interface="action-sheet" [cancelText]="'core.cancel' | translate"
[interfaceOptions]="{header: 'core.group' | translate}">
<ion-select-option *ngFor="let group of groupInfo.groups" [value]=" group.id">
<core-format-text [text]="group.name" contextLevel="course" [contextInstanceId]="courseId" [wsNotFiltered]="true" />
</ion-select-option>

View File

@ -1,16 +1,14 @@
<div class="core-input-error-container" role="alert" *ngIf="(control && control.dirty && !control.valid) || errorText">
<ng-container *ngIf="control && control.dirty && !control.valid">
<ng-container *ngFor="let error of errorKeys">
<div *ngIf="control.hasError(error)" class="core-input-error">
<span *ngIf="errorMessages && errorMessages[error]">{{errorMessages[error]}}</span>
<span *ngIf="(!errorMessages || !errorMessages[error]) && error === 'max' && control.errors?.max">
{{ 'core.login.invalidvaluemax' | translate:{$a: control.errors!.max.max} }}
</span>
<span *ngIf="(!errorMessages || !errorMessages[error]) && error === 'min' && control.errors?.min">
{{ 'core.login.invalidvaluemin' | translate:{$a: control.errors!.min.min} }}
</span>
</div>
</ng-container>
<ng-container *ngIf="control && control.dirty && !control.valid">
<ng-container *ngFor="let error of errorKeys">
<div *ngIf="control.hasError(error)" class="core-input-error">
<span *ngIf="errorMessages && errorMessages[error]">{{ errorMessages[error] | translate }}</span>
<span *ngIf="(!errorMessages || !errorMessages[error]) && error === 'max' && control.errors?.max">
{{ 'core.login.invalidvaluemax' | translate:{$a: control.errors!.max.max} }}
</span>
<span *ngIf="(!errorMessages || !errorMessages[error]) && error === 'min' && control.errors?.min">
{{ 'core.login.invalidvaluemin' | translate:{$a: control.errors!.min.min} }}
</span>
</div>
</ng-container>
<div *ngIf="errorText" class="core-input-error" aria-live="assertive">{{ errorText }}</div>
</div>
</ng-container>
<div *ngIf="errorText" class="core-input-error" aria-live="assertive">{{ errorText }}</div>

View File

@ -1,17 +1,18 @@
:host {
display: contents;
.core-input-error-container {
&.has-errors {
display: block;
width: 100%;
}
.core-input-error {
padding: 4px;
color: var(--danger);
font-size: 12px;
display: none;
.core-input-error {
padding: 4px;
color: var(--danger);
font-size: 12px;
display: none;
&:first-child {
display: block;
}
&:first-child {
display: block;
}
}
}

View File

@ -12,9 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, OnChanges, SimpleChange } from '@angular/core';
import { Component, ElementRef, HostBinding, Input, OnChanges, OnInit, SimpleChange } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Translate } from '@singletons';
/**
* Component to show errors if an input isn't valid.
@ -30,8 +29,7 @@ import { Translate } from '@singletons';
* Example usage:
*
* <ion-item class="ion-text-wrap">
* <ion-label stacked core-mark-required="true">{{ 'core.login.username' | translate }}</ion-label>
* <ion-input type="text" name="username" formControlName="username"></ion-input>
* <ion-input type="text" name="username" formControlName="username" required="true"></ion-input>
* <core-input-errors [control]="myForm.controls.username" [errorMessages]="usernameErrors"></core-input-errors>
* </ion-item>
*/
@ -40,43 +38,87 @@ import { Translate } from '@singletons';
templateUrl: 'core-input-errors.html',
styleUrls: ['input-errors.scss'],
})
export class CoreInputErrorsComponent implements OnChanges {
export class CoreInputErrorsComponent implements OnInit, OnChanges {
@Input() control?: FormControl;
@Input() errorMessages?: Record<string, string>;
@Input() errorText?: string; // Set other non automatic errors.
@Input() control?: FormControl; // Needed to be able to check the validity of the input.
@Input() errorMessages: Record<string, string> = {}; // Error messages to show. Keys must be the name of the error.
@Input() errorText = ''; // Set other non automatic errors.
errorKeys: string[] = [];
protected hostElement: HTMLElement;
@HostBinding('class.has-errors')
get hasErrors(): boolean {
return (this.control && this.control.dirty && !this.control.valid) || !!this.errorText;
}
@HostBinding('role') role = 'alert';
constructor(
element: ElementRef,
) {
this.hostElement = element.nativeElement;
}
/**
* Initialize some common errors if they aren't set.
*/
protected initErrorMessages(): void {
this.errorMessages = this.errorMessages || {};
this.errorMessages = {
required: this.errorMessages.required || 'core.required',
email: this.errorMessages.email || 'core.login.invalidemail',
date: this.errorMessages.date || 'core.login.invaliddate',
datetime: this.errorMessages.datetime || 'core.login.invaliddate',
datetimelocal: this.errorMessages.datetimelocal || 'core.login.invaliddate',
time: this.errorMessages.time || 'core.login.invalidtime',
url: this.errorMessages.url || 'core.login.invalidurl',
// Set empty values by default, the default error messages will be built in the template when needed.
max: this.errorMessages.max || '',
min: this.errorMessages.min || '',
};
this.errorMessages.required = this.errorMessages.required || Translate.instant('core.required');
this.errorMessages.email = this.errorMessages.email || Translate.instant('core.login.invalidemail');
this.errorMessages.date = this.errorMessages.date || Translate.instant('core.login.invaliddate');
this.errorMessages.datetime = this.errorMessages.datetime || Translate.instant('core.login.invaliddate');
this.errorMessages.datetimelocal = this.errorMessages.datetimelocal || Translate.instant('core.login.invaliddate');
this.errorMessages.time = this.errorMessages.time || Translate.instant('core.login.invalidtime');
this.errorMessages.url = this.errorMessages.url || Translate.instant('core.login.invalidurl');
this.errorMessages.requiredTrue = this.errorMessages.required;
// Set empty values by default, the default error messages will be built in the template when needed.
this.errorMessages.max = this.errorMessages.max || '';
this.errorMessages.min = this.errorMessages.min || '';
this.errorKeys = Object.keys(this.errorMessages);
}
/**
* Component being changed.
* @inheritdoc
*/
ngOnInit(): void {
const parent = this.hostElement.parentElement;
let item: HTMLElement | null = null;
if (parent?.tagName === 'ION-ITEM') {
item = parent;
// Get all elements on the parent and wrap them with a div.
// This is needed because otherwise the error message will be shown on the right of the input. Or overflowing the item.
const wrapper = document.createElement('div');
wrapper.classList.add('core-input-errors-wrapper');
Array.from(parent.children).forEach((child) => {
if (!child.slot) {
wrapper.appendChild(child);
}
});
parent.appendChild(wrapper);
} else {
item = this.hostElement.closest('ion-item');
}
item?.classList.add('has-core-input-errors');
}
/**
* @inheritdoc
*/
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
if ((changes.control || changes.errorMessages) && this.control) {
this.initErrorMessages();
this.errorKeys = this.errorMessages ? Object.keys(this.errorMessages) : [];
}
if (changes.errorText) {
this.errorText = changes.errorText.currentValue;
}
}

View File

@ -86,12 +86,3 @@
// Implicit Inline.
@include inline();
}
:host-context(.limited-width > *):not([slot]),
:host-context(.menu > *):not([slot]) {
&.core-loading-loaded {
--contents-display: flex;
flex-direction: column;
}
min-height: 100%;
}

View File

@ -18,7 +18,8 @@
<!-- Form to edit the file's name. -->
<ion-input type="text" name="filename" [placeholder]="'core.filename' | translate" autocapitalize="none" autocorrect="off"
(click)="$event.stopPropagation()" core-auto-focus [(ngModel)]="newFileName" *ngIf="editMode" />
(click)="$event.stopPropagation()" core-auto-focus [(ngModel)]="newFileName" *ngIf="editMode"
[attr.aria-label]="'core.filename' | translate" />
<div class="buttons" slot="end">
<ion-button fill="clear" *ngIf="isIOS && !editMode" (click)="openFile($event, true)"

View File

@ -26,7 +26,7 @@ import { Translate } from '@singletons';
*
* This directive should be applied in the label. Example:
*
* <ion-label core-mark-required="{{field.required}}">{{ 'core.login.username' | translate }}</ion-label>
* <p slot="label" [core-mark-required]="true">Username</p>
*/
@Component({
selector: '[core-mark-required]',
@ -37,41 +37,43 @@ export class CoreMarkRequiredComponent implements OnInit, AfterViewInit {
@Input('core-mark-required') coreMarkRequired: boolean | string = true;
protected element: HTMLElement;
requiredLabel?: string;
protected hostElement: HTMLElement;
requiredLabel = Translate.instant('core.required');
constructor(
element: ElementRef,
) {
this.element = element.nativeElement;
this.hostElement = element.nativeElement;
}
/**
* @inheritdoc
*/
ngOnInit(): void {
this.requiredLabel = Translate.instant('core.required');
this.coreMarkRequired = CoreUtils.isTrueOrOne(this.coreMarkRequired);
}
/**
* Called after the view is initialized.
* @inheritdoc
*/
ngAfterViewInit(): void {
if (this.coreMarkRequired) {
// Add the "required" to the aria-label.
const ariaLabel = this.element.getAttribute('aria-label') ||
CoreTextUtils.cleanTags(this.element.innerHTML, { singleLine: true });
const ariaLabel = this.hostElement.getAttribute('aria-label') ||
CoreTextUtils.cleanTags(this.hostElement.innerHTML, { singleLine: true });
if (ariaLabel) {
this.element.setAttribute('aria-label', ariaLabel + ' ' + this.requiredLabel);
this.hostElement.setAttribute('aria-label', ariaLabel + '. ' + this.requiredLabel);
}
} else {
// Remove the "required" from the aria-label.
const ariaLabel = this.element.getAttribute('aria-label');
const ariaLabel = this.hostElement.getAttribute('aria-label');
if (ariaLabel) {
this.element.setAttribute('aria-label', ariaLabel.replace(' ' + this.requiredLabel, ''));
this.hostElement.setAttribute('aria-label', ariaLabel.replace('. ' + this.requiredLabel, ''));
}
}
const input = this.hostElement.closest('ion-input, ion-textarea');
input?.setAttribute('required', this.coreMarkRequired ? 'true' : 'false');
}
}

View File

@ -14,11 +14,11 @@
<form (ngSubmit)="submitPassword($event)" #passwordForm>
<div>
<ion-item>
<ion-label class="sr-only">{{ placeholder | translate }}</ion-label>
<core-show-password name="password">
<ion-input class="ion-text-wrap core-ioninput-password" name="password" type="password"
placeholder="{{ placeholder | translate }}" [(ngModel)]="password" core-auto-focus [clearOnEdit]="false" />
</core-show-password>
<ion-input [attr.aria-label]="placeholder | translate" class="ion-text-wrap core-ioninput-password" name="password"
type="password" placeholder="{{ placeholder | translate }}" [(ngModel)]="password" core-auto-focus
[clearOnEdit]="false">
<core-show-password slot="end" />
</ion-input>
</ion-item>
<ion-item *ngIf="error" class="ion-text-wrap ion-padding-top text-danger">
<core-format-text [text]="error | translate" />

View File

@ -1,4 +1,4 @@
<ng-content></ng-content>
<ng-content />
<ion-button fill="clear" [attr.aria-label]="(shown ? 'core.hide' : 'core.show') | translate" core-suppress-events (onClick)="toggle($event)"
(mousedown)="doNotBlur($event)" (keydown)="doNotBlur($event)" (keyup)="toggle($event)">
<ion-icon [name]="shown ? 'fas-eye-slash' : 'fas-eye'" slot="icon-only" aria-hidden="true" />

View File

@ -3,30 +3,29 @@
:host {
display: contents;
ion-button {
// Only applies to deprecated way (surrounding).
::ng-deep ion-input + ion-button {
background: transparent;
padding: 0 calc(var(--padding-start) / 2);
position: absolute;
@include safe-area-position(null, 0px, null, null);
padding: 0 var(--inner-padding-end) 0 4px;
margin-top: 0;
margin-bottom: 0;
z-index: 3;
bottom: 0;
position: absolute;
@include safe-area-position(null, 0px, null, null);
top: 0;
}
// Only applies to deprecated way (surrounding).
::ng-deep ion-input {
--padding-end: 56px !important;
}
::ng-deep ion-input.input-label-placement-stacked + ion-button {
top: 14px;
}
}
::ng-deep ion-input {
--padding-end: 47px !important;
}
:host-context(.md.item-label.stacked) ion-button {
bottom: 0;
}
:host-context(.iositem-label.stacked) ion-button {
bottom: -5px;
}
:host-context(.ios) ion-button {
bottom: 0;
ion-button {
z-index: 5;
pointer-events: visible;
}

View File

@ -18,18 +18,31 @@ import { IonInput } from '@ionic/angular';
import { CorePlatform } from '@services/platform';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { CoreLogger } from '@singletons/logger';
/**
* Component to allow showing and hiding a password. The affected input MUST have a name to identify it.
* This component allows to show/hide a password.
* It's meant to be used with ion-input.
* It's recommended to use it as a slot of the input.
*
* @description
* This directive needs to surround the input with the password.
*
* You need to supply the name of the input.
* There are 2 ways to use ths component:
* - Slot it to start or end on the ion-input element.
* - Surround the ion-input with the password with this component. This is deprecated.
*
* Example:
* In order to help finding the input you can specify the name of the input or the ion-input element.
*
* <core-show-password [name]="'password'">
*
* Example of new usage:
*
* <ion-input type="password" name="password">
* <core-show-password slot="end" />
* </ion-input>
*
* Example deprecated usage:
*
* <core-show-password>
* <ion-input type="password" name="password"></ion-input>
* </core-show-password>
*/
@ -40,17 +53,30 @@ import { CoreUtils } from '@services/utils/utils';
})
export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
@Input() name?: string; // Name of the input affected.
@Input() initialShown?: boolean | string; // Whether the password should be shown at start.
@ContentChild(IonInput) ionInput?: IonInput;
shown = false; // Whether the password is shown.
@Input() name = ''; // Deprecated. Not used anymore.
@ContentChild(IonInput) ionInput?: IonInput | HTMLIonInputElement; // Deprecated. Use slot instead.
protected input?: HTMLInputElement; // Input affected.
protected element: HTMLElement; // Current element.
protected input?: HTMLInputElement;
protected hostElement: HTMLElement;
protected logger: CoreLogger;
constructor(element: ElementRef) {
this.element = element.nativeElement;
this.hostElement = element.nativeElement;
this.logger = CoreLogger.getInstance('CoreShowPasswordComponent');
}
get shown(): boolean {
return this.input?.type === 'text';
}
set shown(shown: boolean) {
if (!this.input) {
return;
}
this.input.type = shown ? 'text' : 'password';
}
/**
@ -64,28 +90,12 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
* @inheritdoc
*/
async ngAfterViewInit(): Promise<void> {
if (this.ionInput) {
try {
// It's an ion-input, use it to get the native element.
this.input = await this.ionInput.getInputElement();
this.setData(this.input);
} catch (error) {
// This should never fail, but it does in some testing environment because Ionic elements are not
// rendered properly. So in case this fails, we'll just ignore the error.
}
return;
}
// Search the input.
this.input = this.element.querySelector<HTMLInputElement>('input[name="' + this.name + '"]') ?? undefined;
await this.setInputElement();
if (!this.input) {
return;
}
this.setData(this.input);
// By default, don't autocapitalize and autocorrect.
if (!this.input.getAttribute('autocorrect')) {
this.input.setAttribute('autocorrect', 'off');
@ -96,12 +106,33 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
}
/**
* Set label, icon name and input type.
*
* @param input The input element.
* Set the input element to affect.
*/
protected setData(input: HTMLInputElement): void {
input.type = this.shown ? 'text' : 'password';
protected async setInputElement(): Promise<void> {
if (!this.ionInput) {
this.ionInput = this.hostElement.closest('ion-input') ?? undefined;
this.hostElement.setAttribute('slot', 'end');
} else {
// It's outside ion-input, warn devs.
this.logger.warn('Deprecated CoreShowPasswordComponent usage, it\'s not needed to surround ion-input anymore.');
}
if (!this.ionInput) {
return;
}
try {
this.input = await this.ionInput.getInputElement();
} catch {
// This should never fail, but it does in some testing environment because Ionic elements are not
// rendered properly. So in case this fails it will try to find through the name and ignore the error.
const name = this.ionInput.name;
if (!name) {
return;
}
this.input = this.hostElement.querySelector<HTMLInputElement>('input[name="' + name + '"]') ?? undefined;
}
}
/**
@ -110,7 +141,7 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
* @param event The mouse event.
*/
toggle(event: Event): void {
if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
return;
}
@ -120,13 +151,8 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
const isFocused = document.activeElement === this.input;
this.shown = !this.shown;
if (!this.input) {
return;
}
this.setData(this.input);
// In Android, the keyboard is closed when the input type changes. Focus it again.
if (isFocused && CorePlatform.isAndroid()) {
if (this.input && isFocused && CorePlatform.isAndroid()) {
CoreDomUtils.focusElement(this.input);
}
}

View File

@ -1,7 +1,6 @@
<ion-item *ngIf="sites && sites.length">
<ion-label>{{ 'core.site' | translate }}</ion-label>
<ion-select [(ngModel)]="selectedSite" (ngModelChange)="siteSelected.emit(selectedSite)" interface="action-sheet"
[cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'core.site' | translate}">
<ion-select [label]="'core.site' | translate" [(ngModel)]="selectedSite" (ngModelChange)="siteSelected.emit(selectedSite)"
interface="action-sheet" [cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'core.site' | translate}">
<ion-select-option *ngFor="let site of sites" [value]="site.id">{{ site.fullNameAndSiteName }}</ion-select-option>
</ion-select>
</ion-item>

View File

@ -426,6 +426,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
const data = await CoreDomUtils.openModal<CoreCourseIndexSectionWithModule>({
component: CoreCourseCourseIndexComponent,
initialBreakpoint: 1,
breakpoints: [0, 1],
componentProps: {
course: this.course,
sections: this.sections,

View File

@ -6,7 +6,8 @@
</div>
<ion-textarea [hidden]="rteEnabled" #textarea class="core-textarea" role="textbox" [attr.name]="name" ngControl="control"
[placeholder]="placeholder" (ionChange)="onChange()" (ionFocus)="showToolbar($event)" (ionBlur)="hideToolbar($event)" />
[placeholder]="placeholder" [attr.aria-labelledby]="ariaLabelledBy" (ionChange)="onChange()" (ionFocus)="showToolbar($event)"
(ionBlur)="hideToolbar($event)" />
<div class="core-rte-info-message" *ngIf="infoMessage">
<ion-icon name="fas-circle-info" aria-hidden="true" />

View File

@ -231,11 +231,18 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
return;
}
const updateArialabelledBy = () => this.ariaLabelledBy = label.getAttribute('id') ?? undefined;
const updateArialabelledBy = () => {
this.ariaLabelledBy = label.getAttribute('id') ?? undefined;
};
this.labelObserver = new MutationObserver(updateArialabelledBy);
this.labelObserver.observe(label, { attributes: true, attributeFilter: ['id'] });
// Usually the label won't have an id, so we need to add one.
if (!label.getAttribute('id')) {
label.setAttribute('id', 'rte-'+CoreUtils.getUniqueId('CoreEditorRichTextEditor'));
}
updateArialabelledBy();
}

View File

@ -18,15 +18,9 @@
ion-button.core-button-as-link {
--color: var(--core-login-text-color);
text-decoration-color: var(--core-login-text-color);
ion-label {
color: var(--core-login-text-color);
}
text-decoration-color: var(--color);
}
.core-login-reconnect-warning {
margin: 0px 0px 32px 0px;
}

View File

@ -41,19 +41,17 @@
<div class="core-login-methods">
<form [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #credentialsForm *ngIf="!isBrowserSSO">
<ion-item>
<ion-label class="sr-only">{{ 'core.login.username' | translate }}</ion-label>
<ion-item lines="inset">
<ion-input type="text" name="username" placeholder="{{ 'core.login.username' | translate }}"
formControlName="username" autocapitalize="none" autocorrect="off" autocomplete="username" enterkeyhint="next"
required="true" />
required="true" [attr.aria-label]="'core.login.username' | translate " />
</ion-item>
<ion-item class="ion-margin-bottom">
<ion-label class="sr-only">{{ 'core.login.password' | translate }}</ion-label>
<core-show-password name="password">
<ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}"
formControlName="password" [clearOnEdit]="false" autocomplete="current-password" enterkeyhint="go"
required="true" />
</core-show-password>
<ion-item class="ion-margin-bottom" lines="inset">
<ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}"
formControlName="password" [clearOnEdit]="false" autocomplete="current-password" enterkeyhint="go"
required="true" [attr.aria-label]="'core.login.password' | translate ">
<core-show-password slot="end" />
</ion-input>
</ion-item>
<ion-button expand="block" type="submit" [disabled]="!credForm.valid"
class="ion-margin core-login-login-button ion-text-wrap">

View File

@ -43,18 +43,17 @@
</ion-item-divider>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<span core-mark-required="true">{{ 'core.whatisyourage' | translate }}</span>
</ion-label>
<ion-input type="number" name="age" placeholder="0" formControlName="age" autocapitalize="none" autocorrect="off" />
<ion-input labelPlacement="stacked" type="number" name="age" placeholder="0" formControlName="age"
autocapitalize="none" autocorrect="off">
<div slot="label" [core-mark-required]="true">{{ 'core.whatisyourage' | translate }}</div>
</ion-input>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<span core-mark-required="true">{{ 'core.wheredoyoulive' | translate }}</span>
</ion-label>
<ion-select name="country" formControlName="country" [cancelText]="'core.cancel' | translate"
[okText]="'core.ok' | translate" [placeholder]="'core.login.selectacountry' | translate">
<ion-select labelPlacement="stacked" name="country" formControlName="country"
[cancelText]="'core.cancel' | translate" [okText]="'core.ok' | translate"
[placeholder]="'core.login.selectacountry' | translate">
<div slot="label" [core-mark-required]="true">{{ 'core.wheredoyoulive' | translate }}</div>
<ion-select-option value="">{{ 'core.login.selectacountry' | translate }}</ion-select-option>
<ion-select-option *ngFor="let country of countries" [value]="country.code">{{country.name}}</ion-select-option>
</ion-select>
@ -97,21 +96,20 @@
</ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<span core-mark-required="true">{{ 'core.login.username' | translate }}</span>
</ion-label>
<ion-input type="text" name="username" placeholder="{{ 'core.login.username' | translate }}"
formControlName="username" autocapitalize="none" autocorrect="off" />
<ion-input labelPlacement="stacked" type="text" name="username"
placeholder="{{ 'core.login.username' | translate }}" formControlName="username" autocapitalize="none"
autocorrect="off">
<div slot="label" [core-mark-required]="true">{{ 'core.login.username' | translate }}</div>
</ion-input>
<core-input-errors [control]="signupForm.controls.username" [errorMessages]="usernameErrors" />
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<span core-mark-required="true">{{ 'core.login.password' | translate }}</span>
</ion-label>
<core-show-password name="password">
<ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}"
formControlName="password" [clearOnEdit]="false" autocomplete="new-password" required="true" />
</core-show-password>
<ion-input labelPlacement="stacked" name="password" type="password"
placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false"
autocomplete="new-password" required="true">
<div slot="label" [core-mark-required]="true">{{ 'core.login.password' | translate }}</div>
<core-show-password slot="end" />
</ion-input>
<p *ngIf="settings.passwordpolicy" class="core-input-footnote">
{{settings.passwordpolicy}}
</p>
@ -125,38 +123,35 @@
</ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<span core-mark-required="true">{{ 'core.user.email' | translate }}</span>
</ion-label>
<ion-input type="email" name="email" placeholder="{{ 'core.user.email' | translate }}" formControlName="email"
autocapitalize="none" autocorrect="off" />
<ion-input labelPlacement="stacked" type="email" name="email" placeholder="{{ 'core.user.email' | translate }}"
formControlName="email" autocapitalize="none" autocorrect="off">
<div slot="label" [core-mark-required]="true">{{ 'core.user.email' | translate }}</div>
</ion-input>
<core-input-errors [control]="signupForm.controls.email" [errorMessages]="emailErrors" />
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<span core-mark-required="true">{{ 'core.user.emailagain' | translate }}</span>
</ion-label>
<ion-input type="email" name="email2" placeholder="{{ 'core.user.emailagain' | translate }}" autocapitalize="none"
formControlName="email2" autocorrect="off" [pattern]="escapeMail(signupForm.controls.email.value)" />
<ion-input labelPlacement="stacked" type="email" name="email2"
placeholder="{{ 'core.user.emailagain' | translate }}" autocapitalize="none" formControlName="email2"
autocorrect="off" [pattern]="escapeMail(signupForm.controls.email.value)">
<div slot="label" [core-mark-required]="true">{{ 'core.user.emailagain' | translate }}</div>
</ion-input>
<core-input-errors [control]="signupForm.controls.email2" [errorMessages]="email2Errors" />
</ion-item>
<ion-item *ngFor="let nameField of settings.namefields" class="ion-text-wrap">
<ion-label position="stacked">
<span core-mark-required="true">{{ 'core.user.' + nameField | translate }}</span>
</ion-label>
<ion-input type="text" [name]="nameField" placeholder="{{ 'core.user.' + nameField | translate }}"
formControlName="{{nameField}}" autocorrect="off" />
<ion-input labelPlacement="stacked" type="text" [name]="nameField" formControlName="{{nameField}}" autocorrect="off"
[placeholder]="'core.user.' + nameField | translate">
<div slot="label" [core-mark-required]="true">{{ 'core.user.' + nameField | translate }}</div>
</ion-input>
<core-input-errors [control]="signupForm.controls[nameField]" [errorMessages]="namefieldsErrors![nameField]" />
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">{{ 'core.user.city' | translate }}</ion-label>
<ion-input type="text" name="city" placeholder="{{ 'core.user.city' | translate }}" formControlName="city"
autocorrect="off" />
<ion-input labelPlacement="stacked" type="text" name="city" placeholder="{{ 'core.user.city' | translate }}"
formControlName="city" autocorrect="off" [label]="'core.user.city' | translate" />
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">{{ 'core.user.country' | translate }}</ion-label>
<ion-select name="country" formControlName="country" [placeholder]="'core.login.selectacountry' | translate"
[cancelText]="'core.cancel' | translate" [okText]="'core.ok' | translate">
<ion-select labelPlacement="stacked" name="country" formControlName="country"
[placeholder]="'core.login.selectacountry' | translate" [cancelText]="'core.cancel' | translate"
[okText]="'core.ok' | translate" [label]="'core.user.country' | translate">
<ion-select-option value="">{{ 'core.login.selectacountry' | translate }}</ion-select-option>
<ion-select-option *ngFor="let country of countries" [value]="country.code">{{country.name}}</ion-select-option>
</ion-select>
@ -177,7 +172,7 @@
<ng-container *ngIf="settings.recaptchapublickey">
<ion-item-divider class="ion-text-wrap">
<ion-label>
<h2><span [core-mark-required]="true">{{ 'core.login.security_question' | translate }}</span></h2>
<h2 [core-mark-required]="true">{{ 'core.login.security_question' | translate }}</h2>
</ion-label>
</ion-item-divider>
<core-recaptcha [publicKey]="settings.recaptchapublickey" [model]="captcha" [siteUrl]="site.siteUrl"
@ -199,11 +194,10 @@
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<span [core-mark-required]="true">{{ 'core.login.policyacceptmandatory' | translate }}</span>
<core-input-errors [control]="signupForm.controls.policyagreed" [errorMessages]="policyErrors" />
</ion-label>
<ion-checkbox slot="end" name="policyagreed" formControlName="policyagreed" />
<ion-checkbox labelPlacement="start" justify="space-between" name="policyagreed" formControlName="policyagreed">
<p [core-mark-required]="true">{{ 'core.login.policyacceptmandatory' | translate }}</p>
</ion-checkbox>
<core-input-errors [control]="signupForm.controls.policyagreed" [errorMessages]="policyErrors" />
</ion-item>
</ng-container>

View File

@ -105,15 +105,14 @@ export class CoreLoginEmailSignupPage implements OnInit {
});
// Setup validation errors.
this.usernameErrors = CoreLoginHelper.getErrorMessages('core.login.usernamerequired');
this.passwordErrors = CoreLoginHelper.getErrorMessages('core.login.passwordrequired');
this.emailErrors = CoreLoginHelper.getErrorMessages('core.login.missingemail');
this.policyErrors = CoreLoginHelper.getErrorMessages('core.login.policyagree');
this.email2Errors = CoreLoginHelper.getErrorMessages(
'core.login.missingemail',
undefined,
'core.login.emailnotmatch',
);
this.usernameErrors = { required: 'core.login.usernamerequired' };
this.passwordErrors = { required: 'core.login.passwordrequired' };
this.emailErrors = { required: 'core.login.missingemail' };
this.policyErrors = { required: 'core.login.policyagree' };
this.email2Errors = {
required: 'core.login.missingemail',
pattern: 'core.login.emailnotmatch',
};
}
/**
@ -224,7 +223,7 @@ export class CoreLoginEmailSignupPage implements OnInit {
const namefieldsErrors = {};
if (this.settings.namefields) {
this.settings.namefields.forEach((field) => {
namefieldsErrors[field] = CoreLoginHelper.getErrorMessages('core.login.missing' + field);
namefieldsErrors[field] = { required: 'core.login.missing' + field };
});
}
this.namefieldsErrors = namefieldsErrors;

View File

@ -30,18 +30,16 @@
</ion-item-divider>
<ion-radio-group formControlName="field">
<ion-item>
<ion-label>{{ 'core.login.username' | translate }}</ion-label>
<ion-radio slot="end" value="username" />
<ion-radio value="username">{{ 'core.login.username' | translate }}</ion-radio>
</ion-item>
<ion-item>
<ion-label>{{ 'core.user.email' | translate }}</ion-label>
<ion-radio slot="end" value="email" />
<ion-radio value="email">{{ 'core.user.email' | translate }}</ion-radio>
</ion-item>
</ion-radio-group>
<ion-item>
<ion-label class="sr-only">{{ 'core.login.usernameoremail' | translate }}</ion-label>
<ion-item lines="full">
<ion-input type="text" name="value" placeholder="{{ 'core.login.usernameoremail' | translate }}" formControlName="value"
autocapitalize="none" autocorrect="off" [core-auto-focus]="autoFocus" />
autocapitalize="none" autocorrect="off" [core-auto-focus]="autoFocus"
[attr.aria-label]="'core.login.usernameoremail' | translate" />
</ion-item>
<ion-button type="submit" class="ion-margin" expand="block" [disabled]="!myForm.valid">
{{ 'core.courses.search' | translate }}

View File

@ -57,13 +57,13 @@
<div class="core-login-methods">
<form *ngIf="!isBrowserSSO" [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #reconnectForm>
<ion-item class="ion-margin-bottom">
<ion-label class="sr-only">{{ 'core.login.password' | translate }}</ion-label>
<core-show-password name="password">
<ion-input class="core-ioninput-password" name="password" type="password"
placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false"
autocomplete="current-password" enterkeyhint="go" required="true" />
</core-show-password>
<ion-item class="ion-margin-bottom" lines="inset">
<ion-input class="core-ioninput-password" name="password" type="password"
placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false"
autocomplete="current-password" enterkeyhint="go" required="true"
[attr.aria-label]="'core.login.password' | translate">
<core-show-password slot="end" />
</ion-input>
</ion-item>
<ion-button type="submit" expand="block" [disabled]="!credForm.valid"
class="ion-margin core-login-login-button ion-text-wrap">

View File

@ -22,22 +22,18 @@
</div>
<form [formGroup]="siteForm" (ngSubmit)="connect(siteForm.value.siteUrl, $event)" *ngIf="!fixedSites && siteForm" #siteFormEl>
<!-- Form to input the site URL if there are no fixed sites. -->
<ng-container *ngIf=" siteSelector==='url'">
<ion-item>
<ion-label position=" stacked">
<h2>{{ 'core.login.siteaddress' | translate }}</h2>
</ion-label>
<ng-container *ngIf="siteSelector==='url'">
<ion-item lines="inset">
<ion-input name="url" type="url" placeholder="{{ 'core.login.siteaddressplaceholder' | translate }}"
formControlName="siteUrl" [core-auto-focus]="showKeyboard && !showScanQR" />
formControlName="siteUrl" [core-auto-focus]="showKeyboard && !showScanQR" labelPlacement="stacked"
[label]="'core.login.siteaddress' | translate" [clearInput]="true" />
</ion-item>
</ng-container>
<ng-container *ngIf="siteSelector !== 'url'">
<ion-item>
<ion-label position="stacked">
<h2>{{ 'core.login.siteaddress' | translate }}</h2>
</ion-label>
<ion-item lines="inset">
<ion-input name="url" placeholder="{{ 'core.login.siteaddressplaceholder' | translate }}" formControlName="siteUrl"
[core-auto-focus]="showKeyboard && !showScanQR" (ionChange)="searchSite($event, siteForm.value.siteUrl)" />
[core-auto-focus]="showKeyboard && !showScanQR" (ionChange)="searchSite($event, siteForm.value.siteUrl)"
labelPlacement="stacked" [label]="'core.login.siteaddress' | translate" [clearInput]="true" />
</ion-item>
<ion-list [class.hidden]="!hasSites && !enteredSiteUrl" class="core-login-site-list">

View File

@ -252,60 +252,6 @@ export class CoreLoginHelperProvider {
return CoreTextUtils.treatDisabledFeatures(disabledFeatures);
}
/**
* Builds an object with error messages for some common errors.
* Please notice that this function doesn't support all possible error types.
*
* @param requiredMsg Code of the string for required error.
* @param emailMsg Code of the string for invalid email error.
* @param patternMsg Code of the string for pattern not match error.
* @param urlMsg Code of the string for invalid url error.
* @param minlengthMsg Code of the string for "too short" error.
* @param maxlengthMsg Code of the string for "too long" error.
* @param minMsg Code of the string for min value error.
* @param maxMsg Code of the string for max value error.
* @returns Object with the errors.
*/
getErrorMessages(
requiredMsg?: string,
emailMsg?: string,
patternMsg?: string,
urlMsg?: string,
minlengthMsg?: string,
maxlengthMsg?: string,
minMsg?: string,
maxMsg?: string,
): Record<string, string> {
const errors: Record<string, string> = {};
if (requiredMsg) {
errors.required = errors.requiredTrue = Translate.instant(requiredMsg);
}
if (emailMsg) {
errors.email = Translate.instant(emailMsg);
}
if (patternMsg) {
errors.pattern = Translate.instant(patternMsg);
}
if (urlMsg) {
errors.url = Translate.instant(urlMsg);
}
if (minlengthMsg) {
errors.minlength = Translate.instant(minlengthMsg);
}
if (maxlengthMsg) {
errors.maxlength = Translate.instant(maxlengthMsg);
}
if (minMsg) {
errors.min = Translate.instant(minMsg);
}
if (maxMsg) {
errors.max = Translate.instant(maxMsg);
}
return errors;
}
/**
* Get logo URL from a site public config.
*

View File

@ -1,8 +1,7 @@
<ion-item class="ion-text-wrap" *ngIf="item && (item!.canrate || item!.rating !== null) && !disabled">
<ion-label>{{ 'core.rating.rating' | translate }}</ion-label>
<ion-select class="ion-text-start" [(ngModel)]="rating" (ngModelChange)="userRatingChanged()" interface="action-sheet"
[cancelText]="'core.cancel' | translate" [disabled]="!item!.canrate"
[interfaceOptions]="{header: 'core.rating.rating' | translate}">
[cancelText]="'core.cancel' | translate" [disabled]="!item!.canrate" [interfaceOptions]="{header: 'core.rating.rating' | translate}"
[label]="'core.rating.rating' | translate">
<ion-select-option *ngFor="let scaleItem of scale!.items" [value]="scaleItem.value">{{ scaleItem.name }}</ion-select-option>
</ion-select>
</ion-item>

View File

@ -23,16 +23,14 @@
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>{{ 'core.search.allcategories' | translate }}</ion-label>
<ion-checkbox slot="end" [(ngModel)]="allSearchAreaCategories" [indeterminate]="allSearchAreaCategories === null"
(ionChange)="allSearchAreaCategoriesUpdated()" />
<ion-checkbox labelPlacement="start" [(ngModel)]="allSearchAreaCategories"
[indeterminate]="allSearchAreaCategories === null" (ionChange)="allSearchAreaCategoriesUpdated()">{{
'core.search.allcategories' | translate }}</ion-checkbox>
</ion-item>
<ion-item class="ion-text-wrap" *ngFor="let searchAreaCategory of searchAreaCategories">
<ion-label>
<core-format-text [text]="searchAreaCategory.name" />
</ion-label>
<ion-checkbox slot="end" [(ngModel)]="searchAreaCategory.checked"
(ionChange)="onSearchAreaCategoryInputChanged(searchAreaCategory)" />
<ion-checkbox labelPlacement="start" [(ngModel)]="searchAreaCategory.checked"
(ionChange)="onSearchAreaCategoryInputChanged(searchAreaCategory)"><core-format-text
[text]="searchAreaCategory.name" /></ion-checkbox>
</ion-item>
</ng-container>
<ng-container *ngIf="!hideCourses && courses.length > 0">
@ -43,14 +41,13 @@
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>{{ 'core.search.allcourses' | translate }}</ion-label>
<ion-checkbox slot="end" [(ngModel)]="allCourses" [indeterminate]="allCourses === null" (ionChange)="allCoursesUpdated()" />
<ion-checkbox labelPlacement="start" [(ngModel)]="allCourses" [indeterminate]="allCourses === null"
(ionChange)="allCoursesUpdated()">
{{ 'core.search.allcourses' | translate }}</ion-checkbox>
</ion-item>
<ion-item class="ion-text-wrap" *ngFor="let course of courses">
<ion-label>
<core-format-text [text]="course.shortname" />
</ion-label>
<ion-checkbox slot="end" [(ngModel)]="course.checked" (ionChange)="onCourseInputChanged(course)" />
<ion-checkbox labelPlacement="start" [(ngModel)]="course.checked"
(ionChange)="onCourseInputChanged(course)"><core-format-text [text]="course.shortname" /></ion-checkbox>
</ion-item>
</ng-container>
</ion-list>

View File

@ -1,8 +1,8 @@
<form (ngSubmit)="submitForm($event)" role="search" #searchForm>
<ion-item class="search-box">
<ion-label class="sr-only">{{ placeholder }}</ion-label>
<ion-input type="search" name="search" [(ngModel)]="searchText" [placeholder]="placeholder" [autocorrect]="autocorrect"
[spellcheck]="spellcheck" [core-auto-focus]="autoFocus" [disabled]="disabled" role="searchbox" (ionFocus)="focus($event)" />
<ion-input [attr.aria-label]="placeholder" type="search" name="search" [(ngModel)]="searchText" [placeholder]="placeholder"
[autocorrect]="autocorrect" [spellcheck]="spellcheck" [core-auto-focus]="autoFocus" [disabled]="disabled" role="searchbox"
(ionFocus)="focus($event)" />
<ion-button slot="end" fill="clear" type="submit" [attr.aria-label]="searchLabel"
[disabled]="disabled || !searchText || (searchText.length < lengthCheck)">
<ion-icon name="fas-magnifying-glass" slot="icon-only" aria-hidden="true" />

View File

@ -18,39 +18,33 @@
<ion-content>
<ion-list class="list-item-limited-width">
<ion-item class="ion-text-wrap">
<ion-label>
<ion-toggle [(ngModel)]="rtl" (ionChange)="RTLChanged()">
<p class="item-heading">Change text direction</p>
<p>{{ direction }}</p>
</ion-label>
<ion-toggle [(ngModel)]="rtl" (ionChange)="RTLChanged()" slot="end" />
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<ion-toggle [(ngModel)]="forceSafeAreaMargins" (ionChange)="safeAreaChanged()">
<p class="item-heading">Force safe area margins</p>
</ion-label>
<ion-toggle [(ngModel)]="forceSafeAreaMargins" (ionChange)="safeAreaChanged()" slot="end" />
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="stagingSitesCount && enableStagingSites !== undefined">
<ion-label>
<h2>Enable staging sites ({{stagingSitesCount}})</h2>
</ion-label>
<ion-toggle [(ngModel)]="enableStagingSites" (ionChange)="setEnabledStagingSites($event.detail.checked)" slot="end" />
<ion-toggle [(ngModel)]="enableStagingSites" (ionChange)="setEnabledStagingSites($event.detail.checked)">
<p class="item-heading">Enable staging sites <ion-badge>{{stagingSitesCount}}</ion-badge></p>
</ion-toggle>
</ion-item>
<ng-container *ngIf="siteId">
<ion-item class="ion-text-wrap">
<ion-label>
<ion-toggle [(ngModel)]="remoteStyles" (ionChange)="remoteStylesChanged()">
<p class="item-heading">Enable remote styles <ion-badge>{{remoteStylesCount}}</ion-badge>
</p>
</ion-label>
<ion-toggle [(ngModel)]="remoteStyles" (ionChange)="remoteStylesChanged()" slot="end" />
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<ion-toggle [(ngModel)]="pluginStyles" (ionChange)="pluginStylesChanged()">
<p class="item-heading">Enable site plugin styles <ion-badge>{{pluginStylesCount}}</ion-badge>
</p>
</ion-label>
<ion-toggle [(ngModel)]="pluginStyles" (ionChange)="pluginStylesChanged()" slot="end" />
</ion-toggle>
</ion-item>
</ng-container>

View File

@ -12,11 +12,9 @@
<ion-content>
<ion-list class="list-item-limited-width">
<ion-item class="ion-text-wrap" lines="none">
<ion-label>
<p class="item-heading">{{ 'core.settings.language' | translate }}</p>
</ion-label>
<ion-select [(ngModel)]="selectedLanguage" (ionChange)="languageChanged($event)" interface="action-sheet"
[cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'core.settings.language' | translate}">
<div slot="label" class="item-heading">{{ 'core.settings.language' | translate }}</div>
<ion-select-option *ngFor="let entry of languages" [value]="entry.code">{{ entry.name }}</ion-select-option>
</ion-select>
</ion-item>
@ -36,13 +34,13 @@
</ion-segment>
</ion-item>
<ion-item class="ion-text-wrap core-settings-general-color-scheme" *ngIf="colorSchemes.length > 0" lines="none">
<ion-label>
<p class="item-heading">{{ 'core.settings.colorscheme' | translate }}</p>
<p *ngIf="colorSchemeDisabled" class="text-danger">{{ 'core.settings.forcedsetting' | translate }}</p>
</ion-label>
<ion-select [(ngModel)]="selectedScheme" (ionChange)="colorSchemeChanged($event)" interface="action-sheet"
[cancelText]="'core.cancel' | translate" [disabled]="colorSchemeDisabled"
[interfaceOptions]="{header: 'core.settings.colorscheme' | translate}">
<div slot="label">
<p class="item-heading">{{ 'core.settings.colorscheme' | translate }}</p>
<p *ngIf="colorSchemeDisabled" class="text-danger">{{ 'core.settings.forcedsetting' | translate }}</p>
</div>
<ion-select-option *ngFor="let scheme of colorSchemes" [value]="scheme">
{{ 'core.settings.colorscheme-' + scheme | translate }}</ion-select-option>
</ion-select>
@ -53,11 +51,10 @@
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<ion-toggle [(ngModel)]="richTextEditor" (ionChange)="richTextEditorChanged($event)">
<p class="item-heading">{{ 'core.settings.enablerichtexteditor' | translate }}</p>
<p>{{ 'core.settings.enablerichtexteditordescription' | translate }}</p>
</ion-label>
<ion-toggle [(ngModel)]="richTextEditor" (ionChange)="richTextEditorChanged($event)" slot="end" />
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="displayIframeHelp">
<ion-label>
@ -69,11 +66,10 @@
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<ion-toggle [(ngModel)]="debugDisplay" (ionChange)="debugDisplayChanged($event)">
<p class="item-heading">{{ 'core.settings.debugdisplay' | translate }}</p>
<p>{{ 'core.settings.debugdisplaydescription' | translate }}</p>
</ion-label>
<ion-toggle [(ngModel)]="debugDisplay" (ionChange)="debugDisplayChanged($event)" slot="end" />
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="analyticsAvailable">
<ion-label>

View File

@ -24,12 +24,9 @@
</ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap">
<ion-label>
<p class="item-heading">
{{ 'core.settings.syncdatasaver' | translate }}
</p>
</ion-label>
<ion-toggle slot="end" [(ngModel)]="dataSaver" (ngModelChange)="syncOnlyOnWifiChanged()" />
<ion-toggle [(ngModel)]="dataSaver" (ngModelChange)="syncOnlyOnWifiChanged()">
{{ 'core.settings.syncdatasaver' | translate }}
</ion-toggle>
</ion-item>
<ion-card class="core-warning-card" *ngIf="!isOnline || (dataSaver && limitedConnection)">

View File

@ -77,16 +77,25 @@ export class TestingBehatDomUtilsService {
*
* @param element Element.
* @param container Container.
* @param firstCall Whether this is the first call of the function.
* @returns Whether the element is selected or not.
*/
isElementSelected(element: HTMLElement, container: HTMLElement): boolean {
isElementSelected(element: HTMLElement, container: HTMLElement, firstCall = true): boolean {
const ariaCurrent = element.getAttribute('aria-current');
if (
(ariaCurrent && ariaCurrent !== 'false') ||
(element.getAttribute('aria-selected') === 'true') ||
(element.getAttribute('aria-checked') === 'true')
) {
return true;
const ariaSelected = element.getAttribute('aria-selected');
const ariaChecked = element.getAttribute('aria-checked');
if (ariaCurrent || ariaSelected || ariaChecked) {
return (!!ariaCurrent && ariaCurrent !== 'false') ||
(!!ariaSelected && ariaSelected === 'true') ||
(!!ariaChecked && ariaChecked === 'true');
}
if (firstCall) {
const inputElement = element.closest('ion-checkbox, ion-radio, ion-toggle')?.querySelector('input');
if (inputElement) {
return inputElement.value === 'on';
}
}
const parentElement = this.getParentElement(element);
@ -94,7 +103,7 @@ export class TestingBehatDomUtilsService {
return false;
}
return this.isElementSelected(parentElement, container);
return this.isElementSelected(parentElement, container, false);
}
/**

View File

@ -222,6 +222,10 @@ core-rich-text-editor .core-rte-editor {
margin-block-start: 0;
}
> p:only-child {
margin-bottom: 0;
}
hr {
border-top: 1px solid var(--stroke);
}

View File

@ -66,77 +66,70 @@ body {
.font-lg { font-size: 1.7rem; }
.font-sm { font-size: 1.2rem; }
// Headings.
// Item Headings.
// Some styles taken from ion-label
.md ion-label .item-heading,
.ios ion-label .item-heading {
text-overflow: inherit;
overflow: inherit;
--color: initial;
color: var(--color);
line-height: 20px;
&.item-heading-secondary {
.item > ion-label,
.fake-ion-item > ion-label,
ion-item .in-item {
p {
--color: var(--subdued-text-color);
color: var(--color);
@include margin(2px, 0);
font-size: 0.875rem;
}
}
.ios ion-label > p,
.md ion-label > p {
--color: var(--subdued-text-color);
color: var(--color);
}
.md ion-label .item-heading {
@include margin(2px, 0);
font-size: 16px;
font-weight: normal;
&.item-heading-secondary {
.item-heading {
@include margin(2px, 0);
font-size: var(--text-size);
font-size: 1rem;
font-weight: normal;
line-height: normal;
}
}
text-overflow: inherit;
overflow: inherit;
--color: initial;
color: var(--color);
.ios ion-label .item-heading {
@include margin(0, 0, 2px);
&.item-heading-secondary {
@include margin(2px, 0);
font-size: 17px;
font-weight: normal;
font-size: var(--text-size);
font-weight: normal;
line-height: normal;
&.item-heading-secondary {
@include margin(0, 0, 3px);
font-size: var(--text-size);
font-weight: normal;
line-height: normal;
--color: var(--subdued-text-color);
}
}
}
// Correctly inherit ion-text-wrap onto labels.
.item ion-label core-format-text > *:not(pre),
.fake-ion-item core-format-text > *:not(pre) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.item > ion-label,
.fake-ion-item {
core-format-text,
core-format-text > *:not(pre) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.item.ion-text-wrap > ion-label core-format-text > *:not(pre),
.fake-ion-item.ion-text-wrap core-format-text > *:not(pre) {
white-space: normal;
overflow: inherit;
.item.ion-text-wrap > ion-label,
ion-item > .in-item,
.fake-ion-item.ion-text-wrap {
core-format-text,
core-format-text > *:not(pre) {
white-space: normal;
overflow: inherit;
}
}
.item.ion-text-wrap > ion-label {
white-space: normal !important;
}
ion-item .core-input-errors-wrapper {
width: 100%;
}
@each $color-name, $unused in $colors {
.text-#{$color-name},
p.text-#{$color-name} {
@ -977,6 +970,15 @@ ion-content.limited-width > :not([slot]) {
min-height: 100%;
}
.limited-width > core-loading:not([slot]),
.menu > core-loading:not([slot]) {
&.core-loading-loaded {
--contents-display: flex;
flex-direction: column;
}
min-height: 100%;
}
ion-toolbar h1 img.core-bar-button-image,
ion-toolbar h1 .core-bar-button-image img {
padding: 4px;
@ -1124,12 +1126,16 @@ input[type=checkbox] {
}
// Select.
ion-select::part(text) {
white-space: normal;
}
ion-select::part(icon) {
opacity: 1;
ion-select {
&::part(text) {
white-space: normal;
}
&::part(icon) {
opacity: 1;
}
&::part(label) {
max-width: none;
}
}
ion-select-popover {
@ -1401,7 +1407,7 @@ audio.core-media-adapt-width {
}
ion-item {
font-size: var(--text-size);
// font-size: var(--text-size);
--inner-border-width: 0px;
}
@ -1411,7 +1417,7 @@ ion-item.item-lines-full {
}
ion-item.item-lines-inset {
--inner-border-width: 1px;
--inner-border-width: 0 0 1px 0;
--border-width: 0px;
}
@ -1984,3 +1990,15 @@ ion-item.item-label-stacked ion-datetime-button {
margin-bottom: 8px;
align-self: self-end;
}
// Development styles. Most of them temporary.
html.development {
ion-checkbox.legacy-checkbox,
ion-radio.legacy-radio,
ion-select.legacy-select,
ion-toggle.legacy-toggle,
ion-textarea.legacy-textarea,
ion-input.legacy-input {
background: red !important;
}
}

View File

@ -10,6 +10,7 @@ For more information about upgrading, read the official documentation: https://m
- Removed CoreToLocaleStringPipe deprecated since 3.6.0
- With the upgrade to Ionic 7 ion-slides is no longer supported and now you need to use swiper-container and swiper-slide. More info here: https://ionicframework.com/docs/angular/slides
- With the upgrade to Ionic7 ion-datetime has changed its usage. We recommend using ion-datetime-button. More info here: https://ionicframework.com/docs/updating/6-0#datetime
- CoreLoginHelper.getErrorMessages has been removed. Please create the messages object yourself.
=== 4.3.0 ===