Merge pull request #4038 from crazyserver/MOBILE-4470

Mobile 4470
main
Dani Palou 2024-05-09 11:49:21 +02:00 committed by GitHub
commit 4fddde1a00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 374 additions and 367 deletions

View File

@ -48,7 +48,7 @@
"nl": "Nederlands",
"no": "Norsk",
"pl": "Polski",
"ps": "پښتو",
"ps": "لیسي",
"pt": "Português - Portugal",
"pt-br": "Português - Brasil",
"ro": "Română",

View File

@ -597,6 +597,8 @@
"addon.mod_data.timeadded": "data",
"addon.mod_data.timemodified": "data",
"addon.mod_data.usedate": "data",
"addon.mod_data_fields_file.fieldtypelabel": "datafield_file",
"addon.mod_data_fields_picture.fieldtypelabel": "datafield_picture",
"addon.mod_feedback.analysis": "feedback",
"addon.mod_feedback.anonymous": "feedback",
"addon.mod_feedback.anonymous_entries": "feedback",
@ -1812,6 +1814,7 @@
"core.filenameexist": "local_moodlemobileapp",
"core.filenotfound": "resource",
"core.fileuploader.addfiletext": "repository",
"core.fileuploader.attachedfiles": "repository",
"core.fileuploader.audio": "local_moodlemobileapp",
"core.fileuploader.audiotitle": "tiny_recordrtc",
"core.fileuploader.camera": "local_moodlemobileapp",
@ -1834,6 +1837,7 @@
"core.fileuploader.microphonepermissiondenied": "local_moodlemobileapp",
"core.fileuploader.microphonepermissionrestricted": "local_moodlemobileapp",
"core.fileuploader.more": "data",
"core.fileuploader.nofilesattached": "repository",
"core.fileuploader.pauserecording": "local_moodlemobileapp",
"core.fileuploader.photoalbums": "local_moodlemobileapp",
"core.fileuploader.readingfile": "local_moodlemobileapp",

View File

@ -14,9 +14,7 @@
<form [formGroup]="form">
<ion-item>
<ion-input labelPlacement="stacked" formControlName="subject" type="text"
[placeholder]="'addon.blog.entrytitle' | translate" name="title">
<p>{{ 'addon.blog.entrytitle' | translate }}</p>
</ion-input>
[placeholder]="'addon.blog.entrytitle' | translate" name="title" [label]="'addon.blog.entrytitle' | translate" />
</ion-item>
<ion-item>
@ -27,7 +25,7 @@
[elementId]="entry?.id ?? 'new_entry'" />
</ion-item>
<ion-item>
<ion-item lines="none">
<core-combobox name="addon_blog_publish_to" formControlName="publishState" [label]="'addon.blog.publishto' | translate">
<ion-select-option class="core-select-option-title" [value]="publishState.draft">
{{ 'addon.blog.publishtonoone' | translate }}
@ -54,7 +52,7 @@
</ion-item>
<div id="addon-blog-associations">
@if (associationsExpanded) {
<ion-item class="ion-no-validation">
<ion-item lines="none">
@if (associatedModule) {
<ion-toggle formControlName="associateWithModule">
<core-format-text [text]="'addon.blog.associatewithmodule' | translate: {

View File

@ -20,7 +20,7 @@
<core-loading [hideUntil]="loaded">
@if (showMyEntriesToggle) {
<ion-item class="ion-no-validation">
<ion-item lines="none" class="ion-text-wrap">
<ion-toggle [(ngModel)]="onlyMyEntries" (ionChange)="onlyMyEntriesToggleChanged(onlyMyEntries)">
{{ 'addon.blog.showonlyyourentries' | translate }}
</ion-toggle>

View File

@ -9,7 +9,7 @@
</ion-header>
<ion-content [fullscreen]="true">
<ion-list>
<ion-item *ngFor="let type of types" class="addon-calendar-event ion-no-validation" [ngClass]="['addon-calendar-eventtype-'+type]">
<ion-item *ngFor="let type of types" class="addon-calendar-event" [ngClass]="['addon-calendar-eventtype-'+type]" lines="none">
<ion-icon [name]="typeIcons[type]" slot="start" aria-hidden="true" />
<ion-toggle [(ngModel)]="filter[type]" (ionChange)="onChange()">
{{ 'addon.calendar.' + type + 'events' | translate}}

View File

@ -1,14 +1,16 @@
@use "theme/globals" as *;
:host {
ion-item {
ion-icon, ion-radio {
.item {
ion-radio {
@include margin-horizontal(null, 8px);
}
> ion-icon {
padding: 4px;
font-size: var(--mdl-typography-icon-fontSize-md);
&.addon-calendar-event > ion-icon {
--margin-vertical: 8px;
--margin-end: 8px;
padding: 8px;
font-size: var(--mdl-typography-icon-fontSize-sm);
}
}
}

View File

@ -21,8 +21,8 @@
</ion-item-divider>
<ion-card>
<ion-list>
<ion-item class="ion-text-wrap ion-no-validation" *ngFor="let device of platform.devices"
[class.item-current]="device.current">
<ion-item class="ion-text-wrap" *ngFor="let device of platform.devices" [class.item-current]="device.current"
lines="none">
<ion-label>
<p class="item-heading" id="device-{{device.id}}">
<strong>{{ device.name }} {{ device.model }}</strong> ({{platform.platform}} {{ device.version }})

View File

@ -21,7 +21,7 @@
<h2>{{ 'core.settings.general' | translate }}</h2>
</ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap ion-no-validation">
<ion-item class="ion-text-wrap" lines="none">
<ion-toggle [(ngModel)]="sendOnEnter" (ngModelChange)="sendOnEnterChanged()">
{{ 'addon.messages.useentertosend' | translate }}
</ion-toggle>
@ -31,7 +31,7 @@
<!-- Contactable privacy. -->
<ion-card>
<ion-item *ngIf="!advancedContactable" class="ion-text-wrap ion-no-validation">
<ion-item *ngIf="!advancedContactable" class="ion-text-wrap" lines="none">
<ion-toggle [(ngModel)]="contactablePrivacy" (ngModelChange)="saveContactablePrivacy(contactablePrivacy)">
{{ 'addon.messages.blocknoncontacts' | translate }}
</ion-toggle>
@ -95,7 +95,7 @@
</ion-item-divider>
<!-- If notifications not disabled, show toggles.
If notifications are disabled, show "Disabled" instead of toggle. -->
<ion-item *ngFor="let state of ['loggedin', 'loggedoff']" class="ion-text-wrap ion-no-validation">
<ion-item *ngFor="let state of ['loggedin', 'loggedoff']" class="ion-text-wrap" lines="none">
<ion-label>
<p>{{ 'core.settings.' + state | translate }}</p>
</ion-label>
@ -131,7 +131,7 @@
</ion-item-divider>
<ng-container *ngFor="let processor of notification.processors">
<!-- If group messaging is enabled, display a simplified view. -->
<ion-item class="ion-text-wrap ion-no-validation">
<ion-item class="ion-text-wrap" lines="none">
<ion-label>
<p>{{ processor.displayname }}</p>
</ion-label>

View File

@ -345,7 +345,7 @@
</ion-item>
<!--- Apply grade to all team members. -->
<ion-item class="ion-text-wrap ion-no-validation" *ngIf="assign!.teamsubmission && canSaveGrades">
<ion-item class="ion-text-wrap" *ngIf="assign!.teamsubmission && canSaveGrades" lines="none">
<ion-toggle [(ngModel)]="grade.applyToAll">
<p class="item-heading">{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</p>
<p>{{ 'addon.mod_assign.applytoteam' | translate }}</p>
@ -371,7 +371,7 @@
</p>
</ion-label>
</ion-item>
<ion-item *ngIf="canSaveGrades && allowAddAttempt" class="ion-no-validation">
<ion-item *ngIf="canSaveGrades && allowAddAttempt" lines="none">
<ion-toggle [(ngModel)]="grade.addAttempt">
<p>{{ 'addon.mod_assign.addattempt' | translate }}</p>
</ion-toggle>

View File

@ -50,12 +50,12 @@
</span>
</p>
<p *ngIf="submission.statusTranslated">
<ion-badge class="ion-text-center ion-text-wrap" [color]="submission.statusColor">
<ion-badge class="ion-text-start ion-text-wrap" [color]="submission.statusColor">
{{ submission.statusTranslated }}
</ion-badge>
</p>
<p *ngIf="submission.gradingStatusTranslationId">
<ion-badge class="ion-text-center ion-text-wrap" [color]="submission.gradingColor">
<ion-badge class="ion-text-start ion-text-wrap" [color]="submission.gradingColor">
{{ submission.gradingStatusTranslationId | translate }}
</ion-badge>
</p>

View File

@ -41,7 +41,6 @@ import {
} from '../../services/book';
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
import { CoreUrlUtils } from '@services/utils/url';
import { IonicSlides } from '@ionic/angular';
/**
* Page that displays a book contents.
@ -65,7 +64,6 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy {
displayNavBar = true;
navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = [];
swiperOpts: CoreSwipeSlidesOptions = {
modules: [IonicSlides],
autoHeight: true,
observer: true,
observeParents: true,

View File

@ -16,7 +16,7 @@
<core-loading [hideUntil]="sessions.loaded">
<core-group-selector [groupInfo]="groupInfo" [(selected)]="groupId" (selectedChange)="reloadSessions()" [courseId]="courseId" />
<ion-item class="ion-no-validation">
<ion-item class="ion-text-wrap" lines="none">
<ion-toggle [(ngModel)]="showAll" (ionChange)="reloadSessions()">
{{ 'addon.mod_chat.showincompletesessions' | translate }}
</ion-toggle>

View File

@ -11,9 +11,9 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item class="ion-no-validation">
<ion-item lines="full" class="ion-text-wrap">
<ion-toggle [(ngModel)]="search.searchingAdvanced">
{{ 'addon.mod_data.advancedsearch' | translate }}
<p class="item-heading">{{ 'addon.mod_data.advancedsearch' | translate }}</p>
</ion-toggle>
</ion-item>
<form (ngSubmit)="searchEntries($event)" [formGroup]="searchForm" #searchFormEl>

View File

@ -2,9 +2,6 @@
// Edit and search modal.
:host {
--input-border-color: var(--stroke);
--input-border-width: 1px;
--select-border-width: 0px;
::ng-deep {
table {
@ -13,16 +10,6 @@
td {
vertical-align: top;
}
.addon-data-latlong {
display: flex;
.input-units {
flex-grow: 1;
white-space: nowrap;
align-self: center;
}
}
}
.addon-data-advanced-search {
@ -35,66 +22,15 @@
background-color: var(--ion-item-background);
::ng-deep {
ion-input {
border-bottom: var(--input-border-width) solid var(--input-border-color);
&.has-focus,
&.has-focus.ion-valid,
&.ion-touched.ion-invalid {
--input-border-width: 2px;
.has-errors {
.input-highlight,
.select-highlight,
.textarea-highlight {
transform: scale(1);
}
&.has-focus {
--input-border-color: var(--primary);
core-rich-text-editor.ion-touched.ng-invalid {
--stroke: var(--danger);
}
&.has-focus.ion-valid {
--input-border-color: var(--success);
}
&.ion-touched.ion-invalid {
--input-border-color: var(--danger);
}
}
core-rich-text-editor {
border-bottom: var(--select-border-width) solid var(--input-border-color);
&.ion-touched.ng-valid,
&.ion-touched.ng-invalid {
--select-border-width: 2px;
}
&.ion-touched.ng-valid {
--input-border-color: var(--success);
}
&.ion-touched.ng-invalid {
--input-border-color: var(--danger);
}
}
ion-select {
border-bottom: var(--select-border-width) solid var(--input-border-color);
&.ion-touched.ion-valid,
&.ion-touched.ion-invalid {
--select-border-width: 2px;
}
&.ion-touched.ion-valid {
--input-border-color: var(--success);
}
&.ion-touched.ion-invalid {
--input-border-color: var(--danger);
}
}
.has-errors ion-input.ion-invalid {
--input-border-width: 2px;
--input-border-color: var(--danger);
}
.has-errors ion-select.ion-invalid,
.has-errors core-rich-text-editor.ng-invalid {
--select-border-width: 2px;
--input-border-color: var(--danger);
}
.core-mark-required {

View File

@ -55,7 +55,7 @@ $grid-column-paddings: (
}
// Do not let block elements to define widths or heights.
address, article, aside, blockquote, canvas, dd, div, dl, dt, fieldset, figcaption, figure, footer, form,
address, article, aside, blockquote, canvas, dd, dl, dt, fieldset, figcaption, figure, footer, form,
h1, h2, h3, h4, h5, h6,
header, li, main, nav, noscript, ol, p, pre, section, table, tfoot, ul, video {
width: auto !important;

View File

@ -1,13 +1,13 @@
<span *ngIf="editMode && form">
<span [core-mark-required]="field.required" class="core-mark-required"></span>
<ng-container *ngIf="editMode && form">
<core-attachments [files]="files" [maxSize]="maxSizeBytes" maxSubmissions="1" [component]="component" [componentId]="componentId"
[allowOffline]="true" [courseId]="database?.course" />
[allowOffline]="true" [courseId]="database?.course" [required]="field.required"
[title]="'addon.mod_data_fields_file.fieldtypelabel' | translate" />
<core-input-errors *ngIf="error" [errorText]="error" />
</span>
</ng-container>
<span *ngIf="searchMode && form" [formGroup]="form">
<ng-container *ngIf="searchMode && form" [formGroup]="form">
<ion-input type="text" [formControlName]="'f_'+field.id" [placeholder]="field.name" />
</span>
</ng-container>
<ng-container *ngIf="displayMode">
<div>

View File

@ -0,0 +1,3 @@
{
"fieldtypelabel": "File"
}

View File

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

View File

@ -2,7 +2,7 @@
<span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span>
<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 *ngIf="!editMode || !field.required" value="">{{ 'addon.mod_data.menuchoose' | translate }}</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" />

View File

@ -35,6 +35,7 @@ export class AddonModDataFieldMenuComponent extends AddonModDataFieldPluginBaseC
}
this.options = this.field.param1.split('\n');
this.options = this.options.filter((option) => option !== '');
let val: string | undefined;
if (this.editMode && this.value) {

View File

@ -1,16 +1,16 @@
<span *ngIf="editMode && form" [formGroup]="form">
<span [core-mark-required]="field.required" class="core-mark-required"></span>
<ng-container *ngIf="editMode && form" [formGroup]="form">
<core-attachments [files]="files" [maxSize]="maxSizeBytes" maxSubmissions="1" [component]="component" [componentId]="componentId"
[allowOffline]="true" acceptedTypes="image" [courseId]="database?.course" />
[allowOffline]="true" acceptedTypes="image" [courseId]="database?.course" [required]="field.required"
[title]="'addon.mod_data_fields_picture.fieldtypelabel' | translate" />
<core-input-errors *ngIf="error" [errorText]="error" />
<ion-input [label]="'addon.mod_data.alttext' | translate" labelPlacement="stacked" type="text"
[formControlName]="'f_'+field.id+'_alttext'" [placeholder]=" 'addon.mod_data.alttext' | translate" />
</span>
</ng-container>
<span *ngIf="searchMode && form" [formGroup]="form">
<ng-container *ngIf="searchMode && form" [formGroup]="form">
<ion-input type="text" [formControlName]="'f_'+field.id" [placeholder]="field.name" />
</span>
</ng-container>
<button class="as-link" *ngIf="listMode && imageUrl" (click)="navigateEntry()">
<img [src]="imageUrl" [alt]="title" class="core-media-adapt-width listMode_picture" core-external-content />

View File

@ -0,0 +1,3 @@
{
"fieldtypelabel": "Image"
}

View File

@ -3,7 +3,7 @@
<ion-select [formControlName]="'f_'+field.id" [placeholder]="'addon.mod_data.menuchoose' | translate"
[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 *ngIf="!editMode || !field.required" value="">{{ 'addon.mod_data.menuchoose' | translate }}</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" />

View File

@ -35,6 +35,7 @@ export class AddonModDataFieldRadiobuttonComponent extends AddonModDataFieldPlug
}
this.options = this.field.param1.split('\n');
this.options = this.options.filter((option) => option !== '');
let val: string | undefined;
if (this.editMode && this.value) {

View File

@ -2,7 +2,7 @@
:host .addon-mod_forum-post {
background-color: var(--ion-item-background);
border-bottom: 1px solid var(--addon-forum-border-color);
border-bottom: var(--addon-forum-border-color, 1px) solid var(--addon-forum-border-color);
.addon-forum-star {
color: var(--core-star-color);

View File

@ -1,7 +1,10 @@
:host {
.addon-forum-reply-button .label {
margin: 0;
.addon-forum-reply-button ion-label {
margin: 0px;
}
ion-card addon-mod-forum-post {
--addon-forum-border-color: 0px;
}
}

View File

@ -17,9 +17,7 @@
<form *ngIf="showForm" #newDiscFormEl>
<ion-item>
<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>
[placeholder]="'addon.mod_forum.subject' | translate" name="subject" [label]="'addon.mod_forum.subject' | translate" />
</ion-item>
<ion-item>
<ion-label position="stacked">{{ 'addon.mod_forum.message' | translate }}</ion-label>
@ -38,7 +36,7 @@
</ion-label>
</ion-item>
<div *ngIf="advanced" id="addon-mod-forum-new-discussion-advanced">
<ion-item *ngIf="showGroups && groupIds.length > 1 && accessInfo.cancanposttomygroups" class="ion-no-validation">
<ion-item *ngIf="showGroups && groupIds.length > 1 && accessInfo.cancanposttomygroups" class="ion-text-wrap" lines="none">
<ion-toggle [(ngModel)]="newDiscussion.postToAllGroups" name="postallgroups">
{{ 'addon.mod_forum.posttomygroups' | translate }}
</ion-toggle>
@ -54,12 +52,12 @@
</ion-select-option>
</ion-select>
</ion-item>
<ion-item class="ion-text-wrap ion-no-validation">
<ion-item class="ion-text-wrap" lines="none">
<ion-toggle [(ngModel)]="newDiscussion.subscribe" name="subscribe">
{{ 'addon.mod_forum.discussionsubscription' | translate }}
</ion-toggle>
</ion-item>
<ion-item *ngIf="canPin" class="ion-text-wrap ion-no-validation">
<ion-item *ngIf="canPin" class="ion-text-wrap" lines="none">
<ion-toggle [(ngModel)]="newDiscussion.pin" name="pin">
{{ 'addon.mod_forum.discussionpinned' | translate }}
</ion-toggle>
@ -68,7 +66,8 @@
[maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component" [componentId]="forum.cmid"
[allowOffline]="true" [courseId]="courseId" />
</div>
<ion-item *ngIf="showGroups && postInGroupMessage && !newDiscussion.postToAllGroups" class="addon-forum-group-info">
<ion-item *ngIf="showGroups && postInGroupMessage && !newDiscussion.postToAllGroups"
class="addon-forum-group-info ion-text-wrap">
<ion-icon name="fas-circle-info" slot="start" aria-hidden="true" />
<ion-label>
<core-format-text [text]="postInGroupMessage" contextLevel="course" [contextInstanceId]="courseId"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -17,6 +17,7 @@
<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>
<core-rich-text-editor [control]="definitionControl" (contentChanged)="onDefinitionChange($event)"
@ -24,8 +25,8 @@
[componentId]="cmId" [autoSave]="true" contextLevel="module" [contextInstanceId]="cmId" elementId="definition_editor"
[draftExtraParams]="editorExtraParams" />
</ion-item>
<ion-item *ngIf="categories.length > 0">
<ion-item *ngIf="categories.length > 0">
<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}"
@ -35,40 +36,38 @@
</ion-select-option>
</ion-select>
</ion-item>
<ion-item *ngIf="showAliases">
<ion-item *ngIf="showAliases">
<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>
<h2>{{ 'addon.mod_glossary.attachment' | translate }}</h2>
</ion-label>
</ion-item-divider>
<core-attachments [files]="data.attachments" [component]="component" [componentId]="glossary.coursemodule" [allowOffline]="true"
[courseId]="courseId" />
[courseId]="courseId" [title]="'addon.mod_glossary.attachment' | translate" />
<ng-container *ngIf="glossary.usedynalink">
<ion-item-divider>
<ion-label>
<h2>{{ 'addon.mod_glossary.linking' | translate }}</h2>
</ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap ion-no-validation">
<ion-item class="ion-text-wrap" lines="none">
<ion-toggle [(ngModel)]="data.usedynalink" name="usedynalink">
{{ 'addon.mod_glossary.entryusedynalink' | translate }}
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap ion-no-validation">
<ion-item class="ion-text-wrap" lines="none">
<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-no-validation">
<ion-item class="ion-text-wrap" lines="none">
<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()">
{{ 'core.save' | translate }}
</ion-button>

View File

@ -62,21 +62,21 @@
</ion-list>
<ion-list *ngIf="ownAssessment && !assessment">
<ion-item class="ion-text-wrap">
<ion-item-divider class="ion-text-wrap">
<ion-label>
<h2>{{ 'addon.mod_workshop.yourassessment' | translate }}</h2>
</ion-label>
</ion-item>
</ion-item-divider>
<addon-mod-workshop-assessment [submission]="submission" [assessment]="ownAssessment" [courseId]="courseId" [access]="access"
[module]="module" [workshop]="workshop" />
</ion-list>
<ion-list *ngIf="submissionInfo && submissionInfo.reviewedby && submissionInfo.reviewedby.length && !assessment">
<ion-item class="ion-text-wrap">
<ion-item-divider class="ion-text-wrap">
<ion-label>
<h3 class="item-heading">{{ 'addon.mod_workshop.receivedgrades' | translate }}</h3>
<h2>{{ 'addon.mod_workshop.receivedgrades' | translate }}</h2>
</ion-label>
</ion-item>
</ion-item-divider>
<ng-container *ngFor="let reviewer of submissionInfo.reviewedby">
<addon-mod-workshop-assessment *ngIf="!reviewer.ownAssessment" [submission]="submission" [assessment]="reviewer"
[courseId]="courseId" [access]="access" [module]="module" [workshop]="workshop" />
@ -84,34 +84,34 @@
</ion-list>
<ion-list *ngIf="submissionInfo && submissionInfo.reviewerof && submissionInfo.reviewerof.length && !assessment">
<ion-item class="ion-text-wrap">
<ion-item-divider class="ion-text-wrap">
<ion-label>
<h3 class="item-heading">{{ 'addon.mod_workshop.givengrades' | translate }}</h3>
<h2>{{ 'addon.mod_workshop.givengrades' | translate }}</h2>
</ion-label>
</ion-item>
</ion-item-divider>
<addon-mod-workshop-assessment *ngFor="let reviewer of submissionInfo.reviewerof" [assessment]="reviewer" [courseId]="courseId"
[module]="module" [workshop]="workshop" [access]="access" />
</ion-list>
<form [formGroup]="feedbackForm" *ngIf="canAddFeedback && submission" #feedbackFormEl>
<ion-item class="ion-text-wrap">
<ion-item-divider class="ion-text-wrap">
<ion-label>
<h3 class="item-heading">{{ 'addon.mod_workshop.feedbackauthor' | translate }}</h3>
<h2>{{ 'addon.mod_workshop.feedbackauthor' | translate }}</h2>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap ion-no-validation" *ngIf="access.canpublishsubmissions">
</ion-item-divider>
<ion-item class="ion-text-wrap" lines="none" *ngIf="access.canpublishsubmissions">
<ion-toggle formControlName="published">
<p class="item-heading">{{ 'addon.mod_workshop.publishsubmission' | translate }}</p>
<p>{{ 'addon.mod_workshop.publishsubmission_help' | translate }}</p>
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-item-divider class="ion-text-wrap">
<ion-label>
<h3 class="item-heading">{{ 'addon.mod_workshop.gradecalculated' | translate }}</h3>
<h3>{{ 'addon.mod_workshop.gradecalculated' | translate }}</h3>
<p>{{ submission.grade }}</p>
</ion-label>
</ion-item>
</ion-item-divider>
<ion-item class="ion-text-wrap">
<ion-select labelPlacement="stacked" formControlName="grade" interface="action-sheet"
[cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'addon.mod_workshop.gradeover' | translate}"

View File

@ -21,12 +21,12 @@
</ion-refresher>
<core-loading [hideUntil]="preferencesLoaded">
<ion-card>
<ion-item class="ion-text-wrap ion-no-validation" *ngIf="preferences">
<ion-item class="ion-text-wrap" *ngIf="preferences" lines="none">
<ion-toggle [(ngModel)]="preferences.enableall" (ngModelChange)="enableAll(preferences.enableall)">
<p class="item-heading">{{ 'addon.notifications.allownotifications' | translate }}</p>
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap ion-no-validation" *ngIf="canChangeSound">
<ion-item class="ion-text-wrap" *ngIf="canChangeSound" lines="none">
<ion-toggle [(ngModel)]="notificationSound" (ngModelChange)="changeNotificationSound(notificationSound)">
<p class="item-heading">{{ 'addon.notifications.playsound' | translate }}</p>
</ion-toggle>
@ -79,7 +79,7 @@
</ion-card-header>
<ng-container *ngFor="let notification of component.notifications">
<!-- Tablet view -->
<ion-item class="ion-text-wrap ion-hide-md-down addon-notifications-table-content only-links ion-no-validation">
<ion-item class="ion-text-wrap ion-hide-md-down addon-notifications-table-content only-links" lines="none">
<ion-label>
<ion-row class="ion-no-padding ion-align-items-center">
<ion-col class="ion-margin-horizontal ion-no-padding">
@ -116,7 +116,7 @@
</ion-label>
</ion-item>
<!-- If notifications enabled, show toggles. If disabled, show "Disabled" instead of toggle. -->
<ion-item *ngFor="let state of ['loggedin', 'loggedoff']" class="ion-text-wrap ion-hide-md-up">
<ion-item *ngFor="let state of ['loggedin', 'loggedoff']" class="ion-text-wrap ion-hide-md-up" lines="none">
<ion-label class="ion-margin-horizontal">
<p>{{ 'core.settings.' + state | translate }}</p>
</ion-label>
@ -152,7 +152,7 @@
</ion-item-divider>
<ng-container *ngFor="let notification of component.notifications">
<!-- If notifications enabled, show toggles. If disabled, show "Disabled" instead of toggle. -->
<ion-item class="ion-text-wrap">
<ion-item class="ion-text-wrap" lines="none">
<ion-label>
<p>{{ notification.displayname }}</p>
</ion-label>

View File

@ -17,7 +17,6 @@ import {
Input,
Output,
EventEmitter,
OnInit,
OnChanges,
OnDestroy,
AfterViewInit,
@ -28,7 +27,6 @@ import {
import { BackButtonEvent } from '@ionic/core';
import { Subscription } from 'rxjs';
import { Translate } from '@singletons';
import { CoreSettingsHelper } from '@features/settings/services/settings-helper';
import { CoreAriaRoleTab, CoreAriaRoleTabFindable } from './aria-role-tab';
import { CoreEventObserver } from '@singletons/events';
@ -38,10 +36,9 @@ import { CoreError } from './errors/error';
import { CorePromisedValue } from './promised-value';
import { AsyncDirective } from './async-directive';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CorePlatform } from '@services/platform';
import { Swiper } from 'swiper';
import { SwiperOptions } from 'swiper/types';
import { IonicSlides } from '@ionic/angular';
import { CoreSwiper } from '@singletons/swiper';
/**
* Class to abstract some common code for tabs.
@ -49,7 +46,7 @@ import { IonicSlides } from '@ionic/angular';
@Component({
template: '',
})
export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, AfterViewInit, OnChanges, OnDestroy, AsyncDirective {
export class CoreTabsBaseComponent<T extends CoreTabBase> implements AfterViewInit, OnChanges, OnDestroy, AsyncDirective {
// Minimum tab's width.
protected static readonly MIN_TAB_WIDTH = 107;
@ -59,23 +56,18 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
@Output() protected ionChange = new EventEmitter<T>(); // Emitted when the tab changes.
protected swiper?: Swiper;
@ViewChild('swiperRef')
set swiperRef(swiperRef: ElementRef) {
@ViewChild('swiperRef') set swiperRef(swiperRef: ElementRef) {
/**
* This setTimeout waits for Ionic's async initialization to complete.
* Otherwise, an outdated swiper reference will be used.
*/
setTimeout(() => {
if (swiperRef?.nativeElement?.swiper && !this.swiper) {
this.swiper = swiperRef.nativeElement.swiper as Swiper;
this.swiper.changeLanguageDirection(CorePlatform.isRTL ? 'rtl' : 'ltr');
Object.keys(this.swiperOpts).forEach((key) => {
if (this.swiper) {
this.swiper.params[key] = this.swiperOpts[key];
const swiper = CoreSwiper.initSwiperIfAvailable(this.swiper, swiperRef, this.swiperOpts);
if (!swiper) {
return;
}
});
this.swiper = swiper;
// Subscribe to changes.
this.swiper.on('slideChangeTransitionEnd', () => {
@ -83,8 +75,7 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
});
this.init();
}
}, 0);
});
}
tabs: T[] = []; // List of tabs.
@ -97,7 +88,6 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
numTabsShown = 0;
description = '';
swiperOpts: SwiperOptions = {
modules: [IonicSlides],
slidesPerView: 3,
centerInsufficientSlides: true,
threshold: 10,
@ -127,18 +117,6 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
CoreDirectivesRegistry.register(element.nativeElement, this);
}
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
// Change the side when the language changes.
this.subscriptions.push(Translate.onLangChange.subscribe(() => {
setTimeout(() => {
this.swiper?.changeLanguageDirection(CorePlatform.isRTL ? 'rtl' : 'ltr');
});
}));
}
/**
* @inheritdoc
*/

View File

@ -55,6 +55,7 @@ export class CoreAttachmentsComponent implements OnInit {
@Input() acceptedTypes?: string; // List of supported filetypes. If undefined, all types supported.
@Input() required?: boolean; // Whether to display the required mark.
@Input() courseId?: number; // Course ID.
@Input() title = Translate.instant('core.fileuploader.attachedfiles'); // Title to display.
maxSizeReadable?: string;
maxSubmissionsReadable?: string;

View File

@ -1,12 +1,17 @@
<core-loading [hideUntil]="loaded" [fullscreen]="false">
<ion-card>
<ion-item class="ion-text-wrap">
<ion-label>
<p class="item-heading">{{ title }} <span [core-mark-required]="required" class="core-mark-required"></span></p>
<span *ngIf="maxSubmissionsReadable">
{{ 'core.maxsizeandattachments' | translate:{$a: {size: maxSizeReadable, attachments: maxSubmissionsReadable} } }}
</span>
<span *ngIf="!maxSubmissionsReadable">{{ 'core.maxfilesize' | translate:{$a: maxSizeReadable} }}</span>
<span [core-mark-required]="required" class="core-mark-required"></span>
</ion-label>
<ion-button slot="end" (click)="add()" [attr.aria-label]="'core.fileuploader.addfiletext' | translate"
*ngIf="unlimitedFiles || (maxSubmissions !== undefined && maxSubmissions >= 0 && files && files.length < maxSubmissions)">
<ion-icon name="fas-plus" slot="icon-only" aria-hidden="true" />
</ion-button>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="fileTypes && fileTypes.mimetypes && fileTypes.mimetypes.length">
<ion-label>
@ -18,7 +23,7 @@
</ul>
</ion-label>
</ion-item>
<div *ngFor="let file of files; let index=index">
<ng-container *ngFor="let file of files; let index=index">
<!-- Files already attached to the submission, either in online or in offline. -->
<core-file *ngIf="!file.name" [file]="file" [component]="component" [componentId]="componentId" [canDelete]="true"
(onDelete)="delete(index, true)" [canDownload]="!file.offline" />
@ -26,13 +31,12 @@
<!-- Files added to draft but not attached to submission yet. -->
<core-local-file *ngIf="file.name" [file]="file" [manage]="true" (onDelete)="delete(index, false)"
(onRename)="renamed(index, $event)" />
</div>
</ng-container>
<!-- Button to add more files. -->
<ion-button expand="block"
*ngIf="unlimitedFiles || (maxSubmissions !== undefined && maxSubmissions >= 0 && files && files.length < maxSubmissions)"
class="ion-text-wrap ion-margin" (click)="add()">
<ion-icon name="fas-plus" slot="start" aria-hidden="true" />
{{ 'core.fileuploader.addfiletext' | translate }}
</ion-button>
<ion-item class="ion-text-wrap" *ngIf="!files || !files.length">
<ion-label>
<p>{{ 'core.fileuploader.nofilesattached' | translate }}</p>
</ion-label>
</ion-item>
</ion-card>
</core-loading>

View File

@ -3,7 +3,7 @@
<ion-list-header *ngIf="title">
<ion-label>{{title}}</ion-label>
</ion-list-header>
<ion-item class="ion-text-wrap ion-no-validation" *ngFor="let item of items" core-link [capture]="item.captureLink"
<ion-item class="ion-text-wrap" lines="none" *ngFor="let item of items" core-link [capture]="item.captureLink"
[autoLogin]="item.autoLogin" [href]="item.href" (click)="itemClicked($event, item)" [attr.aria-label]="item.ariaAction"
[hidden]="item.hidden" [detail]="!!(item.href && !item.iconAction)" role="menuitem" [button]="!!(item.href && !item.iconAction)"
[showBrowserWarning]="item.showBrowserWarning">

View File

@ -1,15 +1,19 @@
<ng-container *ngIf="groupInfo && groupInfo.groups.length > 0 && (groupInfo.separateGroups || groupInfo.visibleGroups)">
<ion-card class="core-info-card" *ngIf="multipleGroupsMessage && groupInfo.groups && groupInfo.groups.length > 1">
<ion-item>
<ion-item class="ion-text-wrap">
<ion-icon name="fas-circle-question" slot="start" aria-hidden="true" />
<ion-label>{{ multipleGroupsMessage }}</ion-label>
<ion-label>
<p class="item-label">{{ multipleGroupsMessage }}</p>
</ion-label>
</ion-item>
</ion-card>
<ion-item class="ion-text-wrap core-group-selector">
<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-item class="ion-text-wrap core-group-selector" lines="full">
<ion-icon name="fas-user-group" slot="start" aria-hidden="true" />
<ion-select [(ngModel)]=" selected" (ionChange)="selectedChange.emit(selected)" interface="action-sheet"
[cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'core.group' | translate}">
<p class="item-heading" slot="label">{{ (groupInfo.separateGroups ? 'core.groupsseparate': 'core.groupsvisible') | translate }}
</p>
<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

@ -0,0 +1,4 @@
.core-group-selector ion-icon {
--webkit-margin-end: 16px;
margin-inline-end: 16px;
}

View File

@ -27,6 +27,7 @@ import { CoreGroupInfo } from '@services/groups';
@Component({
selector: 'core-group-selector',
templateUrl: 'group-selector.html',
styleUrl: 'group-selector.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CoreGroupSelectorComponent {

View File

@ -1,5 +1,6 @@
<ng-container *ngIf="loaded">
<ng-container *ngIf="loaded && !svgLoaded">
<img *ngIf="!isLocalUrl" [src]="iconUrl" core-external-content alt="" [component]="linkIconWithComponent ? modname : null"
[componentId]="linkIconWithComponent ? componentId : null" (error)="loadFallbackIcon()">
<img *ngIf="isLocalUrl" [src]="iconUrl" (error)="loadFallbackIcon()" alt="">
</ng-container>
<div [hidden]="!svgLoaded" #svg></div>

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { CoreConstants, ModPurpose } from '@/core/constants';
import { Component, ElementRef, HostBinding, Input, OnChanges, OnInit, SimpleChange } from '@angular/core';
import { Component, ElementRef, HostBinding, Input, OnChanges, OnInit, SimpleChange, ViewChild } from '@angular/core';
import { CoreCourse } from '@features/course/services/course';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreFile } from '@services/file';
@ -63,10 +63,13 @@ export class CoreModIconComponent implements OnInit, OnChanges {
return this.showAlt ? this.modNameTranslated : '';
}
@ViewChild('svg') svgElement!: ElementRef<HTMLElement>;
iconUrl = '';
modNameTranslated = '';
isLocalUrl = false;
svgLoaded = false;
linkIconWithComponent = false;
loaded = false;
@ -141,6 +144,10 @@ export class CoreModIconComponent implements OnInit, OnChanges {
this.iconUrl = CoreTextUtils.decodeHTMLEntities(this.iconUrl);
if (this.isBranded !== undefined) {
return;
}
// If it's an Moodle Theme icon, check if filtericon is set and use it.
if (this.iconUrl && CoreUrlUtils.isThemeImageUrl(this.iconUrl)) {
const filter = CoreUrlUtils.getThemeImageUrlParam(this.iconUrl, 'filtericon');
@ -296,6 +303,7 @@ export class CoreModIconComponent implements OnInit, OnChanges {
protected async setSVGIcon(): Promise<void> {
if (this.iconVersion === IconVersion.LEGACY_VERSION) {
this.loaded = true;
this.svgLoaded = false;
return;
}
@ -338,6 +346,8 @@ export class CoreModIconComponent implements OnInit, OnChanges {
}
if (mimetype !== 'image/svg+xml' || !fileContents) {
this.svgLoaded = false;
return;
}
@ -347,6 +357,8 @@ export class CoreModIconComponent implements OnInit, OnChanges {
// Safety check.
if (doc.documentElement.nodeName !== 'svg') {
this.svgLoaded = false;
return;
}
@ -384,9 +396,10 @@ export class CoreModIconComponent implements OnInit, OnChanges {
}
}
this.element.replaceChildren(doc.documentElement);
this.svgElement.nativeElement.replaceChildren(doc.documentElement);
this.svgLoaded = true;
} catch {
// Ignore errors.
this.svgLoaded = false;
} finally {
this.loaded = true;
}

View File

@ -24,6 +24,7 @@ import { CoreUtils } from '@services/utils/utils';
import { CoreDom } from '@singletons/dom';
import { CoreEventObserver } from '@singletons/events';
import { CoreMath } from '@singletons/math';
import { CoreSwiper } from '@singletons/swiper';
import { Swiper } from 'swiper';
import { SwiperOptions } from 'swiper/types';
/**
@ -42,15 +43,18 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
@Output() onDidChange = new EventEmitter<CoreSwipeCurrentItemData<Item>>();
protected swiper?: Swiper;
@ViewChild('swiperRef')
set swiperRef(swiperRef: ElementRef) {
@ViewChild('swiperRef') set swiperRef(swiperRef: ElementRef) {
/**
* This setTimeout waits for Ionic's async initialization to complete.
* Otherwise, an outdated swiper reference will be used.
*/
setTimeout(async () => {
if (swiperRef?.nativeElement?.swiper) {
this.swiper = swiperRef.nativeElement.swiper as Swiper;
const swiper = CoreSwiper.initSwiperIfAvailable(this.swiper, swiperRef, this.options);
if (!swiper) {
return;
}
this.swiper = swiper;
await this.initialize();
@ -58,13 +62,9 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
this.swiper.slideTo(this.options.initialSlide, 0, this.options.runCallbacksOnInit);
}
this.updateOptions();
this.swiper.on('slideChangeTransitionStart', () => this.slideWillChange());
this.swiper.on('slideChangeTransitionEnd', () => this.slideDidChange());
}
}, 0);
});
}
@ContentChild(TemplateRef) template?: TemplateRef<{item: Item; active: boolean}>; // Template defined by the content.
@ -173,17 +173,21 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
// If slides are being updated, wait for the update to finish.
await this.ready();
if (!this.swiper) {
return;
}
// Verify that the number of slides matches the number of items.
const slidesLength = this.swiper?.slides?.length || 0;
const slidesLength = this.swiper.slides?.length || 0;
if (slidesLength !== this.items.length) {
// Number doesn't match, do a new update to try to match them.
await this.updateSlidesComponent();
}
if (!this.swiper?.slides) {
if (!this.swiper.slides) {
return;
}
this.swiper?.slideTo(index, speed, runCallbacks);
this.swiper.slideTo(index, speed, runCallbacks);
}
/**
@ -195,7 +199,7 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
*/
async slideToItem(item: Item, speed?: number, runCallbacks?: boolean): Promise<void> {
const index = this.manager?.getSource().getItemIndex(item) ?? -1;
if (index != -1) {
if (index !== -1) {
await this.slideToIndex(index, speed, runCallbacks);
}
}
@ -248,15 +252,7 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
return;
}
if (this.swiper.params === undefined) {
this.swiper.params = {};
}
Object.keys(this.options).forEach((key) => {
if (this.swiper) {
this.swiper.params[key] = this.options[key];
}
});
CoreSwiper.updateOptions(this.swiper, this.options);
}
/**

View File

@ -15,7 +15,6 @@
import {
Component,
Input,
OnInit,
OnChanges,
OnDestroy,
AfterViewInit,
@ -52,7 +51,7 @@ import { CoreDirectivesRegistry } from '@singletons/directives-registry';
styleUrls: ['../tabs/tabs.scss'],
})
export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutletTab>
implements OnInit, AfterViewInit, OnChanges, OnDestroy {
implements AfterViewInit, OnChanges, OnDestroy {
/**
* Determine tabs layout.

View File

@ -98,16 +98,18 @@
.core-textarea {
position: relative;
--highlight-color: transparent !important;
::ng-deep textarea {
margin: 0 !important;
padding: 0;
margin: 0px !important;
padding: 0px;
resize: none;
overflow-x: hidden;
overflow-y: auto;
position: absolute;
height: auto;
top: 0;
bottom: 0;
top: 0px;
bottom: 0px;
}
}
@ -180,6 +182,10 @@
min-height: 200px;
}
:host-context(.item-label-stacked) {
margin-top: 10px;
:host-context(.item) {
margin-bottom: 12px;
}
:host-context(.item-label-stacked) {
margin-top: 12px;
}

View File

@ -25,7 +25,7 @@ import {
AfterViewInit,
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { IonTextarea, IonContent, IonicSlides } from '@ionic/angular';
import { IonTextarea, IonContent } from '@ionic/angular';
import { Subscription } from 'rxjs';
import { CoreSites } from '@services/sites';
@ -33,7 +33,6 @@ import { CoreFilepool } from '@services/filepool';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreEventFormActionData, CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreEditorOffline } from '../../services/editor-offline';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
@ -45,6 +44,7 @@ import { CorePlatform } from '@services/platform';
import { Swiper } from 'swiper';
import { SwiperOptions } from 'swiper/types';
import { ContextLevel } from '@/core/constants';
import { CoreSwiper } from '@singletons/swiper';
/**
* Component to display a rich text editor if enabled.
@ -81,27 +81,22 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
@ViewChild('editor') editor?: ElementRef; // WYSIWYG editor.
@ViewChild('textarea') textarea?: IonTextarea; // Textarea editor.
@ViewChild('toolbar') toolbar?: ElementRef;
protected toolbarSlides?: Swiper;
@ViewChild('swiperRef')
set swiperRef(swiperRef: ElementRef) {
@ViewChild('swiperRef') set swiperRef(swiperRef: ElementRef) {
/**
* This setTimeout waits for Ionic's async initialization to complete.
* Otherwise, an outdated swiper reference will be used.
*/
setTimeout(() => {
if (swiperRef.nativeElement?.swiper) {
this.toolbarSlides = swiperRef.nativeElement.swiper as Swiper;
this.toolbarSlides.changeLanguageDirection(CorePlatform.isRTL ? 'rtl' : 'ltr');
Object.keys(this.swiperOpts).forEach((key) => {
if (this.toolbarSlides) {
this.toolbarSlides.params[key] = this.swiperOpts[key];
const swiper = CoreSwiper.initSwiperIfAvailable(this.toolbarSlides, swiperRef, this.swiperOpts);
if (!swiper) {
return;
}
this.toolbarSlides = swiper;
});
}
}, 0);
}
protected readonly DRAFT_AUTOSAVE_FREQUENCY = 30000;
protected readonly RESTORE_MESSAGE_CLEAR_TIME = 6000;
@ -128,7 +123,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
protected originalContent?: string;
protected resizeFunction?: () => Promise<number>;
protected selectionChangeFunction = (): void => this.updateToolbarStyles();
protected languageChangedSubscription?: Subscription;
protected resizeListener?: CoreEventObserver;
protected domPromise?: CoreCancellablePromise<void>;
protected buttonsDomPromise?: CoreCancellablePromise<void>;
@ -159,7 +153,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
isEmpty = true;
swiperOpts: SwiperOptions = {
modules: [IonicSlides],
slidesPerView: 6,
centerInsufficientSlides: true,
watchSlidesProgress: true,
@ -301,13 +294,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
// Check the height again, now the window height should have been updated.
this.maximizeEditorSize();
});
// Change the side when the language changes.
this.languageChangedSubscription = Translate.onLangChange.subscribe(() => {
setTimeout(() => {
this.toolbarSlides?.changeLanguageDirection(CorePlatform.isRTL ? 'rtl' : 'ltr');
});
});
}
/**
@ -1120,7 +1106,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
*/
ngOnDestroy(): void {
this.valueChangeSubscription?.unsubscribe();
this.languageChangedSubscription?.unsubscribe();
document.removeEventListener('selectionchange', this.selectionChangeFunction);

View File

@ -1,5 +1,6 @@
{
"addfiletext": "Add file",
"attachedfiles": "Attached files",
"audio": "Audio",
"audiotitle": "Record audio",
"camera": "Camera",
@ -22,6 +23,7 @@
"microphonepermissiondenied": "Permission to access the microphone has been denied.",
"microphonepermissionrestricted": "Microphone access is restricted.",
"more": "More",
"nofilesattached": "No files attached",
"pauserecording": "Pause recording",
"photoalbums": "Photo albums",
"readingfile": "Reading file",

View File

@ -204,7 +204,7 @@ export class CorePushNotificationsProvider {
protected async initializeDatabase(): Promise<void> {
try {
await CoreApp.createTablesFromSchema(APP_SCHEMA);
} catch (e) {
} catch {
// Ignore errors.
}

View File

@ -1,4 +1,4 @@
<ion-item class="ion-text-wrap" *ngIf="item && (item!.canrate || item!.rating !== null) && !disabled">
<ion-item class="ion-text-wrap" *ngIf="item && (item!.canrate || item!.rating !== null) && !disabled" lines="none">
<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}"
[label]="'core.rating.rating' | translate">

View File

@ -17,13 +17,13 @@
</ion-header>
<ion-content>
<ion-list class="list-item-limited-width">
<ion-item class="ion-text-wrap ion-no-validation">
<ion-item class="ion-text-wrap" lines="none">
<ion-toggle [(ngModel)]="rtl" (ionChange)="RTLChanged()">
<p class="item-heading">Change text direction</p>
<p>{{ direction }}</p>
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap ion-no-validation">
<ion-item class="ion-text-wrap" lines="none">
<ion-toggle [(ngModel)]="forceSafeAreaMargins" (ionChange)="safeAreaChanged()">
<p class="item-heading">Force safe area margins</p>
</ion-toggle>
@ -34,13 +34,13 @@
</ion-toggle>
</ion-item>
<ng-container *ngIf="siteId">
<ion-item class="ion-text-wrap ion-no-validation">
<ion-item class="ion-text-wrap" lines="none">
<ion-toggle [(ngModel)]="remoteStyles" (ionChange)="remoteStylesChanged()">
<p class="item-heading">Enable remote styles <ion-badge>{{remoteStylesCount}}</ion-badge>
</p>
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap ion-no-validation">
<ion-item class="ion-text-wrap" lines="none">
<ion-toggle [(ngModel)]="pluginStyles" (ionChange)="pluginStylesChanged()">
<p class="item-heading">Enable site plugin styles <ion-badge>{{pluginStylesCount}}</ion-badge>
</p>

View File

@ -11,14 +11,14 @@
</ion-header>
<ion-content>
<ion-list class="list-item-limited-width">
<ion-item class="ion-text-wrap ion-no-validation" lines="none">
<ion-item class="ion-text-wrap" lines="none">
<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 ion-text-wrap">{{ '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>
<ion-item class="ion-text-wrap core-settings-general-font-size item-interactive ion-no-validation" lines="none">
<ion-item class="ion-text-wrap core-settings-general-font-size item-interactive" lines="none">
<ion-label>
<p class="item-heading ion-text-wrap">{{ 'core.settings.fontsize' | translate }}</p>
</ion-label>
@ -33,7 +33,7 @@
</ion-segment-button>
</ion-segment>
</ion-item>
<ion-item class="ion-text-wrap core-settings-general-color-scheme ion-no-validation" *ngIf="colorSchemes.length > 0" lines="none">
<ion-item class="ion-text-wrap core-settings-general-color-scheme" *ngIf="colorSchemes.length > 0" lines="none">
<ion-select [(ngModel)]="selectedScheme" (ionChange)="colorSchemeChanged($event)" interface="action-sheet"
[cancelText]="'core.cancel' | translate" [disabled]="colorSchemeDisabled"
[interfaceOptions]="{header: 'core.settings.colorscheme' | translate}">
@ -50,13 +50,13 @@
<p class="ion-text-wrap">{{ 'core.settings.colorscheme-system-notice' | translate }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-no-validation">
<ion-item lines="none">
<ion-toggle [(ngModel)]="richTextEditor" (ionChange)="richTextEditorChanged($event)">
<p class="item-heading ion-text-wrap">{{ 'core.settings.enablerichtexteditor' | translate }}</p>
<p class="ion-text-wrap">{{ 'core.settings.enablerichtexteditordescription' | translate }}</p>
</ion-toggle>
</ion-item>
<ion-item *ngIf="displayIframeHelp" class="ion-no-validation">
<ion-item *ngIf="displayIframeHelp" lines="none">
<ion-label>
<p class="item-heading ion-text-wrap">{{ 'core.settings.ioscookies' | translate }}</p>
<p class="ion-text-wrap">{{ 'core.settings.ioscookiesdescription' | translate }}</p>
@ -65,13 +65,13 @@
</ion-button>
</ion-label>
</ion-item>
<ion-item class="ion-no-validation">
<ion-item lines="none">
<ion-toggle [(ngModel)]="debugDisplay" (ionChange)="debugDisplayChanged($event)">
<p class="item-heading ion-text-wrap">{{ 'core.settings.debugdisplay' | translate }}</p>
<p class="ion-text-wrap">{{ 'core.settings.debugdisplaydescription' | translate }}</p>
</ion-toggle>
</ion-item>
<ion-item *ngIf="analyticsAvailable" class="ion-no-validation">
<ion-item *ngIf="analyticsAvailable" lines="none">
<ion-label>
<p class="item-heading ion-text-wrap">{{ 'core.settings.enableanalytics' | translate }}</p>
<p class="ion-text-wrap">{{ 'core.settings.enableanalyticsdescription' | translate }}</p>

View File

@ -168,6 +168,9 @@ export class CoreSettingsGeneralPage {
/**
* Apply language changes and restart the app.
*
* IMPORTANT NOTE: If for any reason we decide to remove this method,
* we'll need to listen to lang change on Slides to change direction.
*/
protected async applyLanguageAndRestart(): Promise<void> {
// Invalidate cache for all sites to get the content in the right language.

View File

@ -15,7 +15,9 @@
<ion-item *ngFor="let section of sections.items" [attr.aria-current]="sections.getItemAriaCurrent(section)" button
[detail]="true" (click)="sections.select(section)">
<ion-icon [name]="section.icon" slot="start" aria-hidden="true" />
<ion-label>{{ section.name | translate }}</ion-label>
<ion-label class="ion-text-wrap">
<p class="item-heading">{{ section.name | translate }}</p>
</ion-label>
</ion-item>
</ion-list>
</core-split-view>

View File

@ -16,7 +16,7 @@
</ion-refresher>
<core-loading [hideUntil]="handlers.loaded">
<ion-list>
<ion-item *ngFor="let handler of handlerItems" class="core-settings-handler ion-no-validation" [ngClass]="handler.class"
<ion-item *ngFor="let handler of handlerItems" class="core-settings-handler" lines="none" [ngClass]="handler.class"
[attr.aria-label]="handler.title | translate" (click)="!handler.toggle && handlers.select(handler)"
[button]="!handler.toggle" [detail]="!handler.toggle" [attr.aria-current]="handlers.getItemAriaCurrent(handler)">
<ion-icon [name]="handler.icon" slot="start" *ngIf="handler.icon" aria-hidden="true" />

View File

@ -23,7 +23,7 @@
<h2>{{ 'core.settings.syncsettings' | translate }}</h2>
</ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap ion-no-validation">
<ion-item class="ion-text-wrap" lines="none">
<ion-toggle [(ngModel)]="dataSaver" (ngModelChange)="syncOnlyOnWifiChanged()">
{{ 'core.settings.syncdatasaver' | translate }}
</ion-toggle>

View File

@ -17,7 +17,7 @@ import { ModalController, Translate } from '@singletons';
import { CoreMath } from '@singletons/math';
import { Swiper } from 'swiper';
import { SwiperOptions } from 'swiper/types';
import { IonicSlides } from '@ionic/angular';
import { CoreSwiper } from '@singletons/swiper';
/**
* Modal component to view an image.
@ -30,24 +30,22 @@ import { IonicSlides } from '@ionic/angular';
export class CoreViewerImageComponent implements OnInit {
protected swiper?: Swiper;
@ViewChild('swiperRef')
set swiperRef(swiperRef: ElementRef) {
@ViewChild('swiperRef') set swiperRef(swiperRef: ElementRef) {
/**
* This setTimeout waits for Ionic's async initialization to complete.
* Otherwise, an outdated swiper reference will be used.
*/
setTimeout(() => {
if (swiperRef.nativeElement?.swiper) {
this.swiper = swiperRef.nativeElement.swiper as Swiper;
const swiper = CoreSwiper.initSwiperIfAvailable(this.swiper, swiperRef, this.swiperOpts);
if (!swiper) {
return;
}
Object.keys(this.swiperOpts).forEach((key) => {
if (this.swiper) {
this.swiper.params[key] = this.swiperOpts[key];
}
this.swiper = swiper;
this.swiper.zoom.enable();
});
}
}, 0);
}
@Input() title = ''; // Modal title.
@Input() image = ''; // Image URL.
@ -55,24 +53,20 @@ export class CoreViewerImageComponent implements OnInit {
@Input() componentId?: string | number; // Component ID to use in external-content.
private static readonly MAX_RATIO = 8;
private static readonly MIN_RATIO = 0.5;
protected swiperOpts: SwiperOptions = {
modules: [IonicSlides],
freeMode: true,
slidesPerView: 1,
centerInsufficientSlides: true,
centeredSlides: true,
zoom: {
maxRatio: CoreViewerImageComponent.MAX_RATIO,
minRatio: 0.5, // User can zoom out to 0.5 only using pinch gesture.
minRatio: CoreViewerImageComponent.MIN_RATIO,
toggle: true,
},
};
protected zoomRatio = 1;
constructor(protected element: ElementRef<HTMLElement>) {
}
/**
* @inheritdoc
*/
@ -93,27 +87,18 @@ export class CoreViewerImageComponent implements OnInit {
* @param zoomIn True to zoom in, false to zoom out.
*/
zoom(zoomIn = true): void {
const imageElement = this.element.nativeElement.querySelector('img');
if (!this.swiper || !imageElement) {
if (!this.swiper) {
return;
}
let zoomRatio = this.swiper.zoom.scale;
zoomIn
? this.zoomRatio *= 2
: this.zoomRatio /= 2;
? zoomRatio *= 2
: zoomRatio /= 2;
// Using 1 as minimum for manual zoom.
this.zoomRatio = CoreMath.clamp(this.zoomRatio, 1, CoreViewerImageComponent.MAX_RATIO);
zoomRatio = CoreMath.clamp(zoomRatio, CoreViewerImageComponent.MIN_RATIO, CoreViewerImageComponent.MAX_RATIO);
if (this.zoomRatio > 1) {
this.swiper.zoom.in();
imageElement.style.transform =
'translate3d(0px, 0px, 0px) scale(' + this.zoomRatio + ')';
} else {
this.swiper.zoom.out();
}
this.swiper.zoom.in(zoomRatio);
}
}

View File

@ -0,0 +1,70 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { ElementRef } from '@angular/core';
import { IonicSlides } from '@ionic/angular';
import { CorePlatform } from '@services/platform';
import Swiper from 'swiper';
import { SwiperOptions } from 'swiper/types';
/**
* Singleton with helper functions for SwiperJS.
*/
export class CoreSwiper {
/**
* Initialize a Swiper instance.
* It will return swiper instance if current is not set or destroyed and new is set and not destroyed.
*
* @param currentSwiper Current Swiper instance.
* @param newSwiperRef New Swiper Element Ref.
* @param swiperOpts Swiper options.
* @returns Initialized Swiper instance.
*/
static initSwiperIfAvailable(
currentSwiper?: Swiper,
newSwiperRef?: ElementRef,
swiperOpts?: SwiperOptions,
): Swiper | undefined {
const swiper = newSwiperRef?.nativeElement?.swiper as Swiper | undefined;
if (!swiper || swiper.destroyed || (currentSwiper && !currentSwiper.destroyed)) {
return;
}
Swiper.use([IonicSlides]);
CoreSwiper.updateOptions(swiper, swiperOpts);
swiper.changeLanguageDirection(CorePlatform.isRTL ? 'rtl' : 'ltr');
return swiper;
}
/**
* Update Swiper options.
*
* @param swiper Swiper instance.
* @param swiperOpts Swiper options.
*/
static updateOptions(swiper: Swiper, swiperOpts?: SwiperOptions): void {
if (!swiperOpts) {
return;
}
Object.assign(swiper.el, swiperOpts);
swiper.update();
}
}

View File

@ -13,15 +13,18 @@ ion-item.item {
}
&.item-lines-default {
/** Remove lines by default */
--inner-border-width: 0px;
--border-width: 0px;
}
&.ion-valid,
&.ion-invalid {
--inner-border-width: 0 0 1px 0;
&.item-lines-default {
--border-width: 0 0 1px 0;
}
&.ion-touched:not(.ion-no-validation) {
&.ion-touched {
&.ion-invalid {
--ion-item-border-color: var(--highlight-color-invalid);
--highlight-background: var(--ion-item-border-color);
@ -35,10 +38,6 @@ ion-item.item {
}
}
&.ion-no-validation {
--inner-border-width: 0 0 1px 0;
}
// Hide details on items to align badges.
&.hide-detail {
--detail-icon-opacity: 0;
@ -185,7 +184,9 @@ ion-item .in-item {
.item > ion-label,
.fake-ion-item,
.item.ion-text-wrap > ion-checkbox::part(label),
ion-checkbox.ion-text-wrap::part(label) {
ion-checkbox.ion-text-wrap::part(label)
.item.ion-text-wrap ion-toggle::part(label),
ion-toggle.ion-text-wrap::part(label) {
core-format-text,
core-format-text > *:not(pre) {
white-space: nowrap;
@ -198,7 +199,8 @@ ion-checkbox.ion-text-wrap::part(label) {
ion-item > .in-item,
.fake-ion-item.ion-text-wrap,
.item.ion-text-wrap > ion-checkbox::part(label),
ion-checkbox.ion-text-wrap::part(label) {
ion-checkbox.ion-text-wrap::part(label),
ion-toggle.ion-text-wrap::part(label) {
core-format-text,
core-format-text > *:not(pre) {
white-space: normal;
@ -210,7 +212,9 @@ ion-checkbox.ion-text-wrap::part(label) {
.item.ion-text-wrap ion-checkbox::part(label),
ion-checkbox.ion-text-wrap::part(label),
.item.ion-text-wrap ion-radio::part(label),
ion-radio.ion-text-wrap::part(label) {
ion-radio.ion-text-wrap::part(label),
.item.ion-text-wrap ion-toggle::part(label),
ion-toggle.ion-text-wrap::part(label) {
white-space: normal !important;
}