Merge pull request #3864 from crazyserver/MOBILE-3947

Mobile 3947
main
Noel De Martin 2023-11-28 13:50:36 +01:00 committed by GitHub
commit 89123e0df7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 140 additions and 164 deletions

View File

@ -10,8 +10,5 @@ module.exports = {
'!src/testing/**/*',
],
transformIgnorePatterns: ['node_modules/(?!@stencil|@angular|@ionic|@moodlehq|@ngx-translate|swiper)'],
moduleNameMapper: {
...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/src/' }),
'^!raw-loader!.*': 'jest-raw-loader',
},
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/src/' }),
};

7
package-lock.json generated
View File

@ -141,7 +141,6 @@
"gulp-slash": "^1.1.3",
"jest": "^29.7.0",
"jest-preset-angular": "^13.1.4",
"jest-raw-loader": "^1.0.1",
"jsonc-parser": "^2.3.1",
"keytar": "^7.2.0",
"minimatch": "^5.1.0",
@ -18822,12 +18821,6 @@
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true
},
"node_modules/jest-raw-loader": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/jest-raw-loader/-/jest-raw-loader-1.0.1.tgz",
"integrity": "sha512-g9oaAjeC4/rIJk1Wd3RxVbOfMizowM7LSjEJqa4R9qDX0OjQNABXOhH+GaznUp+DjTGVPi2vPPbQXyX87DOnYg==",
"dev": true
},
"node_modules/jest-regex-util": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz",

View File

@ -176,7 +176,6 @@
"gulp-slash": "^1.1.3",
"jest": "^29.7.0",
"jest-preset-angular": "^13.1.4",
"jest-raw-loader": "^1.0.1",
"jsonc-parser": "^2.3.1",
"keytar": "^7.2.0",
"minimatch": "^5.1.0",

View File

@ -29,8 +29,14 @@
<ion-label position="stacked">
<p class="item-heading" [core-mark-required]="true">{{ 'core.date' | translate }}</p>
</ion-label>
<ion-datetime formControlName="timestart" [placeholder]="'core.date' | translate" [displayFormat]="dateFormat"
[max]="maxDate" [min]="minDate" [displayTimezone]="displayTimezone" />
<ion-datetime-button datetime="timestart" />
<ion-modal [keepContentsMounted]="true">
<ng-template>
<ion-datetime id="timestart" formControlName="timestart" presentation="date-time" [max]="maxDate" [min]="minDate">
<span slot="title">{{'core.date' | translate}}</span>
</ion-datetime>
</ng-template>
</ion-modal>
<core-input-errors [control]="form.controls.timestart" [errorMessages]="errors" />
</ion-item>
@ -154,9 +160,15 @@
</ion-item>
<ion-item *ngIf="form.controls.duration.value === 1">
<ion-label position="stacked" />
<ion-datetime formControlName="timedurationuntil" [max]="maxDate" [min]="minDate"
[placeholder]="'addon.calendar.durationuntil' | translate" [displayFormat]="dateFormat"
[displayTimezone]="displayTimezone" />
<ion-datetime-button datetime="timedurationuntil" />
<ion-modal [keepContentsMounted]="true">
<ng-template>
<ion-datetime id="timedurationuntil" formControlName="timedurationuntil" [max]="maxDate" [min]="minDate"
presentation="date-time">
<span slot="title">{{'addon.calendar.durationuntil' | translate}}</span>
</ion-datetime>
</ng-template>
</ion-modal>
</ion-item>
<ion-item>
<ion-label>

View File

@ -45,7 +45,6 @@ import { CoreForms } from '@singletons/form';
import { CoreReminders, CoreRemindersService, CoreRemindersUnits } from '@features/reminders/services/reminders';
import { CoreRemindersSetReminderMenuComponent } from '@features/reminders/components/set-reminder-menu/set-reminder-menu';
import moment from 'moment-timezone';
import { CoreAppProvider } from '@services/app';
/**
* Page that displays a form to create/edit an event.
@ -61,7 +60,6 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
@ViewChild('editEventForm') formElement!: ElementRef;
title = 'addon.calendar.newevent';
dateFormat: string;
component = AddonCalendarProvider.COMPONENT;
loaded = false;
hasOffline = false;
@ -78,7 +76,6 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
eventId?: number;
maxDate: string;
minDate: string;
displayTimezone?: string;
// Form variables.
form: FormGroup;
@ -107,11 +104,6 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
required: Translate.instant('core.required'),
};
// Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them.
this.dateFormat = CoreTimeUtils.convertPHPToMoment(Translate.instant('core.strftimedatetimeshort'))
.replace(/[[\]]/g, '');
this.displayTimezone = CoreAppProvider.getForcedTimezone();
this.form = new FormGroup({});
// Initialize form variables.

View File

@ -1,12 +1,18 @@
<span *ngIf="inputMode && form" [formGroup]="form">
<span *ngIf="inputMode && form && searchFields" [formGroup]="form">
<span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-datetime [formControlName]="'f_'+field.id" [placeholder]="'core.date' | translate" [max]="maxDate" [min]="minDate"
[disabled]="searchMode && !searchFields!['f_'+field.id+'_z']" [displayFormat]="format" [displayTimezone]="displayTimezone" />
<ion-datetime-button datetime="datetime" />
<ion-modal [keepContentsMounted]="true">
<ng-template>
<ion-datetime id="datetime" [formControlName]="'f_'+field.id" [max]="maxDate" [min]="minDate"
[disabled]="searchMode && !searchFields['f_'+field.id+'_z']" presentation="date" />
</ng-template>
</ion-modal>
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error" />
<ion-item *ngIf="searchMode">
<ion-label>{{ 'addon.mod_data.usedate' | translate }}</ion-label>
<ion-checkbox slot="end" [formControlName]="'f_'+field.id+'_z'" [(ngModel)]="searchFields!['f_'+field.id+'_z']" />
<ion-checkbox slot="end" [formControlName]="'f_'+field.id+'_z'" [(ngModel)]="searchFields['f_'+field.id+'_z']" />
</ion-item>
</span>

View File

@ -13,9 +13,7 @@
// limitations under the License.
import { Component } from '@angular/core';
import { CoreAppProvider } from '@services/app';
import { CoreTimeUtils } from '@services/utils/time';
import { Translate } from '@singletons';
import moment, { Moment } from 'moment-timezone';
import { AddonModDataFieldPluginBaseComponent } from '../../../classes/base-field-plugin-component';
@ -28,11 +26,9 @@ import { AddonModDataFieldPluginBaseComponent } from '../../../classes/base-fiel
})
export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginBaseComponent {
format!: string;
displayDate?: number;
maxDate?: string;
minDate?: string;
displayTimezone?: string;
/**
* @inheritdoc
@ -48,23 +44,18 @@ export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginBaseC
let momentInstance: Moment;
// Calculate format to use.
this.format = CoreTimeUtils.fixFormatForDatetime(CoreTimeUtils.convertPHPToMoment(
Translate.instant('core.strftimedate'),
));
this.maxDate = CoreTimeUtils.getDatetimeDefaultMax();
this.minDate = CoreTimeUtils.getDatetimeDefaultMin();
this.displayTimezone = CoreAppProvider.getForcedTimezone();
if (this.searchMode) {
if (this.searchMode && this.searchFields) {
this.addControl('f_' + this.field.id + '_z');
momentInstance = this.searchFields!['f_' + this.field.id + '_y']
? moment(this.searchFields!['f_' + this.field.id + '_y'] + '-' +
this.searchFields!['f_' + this.field.id + '_m'] + '-' + this.searchFields!['f_' + this.field.id + '_d'])
momentInstance = this.searchFields['f_' + this.field.id + '_y']
? moment(this.searchFields['f_' + this.field.id + '_y'] + '-' +
this.searchFields['f_' + this.field.id + '_m'] + '-' + this.searchFields['f_' + this.field.id + '_d'])
: moment();
this.searchFields!['f_' + this.field.id] = CoreTimeUtils.toDatetimeFormat(momentInstance.unix() * 1000);
this.searchFields['f_' + this.field.id] = CoreTimeUtils.toDatetimeFormat(momentInstance.unix() * 1000);
} else {
momentInstance = this.value?.content
? moment(parseInt(this.value.content, 10) * 1000)

View File

@ -18,7 +18,12 @@
[courseId]="courseId" [wsNotFiltered]="true" />
</span>
</ion-label>
<ion-datetime [formControlName]="modelName" [placeholder]="'core.choosedots' | translate" [displayFormat]="format" [max]="max"
[min]="min" [monthNames]="monthNames" [displayTimezone]="displayTimezone" />
<ion-datetime-button datetime="datetime" />
<ion-modal [keepContentsMounted]="true">
<ng-template>
<ion-datetime id="datetime" [formControlName]="modelName" [presentation]="ionDateTimePresentation" [max]="max" [min]="min" />
</ng-template>
</ion-modal>
<core-input-errors [control]="form.controls[modelName]" />
</ion-item>

View File

@ -19,10 +19,7 @@ import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils';
import { AuthEmailSignupProfileField } from '@features/login/services/login-helper';
import { CoreUserProfileField } from '@features/user/services/user';
import { Translate } from '@singletons';
import { CoreUserProfileFieldBaseComponent } from '@features/user/classes/base-profilefield-component';
import { CoreLang } from '@services/lang';
import { CoreAppProvider } from '@services/app';
/**
* Directive to render a datetime user profile field.
@ -33,13 +30,11 @@ import { CoreAppProvider } from '@services/app';
})
export class AddonUserProfileFieldDatetimeComponent extends CoreUserProfileFieldBaseComponent {
format?: string;
ionDateTimePresentation = 'date';
min?: string;
max?: string;
valueNumber?: number;
displayValue?: string;
monthNames?: string[];
displayTimezone?: string;
/**
* Init the data when the field is meant to be displayed without editing.
@ -64,16 +59,11 @@ export class AddonUserProfileFieldDatetimeComponent extends CoreUserProfileField
protected initForEdit(field: AuthEmailSignupProfileField): void {
super.initForEdit(field);
this.monthNames = CoreLang.getMonthNames();
this.displayTimezone = CoreAppProvider.getForcedTimezone();
// Check if it's only date or it has time too.
const hasTime = CoreUtils.isTrueOrOne(field.param3);
// Calculate format to use.
this.format = CoreTimeUtils.fixFormatForDatetime(CoreTimeUtils.convertPHPToMoment(
Translate.instant('core.' + (hasTime ? 'strftimedatetime' : 'strftimedate')),
));
this.ionDateTimePresentation = hasTime ? 'date-time' : 'date';
// Check min value.
if (field.param1 && Number(field.param1)) {

View File

@ -52,6 +52,8 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
IonicModule.forRoot(
{
navAnimation: moodleTransitionAnimation,
innerHTMLTemplatesEnabled: true,
sanitizerEnabled: true,
},
),
HttpClientModule, // HttpClient is used to make JSON requests. It fails for HEAD requests because there is no content.

View File

@ -15,8 +15,6 @@
import { Component, ElementRef, Input, OnChanges, OnInit } from '@angular/core';
import { Translate } from '@singletons';
import { CoreForms } from '@singletons/form';
import ChevronUpSVG from '!raw-loader!ionicons/dist/svg/chevron-up.svg';
import ChevronDownSVG from '!raw-loader!ionicons/dist/svg/chevron-down.svg';
/**
* Component to show error details.
@ -54,11 +52,11 @@ export class CoreErrorInfoComponent implements OnInit, OnChanges {
<label for="${toggleId}" class="core-error-info--toggle" aria-hidden="true">
<span class="core-error-info--hide-content">
${hideDetailsLabel}
${ChevronUpSVG}
<ion-icon name="chevron-up" />
</span>
<span class="core-error-info--show-content">
${showDetailsLabel}
${ChevronDownSVG}
<ion-icon name="chevron-down" />
</span>
</label>
</div>

View File

@ -1,7 +1,8 @@
:host {
width: 100%;
display: contents;
.core-input-error-container {
width: 100%;
.core-input-error {
padding: 4px;
color: var(--danger);

View File

@ -2,6 +2,7 @@
:host {
--backdrop-opacity: var(--ion-backdrop-opacity, 0.4);
--border-radius: var(--modal-radius);
width: 100%;
height: 100%;
@ -17,7 +18,7 @@
}
.sheet-modal--wrapper {
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
border-radius: var(--border-radius) var(--border-radius) 0 0;
@include padding(24px, 16px, 24px, 16px);
background-color: var(--ion-overlay-background-color, var(--ion-background-color, #fff));

View File

@ -92,7 +92,7 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
}
/**
* Detect and act upon changes that Angular cant or wont detect on its own (objects and arrays).
* @inheritdoc
*/
ngDoCheck(): void {
if (!this.componentInstance || this.creatingComponent) {
@ -111,7 +111,7 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
}
/**
* Detect changes on input properties.
* @inheritdoc
*/
async ngOnChanges(changes: Record<string, SimpleChange>): Promise<void> {
// Only compile if text/javascript has changed or the forceCompile flag has been set to true.
@ -148,7 +148,7 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
}
/**
* Component destroyed.
* @inheritdoc
*/
ngOnDestroy(): void {
this.componentRef?.destroy();

View File

@ -81,5 +81,9 @@ ion-item.item {
&.restricted {
font-size: var(--text-size);
}
&[role=button] {
min-height: auto;
min-width: auto;
}
}
}

View File

@ -459,8 +459,8 @@ export class CoreLoginSitePage implements OnInit {
});
if (errorDetails) {
// Avoid sanitizing JS.
const containerElement = alertElement.querySelector('.core-error-info-container');
if (containerElement) {
containerElement.innerHTML = CoreErrorInfoComponent.render(errorDetails, errorCode);
}

View File

@ -27,6 +27,7 @@ import { CorePlatform } from '@services/platform';
import { AddonFilterMultilangHandler } from '@addons/filter/multilang/services/handlers/multilang';
import { AddonFilterMultilang2Handler } from '@addons/filter/multilang2/services/handlers/multilang2';
import { firstValueFrom } from 'rxjs';
import { CoreLogger } from '@singletons/logger';
/*
* Service to handle language features, like changing the current language.
@ -40,6 +41,11 @@ export class CoreLangProvider {
protected customStrings: CoreLanguageObject = {}; // Strings defined using the admin tool.
protected customStringsRaw?: string;
protected sitePluginsStrings: CoreLanguageObject = {}; // Strings defined by site plugins.
protected logger: CoreLogger;
constructor() {
this.logger = CoreLogger.getInstance('CoreLang');
}
async initialize(): Promise<void> {
// Set fallback language and language to use until the app determines the right language to use.
@ -171,13 +177,21 @@ export class CoreLangProvider {
// Use british english when parent english is loaded.
moment.locale(language == 'en' ? 'en-gb' : language);
const previousLanguage = this.currentLanguage ?? this.getDefaultLanguage();
this.currentLanguage = language;
try {
await Promise.all([
this.reloadLanguageStrings(),
CoreConfig.set('current_language', language),
]);
await this.reloadLanguageStrings();
await CoreConfig.set('current_language', language);
} catch (error) {
if (language !== previousLanguage) {
this.logger.error(`Language ${language} not available, reverting to ${previousLanguage}`, error);
return this.changeCurrentLanguage(previousLanguage);
}
throw error;
} finally {
// Load the custom and site plugins strings for the language.
if (this.loadLangStrings(this.customStrings, language) || this.loadLangStrings(this.sitePluginsStrings, language)) {

View File

@ -815,14 +815,20 @@ export class CoreDomUtilsProvider {
* @returns Promise resolved with the alert modal.
*/
async showAlertWithOptions(options: AlertOptions = {}, autocloseTime?: number): Promise<HTMLIonAlertElement> {
const hasHTMLTags = CoreTextUtils.hasHTMLTags(<string> options.message || '');
let message = typeof options.message == 'string'
? options.message
: options.message?.value || '';
const hasHTMLTags = CoreTextUtils.hasHTMLTags(message);
if (hasHTMLTags && !CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.7')) {
// Treat multilang.
options.message = await CoreLang.filterMultilang(<string> options.message);
message = await CoreLang.filterMultilang(message);
}
const alertId = <string> Md5.hashAsciiStr((options.header || '') + '#' + (options.message || ''));
options.message = message;
const alertId = Md5.hashAsciiStr((options.header || '') + '#' + (message|| ''));
if (this.displayedAlerts[alertId]) {
// There's already an alert with the same message and title. Return it.

View File

@ -117,7 +117,6 @@ export interface AsyncInstanceWrapper<
resetInstance(): void;
}
// eslint-disable-next-line @typescript-eslint/ban-types
export type AsyncObject = object;
/**

View File

@ -518,7 +518,7 @@ ion-alert .alert-button.timed-button{
}
ion-alert {
--border-radius: var(--radius-xl);
--border-radius: var(--modal-radius);
&.md, &.ios {
--max-width: 80%;
@ -545,7 +545,7 @@ ion-alert {
}
ion-loading {
--border-radius: var(--radius-xl);
--border-radius: var(--modal-radius);
.loading-wrapper {
border-radius: var(--border-radius) !important;
@ -1585,37 +1585,6 @@ ion-item.item-input.item-multiple-inputs {
}
}
// Focus highlight for accessibility.
.ion-focused:not(.item-multiple-inputs):not(:focus),
ion-input.has-focus,
ion-card:focus {
@include core-focus();
}
.ion-focused.item-multiple-inputs,
.ion-focused.ion-activatable {
ion-toggle:focus-within,
ion-select:focus-within,
ion-checkbox:focus-within,
ion-radio:focus-within {
@include core-focus();
}
}
// Treat cases where there's a focusable element inside an item, like a button.
ion-item.item-input:not(.item-multiple-inputs):not(:focus),
ion-item.item-has-focus:not(.item-multiple-inputs):not(:focus),
ion-item.item-input ion-input.has-focus {
position: relative;
&::after {
box-shadow: revert;
opacity: revert;
z-index: revert;
}
.item-highlight, .item-inner-highlight {
position: unset;
}
}
ion-item-divider.item,
ion-item.item.divider {
min-height: var(--min-height);
@ -1653,6 +1622,42 @@ ion-item.item {
outline: none;
}
// Focus highlight for accessibility.
.ion-focused:not(.item-multiple-inputs):not(:focus),
ion-input.has-focus,
ion-card:focus {
@include core-focus();
:focus-visible,
.clickable:focus {
box-shadow: none;
}
}
.ion-focused.item-multiple-inputs {
ion-toggle:focus-within,
ion-select:focus-within,
ion-checkbox:focus-within,
ion-radio:focus-within {
@include core-focus();
}
}
// Treat cases where there's a focusable element inside an item, like a button.
ion-item.item-input:not(.item-multiple-inputs):not(:focus),
ion-item.item-has-focus:not(.item-multiple-inputs):not(:focus),
ion-item.item-input ion-input.has-focus {
position: relative;
&::after {
box-shadow: revert;
opacity: revert;
z-index: revert;
}
.item-highlight, .item-inner-highlight {
position: unset;
}
}
textarea, button, select, input, a, .clickable {
&:focus {
@include core-focus-style();
@ -1660,10 +1665,6 @@ textarea, button, select, input, a, .clickable {
}
}
.ion-focused:not(.item-multiple-inputs):not(:focus) .clickable:focus {
box-shadow: none;
}
ion-loading:focus-visible,
ion-alert:focus-visible,
ion-popover:focus-visible,
@ -1672,11 +1673,6 @@ ion-modal:focus-visible {
border-radius: 0;
}
input {
--placeholder-color: var(--ion-placeholder-color);
--placeholder-opacity: .85;
}
ion-input .native-input {
&:focus, &:focus-visible {
box-shadow: none;
@ -1684,6 +1680,11 @@ ion-input .native-input {
}
}
input {
--placeholder-color: var(--ion-placeholder-color);
--placeholder-opacity: .85;
}
// Disable scroll on parent ion contents to enabled PTR on the ones inside the splitview. See split-view component for more info.
ion-content.disable-scroll-y::part(scroll) {
touch-action: auto;
@ -1916,6 +1917,9 @@ ion-modal {
--border-radius: var(--modal-radius);
}
}
&.ion-datetime-button-overlay {
--border-radius: var(--modal-radius);
}
&.core-modal-lateral,
&.core-modal-fullscreen {
@ -1973,3 +1977,10 @@ ion-popover {
[hidden] {
display: none !important;
}
// Ion Datetime
ion-item.item-label-stacked ion-datetime-button {
margin-top: 8px;
margin-bottom: 8px;
align-self: self-end;
}

View File

@ -1,27 +0,0 @@
// (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 { UrlTree } from '@angular/router';
import { NavigationOptions } from '@ionic/angular/common/providers/nav-controller';
declare module '@ionic/angular' {
export class NavController {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
navigateForward(url: string | UrlTree | any[], options?: NavigationOptions): Promise<boolean | null>;
}
}

19
src/types/raw.d.ts vendored
View File

@ -1,19 +0,0 @@
// (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.
declare module '!raw-loader!*' {
const contents: string;
export = contents;
}

View File

@ -9,6 +9,7 @@ For more information about upgrading, read the official documentation: https://m
- Renamed CoreLoginSitesComponent to CoreLoginSitesModalComponent to make it clear that it's a modal and to avoid confusing it with the new CoreSitesListComponent.
- Removed CoreToLocaleStringPipe deprecated since 3.6.0
- With the upgrade to Ionic 7 ion-slides is no longer supported and now you need to use swiper-container and swiper-slide. More info here: https://ionicframework.com/docs/angular/slides
- With the upgrade to Ionic7 ion-datetime has changed its usage. We recommend using ion-datetime-button. More info here: https://ionicframework.com/docs/updating/6-0#datetime
=== 4.3.0 ===