commit
4fddde1a00
|
@ -48,7 +48,7 @@
|
|||
"nl": "Nederlands",
|
||||
"no": "Norsk",
|
||||
"pl": "Polski",
|
||||
"ps": "پښتو",
|
||||
"ps": "لیسي",
|
||||
"pt": "Português - Portugal",
|
||||
"pt-br": "Português - Brasil",
|
||||
"ro": "Română",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }})
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"fieldtypelabel": "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)">
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"fieldtypelabel": "Image"
|
||||
}
|
|
@ -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" />
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -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>
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.core-group-selector ion-icon {
|
||||
--webkit-margin-end: 16px;
|
||||
margin-inline-end: 16px;
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -204,7 +204,7 @@ export class CorePushNotificationsProvider {
|
|||
protected async initializeDatabase(): Promise<void> {
|
||||
try {
|
||||
await CoreApp.createTablesFromSchema(APP_SCHEMA);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue