From caa100979a1f95b5ed39c89ea64efff36b9b9099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 15 Jul 2024 16:00:01 +0200 Subject: [PATCH 1/5] MOBILE-4616 chore: Import modals during runtime --- .../classes/base-feedback-plugin-component.ts | 4 +- .../assign/components/components.module.ts | 3 -- .../edit-feedback-modal.ts | 7 ++++ .../mod/data/components/components.module.ts | 3 -- src/addons/mod/data/components/index/index.ts | 5 ++- .../search-modal.html} | 0 .../search-modal.ts} | 11 +++++- .../preflight-modal/preflight-modal.module.ts | 36 ------------------ ...-modal.component.ts => preflight-modal.ts} | 5 +++ src/addons/mod/quiz/services/quiz-helper.ts | 2 +- .../workshop/components/components.module.ts | 3 -- .../mod/workshop/components/index/index.ts | 5 ++- .../phase-modal.html} | 0 .../phase.ts => phase-modal/phase-modal.ts} | 9 ++++- src/addons/notes/components/add/add-modal.ts | 5 +++ .../notes/components/components.module.ts | 30 --------------- src/addons/notes/notes.module.ts | 2 - src/addons/notes/pages/list/list.ts | 4 +- src/core/classes/page-loads-manager.ts | 4 +- src/core/components/components.module.ts | 3 -- .../refresh-button-modal.ts | 7 +++- .../choose-site-modal/choose-site-modal.ts | 5 +++ .../components/components.module.ts | 30 --------------- .../contentlinks/contentlinks.module.ts | 4 -- .../services/contentlinks-helper.ts | 4 +- .../course/components/components.module.ts | 3 -- .../components/course-format/course-format.ts | 4 +- .../components/course-index/course-index.ts | 7 +++- .../components/components.module.ts | 33 ---------------- .../components/contactdpo/contactdpo.ts | 6 +++ .../components/newrequest/newrequest.ts | 5 +++ .../dataprivacy/dataprivacy.module.ts | 2 - .../features/dataprivacy/pages/main/main.ts | 8 +++- .../audio-histogram/audio-histogram.ts | 7 +++- .../audio-recorder.component.ts | 9 ++++- .../audio-recorder/audio-recorder.module.ts | 32 ---------------- .../fileuploader/services/fileuploader.ts | 2 +- .../login/components/components.module.ts | 6 --- .../login/components/site-help/site-help.ts | 7 +++- .../site-onboarding/site-onboarding.ts | 5 +++ src/core/features/login/pages/site/site.ts | 8 +++- .../policy/components/components.module.ts | 31 --------------- .../components/policy-modal/policy-modal.ts | 5 +++ .../policy/pages/acceptances/acceptances.ts | 6 ++- .../policy/pages/site-policy/site-policy.ts | 6 ++- .../features/policy/policy-lazy.module.ts | 2 - .../rating/components/aggregate/aggregate.ts | 4 +- .../rating/components/components.module.ts | 3 -- .../rating/components/ratings/ratings.ts | 5 +++ .../components/components.module.ts | 3 -- .../components/list-modal/list-modal.ts | 7 ++++ .../services/sharedfiles-helper.ts | 4 +- .../viewer/components/components.module.ts | 38 ------------------- .../features/viewer/components/image/image.ts | 10 ++++- .../components/qr-scanner/qr-scanner.ts | 5 +++ .../features/viewer/components/text/text.ts | 7 +++- .../features/viewer/viewer-lazy.module.ts | 2 +- src/core/features/viewer/viewer.module.ts | 4 +- src/core/services/navigator.ts | 2 +- src/core/services/utils/dom.ts | 2 +- src/core/services/utils/text.ts | 2 +- src/core/services/utils/utils.ts | 3 +- 62 files changed, 178 insertions(+), 308 deletions(-) rename src/addons/mod/data/components/{search/search.html => search-modal/search-modal.html} (100%) rename src/addons/mod/data/components/{search/search.ts => search-modal/search-modal.ts} (95%) delete mode 100644 src/addons/mod/quiz/components/preflight-modal/preflight-modal.module.ts rename src/addons/mod/quiz/components/preflight-modal/{preflight-modal.component.ts => preflight-modal.ts} (97%) rename src/addons/mod/workshop/components/{phase/phase.html => phase-modal/phase-modal.html} (100%) rename src/addons/mod/workshop/components/{phase/phase.ts => phase-modal/phase-modal.ts} (90%) delete mode 100644 src/addons/notes/components/components.module.ts delete mode 100644 src/core/features/contentlinks/components/components.module.ts delete mode 100644 src/core/features/dataprivacy/components/components.module.ts delete mode 100644 src/core/features/fileuploader/components/audio-recorder/audio-recorder.module.ts delete mode 100644 src/core/features/policy/components/components.module.ts delete mode 100644 src/core/features/viewer/components/components.module.ts diff --git a/src/addons/mod/assign/classes/base-feedback-plugin-component.ts b/src/addons/mod/assign/classes/base-feedback-plugin-component.ts index 939343c0c..8af588332 100644 --- a/src/addons/mod/assign/classes/base-feedback-plugin-component.ts +++ b/src/addons/mod/assign/classes/base-feedback-plugin-component.ts @@ -16,7 +16,6 @@ import { Component, Input } from '@angular/core'; import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreError } from '@classes/errors/error'; import { CoreDomUtils } from '@services/utils/dom'; -import { AddonModAssignEditFeedbackModalComponent } from '../components/edit-feedback-modal/edit-feedback-modal'; import { AddonModAssignFeedbackCommentsTextData } from '../feedback/comments/services/handler'; import { AddonModAssignAssign, AddonModAssignPlugin, AddonModAssignSubmission } from '../services/assign'; @@ -46,6 +45,9 @@ export class AddonModAssignFeedbackPluginBaseComponent implements IAddonModAssig throw new CoreError('Cannot edit feedback'); } + const { AddonModAssignEditFeedbackModalComponent } = + await import('@addons/mod/assign/components/edit-feedback-modal/edit-feedback-modal'); + // Create the navigation modal. const modalData = await CoreDomUtils.openModal({ component: AddonModAssignEditFeedbackModalComponent, diff --git a/src/addons/mod/assign/components/components.module.ts b/src/addons/mod/assign/components/components.module.ts index d215e9d79..8910da8fb 100644 --- a/src/addons/mod/assign/components/components.module.ts +++ b/src/addons/mod/assign/components/components.module.ts @@ -20,7 +20,6 @@ import { AddonModAssignIndexComponent } from './index/index'; import { AddonModAssignSubmissionComponent } from './submission/submission'; import { AddonModAssignSubmissionPluginComponent } from './submission-plugin/submission-plugin'; import { AddonModAssignFeedbackPluginComponent } from './feedback-plugin/feedback-plugin'; -import { AddonModAssignEditFeedbackModalComponent } from './edit-feedback-modal/edit-feedback-modal'; @NgModule({ declarations: [ @@ -28,7 +27,6 @@ import { AddonModAssignEditFeedbackModalComponent } from './edit-feedback-modal/ AddonModAssignSubmissionComponent, AddonModAssignSubmissionPluginComponent, AddonModAssignFeedbackPluginComponent, - AddonModAssignEditFeedbackModalComponent, ], imports: [ CoreSharedModule, @@ -39,7 +37,6 @@ import { AddonModAssignEditFeedbackModalComponent } from './edit-feedback-modal/ AddonModAssignSubmissionComponent, AddonModAssignSubmissionPluginComponent, AddonModAssignFeedbackPluginComponent, - AddonModAssignEditFeedbackModalComponent, ], }) export class AddonModAssignComponentsModule {} diff --git a/src/addons/mod/assign/components/edit-feedback-modal/edit-feedback-modal.ts b/src/addons/mod/assign/components/edit-feedback-modal/edit-feedback-modal.ts index 404887898..35682d76f 100644 --- a/src/addons/mod/assign/components/edit-feedback-modal/edit-feedback-modal.ts +++ b/src/addons/mod/assign/components/edit-feedback-modal/edit-feedback-modal.ts @@ -20,6 +20,8 @@ import { CoreUtils } from '@services/utils/utils'; import { ModalController, Translate } from '@singletons'; import { AddonModAssignAssign, AddonModAssignPlugin, AddonModAssignSubmission } from '../../services/assign'; import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate'; +import { CoreSharedModule } from '@/core/shared.module'; +import { AddonModAssignComponentsModule } from '../components.module'; /** * Modal that allows editing a feedback plugin. @@ -27,6 +29,11 @@ import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate @Component({ selector: 'addon-mod-assign-edit-feedback-modal', templateUrl: 'edit-feedback-modal.html', + standalone: true, + imports: [ + CoreSharedModule, + AddonModAssignComponentsModule, + ], }) export class AddonModAssignEditFeedbackModalComponent { diff --git a/src/addons/mod/data/components/components.module.ts b/src/addons/mod/data/components/components.module.ts index 5d3021daf..0c5ed7310 100644 --- a/src/addons/mod/data/components/components.module.ts +++ b/src/addons/mod/data/components/components.module.ts @@ -16,14 +16,12 @@ import { NgModule } from '@angular/core'; import { CoreSharedModule } from '@/core/shared.module'; import { CoreCourseComponentsModule } from '@features/course/components/components.module'; import { AddonModDataIndexComponent } from './index'; -import { AddonModDataSearchComponent } from './search/search'; import { CoreCompileHtmlComponentModule } from '@features/compile/components/compile-html/compile-html.module'; import { AddonModDataActionsMenuComponent } from './actionsmenu/actionsmenu'; @NgModule({ declarations: [ AddonModDataIndexComponent, - AddonModDataSearchComponent, AddonModDataActionsMenuComponent, ], imports: [ @@ -33,7 +31,6 @@ import { AddonModDataActionsMenuComponent } from './actionsmenu/actionsmenu'; ], exports: [ AddonModDataIndexComponent, - AddonModDataSearchComponent, AddonModDataActionsMenuComponent, ], }) diff --git a/src/addons/mod/data/components/index/index.ts b/src/addons/mod/data/components/index/index.ts index 55abc74be..aaf630e75 100644 --- a/src/addons/mod/data/components/index/index.ts +++ b/src/addons/mod/data/components/index/index.ts @@ -40,7 +40,6 @@ import { AddonModDataHelper, AddonModDatDisplayFieldsOptions } from '../../servi import { AddonModDataAutoSyncData, AddonModDataSyncResult } from '../../services/data-sync'; import { AddonModDataPrefetchHandler } from '../../services/handlers/prefetch-lazy'; import { AddonModDataComponentsCompileModule } from '../components-compile.module'; -import { AddonModDataSearchComponent } from '../search/search'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreTime } from '@singletons/time'; import { @@ -400,8 +399,10 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp * Display the chat users modal. */ async showSearch(): Promise { + const { AddonModDataSearchModalComponent } = await import('@addons/mod/data/components/search-modal/search-modal'); + const modalData = await CoreDomUtils.openModal({ - component: AddonModDataSearchComponent, + component: AddonModDataSearchModalComponent, componentProps: { search: this.search, fields: this.fields, diff --git a/src/addons/mod/data/components/search/search.html b/src/addons/mod/data/components/search-modal/search-modal.html similarity index 100% rename from src/addons/mod/data/components/search/search.html rename to src/addons/mod/data/components/search-modal/search-modal.html diff --git a/src/addons/mod/data/components/search/search.ts b/src/addons/mod/data/components/search-modal/search-modal.ts similarity index 95% rename from src/addons/mod/data/components/search/search.ts rename to src/addons/mod/data/components/search-modal/search-modal.ts index b51776d5f..2867c9fea 100644 --- a/src/addons/mod/data/components/search/search.ts +++ b/src/addons/mod/data/components/search-modal/search-modal.ts @@ -30,16 +30,23 @@ import { AddonModDataHelper } from '../../services/data-helper'; import { AddonModDataComponentsCompileModule } from '../components-compile.module'; import { AddonModDataSearchDataParams } from '../index'; import { AddonModDataTemplateType } from '../../constants'; +import { CoreSharedModule } from '@/core/shared.module'; +import { CoreCompileHtmlComponentModule } from '../../../../../core/features/compile/components/compile-html/compile-html.module'; /** * Page that displays the search modal. */ @Component({ selector: 'addon-mod-data-search-modal', - templateUrl: 'search.html', + templateUrl: 'search-modal.html', styleUrls: ['../../data.scss', '../../data-forms.scss'], + standalone: true, + imports: [ + CoreSharedModule, + CoreCompileHtmlComponentModule, + ], }) -export class AddonModDataSearchComponent implements OnInit { +export class AddonModDataSearchModalComponent implements OnInit { @ViewChild('searchFormEl') formElement!: ElementRef; diff --git a/src/addons/mod/quiz/components/preflight-modal/preflight-modal.module.ts b/src/addons/mod/quiz/components/preflight-modal/preflight-modal.module.ts deleted file mode 100644 index 77952a6f8..000000000 --- a/src/addons/mod/quiz/components/preflight-modal/preflight-modal.module.ts +++ /dev/null @@ -1,36 +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 { NgModule } from '@angular/core'; - -import { CoreSharedModule } from '@/core/shared.module'; -import { CoreCourseComponentsModule } from '@features/course/components/components.module'; -import { AddonModQuizPreflightModalComponent } from './preflight-modal.component'; - -export { AddonModQuizPreflightModalComponent }; - -@NgModule({ - declarations: [ - AddonModQuizPreflightModalComponent, - ], - imports: [ - CoreSharedModule, - CoreCourseComponentsModule, - ], - exports: [ - AddonModQuizPreflightModalComponent, - - ], -}) -export class AddonModQuizPreflightModalComponentModule {} diff --git a/src/addons/mod/quiz/components/preflight-modal/preflight-modal.component.ts b/src/addons/mod/quiz/components/preflight-modal/preflight-modal.ts similarity index 97% rename from src/addons/mod/quiz/components/preflight-modal/preflight-modal.component.ts rename to src/addons/mod/quiz/components/preflight-modal/preflight-modal.ts index afb82240f..4ec740b22 100644 --- a/src/addons/mod/quiz/components/preflight-modal/preflight-modal.component.ts +++ b/src/addons/mod/quiz/components/preflight-modal/preflight-modal.ts @@ -22,6 +22,7 @@ import { ModalController, Translate } from '@singletons'; import { AddonModQuizAccessRuleDelegate } from '../../services/access-rules-delegate'; import { AddonModQuizAttemptWSData, AddonModQuizQuizWSData } from '../../services/quiz'; import { CoreDom } from '@singletons/dom'; +import { CoreSharedModule } from '@/core/shared.module'; /** * Modal that renders the access rules for a quiz. @@ -29,6 +30,10 @@ import { CoreDom } from '@singletons/dom'; @Component({ selector: 'page-addon-mod-quiz-preflight-modal', templateUrl: 'preflight-modal.html', + standalone: true, + imports: [ + CoreSharedModule, + ], }) export class AddonModQuizPreflightModalComponent implements OnInit { diff --git a/src/addons/mod/quiz/services/quiz-helper.ts b/src/addons/mod/quiz/services/quiz-helper.ts index 383cf2320..2bc6d85ea 100644 --- a/src/addons/mod/quiz/services/quiz-helper.ts +++ b/src/addons/mod/quiz/services/quiz-helper.ts @@ -269,7 +269,7 @@ export class AddonModQuizHelperProvider { } const { AddonModQuizPreflightModalComponent } = - await import('@addons/mod/quiz/components/preflight-modal/preflight-modal.module'); + await import('@addons/mod/quiz/components/preflight-modal/preflight-modal'); // Create and show the modal. const modalData = await CoreDomUtils.openModal>({ diff --git a/src/addons/mod/workshop/components/components.module.ts b/src/addons/mod/workshop/components/components.module.ts index 46f93d91f..a65151b75 100644 --- a/src/addons/mod/workshop/components/components.module.ts +++ b/src/addons/mod/workshop/components/components.module.ts @@ -19,7 +19,6 @@ import { CoreCourseComponentsModule } from '@features/course/components/componen import { CoreEditorComponentsModule } from '@features/editor/components/components.module'; import { CoreSharedModule } from '@/core/shared.module'; import { AddonModWorkshopAssessmentComponentsModule } from '@addons/mod/workshop/assessment/assesment-components.module'; -import { AddonModWorkshopPhaseInfoComponent } from './phase/phase'; import { AddonModWorkshopAssessmentComponent } from './assessment/assessment'; import { AddonModWorkshopAssessmentStrategyComponent } from './assessment-strategy/assessment-strategy'; @@ -27,7 +26,6 @@ import { AddonModWorkshopAssessmentStrategyComponent } from './assessment-strate declarations: [ AddonModWorkshopIndexComponent, AddonModWorkshopSubmissionComponent, - AddonModWorkshopPhaseInfoComponent, AddonModWorkshopAssessmentComponent, AddonModWorkshopAssessmentStrategyComponent, ], @@ -40,7 +38,6 @@ import { AddonModWorkshopAssessmentStrategyComponent } from './assessment-strate exports: [ AddonModWorkshopIndexComponent, AddonModWorkshopSubmissionComponent, - AddonModWorkshopPhaseInfoComponent, AddonModWorkshopAssessmentComponent, AddonModWorkshopAssessmentStrategyComponent, ], diff --git a/src/addons/mod/workshop/components/index/index.ts b/src/addons/mod/workshop/components/index/index.ts index b86e774ea..628d70853 100644 --- a/src/addons/mod/workshop/components/index/index.ts +++ b/src/addons/mod/workshop/components/index/index.ts @@ -48,7 +48,6 @@ import { AddonModWorkshopAutoSyncData, AddonModWorkshopSyncResult, } from '../../services/workshop-sync'; -import { AddonModWorkshopPhaseInfoComponent } from '../phase/phase'; import { ADDON_MOD_WORKSHOP_ASSESSMENT_SAVED, ADDON_MOD_WORKSHOP_AUTO_SYNCED, @@ -398,9 +397,11 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity if (!this.phases || !this.workshop) { return; } + const { AddonModWorkshopPhaseInfoModalComponent } = + await import('@addons/mod/workshop/components/phase-modal/phase-modal'); const modalData = await CoreDomUtils.openModal({ - component: AddonModWorkshopPhaseInfoComponent, + component: AddonModWorkshopPhaseInfoModalComponent, componentProps: { phases: CoreUtils.objectToArray(this.phases), workshopPhase: this.workshop.phase, diff --git a/src/addons/mod/workshop/components/phase/phase.html b/src/addons/mod/workshop/components/phase-modal/phase-modal.html similarity index 100% rename from src/addons/mod/workshop/components/phase/phase.html rename to src/addons/mod/workshop/components/phase-modal/phase-modal.html diff --git a/src/addons/mod/workshop/components/phase/phase.ts b/src/addons/mod/workshop/components/phase-modal/phase-modal.ts similarity index 90% rename from src/addons/mod/workshop/components/phase/phase.ts rename to src/addons/mod/workshop/components/phase-modal/phase-modal.ts index 5fa88c08b..95bc52faa 100644 --- a/src/addons/mod/workshop/components/phase/phase.ts +++ b/src/addons/mod/workshop/components/phase-modal/phase-modal.ts @@ -17,14 +17,19 @@ import { CoreUtils } from '@services/utils/utils'; import { ModalController } from '@singletons'; import { AddonModWorkshopPhaseData, AddonModWorkshopPhaseTaskData } from '../../services/workshop'; import { AddonModWorkshopPhase } from '../../constants'; +import { CoreSharedModule } from '@/core/shared.module'; /** * Page that displays the phase info modal. */ @Component({ - templateUrl: 'phase.html', + templateUrl: 'phase-modal.html', + standalone: true, + imports: [ + CoreSharedModule, + ], }) -export class AddonModWorkshopPhaseInfoComponent implements OnInit { +export class AddonModWorkshopPhaseInfoModalComponent implements OnInit { @Input() phases!: AddonModWorkshopPhaseDataWithSwitch[]; @Input() workshopPhase!: AddonModWorkshopPhase; diff --git a/src/addons/notes/components/add/add-modal.ts b/src/addons/notes/components/add/add-modal.ts index be47b9be6..1fdf03ea8 100644 --- a/src/addons/notes/components/add/add-modal.ts +++ b/src/addons/notes/components/add/add-modal.ts @@ -19,12 +19,17 @@ import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreForms } from '@singletons/form'; import { ModalController } from '@singletons'; import { CoreKeyboard } from '@singletons/keyboard'; +import { CoreSharedModule } from '@/core/shared.module'; /** * Component that displays a text area for composing a note. */ @Component({ templateUrl: 'add-modal.html', + standalone: true, + imports: [ + CoreSharedModule, + ], }) export class AddonNotesAddComponent { diff --git a/src/addons/notes/components/components.module.ts b/src/addons/notes/components/components.module.ts deleted file mode 100644 index 089653d03..000000000 --- a/src/addons/notes/components/components.module.ts +++ /dev/null @@ -1,30 +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 { CoreSharedModule } from '@/core/shared.module'; -import { NgModule } from '@angular/core'; -import { AddonNotesAddComponent } from './add/add-modal'; - -@NgModule({ - declarations: [ - AddonNotesAddComponent, - ], - imports: [ - CoreSharedModule, - ], - exports: [ - AddonNotesAddComponent, - ], -}) -export class AddonNotesComponentsModule {} diff --git a/src/addons/notes/notes.module.ts b/src/addons/notes/notes.module.ts index c7aac6ba1..3791c309e 100644 --- a/src/addons/notes/notes.module.ts +++ b/src/addons/notes/notes.module.ts @@ -21,7 +21,6 @@ import { AddonNotesSyncCronHandler } from './services/handlers/sync-cron'; import { AddonNotesUserHandler } from './services/handlers/user'; import { CORE_SITE_SCHEMAS } from '@services/sites'; import { NOTES_OFFLINE_SITE_SCHEMA } from './services/database/notes'; -import { AddonNotesComponentsModule } from './components/components.module'; import { Routes } from '@angular/router'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; import { CoreCourseIndexRoutingModule } from '@features/course/course-routing.module'; @@ -54,7 +53,6 @@ const routes: Routes = [ imports: [ CoreMainMenuTabRoutingModule.forChild(routes), CoreCourseIndexRoutingModule.forChild({ children: routes }), - AddonNotesComponentsModule, ], providers: [ { diff --git a/src/addons/notes/pages/list/list.ts b/src/addons/notes/pages/list/list.ts index ef174a017..0976982c3 100644 --- a/src/addons/notes/pages/list/list.ts +++ b/src/addons/notes/pages/list/list.ts @@ -13,7 +13,7 @@ // limitations under the License. import { CoreConstants } from '@/core/constants'; -import { AddonNotesAddComponent, AddonNotesAddModalReturn } from '@addons/notes/components/add/add-modal'; +import { AddonNotesAddModalReturn } from '@addons/notes/components/add/add-modal'; import { AddonNotes, AddonNotesNoteFormatted, AddonNotesPublishState } from '@addons/notes/services/notes'; import { AddonNotesOffline } from '@addons/notes/services/notes-offline'; import { AddonNotesSync, AddonNotesSyncProvider } from '@addons/notes/services/notes-sync'; @@ -194,6 +194,8 @@ export class AddonNotesListPage implements OnInit, OnDestroy { this.logViewAdd(); + const { AddonNotesAddComponent } = await import('@addons/notes/components/add/add-modal'); + const modalData = await CoreDomUtils.openModal({ component: AddonNotesAddComponent, componentProps: { diff --git a/src/core/classes/page-loads-manager.ts b/src/core/classes/page-loads-manager.ts index 8d9cd1bce..a67816df0 100644 --- a/src/core/classes/page-loads-manager.ts +++ b/src/core/classes/page-loads-manager.ts @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreRefreshButtonModalComponent } from '@components/refresh-button-modal/refresh-button-modal'; import { CoreNavigator } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; import { Subject } from 'rxjs'; @@ -129,6 +128,9 @@ export class PageLoadsManager { * Notify the user, asking him if he wants to update the data. */ protected async notifyUser(): Promise { + const { CoreRefreshButtonModalComponent } + = await import('@components/refresh-button-modal/refresh-button-modal'); + await CoreDomUtils.openModal({ component: CoreRefreshButtonModalComponent, cssClass: 'core-modal-no-background core-modal-fullscreen', diff --git a/src/core/components/components.module.ts b/src/core/components/components.module.ts index fb73105cf..4eb448fc5 100644 --- a/src/core/components/components.module.ts +++ b/src/core/components/components.module.ts @@ -61,7 +61,6 @@ import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides'; import { CoreSwipeNavigationTourComponent } from './swipe-navigation-tour/swipe-navigation-tour'; import { CoreMessageComponent } from './message/message'; import { CoreGroupSelectorComponent } from './group-selector/group-selector'; -import { CoreRefreshButtonModalComponent } from './refresh-button-modal/refresh-button-modal'; import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal'; import { CoreCourseImageComponent } from '@components/course-image/course-image'; import { CoreSitesListComponent } from './sites-list/sites-list'; @@ -123,7 +122,6 @@ export async function getCoreStandaloneComponents(): Promise[]> { CoreSpacerComponent, CoreHorizontalScrollControlsComponent, CoreSwipeNavigationTourComponent, - CoreRefreshButtonModalComponent, CoreSheetModalComponent, CoreSitesListComponent, ], @@ -177,7 +175,6 @@ export async function getCoreStandaloneComponents(): Promise[]> { CoreSpacerComponent, CoreHorizontalScrollControlsComponent, CoreSwipeNavigationTourComponent, - CoreRefreshButtonModalComponent, CoreSheetModalComponent, CoreSitesListComponent, ], diff --git a/src/core/components/refresh-button-modal/refresh-button-modal.ts b/src/core/components/refresh-button-modal/refresh-button-modal.ts index af0c7078a..f27d93100 100644 --- a/src/core/components/refresh-button-modal/refresh-button-modal.ts +++ b/src/core/components/refresh-button-modal/refresh-button-modal.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CoreSharedModule } from '@/core/shared.module'; import { Component } from '@angular/core'; import { ModalController } from '@singletons'; @@ -20,7 +21,11 @@ import { ModalController } from '@singletons'; */ @Component({ templateUrl: 'refresh-button-modal.html', - styleUrls: ['refresh-button-modal.scss'], + styleUrl: 'refresh-button-modal.scss', + standalone: true, + imports: [ + CoreSharedModule, + ], }) export class CoreRefreshButtonModalComponent { diff --git a/src/core/features/contentlinks/components/choose-site-modal/choose-site-modal.ts b/src/core/features/contentlinks/components/choose-site-modal/choose-site-modal.ts index 193b1996b..15c4e29d1 100644 --- a/src/core/features/contentlinks/components/choose-site-modal/choose-site-modal.ts +++ b/src/core/features/contentlinks/components/choose-site-modal/choose-site-modal.ts @@ -21,6 +21,7 @@ import { CoreContentLinksHelper } from '../../services/contentlinks-helper'; import { CoreError } from '@classes/errors/error'; import { CoreNavigator } from '@services/navigator'; import { CoreSitesFactory } from '@services/sites-factory'; +import { CoreSharedModule } from '@/core/shared.module'; /** * Page to display the list of sites to choose one to perform a content link action. @@ -28,6 +29,10 @@ import { CoreSitesFactory } from '@services/sites-factory'; @Component({ selector: 'core-content-links-choose-site-modal', templateUrl: 'choose-site-modal.html', + standalone: true, + imports: [ + CoreSharedModule, + ], }) export class CoreContentLinksChooseSiteModalComponent implements OnInit { diff --git a/src/core/features/contentlinks/components/components.module.ts b/src/core/features/contentlinks/components/components.module.ts deleted file mode 100644 index 99ac28edd..000000000 --- a/src/core/features/contentlinks/components/components.module.ts +++ /dev/null @@ -1,30 +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 { CoreSharedModule } from '@/core/shared.module'; -import { NgModule } from '@angular/core'; -import { CoreContentLinksChooseSiteModalComponent } from './choose-site-modal/choose-site-modal'; - -@NgModule({ - declarations: [ - CoreContentLinksChooseSiteModalComponent, - ], - imports: [ - CoreSharedModule, - ], - exports: [ - CoreContentLinksChooseSiteModalComponent, - ], -}) -export class CoreContentLinksComponentsModule {} diff --git a/src/core/features/contentlinks/contentlinks.module.ts b/src/core/features/contentlinks/contentlinks.module.ts index 42ba8b978..04ba1747d 100644 --- a/src/core/features/contentlinks/contentlinks.module.ts +++ b/src/core/features/contentlinks/contentlinks.module.ts @@ -13,7 +13,6 @@ // limitations under the License. import { NgModule, Type } from '@angular/core'; -import { CoreContentLinksComponentsModule } from './components/components.module'; /** * Get content links services. @@ -50,8 +49,5 @@ export async function getContentLinksExportedObjects(): Promise { + const { CoreContentLinksChooseSiteModalComponent } + = await import('@features/contentlinks/components/choose-site-modal/choose-site-modal'); + await CoreDomUtils.openModal({ component: CoreContentLinksChooseSiteModalComponent, componentProps: { diff --git a/src/core/features/course/components/components.module.ts b/src/core/features/course/components/components.module.ts index a9d757fa5..4a8a1d4f1 100644 --- a/src/core/features/course/components/components.module.ts +++ b/src/core/features/course/components/components.module.ts @@ -19,7 +19,6 @@ import { CoreBlockComponentsModule } from '@features/block/components/components import { CoreCourseFormatComponent } from './course-format/course-format'; import { CoreCourseModuleComponent } from './module/module'; import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion'; -import { CoreCourseCourseIndexComponent } from './course-index/course-index'; import { CoreCourseTagAreaComponent } from './tag-area/tag-area'; import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module'; import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-legacy/module-completion-legacy'; @@ -37,7 +36,6 @@ import { CoreCourseModuleCompletionDetailsComponent } from './module-completion- CoreCourseModuleCompletionComponent, CoreCourseModuleCompletionLegacyComponent, CoreCourseModuleInfoComponent, - CoreCourseCourseIndexComponent, CoreCourseCourseIndexTourComponent, CoreCourseTagAreaComponent, CoreCourseUnsupportedModuleComponent, @@ -56,7 +54,6 @@ import { CoreCourseModuleCompletionDetailsComponent } from './module-completion- CoreCourseModuleCompletionComponent, CoreCourseModuleCompletionLegacyComponent, CoreCourseModuleInfoComponent, - CoreCourseCourseIndexComponent, CoreCourseCourseIndexTourComponent, CoreCourseTagAreaComponent, CoreCourseUnsupportedModuleComponent, diff --git a/src/core/features/course/components/course-format/course-format.ts b/src/core/features/course/components/course-format/course-format.ts index c658ea48a..63ddb340f 100644 --- a/src/core/features/course/components/course-format/course-format.ts +++ b/src/core/features/course/components/course-format/course-format.ts @@ -41,7 +41,7 @@ import { CoreCourseFormatDelegate } from '@features/course/services/format-deleg import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { IonContent } from '@ionic/angular'; import { CoreUtils } from '@services/utils/utils'; -import { CoreCourseCourseIndexComponent, CoreCourseIndexSectionWithModule } from '../course-index/course-index'; +import { CoreCourseIndexSectionWithModule } from '../course-index/course-index'; import { CoreBlockHelper } from '@features/block/services/block-helper'; import { CoreNavigator } from '@services/navigator'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; @@ -429,6 +429,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { async openCourseIndex(): Promise { const selectedId = await this.getSelectedSectionId(); + const { CoreCourseCourseIndexComponent } = await import('@features/course/components/course-index/course-index'); + const data = await CoreDomUtils.openModal({ component: CoreCourseCourseIndexComponent, initialBreakpoint: 1, diff --git a/src/core/features/course/components/course-index/course-index.ts b/src/core/features/course/components/course-index/course-index.ts index 92576c5c0..804846ee3 100644 --- a/src/core/features/course/components/course-index/course-index.ts +++ b/src/core/features/course/components/course-index/course-index.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CoreSharedModule } from '@/core/shared.module'; import { Component, ElementRef, Input, OnInit } from '@angular/core'; import { CoreCourse, @@ -33,7 +34,11 @@ import { CoreDom } from '@singletons/dom'; @Component({ selector: 'core-course-course-index', templateUrl: 'course-index.html', - styleUrls: ['course-index.scss'], + styleUrl: 'course-index.scss', + standalone: true, + imports: [ + CoreSharedModule, + ], }) export class CoreCourseCourseIndexComponent implements OnInit { diff --git a/src/core/features/dataprivacy/components/components.module.ts b/src/core/features/dataprivacy/components/components.module.ts deleted file mode 100644 index 9b857a50a..000000000 --- a/src/core/features/dataprivacy/components/components.module.ts +++ /dev/null @@ -1,33 +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 { NgModule } from '@angular/core'; -import { CoreSharedModule } from '@/core/shared.module'; -import { CoreDataPrivacyContactDPOComponent } from './contactdpo/contactdpo'; -import { CoreDataPrivacyNewRequestComponent } from './newrequest/newrequest'; - -@NgModule({ - declarations: [ - CoreDataPrivacyContactDPOComponent, - CoreDataPrivacyNewRequestComponent, - ], - imports: [ - CoreSharedModule, - ], - exports: [ - CoreDataPrivacyContactDPOComponent, - CoreDataPrivacyNewRequestComponent, - ], -}) -export class CoreDataPrivacyComponentsModule {} diff --git a/src/core/features/dataprivacy/components/contactdpo/contactdpo.ts b/src/core/features/dataprivacy/components/contactdpo/contactdpo.ts index 2e531b444..6bcd74ba6 100644 --- a/src/core/features/dataprivacy/components/contactdpo/contactdpo.ts +++ b/src/core/features/dataprivacy/components/contactdpo/contactdpo.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CoreSharedModule } from '@/core/shared.module'; import { Component, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { CoreDataPrivacy } from '@features/dataprivacy/services/dataprivacy'; @@ -28,6 +29,11 @@ import { ModalController } from '@singletons'; @Component({ selector: 'core-data-privacy-contact-dpo', templateUrl: 'contactdpo.html', + standalone: true, + imports: [ + CoreSharedModule, + ], + }) export class CoreDataPrivacyContactDPOComponent implements OnInit { diff --git a/src/core/features/dataprivacy/components/newrequest/newrequest.ts b/src/core/features/dataprivacy/components/newrequest/newrequest.ts index fb603eae5..7939da1fd 100644 --- a/src/core/features/dataprivacy/components/newrequest/newrequest.ts +++ b/src/core/features/dataprivacy/components/newrequest/newrequest.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CoreSharedModule } from '@/core/shared.module'; import { Component, Input, OnInit } from '@angular/core'; import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { @@ -29,6 +30,10 @@ import { ModalController } from '@singletons'; @Component({ selector: 'core-data-privacy-new-request', templateUrl: 'newrequest.html', + standalone: true, + imports: [ + CoreSharedModule, + ], }) export class CoreDataPrivacyNewRequestComponent implements OnInit { diff --git a/src/core/features/dataprivacy/dataprivacy.module.ts b/src/core/features/dataprivacy/dataprivacy.module.ts index d0c6a22ce..fdf75e04e 100644 --- a/src/core/features/dataprivacy/dataprivacy.module.ts +++ b/src/core/features/dataprivacy/dataprivacy.module.ts @@ -17,7 +17,6 @@ import { CoreUserDelegate } from '@features/user/services/user-delegate'; import { CoreDataPrivacyUserHandler } from './services/handlers/user'; import { Routes } from '@angular/router'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; -import { CoreDataPrivacyComponentsModule } from './components/components.module'; import { CORE_DATAPRIVACY_PAGE_NAME } from './constants'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreDataPrivacyDataRequestsLinkHandler } from './services/handlers/datarequests-link'; @@ -33,7 +32,6 @@ const routes: Routes = [ @NgModule({ imports: [ CoreMainMenuTabRoutingModule.forChild(routes), - CoreDataPrivacyComponentsModule, ], providers: [ { diff --git a/src/core/features/dataprivacy/pages/main/main.ts b/src/core/features/dataprivacy/pages/main/main.ts index 037f59970..6ac26a9cc 100644 --- a/src/core/features/dataprivacy/pages/main/main.ts +++ b/src/core/features/dataprivacy/pages/main/main.ts @@ -13,8 +13,6 @@ // limitations under the License. import { Component, OnInit } from '@angular/core'; -import { CoreDataPrivacyContactDPOComponent } from '@features/dataprivacy/components/contactdpo/contactdpo'; -import { CoreDataPrivacyNewRequestComponent } from '@features/dataprivacy/components/newrequest/newrequest'; import { CoreDataPrivacy, CoreDataPrivacyDataRequestType, @@ -115,6 +113,9 @@ export class CoreDataPrivacyMainPage implements OnInit { * Open the contact DPO modal. */ async contactDPO(): Promise { + const { CoreDataPrivacyContactDPOComponent } = + await import('@features/dataprivacy/components/contactdpo/contactdpo'); + // Create and show the modal. const succeed = await CoreDomUtils.openModal({ component: CoreDataPrivacyContactDPOComponent, @@ -134,6 +135,9 @@ export class CoreDataPrivacyMainPage implements OnInit { * Open the new request modal. */ async newRequest(createType?: CoreDataPrivacyDataRequestType): Promise { + const { CoreDataPrivacyNewRequestComponent } = + await import('@features/dataprivacy/components/newrequest/newrequest'); + // Create and show the modal. const succeed = await CoreDomUtils.openModal({ component: CoreDataPrivacyNewRequestComponent, diff --git a/src/core/features/fileuploader/components/audio-histogram/audio-histogram.ts b/src/core/features/fileuploader/components/audio-histogram/audio-histogram.ts index 671680473..7ced49f26 100644 --- a/src/core/features/fileuploader/components/audio-histogram/audio-histogram.ts +++ b/src/core/features/fileuploader/components/audio-histogram/audio-histogram.ts @@ -12,13 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CoreSharedModule } from '@/core/shared.module'; import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnDestroy, ViewChild } from '@angular/core'; @Component({ selector: 'core-audio-histogram', templateUrl: 'audio-histogram.html', - styleUrls: ['audio-histogram.scss'], + styleUrl: 'audio-histogram.scss', changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CoreSharedModule, + ], }) export class CoreFileUploaderAudioHistogramComponent implements AfterViewInit, OnDestroy { diff --git a/src/core/features/fileuploader/components/audio-recorder/audio-recorder.component.ts b/src/core/features/fileuploader/components/audio-recorder/audio-recorder.component.ts index 2c2a3187f..85f37c6c2 100644 --- a/src/core/features/fileuploader/components/audio-recorder/audio-recorder.component.ts +++ b/src/core/features/fileuploader/components/audio-recorder/audio-recorder.component.ts @@ -27,12 +27,19 @@ import { CoreFileUploaderAudioRecording } from '@features/fileuploader/services/ import { CoreFile, CoreFileProvider } from '@services/file'; import { CorePath } from '@singletons/path'; import { CoreNative } from '@features/native/services/native'; +import { CoreSharedModule } from '@/core/shared.module'; +import { CoreFileUploaderAudioHistogramComponent } from '../audio-histogram/audio-histogram'; @Component({ selector: 'core-fileuploader-audio-recorder', - styleUrls: ['./audio-recorder.scss'], + styleUrl: 'audio-recorder.scss', templateUrl: 'audio-recorder.html', changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CoreSharedModule, + CoreFileUploaderAudioHistogramComponent, + ], }) export class CoreFileUploaderAudioRecorderComponent extends CoreModalComponent implements OnDestroy { diff --git a/src/core/features/fileuploader/components/audio-recorder/audio-recorder.module.ts b/src/core/features/fileuploader/components/audio-recorder/audio-recorder.module.ts deleted file mode 100644 index 3b50dd3b7..000000000 --- a/src/core/features/fileuploader/components/audio-recorder/audio-recorder.module.ts +++ /dev/null @@ -1,32 +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 { CoreSharedModule } from '@/core/shared.module'; -import { NgModule } from '@angular/core'; -import { CoreFileUploaderAudioHistogramComponent } from '@features/fileuploader/components/audio-histogram/audio-histogram'; - -import { CoreFileUploaderAudioRecorderComponent } from './audio-recorder.component'; - -export { CoreFileUploaderAudioRecorderComponent }; - -@NgModule({ - imports: [ - CoreSharedModule, - ], - declarations: [ - CoreFileUploaderAudioRecorderComponent, - CoreFileUploaderAudioHistogramComponent, - ], -}) -export class CoreFileUploaderAudioRecorderComponentModule {} diff --git a/src/core/features/fileuploader/services/fileuploader.ts b/src/core/features/fileuploader/services/fileuploader.ts index 927011824..62d83482e 100644 --- a/src/core/features/fileuploader/services/fileuploader.ts +++ b/src/core/features/fileuploader/services/fileuploader.ts @@ -159,7 +159,7 @@ export class CoreFileUploaderProvider { */ async captureAudioInApp(): Promise { const { CoreFileUploaderAudioRecorderComponent } = - await import('@features/fileuploader/components/audio-recorder/audio-recorder.module'); + await import('@features/fileuploader/components/audio-recorder/audio-recorder.component'); const recording = await CoreModals.openSheet(CoreFileUploaderAudioRecorderComponent); diff --git a/src/core/features/login/components/components.module.ts b/src/core/features/login/components/components.module.ts index 0d7cf51e7..6407dd57b 100644 --- a/src/core/features/login/components/components.module.ts +++ b/src/core/features/login/components/components.module.ts @@ -14,8 +14,6 @@ import { NgModule } from '@angular/core'; import { CoreSharedModule } from '@/core/shared.module'; -import { CoreLoginSiteOnboardingComponent } from './site-onboarding/site-onboarding'; -import { CoreLoginSiteHelpComponent } from './site-help/site-help'; import { CoreLoginSitesModalComponent } from './sites-modal/sites-modal'; import { CoreLoginMethodsComponent } from './login-methods/login-methods'; import { CoreLoginExceededAttemptsComponent } from '@features/login/components/exceeded-attempts/exceeded-attempts'; @@ -23,8 +21,6 @@ import { CoreLoginExceededAttemptsComponent } from '@features/login/components/e @NgModule({ declarations: [ CoreLoginExceededAttemptsComponent, - CoreLoginSiteOnboardingComponent, - CoreLoginSiteHelpComponent, CoreLoginSitesModalComponent, CoreLoginMethodsComponent, ], @@ -33,8 +29,6 @@ import { CoreLoginExceededAttemptsComponent } from '@features/login/components/e ], exports: [ CoreLoginExceededAttemptsComponent, - CoreLoginSiteOnboardingComponent, - CoreLoginSiteHelpComponent, CoreLoginSitesModalComponent, CoreLoginMethodsComponent, ], diff --git a/src/core/features/login/components/site-help/site-help.ts b/src/core/features/login/components/site-help/site-help.ts index 2bb6fa923..6bee64115 100644 --- a/src/core/features/login/components/site-help/site-help.ts +++ b/src/core/features/login/components/site-help/site-help.ts @@ -20,6 +20,7 @@ import { FAQ_QRCODE_IMAGE_HTML, FAQ_URL_IMAGE_HTML, GET_STARTED_URL } from '@fea import { CoreDomUtils } from '@services/utils/dom'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { SubPartial } from '@/core/utils/types'; +import { CoreSharedModule } from '@/core/shared.module'; /** * Component that displays help to connect to a site. @@ -27,7 +28,11 @@ import { SubPartial } from '@/core/utils/types'; @Component({ selector: 'core-login-site-help', templateUrl: 'site-help.html', - styleUrls: ['site-help.scss'], + styleUrl: 'site-help.scss', + standalone: true, + imports: [ + CoreSharedModule, + ], }) export class CoreLoginSiteHelpComponent implements AfterViewInit, OnDestroy { diff --git a/src/core/features/login/components/site-onboarding/site-onboarding.ts b/src/core/features/login/components/site-onboarding/site-onboarding.ts index 2b5896b60..da8e6b39e 100644 --- a/src/core/features/login/components/site-onboarding/site-onboarding.ts +++ b/src/core/features/login/components/site-onboarding/site-onboarding.ts @@ -18,6 +18,7 @@ import { CoreConfig } from '@services/config'; import { CoreUtils } from '@services/utils/utils'; import { GET_STARTED_URL, ONBOARDING_DONE } from '@features/login/constants'; import { ModalController } from '@singletons'; +import { CoreSharedModule } from '@/core/shared.module'; /** * Component that displays onboarding help regarding the CoreLoginSitePage. @@ -26,6 +27,10 @@ import { ModalController } from '@singletons'; selector: 'core-login-site-onboarding', templateUrl: 'site-onboarding.html', styleUrls: ['site-onboarding.scss', '../../login.scss'], + standalone: true, + imports: [ + CoreSharedModule, + ], }) export class CoreLoginSiteOnboardingComponent { diff --git a/src/core/features/login/pages/site/site.ts b/src/core/features/login/pages/site/site.ts index 0d7d06bf1..9702f05c4 100644 --- a/src/core/features/login/pages/site/site.ts +++ b/src/core/features/login/pages/site/site.ts @@ -30,8 +30,6 @@ import { CoreConstants } from '@/core/constants'; import { Translate } from '@singletons'; import { CoreUrl } from '@singletons/url'; import { CoreUrlUtils } from '@services/utils/url'; -import { CoreLoginSiteHelpComponent } from '@features/login/components/site-help/site-help'; -import { CoreLoginSiteOnboardingComponent } from '@features/login/components/site-onboarding/site-onboarding'; import { CoreNavigator } from '@services/navigator'; import { CoreCustomURLSchemes, CoreCustomURLSchemesHandleError } from '@services/urlschemes'; import { CoreTextUtils } from '@services/utils/text'; @@ -258,6 +256,9 @@ export class CoreLoginSitePage implements OnInit { * Show a help modal. */ async showHelp(): Promise { + const { CoreLoginSiteHelpComponent } = + await import('@features/login/components/site-help/site-help'); + await CoreDomUtils.openModal({ component: CoreLoginSiteHelpComponent, cssClass: 'core-modal-fullscreen', @@ -268,6 +269,9 @@ export class CoreLoginSitePage implements OnInit { * Show an onboarding modal. */ async showOnboarding(): Promise { + const { CoreLoginSiteOnboardingComponent } = + await import('@features/login/components/site-onboarding/site-onboarding'); + await CoreDomUtils.openModal({ component: CoreLoginSiteOnboardingComponent, cssClass: 'core-modal-fullscreen', diff --git a/src/core/features/policy/components/components.module.ts b/src/core/features/policy/components/components.module.ts deleted file mode 100644 index 21c3b118b..000000000 --- a/src/core/features/policy/components/components.module.ts +++ /dev/null @@ -1,31 +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 { NgModule } from '@angular/core'; - -import { CoreSharedModule } from '@/core/shared.module'; -import { CorePolicyViewPolicyModalComponent } from './policy-modal/policy-modal'; - -@NgModule({ - declarations: [ - CorePolicyViewPolicyModalComponent, - ], - imports: [ - CoreSharedModule, - ], - exports: [ - CorePolicyViewPolicyModalComponent, - ], -}) -export class CorePolicyComponentsModule {} diff --git a/src/core/features/policy/components/policy-modal/policy-modal.ts b/src/core/features/policy/components/policy-modal/policy-modal.ts index d9f9a6da2..ff5a1c8f4 100644 --- a/src/core/features/policy/components/policy-modal/policy-modal.ts +++ b/src/core/features/policy/components/policy-modal/policy-modal.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CoreSharedModule } from '@/core/shared.module'; import { Component, Input } from '@angular/core'; import { CorePolicySitePolicy } from '@features/policy/services/policy'; import { ModalController } from '@singletons'; @@ -22,6 +23,10 @@ import { ModalController } from '@singletons'; @Component({ selector: 'core-policy-view-policy-modal', templateUrl: 'policy-modal.html', + standalone: true, + imports: [ + CoreSharedModule, + ], }) export class CorePolicyViewPolicyModalComponent { diff --git a/src/core/features/policy/pages/acceptances/acceptances.ts b/src/core/features/policy/pages/acceptances/acceptances.ts index 76938983d..3344c6957 100644 --- a/src/core/features/policy/pages/acceptances/acceptances.ts +++ b/src/core/features/policy/pages/acceptances/acceptances.ts @@ -20,7 +20,6 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { Translate } from '@singletons'; import { CorePolicy, CorePolicySitePolicy, CorePolicyStatus } from '@features/policy/services/policy'; -import { CorePolicyViewPolicyModalComponent } from '@features/policy/components/policy-modal/policy-modal'; import { CoreTime } from '@singletons/time'; import { CoreScreen } from '@services/screen'; import { Subscription } from 'rxjs'; @@ -188,10 +187,13 @@ export class CorePolicyAcceptancesPage implements OnInit, OnDestroy { * @param event Event. * @param policy Policy. */ - viewFullPolicy(event: Event, policy: CorePolicySitePolicy): void { + async viewFullPolicy(event: Event, policy: CorePolicySitePolicy): Promise { event.preventDefault(); event.stopPropagation(); + const { CorePolicyViewPolicyModalComponent } = + await import('@features/policy/components/policy-modal/policy-modal'); + CoreDomUtils.openModal({ component: CorePolicyViewPolicyModalComponent, componentProps: { policy }, diff --git a/src/core/features/policy/pages/site-policy/site-policy.ts b/src/core/features/policy/pages/site-policy/site-policy.ts index fd5d18c04..aca9f223d 100644 --- a/src/core/features/policy/pages/site-policy/site-policy.ts +++ b/src/core/features/policy/pages/site-policy/site-policy.ts @@ -30,7 +30,6 @@ import { IonContent } from '@ionic/angular'; import { CoreScreen } from '@services/screen'; import { Subscription } from 'rxjs'; import { CoreDom } from '@singletons/dom'; -import { CorePolicyViewPolicyModalComponent } from '@features/policy/components/policy-modal/policy-modal'; /** * Page to accept a site policy. @@ -464,7 +463,10 @@ export class CorePolicySitePolicyPage implements OnInit, OnDestroy { * * @param policy Policy. */ - viewFullPolicy(policy: CorePolicySitePolicy): void { + async viewFullPolicy(policy: CorePolicySitePolicy): Promise { + const { CorePolicyViewPolicyModalComponent } = + await import('@features/policy/components/policy-modal/policy-modal'); + CoreDomUtils.openModal({ component: CorePolicyViewPolicyModalComponent, componentProps: { policy }, diff --git a/src/core/features/policy/policy-lazy.module.ts b/src/core/features/policy/policy-lazy.module.ts index 660117563..7fc310608 100644 --- a/src/core/features/policy/policy-lazy.module.ts +++ b/src/core/features/policy/policy-lazy.module.ts @@ -18,7 +18,6 @@ import { RouterModule, Routes } from '@angular/router'; import { CoreSharedModule } from '@/core/shared.module'; import { CorePolicySitePolicyPage } from '@features/policy/pages/site-policy/site-policy'; import { ACCEPTANCES_PAGE_NAME, SITE_POLICY_PAGE_NAME } from './constants'; -import { CorePolicyComponentsModule } from './components/components.module'; import { CorePolicyAcceptancesPage } from './pages/acceptances/acceptances'; const routes: Routes = [ @@ -36,7 +35,6 @@ const routes: Routes = [ imports: [ CoreSharedModule, RouterModule.forChild(routes), - CorePolicyComponentsModule, ], declarations: [ CorePolicySitePolicyPage, diff --git a/src/core/features/rating/components/aggregate/aggregate.ts b/src/core/features/rating/components/aggregate/aggregate.ts index c9c511286..8f81aac08 100644 --- a/src/core/features/rating/components/aggregate/aggregate.ts +++ b/src/core/features/rating/components/aggregate/aggregate.ts @@ -23,7 +23,6 @@ import { import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; -import { CoreRatingRatingsComponent } from '../ratings/ratings'; /** * Component that displays the aggregation of a rating item. @@ -118,6 +117,9 @@ export class CoreRatingAggregateComponent implements OnChanges, OnDestroy { return; } + const { CoreRatingRatingsComponent } = + await import('@features/rating/components/ratings/ratings'); + await CoreDomUtils.openModal({ component: CoreRatingRatingsComponent, componentProps: { diff --git a/src/core/features/rating/components/components.module.ts b/src/core/features/rating/components/components.module.ts index 3315306b4..e6f02e1aa 100644 --- a/src/core/features/rating/components/components.module.ts +++ b/src/core/features/rating/components/components.module.ts @@ -16,13 +16,11 @@ import { NgModule } from '@angular/core'; import { CoreRatingAggregateComponent } from './aggregate/aggregate'; import { CoreRatingRateComponent } from './rate/rate'; import { CoreSharedModule } from '@/core/shared.module'; -import { CoreRatingRatingsComponent } from './ratings/ratings'; @NgModule({ declarations: [ CoreRatingAggregateComponent, CoreRatingRateComponent, - CoreRatingRatingsComponent, ], imports: [ CoreSharedModule, @@ -30,7 +28,6 @@ import { CoreRatingRatingsComponent } from './ratings/ratings'; exports: [ CoreRatingAggregateComponent, CoreRatingRateComponent, - CoreRatingRatingsComponent, ], }) export class CoreRatingComponentsModule {} diff --git a/src/core/features/rating/components/ratings/ratings.ts b/src/core/features/rating/components/ratings/ratings.ts index 5e938cb7a..5dc9aa6f8 100644 --- a/src/core/features/rating/components/ratings/ratings.ts +++ b/src/core/features/rating/components/ratings/ratings.ts @@ -13,6 +13,7 @@ // limitations under the License. import { ContextLevel } from '@/core/constants'; +import { CoreSharedModule } from '@/core/shared.module'; import { Component, Input, OnInit } from '@angular/core'; import { CoreRating, CoreRatingItemRating } from '@features/rating/services/rating'; import { CoreDomUtils } from '@services/utils/dom'; @@ -23,6 +24,10 @@ import { ModalController } from '@singletons'; */ @Component({ templateUrl: 'ratings-modal.html', + standalone: true, + imports: [ + CoreSharedModule, + ], }) export class CoreRatingRatingsComponent implements OnInit { diff --git a/src/core/features/sharedfiles/components/components.module.ts b/src/core/features/sharedfiles/components/components.module.ts index 32b1a9f30..a6b44c7ba 100644 --- a/src/core/features/sharedfiles/components/components.module.ts +++ b/src/core/features/sharedfiles/components/components.module.ts @@ -16,19 +16,16 @@ import { NgModule } from '@angular/core'; import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedFilesListComponent } from './list/list'; -import { CoreSharedFilesListModalComponent } from './list-modal/list-modal'; @NgModule({ declarations: [ CoreSharedFilesListComponent, - CoreSharedFilesListModalComponent, ], imports: [ CoreSharedModule, ], exports: [ CoreSharedFilesListComponent, - CoreSharedFilesListModalComponent, ], }) export class CoreSharedFilesComponentsModule {} diff --git a/src/core/features/sharedfiles/components/list-modal/list-modal.ts b/src/core/features/sharedfiles/components/list-modal/list-modal.ts index 34f9cd550..f084b0cec 100644 --- a/src/core/features/sharedfiles/components/list-modal/list-modal.ts +++ b/src/core/features/sharedfiles/components/list-modal/list-modal.ts @@ -12,11 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CoreSharedModule } from '@/core/shared.module'; import { Component, OnInit, Input } from '@angular/core'; import { FileEntry } from '@awesome-cordova-plugins/file/ngx'; import { CoreFile } from '@services/file'; import { ModalController, Translate } from '@singletons'; +import { CoreSharedFilesComponentsModule } from '../components.module'; /** * Modal to display the list of shared files. @@ -24,6 +26,11 @@ import { ModalController, Translate } from '@singletons'; @Component({ selector: 'core-shared-files-list-modal', templateUrl: 'list-modal.html', + standalone: true, + imports: [ + CoreSharedModule, + CoreSharedFilesComponentsModule, + ], }) export class CoreSharedFilesListModalComponent implements OnInit { diff --git a/src/core/features/sharedfiles/services/sharedfiles-helper.ts b/src/core/features/sharedfiles/services/sharedfiles-helper.ts index 357b0db6a..ba178ba95 100644 --- a/src/core/features/sharedfiles/services/sharedfiles-helper.ts +++ b/src/core/features/sharedfiles/services/sharedfiles-helper.ts @@ -25,7 +25,6 @@ import { CoreDomUtils } from '@services/utils/dom'; import { AlertController, ApplicationInit, makeSingleton, Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { CoreLogger } from '@singletons/logger'; -import { CoreSharedFilesListModalComponent } from '../components/list-modal/list-modal'; import { CoreSharedFiles } from './sharedfiles'; import { SHAREDFILES_PAGE_NAME } from '../constants'; import { CoreSharedFilesChooseSitePage } from '../pages/choose-site/choose-site'; @@ -150,6 +149,9 @@ export class CoreSharedFilesHelperProvider { * @returns Promise resolved when a file is picked, rejected if file picker is closed without selecting a file. */ async pickSharedFile(mimetypes?: string[]): Promise { + const { CoreSharedFilesListModalComponent } = + await import('@features/sharedfiles/components/list-modal/list-modal'); + const file = await CoreDomUtils.openModal({ component: CoreSharedFilesListModalComponent, cssClass: 'core-modal-fullscreen', diff --git a/src/core/features/viewer/components/components.module.ts b/src/core/features/viewer/components/components.module.ts deleted file mode 100644 index 1baa9b84c..000000000 --- a/src/core/features/viewer/components/components.module.ts +++ /dev/null @@ -1,38 +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 { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; - -import { CoreSharedModule } from '@/core/shared.module'; -import { CoreViewerImageComponent } from './image/image'; -import { CoreViewerQRScannerComponent } from './qr-scanner/qr-scanner'; -import { CoreViewerTextComponent } from './text/text'; - -@NgModule({ - declarations: [ - CoreViewerImageComponent, - CoreViewerQRScannerComponent, - CoreViewerTextComponent, - ], - imports: [ - CoreSharedModule, - ], - exports: [ - CoreViewerImageComponent, - CoreViewerQRScannerComponent, - CoreViewerTextComponent, - ], - schemas: [CUSTOM_ELEMENTS_SCHEMA], -}) -export class CoreViewerComponentsModule {} diff --git a/src/core/features/viewer/components/image/image.ts b/src/core/features/viewer/components/image/image.ts index 87deee8c7..d8c8b4f64 100644 --- a/src/core/features/viewer/components/image/image.ts +++ b/src/core/features/viewer/components/image/image.ts @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; +import { Component, ElementRef, Input, OnInit, ViewChild, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { DomSanitizer, ModalController, Translate } from '@singletons'; import { CoreMath } from '@singletons/math'; import { Swiper } from 'swiper'; import { SwiperOptions } from 'swiper/types'; import { CoreSwiper } from '@singletons/swiper'; import { SafeResourceUrl } from '@angular/platform-browser'; +import { CoreSharedModule } from '@/core/shared.module'; /** * Modal component to view an image. @@ -26,7 +27,12 @@ import { SafeResourceUrl } from '@angular/platform-browser'; @Component({ selector: 'core-viewer-image', templateUrl: 'image.html', - styleUrls: ['image.scss'], + styleUrl: 'image.scss', + standalone: true, + imports: [ + CoreSharedModule, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class CoreViewerImageComponent implements OnInit { diff --git a/src/core/features/viewer/components/qr-scanner/qr-scanner.ts b/src/core/features/viewer/components/qr-scanner/qr-scanner.ts index 318e3eb09..25e6ac9c0 100644 --- a/src/core/features/viewer/components/qr-scanner/qr-scanner.ts +++ b/src/core/features/viewer/components/qr-scanner/qr-scanner.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CoreSharedModule } from '@/core/shared.module'; import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; @@ -23,6 +24,10 @@ import { ModalController, Translate } from '@singletons'; @Component({ selector: 'core-viewer-qr-scanner', templateUrl: 'qr-scanner.html', + standalone: true, + imports: [ + CoreSharedModule, + ], }) export class CoreViewerQRScannerComponent implements OnInit, OnDestroy { diff --git a/src/core/features/viewer/components/text/text.ts b/src/core/features/viewer/components/text/text.ts index a7c82dfed..a35e46d09 100644 --- a/src/core/features/viewer/components/text/text.ts +++ b/src/core/features/viewer/components/text/text.ts @@ -13,6 +13,7 @@ // limitations under the License. import { ContextLevel } from '@/core/constants'; +import { CoreSharedModule } from '@/core/shared.module'; import { Component, Input } from '@angular/core'; import { CoreFileEntry } from '@services/file-helper'; @@ -25,7 +26,11 @@ import { ModalController } from '@singletons'; @Component({ selector: 'page-core-viewer-text', templateUrl: 'text.html', - styleUrls: ['text.scss'], + styleUrl: 'text.scss', + standalone: true, + imports: [ + CoreSharedModule, + ], }) export class CoreViewerTextComponent { diff --git a/src/core/features/viewer/viewer-lazy.module.ts b/src/core/features/viewer/viewer-lazy.module.ts index c6f4e8049..5880afb09 100644 --- a/src/core/features/viewer/viewer-lazy.module.ts +++ b/src/core/features/viewer/viewer-lazy.module.ts @@ -33,4 +33,4 @@ const routes: Routes = [ CoreViewerIframePage, ], }) -export class CoreViewerLazyModule {} +export default class CoreViewerLazyModule {} diff --git a/src/core/features/viewer/viewer.module.ts b/src/core/features/viewer/viewer.module.ts index bfa5806c6..64dbf46c7 100644 --- a/src/core/features/viewer/viewer.module.ts +++ b/src/core/features/viewer/viewer.module.ts @@ -16,19 +16,17 @@ import { NgModule } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; -import { CoreViewerComponentsModule } from './components/components.module'; const routes: Routes = [ { path: 'viewer', - loadChildren: () => import('./viewer-lazy.module').then(m => m.CoreViewerLazyModule), + loadChildren: () => import('./viewer-lazy.module'), }, ]; @NgModule({ imports: [ CoreMainMenuTabRoutingModule.forChild(routes), - CoreViewerComponentsModule, ], }) export class CoreViewerModule {} diff --git a/src/core/services/navigator.ts b/src/core/services/navigator.ts index 1e1001cd9..82c5a906d 100644 --- a/src/core/services/navigator.ts +++ b/src/core/services/navigator.ts @@ -244,7 +244,7 @@ export class CoreNavigatorService { // User has been redirected to the login page and will be redirected to the site path after login. return true; } - } catch (error) { + } catch { // Site doesn't exist. return this.navigate('/login/sites', { reset: true }); } finally { diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index f41dd5961..38846b8e9 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -43,7 +43,6 @@ import { CoreLogger } from '@singletons/logger'; import { CoreFileSizeSum } from '@services/plugin-file-delegate'; import { CoreNetworkError } from '@classes/errors/network-error'; import { CoreBSTooltipComponent } from '@components/bs-tooltip/bs-tooltip'; -import { CoreViewerImageComponent } from '@features/viewer/components/image/image'; import { CoreModalLateralTransitionEnter, CoreModalLateralTransitionLeave } from '@classes/modal-lateral-transition'; import { CoreSites } from '@services/sites'; import { NavigationStart } from '@angular/router'; @@ -1648,6 +1647,7 @@ export class CoreDomUtilsProvider { if (!image) { return; } + const { CoreViewerImageComponent } = await import('@features/viewer/components/image/image'); await CoreDomUtils.openModal({ component: CoreViewerImageComponent, diff --git a/src/core/services/utils/text.ts b/src/core/services/utils/text.ts index 526f3c732..a40bcdbd9 100644 --- a/src/core/services/utils/text.ts +++ b/src/core/services/utils/text.ts @@ -20,7 +20,6 @@ import { CoreAnyError, CoreError } from '@classes/errors/error'; import { DomSanitizer, makeSingleton, Translate } from '@singletons'; import { CoreWSFile } from '@services/ws'; import { Locutus } from '@singletons/locutus'; -import { CoreViewerTextComponent } from '@features/viewer/components/text/text'; import { CoreFileHelper } from '@services/file-helper'; import { CoreDomUtils } from './dom'; import { CoreUrl } from '@singletons/url'; @@ -1037,6 +1036,7 @@ export class CoreTextUtilsProvider { } options = options || {}; + const { CoreViewerTextComponent } = await import('@features/viewer/components/text/text'); const modalOptions: ModalOptions = Object.assign(options.modalOptions || {}, { component: CoreViewerTextComponent, diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 1a6cc9a28..67ad4993f 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -25,7 +25,6 @@ import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; import { makeSingleton, Clipboard, InAppBrowser, FileOpener, WebIntent, Translate, NgZone } from '@singletons'; import { CoreLogger } from '@singletons/logger'; -import { CoreViewerQRScannerComponent } from '@features/viewer/components/qr-scanner/qr-scanner'; import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreFileEntry } from '@services/file-helper'; import { CoreConstants } from '@/core/constants'; @@ -1664,6 +1663,8 @@ export class CoreUtilsProvider { * @returns Promise resolved with the captured text or undefined if cancelled or error. */ async scanQR(title?: string): Promise { + const { CoreViewerQRScannerComponent } = await import('@features/viewer/components/qr-scanner/qr-scanner'); + return CoreDomUtils.openModal({ component: CoreViewerQRScannerComponent, cssClass: 'core-modal-fullscreen', From 7068db3f62aead8806a401d88bfd52caf2a55a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 16 Jul 2024 12:49:28 +0200 Subject: [PATCH 2/5] MOBILE-4616 chore: Move copyClipboard to CoreText --- .../messages/pages/discussion/discussion.ts | 3 ++- src/core/components/message/message.ts | 4 ++-- src/core/features/settings/pages/dev/dev.ts | 4 ++-- .../settings/pages/deviceinfo/deviceinfo.ts | 5 ++-- .../settings/pages/error-log/error-log.ts | 6 ++--- .../features/viewer/components/text/text.ts | 4 ++-- src/core/services/utils/utils.ts | 21 +++++----------- src/core/singletons/text.ts | 24 +++++++++++++++++++ 8 files changed, 44 insertions(+), 27 deletions(-) diff --git a/src/addons/messages/pages/discussion/discussion.ts b/src/addons/messages/pages/discussion/discussion.ts index dbc9c0ca4..6afb3bb64 100644 --- a/src/addons/messages/pages/discussion/discussion.ts +++ b/src/addons/messages/pages/discussion/discussion.ts @@ -45,6 +45,7 @@ import { ActivatedRoute } from '@angular/router'; import { CoreConstants } from '@/core/constants'; import { CoreDom } from '@singletons/dom'; import { CoreKeyboard } from '@singletons/keyboard'; +import { CoreText } from '@singletons/text'; /** * Page that displays a message discussion page. @@ -924,7 +925,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView */ copyMessage(message: AddonMessagesConversationMessageFormatted): void { const text = 'smallmessage' in message ? message.smallmessage || message.text || '' : message.text || ''; - CoreUtils.copyToClipboard(CoreTextUtils.decodeHTMLEntities(text)); + CoreText.copyToClipboard(CoreTextUtils.decodeHTMLEntities(text)); } /** diff --git a/src/core/components/message/message.ts b/src/core/components/message/message.ts index 5247e54cd..d9d1a0572 100644 --- a/src/core/components/message/message.ts +++ b/src/core/components/message/message.ts @@ -16,7 +16,7 @@ import { ContextLevel } from '@/core/constants'; import { Component, EventEmitter, HostBinding, Input, OnInit, Output } from '@angular/core'; import { CoreAnimations } from '@components/animations'; import { CoreSites } from '@services/sites'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreText } from '@singletons/text'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUserWithAvatar } from '@components/user-avatar/user-avatar'; @@ -102,7 +102,7 @@ export class CoreMessageComponent implements OnInit { * Copy message to clipboard. */ copyMessage(): void { - CoreUtils.copyToClipboard(CoreTextUtils.decodeHTMLEntities(this.text)); + CoreText.copyToClipboard(CoreTextUtils.decodeHTMLEntities(this.text)); } } diff --git a/src/core/features/settings/pages/dev/dev.ts b/src/core/features/settings/pages/dev/dev.ts index 0877d74f0..ce9bac44b 100644 --- a/src/core/features/settings/pages/dev/dev.ts +++ b/src/core/features/settings/pages/dev/dev.ts @@ -25,7 +25,7 @@ import { CoreNavigator } from '@services/navigator'; import { CorePlatform } from '@services/platform'; import { CoreSites } from '@services/sites'; import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreText } from '@singletons/text'; /** * Page that displays the developer options. @@ -165,7 +165,7 @@ export class CoreSettingsDevPage implements OnInit { * Copies site info. */ copyInfo(): void { - CoreUtils.copyToClipboard(JSON.stringify({ disabledFeatures: this.disabledFeatures, sitePlugins: this.sitePlugins })); + CoreText.copyToClipboard(JSON.stringify({ disabledFeatures: this.disabledFeatures, sitePlugins: this.sitePlugins })); } /** diff --git a/src/core/features/settings/pages/deviceinfo/deviceinfo.ts b/src/core/features/settings/pages/deviceinfo/deviceinfo.ts index 4fa595af8..815976b56 100644 --- a/src/core/features/settings/pages/deviceinfo/deviceinfo.ts +++ b/src/core/features/settings/pages/deviceinfo/deviceinfo.ts @@ -29,6 +29,7 @@ import { CorePlatform } from '@services/platform'; import { CoreNetwork } from '@services/network'; import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreSitesFactory } from '@services/sites-factory'; +import { CoreText } from '@singletons/text'; /** * Device Info to be shown and copied to clipboard. @@ -226,7 +227,7 @@ export class CoreSettingsDeviceInfoPage implements OnDestroy { * Copies device info into the clipboard. */ copyInfo(): void { - CoreUtils.copyToClipboard(JSON.stringify(this.deviceInfo)); + CoreText.copyToClipboard(JSON.stringify(this.deviceInfo)); } /** @@ -238,7 +239,7 @@ export class CoreSettingsDeviceInfoPage implements OnDestroy { const el = e.target; const text = el?.closest('ion-item')?.textContent?.trim(); - text && CoreUtils.copyToClipboard(text); + text && CoreText.copyToClipboard(text); } /** diff --git a/src/core/features/settings/pages/error-log/error-log.ts b/src/core/features/settings/pages/error-log/error-log.ts index 65a6e3c38..4d7a71e4e 100644 --- a/src/core/features/settings/pages/error-log/error-log.ts +++ b/src/core/features/settings/pages/error-log/error-log.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, OnInit } from '@angular/core'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreText } from '@singletons/text'; import { CoreErrorLogs, CoreSettingsErrorLog } from '@singletons/error-logs'; /** @@ -39,9 +39,9 @@ export class CoreSettingsErrorLogPage implements OnInit { */ async copyError(error?: CoreSettingsErrorLog): Promise { if (error) { - await CoreUtils.copyToClipboard(JSON.stringify(error)); + await CoreText.copyToClipboard(JSON.stringify(error)); } else { - await CoreUtils.copyToClipboard(JSON.stringify({ errors: this.errorLogs })); + await CoreText.copyToClipboard(JSON.stringify({ errors: this.errorLogs })); } } diff --git a/src/core/features/viewer/components/text/text.ts b/src/core/features/viewer/components/text/text.ts index a35e46d09..37029e7ba 100644 --- a/src/core/features/viewer/components/text/text.ts +++ b/src/core/features/viewer/components/text/text.ts @@ -17,7 +17,7 @@ import { CoreSharedModule } from '@/core/shared.module'; import { Component, Input } from '@angular/core'; import { CoreFileEntry } from '@services/file-helper'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreText } from '@singletons/text'; import { ModalController } from '@singletons'; /** @@ -56,7 +56,7 @@ export class CoreViewerTextComponent { * Copy the text to clipboard. */ copyText(): void { - CoreUtils.copyToClipboard(this.content || ''); + CoreText.copyToClipboard(this.content || ''); } } diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 67ad4993f..cc05c35ae 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -23,7 +23,7 @@ import { CoreWS } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; -import { makeSingleton, Clipboard, InAppBrowser, FileOpener, WebIntent, Translate, NgZone } from '@singletons'; +import { makeSingleton, InAppBrowser, FileOpener, WebIntent, Translate, NgZone } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreFileEntry } from '@services/file-helper'; @@ -40,6 +40,7 @@ import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreUrlUtils } from './url'; import { QRScanner } from '@features/native/plugins'; import { CoreArray } from '@singletons/array'; +import { CoreText } from '@singletons/text'; export type TreeNode = T & { children: TreeNode[] }; @@ -366,22 +367,12 @@ export class CoreUtilsProvider { * Copies a text to clipboard and shows a toast message. * * @param text Text to be copied - * @returns Promise resolved when text is copied. + * @returns Promise resolved when the text is copied. + * + * @deprecated since 4.5 Use CoreText.copyToClipboard instead. */ async copyToClipboard(text: string): Promise { - try { - await Clipboard.copy(text); - } catch { - // Use HTML Copy command. - const virtualInput = document.createElement('textarea'); - virtualInput.innerHTML = text; - virtualInput.select(); - virtualInput.setSelectionRange(0, 99999); - document.execCommand('copy'); // eslint-disable-line deprecation/deprecation - } - - // Show toast using ionicLoading. - CoreDomUtils.showToast('core.copiedtoclipboard', true); + return CoreText.copyToClipboard(text); } /** diff --git a/src/core/singletons/text.ts b/src/core/singletons/text.ts index 05768f8fc..100c9be74 100644 --- a/src/core/singletons/text.ts +++ b/src/core/singletons/text.ts @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { Clipboard } from '@singletons'; +import { CoreDomUtils } from '@services/utils/dom'; + /** * Singleton with helper functions for text manipulation. */ @@ -68,4 +71,25 @@ export class CoreText { return text.substring(1); } + /** + * Copies a text to clipboard and shows a toast message. + * + * @param text Text to be copied + */ + static async copyToClipboard(text: string): Promise { + try { + await Clipboard.copy(text); + } catch { + // Use HTML Copy command. + const virtualInput = document.createElement('textarea'); + virtualInput.innerHTML = text; + virtualInput.select(); + virtualInput.setSelectionRange(0, 99999); + document.execCommand('copy'); // eslint-disable-line deprecation/deprecation + } + + // Show toast using ionicLoading. + CoreDomUtils.showToast('core.copiedtoclipboard', true); + } + } From 7d9c9b6fe9d21eac9e1cf14855099874680f894f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 22 Jul 2024 17:06:19 +0200 Subject: [PATCH 3/5] MOBILE-4616 chore: Create CoreWait to add waiting functions --- .../services/handlers/mathjaxloader.ts | 3 +- .../messages/pages/discussion/discussion.ts | 5 +- src/addons/messages/services/messages-sync.ts | 3 +- src/addons/mod/assign/pages/index/index.ts | 4 +- src/addons/mod/chat/pages/chat/chat.ts | 3 +- src/addons/mod/quiz/pages/player/player.ts | 3 +- .../mod/scorm/components/index/index.ts | 3 +- src/addons/mod/scorm/pages/player/player.ts | 3 +- src/addons/qtype/ddwtos/classes/ddwtos.ts | 4 +- .../qtype/ordering/component/ordering.ts | 4 +- src/app/app.component.ts | 3 +- src/core/classes/ion-loading.ts | 4 +- src/core/classes/page-load-watcher.ts | 3 +- src/core/classes/sites/authenticated-site.ts | 5 +- src/core/classes/tabs.ts | 6 +- src/core/classes/tests/queue-runner.test.ts | 4 +- .../infinite-loading/infinite-loading.ts | 8 +- src/core/components/loading/loading.ts | 5 +- .../components/sheet-modal/sheet-modal.ts | 12 +-- .../components/swipe-slides/swipe-slides.ts | 6 +- src/core/directives/auto-focus.ts | 3 +- src/core/directives/collapsible-footer.ts | 3 +- src/core/directives/collapsible-header.ts | 5 +- src/core/directives/format-text.ts | 7 +- .../components/side-blocks/side-blocks.ts | 5 +- src/core/features/compile/services/compile.ts | 2 + .../components/course-index/course-index.ts | 6 +- .../course/pages/contents/contents.ts | 3 +- src/core/features/course/pages/index/index.ts | 3 +- src/core/features/courses/pages/my/my.ts | 3 +- .../rich-text-editor/rich-text-editor.ts | 7 +- .../emulator/services/local-notifications.ts | 4 +- .../login/components/site-help/site-help.ts | 3 +- src/core/features/mainmenu/pages/menu/menu.ts | 9 +- .../policy/pages/site-policy/site-policy.ts | 3 +- .../set-reminder-menu/set-reminder-menu.ts | 4 +- .../features/usertours/services/user-tours.ts | 3 +- src/core/services/error-accordion.ts | 4 +- src/core/services/tests/sites.test.ts | 8 +- src/core/services/utils/dom.ts | 7 +- src/core/services/utils/utils.ts | 53 ++++------- src/core/singletons/directives-registry.ts | 6 +- src/core/singletons/subscriptions.ts | 4 +- src/core/singletons/wait.ts | 94 +++++++++++++++++++ src/testing/services/behat-blocking.ts | 6 +- src/testing/services/behat-dom.ts | 6 +- 46 files changed, 228 insertions(+), 124 deletions(-) create mode 100644 src/core/singletons/wait.ts diff --git a/src/addons/filter/mathjaxloader/services/handlers/mathjaxloader.ts b/src/addons/filter/mathjaxloader/services/handlers/mathjaxloader.ts index 7ca996ae7..caa72b2d2 100644 --- a/src/addons/filter/mathjaxloader/services/handlers/mathjaxloader.ts +++ b/src/addons/filter/mathjaxloader/services/handlers/mathjaxloader.ts @@ -23,6 +23,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreEvents } from '@singletons/events'; import { CoreSite } from '@classes/sites/site'; import { makeSingleton } from '@singletons'; +import { CoreWait } from '@singletons/wait'; /** * Handler to support the MathJax filter. @@ -321,7 +322,7 @@ export class AddonFilterMathJaxLoaderHandlerService extends CoreFilterDefaultHan return; } - await CoreUtils.wait(250); + await CoreWait.wait(250); await CoreUtils.ignoreErrors(this.waitForReady(retries + 1)); } diff --git a/src/addons/messages/pages/discussion/discussion.ts b/src/addons/messages/pages/discussion/discussion.ts index 6afb3bb64..78df7f2e3 100644 --- a/src/addons/messages/pages/discussion/discussion.ts +++ b/src/addons/messages/pages/discussion/discussion.ts @@ -46,6 +46,7 @@ import { CoreConstants } from '@/core/constants'; import { CoreDom } from '@singletons/dom'; import { CoreKeyboard } from '@singletons/keyboard'; import { CoreText } from '@singletons/text'; +import { CoreWait } from '@singletons/wait'; /** * Page that displays a message discussion page. @@ -884,7 +885,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView return; } - await CoreUtils.wait(400); + await CoreWait.wait(400); await CoreUtils.ignoreErrors(this.waitForFetch()); } @@ -1072,7 +1073,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView this.setNewMessagesBadge(0); // Leave time for the view to be rendered. - await CoreUtils.nextTicks(5); + await CoreWait.nextTicks(5); if (!this.viewDestroyed && this.content) { this.content.scrollToBottom(0); diff --git a/src/addons/messages/services/messages-sync.ts b/src/addons/messages/services/messages-sync.ts index ef66db791..73b09de33 100644 --- a/src/addons/messages/services/messages-sync.ts +++ b/src/addons/messages/services/messages-sync.ts @@ -32,6 +32,7 @@ import { CoreUser } from '@features/user/services/user'; import { CoreError } from '@classes/errors/error'; import { CoreTextErrorObject, CoreTextUtils } from '@services/utils/text'; import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site'; +import { CoreWait } from '@singletons/wait'; /** * Service to sync messages. @@ -252,7 +253,7 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { switch (this.action) { case 'editsubmission': - await CoreUtils.waitFor(() => !!this.activityComponent?.submissionComponent, { timeout: 5000 }); + await CoreWait.waitFor(() => !!this.activityComponent?.submissionComponent, { timeout: 5000 }); await this.activityComponent?.submissionComponent?.goToEdit(); break; diff --git a/src/addons/mod/chat/pages/chat/chat.ts b/src/addons/mod/chat/pages/chat/chat.ts index cbffd3a3a..f1d770852 100644 --- a/src/addons/mod/chat/pages/chat/chat.ts +++ b/src/addons/mod/chat/pages/chat/chat.ts @@ -30,6 +30,7 @@ import { AddonModChatFormattedMessage, AddonModChatHelper } from '../../services import { CoreTime } from '@singletons/time'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreKeyboard } from '@singletons/keyboard'; +import { CoreWait } from '@singletons/wait'; /** * Page that displays a chat session. @@ -358,7 +359,7 @@ export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave { */ async scrollToBottom(): Promise { // Need a timeout to leave time to the view to be rendered. - await CoreUtils.nextTick(); + await CoreWait.nextTick(); if (!this.viewDestroyed) { this.content?.scrollToBottom(); } diff --git a/src/addons/mod/quiz/pages/player/player.ts b/src/addons/mod/quiz/pages/player/player.ts index 6dd4dced4..c67450887 100644 --- a/src/addons/mod/quiz/pages/player/player.ts +++ b/src/addons/mod/quiz/pages/player/player.ts @@ -54,6 +54,7 @@ import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreWSError } from '@classes/errors/wserror'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { ADDON_MOD_QUIZ_ATTEMPT_FINISHED_EVENT, AddonModQuizAttemptStates, ADDON_MOD_QUIZ_COMPONENT } from '../../constants'; +import { CoreWait } from '@singletons/wait'; /** * Page that allows attempting a quiz. @@ -831,7 +832,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { * @param slot Slot of the question to scroll to. */ protected async scrollToQuestion(slot: number): Promise { - await CoreUtils.nextTick(); + await CoreWait.nextTick(); await CoreDirectivesRegistry.waitDirectivesReady(this.elementRef.nativeElement, 'core-question'); await CoreDom.scrollToElement( this.elementRef.nativeElement, diff --git a/src/addons/mod/scorm/components/index/index.ts b/src/addons/mod/scorm/components/index/index.ts index 4c8e7ee63..eb9febdb4 100644 --- a/src/addons/mod/scorm/components/index/index.ts +++ b/src/addons/mod/scorm/components/index/index.ts @@ -49,6 +49,7 @@ import { ADDON_MOD_SCORM_DATA_AUTO_SYNCED, ADDON_MOD_SCORM_PAGE_NAME, } from '../../constants'; +import { CoreWait } from '@singletons/wait'; /** * Component that displays a SCORM entry page. @@ -616,7 +617,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom if (CoreSync.isBlocked(ADDON_MOD_SCORM_COMPONENT, this.scorm.id) && retries < 5) { // Sync is currently blocked, this can happen when SCORM player is left. Retry in a bit. - await CoreUtils.wait(400); + await CoreWait.wait(400); return this.sync(retries + 1); } diff --git a/src/addons/mod/scorm/pages/player/player.ts b/src/addons/mod/scorm/pages/player/player.ts index be6162944..c9d4f6aa5 100644 --- a/src/addons/mod/scorm/pages/player/player.ts +++ b/src/addons/mod/scorm/pages/player/player.ts @@ -43,6 +43,7 @@ import { ADDON_MOD_SCORM_LAUNCH_PREV_SCO_EVENT, ADDON_MOD_SCORM_UPDATE_TOC_EVENT, } from '../../constants'; +import { CoreWait } from '@singletons/wait'; /** * Page that allows playing a SCORM. @@ -436,7 +437,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { // Changing SCO. First unload the existing SCO to make sure the callback to send the data has been called. this.src = ''; - await CoreUtils.nextTick(); + await CoreWait.nextTick(); // Load the SCO in the existing model. this.dataModel.loadSco(sco.id); diff --git a/src/addons/qtype/ddwtos/classes/ddwtos.ts b/src/addons/qtype/ddwtos/classes/ddwtos.ts index 2133ecb26..338217d2e 100644 --- a/src/addons/qtype/ddwtos/classes/ddwtos.ts +++ b/src/addons/qtype/ddwtos/classes/ddwtos.ts @@ -14,12 +14,12 @@ import { CoreFormatTextDirective } from '@directives/format-text'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUtils } from '@services/utils/utils'; import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreCoordinates, CoreDom } from '@singletons/dom'; import { CoreEventObserver } from '@singletons/events'; import { CoreLogger } from '@singletons/logger'; import { AddonModQuizDdwtosQuestionData } from '../component/ddwtos'; +import { CoreWait } from '@singletons/wait'; /** * Class to make a question of ddwtos type work. @@ -491,7 +491,7 @@ export class AddonQtypeDdwtosQuestion { } else { // Group items should always have a parent, add a fallback just in case. await CoreDom.waitToBeInDOM(groupItems[0]); - await CoreUtils.nextTicks(5); + await CoreWait.nextTicks(5); } // Find max height and width. diff --git a/src/addons/qtype/ordering/component/ordering.ts b/src/addons/qtype/ordering/component/ordering.ts index 5bd693420..bd4da3eda 100644 --- a/src/addons/qtype/ordering/component/ordering.ts +++ b/src/addons/qtype/ordering/component/ordering.ts @@ -18,7 +18,7 @@ import { CoreQuestionHelper } from '@features/question/services/question-helper' import { CoreDomUtils } from '@services/utils/dom'; import { ItemReorderEventDetail } from '@ionic/angular'; import { Translate } from '@singletons'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreWait } from '@singletons/wait'; import { CorePlatform } from '@services/platform'; /** @@ -153,7 +153,7 @@ export class AddonQtypeOrderingComponent extends CoreQuestionBaseComponent {}, // eslint-disable-line @typescript-eslint/no-empty-function }); - await CoreUtils.nextTick(); + await CoreWait.nextTick(); // When moving an item to the first or last position, the button that was clicked will be hidden. In this case, we need to // focus the other button. Otherwise, re-focus the same button since the focus is lost in some cases. diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 4b93666b8..e821baaec 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -27,6 +27,7 @@ import { CorePlatform } from '@services/platform'; import { CoreLogger } from '@singletons/logger'; import { CorePromisedValue } from '@classes/promised-value'; import { register } from 'swiper/element/bundle'; +import { CoreWait } from '@singletons/wait'; register(); @@ -83,7 +84,7 @@ export class AppComponent implements OnInit, AfterViewInit { // Check if the path changes due to the back navigation handler, to know if we're at root level. // Ionic doc recommends IonRouterOutlet.canGoBack, but there's no easy way to get the current outlet from here. // The path seems to change immediately (0 ms timeout), but use 50ms just in case. - await CoreUtils.wait(50); + await CoreWait.wait(50); if (CoreNavigator.getCurrentPath() != initialPath) { // Ionic has navigated back, nothing else to do. diff --git a/src/core/classes/ion-loading.ts b/src/core/classes/ion-loading.ts index 1e831e54c..ff6b56cef 100644 --- a/src/core/classes/ion-loading.ts +++ b/src/core/classes/ion-loading.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreUtils } from '@services/utils/utils'; +import { CoreWait } from '@singletons/wait'; import { LoadingController } from '@singletons'; /** @@ -105,7 +105,7 @@ export class CoreIonLoadingElement { // Wait a bit before presenting the modal, to prevent it being displayed if dismiss is called fast. this.scheduled = true; - await CoreUtils.wait(40); + await CoreWait.wait(40); if (!this.scheduled) { return; diff --git a/src/core/classes/page-load-watcher.ts b/src/core/classes/page-load-watcher.ts index 870dcc2ac..2bc2ef7aa 100644 --- a/src/core/classes/page-load-watcher.ts +++ b/src/core/classes/page-load-watcher.ts @@ -19,6 +19,7 @@ import { AsyncDirective } from './async-directive'; import { PageLoadsManager } from './page-loads-manager'; import { CorePromisedValue } from './promised-value'; import { WSObservable } from './sites/authenticated-site'; +import { CoreWait } from '@singletons/wait'; /** * Class to watch requests from a page load (including requests from page sub-components). @@ -111,7 +112,7 @@ export class PageLoadWatcher { this.checkHasLoaded(); // Subscription variable might not be set because the observable completed immediately. Wait for next tick. - await CoreUtils.nextTick(); + await CoreWait.nextTick(); subscription?.unsubscribe(); }; diff --git a/src/core/classes/sites/authenticated-site.ts b/src/core/classes/sites/authenticated-site.ts index be2204b73..25e98e451 100644 --- a/src/core/classes/sites/authenticated-site.ts +++ b/src/core/classes/sites/authenticated-site.ts @@ -42,6 +42,7 @@ import { Md5 } from 'ts-md5'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreSiteWSCacheRecord } from '@services/database/sites'; import { CoreErrorLogs } from '@singletons/error-logs'; +import { CoreWait } from '@singletons/wait'; /** * Class that represents a site (combination of site + user) where the user has authenticated but the site hasn't been validated @@ -1584,7 +1585,7 @@ export function chainRequests>( firstValue = false; // Wait to see if the observable is completed (no more values). - await CoreUtils.nextTick(); + await CoreWait.nextTick(); if (isCompleted) { // Current request only returns cached data. Let chained requests update in background. @@ -1601,7 +1602,7 @@ export function chainRequests>( complete: async () => { isCompleted = true; - await CoreUtils.nextTick(); + await CoreWait.nextTick(); subscriber.complete(); }, diff --git a/src/core/classes/tabs.ts b/src/core/classes/tabs.ts index aae11a6b0..08d76b564 100644 --- a/src/core/classes/tabs.ts +++ b/src/core/classes/tabs.ts @@ -31,7 +31,7 @@ import { CoreSettingsHelper } from '@features/settings/services/settings-helper' import { CoreAriaRoleTab, CoreAriaRoleTabFindable } from './aria-role-tab'; import { CoreEventObserver } from '@singletons/events'; import { CoreDom } from '@singletons/dom'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreWait } from '@singletons/wait'; import { CoreError } from './errors/error'; import { CorePromisedValue } from './promised-value'; import { AsyncDirective } from './async-directive'; @@ -218,7 +218,7 @@ export class CoreTabsBaseComponent implements AfterViewIn this.slideChanged(); this.swiper.update(); - await CoreUtils.nextTick(); + await CoreWait.nextTick(); if (!this.hasSliddenToInitial && this.selectedIndex && this.selectedIndex >= this.swiper.slidesPerViewDynamic()) { this.hasSliddenToInitial = true; @@ -344,7 +344,7 @@ export class CoreTabsBaseComponent implements AfterViewIn } this.maxSlides = 3; - await CoreUtils.nextTick(); + await CoreWait.nextTick(); if (!this.swiper.width) { return; diff --git a/src/core/classes/tests/queue-runner.test.ts b/src/core/classes/tests/queue-runner.test.ts index 748c7c848..4ead65ac1 100644 --- a/src/core/classes/tests/queue-runner.test.ts +++ b/src/core/classes/tests/queue-runner.test.ts @@ -13,7 +13,7 @@ // limitations under the License. import { CoreQueueRunner } from '@classes/queue-runner'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreWait } from '@singletons/wait'; describe('CoreQueueRunner', () => { @@ -26,7 +26,7 @@ describe('CoreQueueRunner', () => { // Act await Promise.all(range.map((i) => lock.run(async () => { - await CoreUtils.wait(Math.floor(Math.random() * 10)); + await CoreWait.wait(Math.floor(Math.random() * 10)); items.push(`Item #${i}`); }))); diff --git a/src/core/components/infinite-loading/infinite-loading.ts b/src/core/components/infinite-loading/infinite-loading.ts index 0b519da2d..49103a3df 100644 --- a/src/core/components/infinite-loading/infinite-loading.ts +++ b/src/core/components/infinite-loading/infinite-loading.ts @@ -14,7 +14,7 @@ import { Component, Input, Output, EventEmitter, OnChanges, SimpleChange, ViewChild, ElementRef } from '@angular/core'; import { IonInfiniteScroll } from '@ionic/angular'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreWait } from '@singletons/wait'; const THRESHOLD = .15; // % of the scroll element height that must be close to the edge to consider loading more items necessary. @@ -77,8 +77,8 @@ export class CoreInfiniteLoadingComponent implements OnChanges { } // Wait to allow items to render and scroll content to grow. - await CoreUtils.nextTick(); - await CoreUtils.waitFor(() => scrollElement.scrollHeight > scrollElement.clientHeight, { timeout: 1000 }); + await CoreWait.nextTick(); + await CoreWait.waitFor(() => scrollElement.scrollHeight > scrollElement.clientHeight, { timeout: 1000 }); // Calculate distance from edge. const infiniteHeight = this.hostElement.getBoundingClientRect().height; @@ -116,7 +116,7 @@ export class CoreInfiniteLoadingComponent implements OnChanges { */ async complete(): Promise { // Wait a bit before allowing loading more, otherwise it could be re-triggered automatically when it shouldn't. - await CoreUtils.wait(400); + await CoreWait.wait(400); await this.completeLoadMore(); } diff --git a/src/core/components/loading/loading.ts b/src/core/components/loading/loading.ts index 8e66276e4..f890c40ee 100644 --- a/src/core/components/loading/loading.ts +++ b/src/core/components/loading/loading.ts @@ -21,6 +21,7 @@ import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CorePromisedValue } from '@classes/promised-value'; import { AsyncDirective } from '@classes/async-directive'; import { CorePlatform } from '@services/platform'; +import { CoreWait } from '@singletons/wait'; /** * Component to show a loading spinner and message while data is being loaded. @@ -72,13 +73,13 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit, A // Throttle 20ms to let mutations resolve. const throttleMutation = CoreUtils.throttle(async () => { - await CoreUtils.nextTick(); + await CoreWait.nextTick(); if (!this.loaded) { return; } this.element.style.display = 'inline'; - await CoreUtils.nextTick(); + await CoreWait.nextTick(); this.element.style.removeProperty('display'); }, 20); diff --git a/src/core/components/sheet-modal/sheet-modal.ts b/src/core/components/sheet-modal/sheet-modal.ts index b3c409775..e93409c7c 100644 --- a/src/core/components/sheet-modal/sheet-modal.ts +++ b/src/core/components/sheet-modal/sheet-modal.ts @@ -17,7 +17,7 @@ import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular import { CoreModalComponent } from '@classes/modal-component'; import { CorePromisedValue } from '@classes/promised-value'; import { CoreModals } from '@services/modals'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreWait } from '@singletons/wait'; import { AngularFrameworkDelegate } from '@singletons'; import { CoreDirectivesRegistry } from '@singletons/directives-registry'; @@ -64,13 +64,13 @@ export class CoreSheetModalComponent implements Af const wrapper = await this.wrapperElement; this.content = await AngularFrameworkDelegate.attachViewToDom(wrapper, this.component, this.componentProps ?? {}); - await CoreUtils.nextTick(); + await CoreWait.nextTick(); this.element.classList.add('active'); this.element.style.zIndex = `${20000 + CoreModals.getTopOverlayIndex()}`; - await CoreUtils.nextTick(); - await CoreUtils.wait(300); + await CoreWait.nextTick(); + await CoreWait.wait(300); const instance = CoreDirectivesRegistry.resolve(this.content, this.component); @@ -89,8 +89,8 @@ export class CoreSheetModalComponent implements Af this.element.classList.remove('active'); - await CoreUtils.nextTick(); - await CoreUtils.wait(300); + await CoreWait.nextTick(); + await CoreWait.wait(300); await AngularFrameworkDelegate.removeViewFromDom(wrapper, this.content); } diff --git a/src/core/components/swipe-slides/swipe-slides.ts b/src/core/components/swipe-slides/swipe-slides.ts index 0154671e1..182fdc890 100644 --- a/src/core/components/swipe-slides/swipe-slides.ts +++ b/src/core/components/swipe-slides/swipe-slides.ts @@ -20,7 +20,7 @@ import { CoreSwipeSlidesItemsManager } from '@classes/items-management/swipe-sli import { CorePromisedValue } from '@classes/promised-value'; import { IonContent } from '@ionic/angular'; import { CoreDomUtils, VerticalPoint } from '@services/utils/dom'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreWait } from '@singletons/wait'; import { NgZone } from '@singletons'; import { CoreDom } from '@singletons/dom'; import { CoreEventObserver } from '@singletons/events'; @@ -230,7 +230,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe */ protected async onItemsUpdated(): Promise { // Wait for slides to be added in DOM. - await CoreUtils.nextTick(); + await CoreWait.nextTick(); // Update the slides component so the slides list reflects the new items. await this.updateSlidesComponent(); @@ -348,7 +348,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe this.swiper.update(); // We need to ensure the slides are updated before continuing. - await CoreUtils.nextTicks(2); + await CoreWait.nextTicks(2); } /** diff --git a/src/core/directives/auto-focus.ts b/src/core/directives/auto-focus.ts index eb550ac35..f531d74c0 100644 --- a/src/core/directives/auto-focus.ts +++ b/src/core/directives/auto-focus.ts @@ -17,6 +17,7 @@ import { Directive, Input, ElementRef, AfterViewInit } from '@angular/core'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { CoreDom } from '@singletons/dom'; +import { CoreWait } from '@singletons/wait'; /** * Directive to auto focus an element when a view is loaded. @@ -51,7 +52,7 @@ export class CoreAutoFocusDirective implements AfterViewInit { // Wait in case there is an animation to enter the page, otherwise the interaction // between the keyboard appearing and the animation causes a visual glitch. - await CoreUtils.wait(540); + await CoreWait.wait(540); CoreDomUtils.focusElement(this.element); diff --git a/src/core/directives/collapsible-footer.ts b/src/core/directives/collapsible-footer.ts index de58fcedb..6fd746d42 100644 --- a/src/core/directives/collapsible-footer.ts +++ b/src/core/directives/collapsible-footer.ts @@ -23,6 +23,7 @@ import { CoreEventObserver } from '@singletons/events'; import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { CoreDom } from '@singletons/dom'; +import { CoreWait } from '@singletons/wait'; /** * Directive to make an element fixed at the bottom collapsible when scrolling. @@ -101,7 +102,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { await this.viewportPromise; this.element.classList.remove('is-active'); - await CoreUtils.nextTick(); + await CoreWait.nextTick(); // Set a minimum height value. this.initialHeight = this.element.getBoundingClientRect().height || this.initialHeight; diff --git a/src/core/directives/collapsible-header.ts b/src/core/directives/collapsible-header.ts index 30613d0c3..b20031439 100644 --- a/src/core/directives/collapsible-header.ts +++ b/src/core/directives/collapsible-header.ts @@ -27,6 +27,7 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreMath } from '@singletons/math'; import { Subscription } from 'rxjs'; import { CoreFormatTextDirective } from './format-text'; +import { CoreWait } from '@singletons/wait'; declare module '@singletons/events' { @@ -344,7 +345,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest await this.visiblePromise; this.page.classList.remove('collapsible-header-page-is-active'); - await CoreUtils.nextTick(); + await CoreWait.nextTick(); // Add floating title and measure initial position. const collapsedHeaderTitle = this.collapsedHeader.querySelector('h1') as HTMLHeadingElement; @@ -429,7 +430,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest } // Make sure elements have been added to the DOM. - await CoreUtils.nextTick(); + await CoreWait.nextTick(); // Wait all loadings and tabs to finish loading. await CoreDirectivesRegistry.waitMultipleDirectivesReady(this.page, [ diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index 3bac96c9d..4d8f34b0d 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -56,6 +56,7 @@ import { FrameElement, FrameElementController } from '@classes/element-controlle import { CoreUrl } from '@singletons/url'; import { CoreIcons } from '@singletons/icons'; import { ContextLevel } from '../constants'; +import { CoreWait } from '@singletons/wait'; /** * Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective @@ -339,7 +340,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec // Show the element again. this.element.classList.remove('core-loading'); - await CoreUtils.nextTick(); + await CoreWait.nextTick(); // Emit the afterRender output. this.afterRender.emit(); @@ -380,7 +381,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec this.elementControllers.forEach(controller => controller.destroy()); this.elementControllers = result.elementControllers; - await CoreUtils.nextTick(); + await CoreWait.nextTick(); // Add magnifying glasses to images. this.addImageViewerButton(); @@ -705,7 +706,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec const previousDisplay = getComputedStyle(this.element).display; this.element.style.display = 'inline-block'; - await CoreUtils.nextTick(); + await CoreWait.nextTick(); width = this.element.getBoundingClientRect().width; diff --git a/src/core/features/block/components/side-blocks/side-blocks.ts b/src/core/features/block/components/side-blocks/side-blocks.ts index e80b9a3b6..b677274a5 100644 --- a/src/core/features/block/components/side-blocks/side-blocks.ts +++ b/src/core/features/block/components/side-blocks/side-blocks.ts @@ -23,6 +23,7 @@ import { CoreCoursesDashboard } from '@features/courses/services/dashboard'; import { CoreTextUtils } from '@services/utils/text'; import { CoreDom } from '@singletons/dom'; import { ContextLevel } from '@/core/constants'; +import { CoreWait } from '@singletons/wait'; /** * Component that displays the list of side blocks. @@ -135,8 +136,8 @@ export class CoreBlockSideBlocksComponent implements OnInit { const selector = '#block-' + this.initialBlockInstanceId; - await CoreUtils.waitFor(() => !!this.elementRef.nativeElement.querySelector(selector)); - await CoreUtils.wait(200); + await CoreWait.waitFor(() => !!this.elementRef.nativeElement.querySelector(selector)); + await CoreWait.wait(200); CoreDom.scrollToElement(this.elementRef.nativeElement, selector, { addYAxis: -10 }); } diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index f85f156b8..daee0221a 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -85,6 +85,7 @@ import { CorePath } from '@singletons/path'; import { CoreText } from '@singletons/text'; import { CoreTime } from '@singletons/time'; import { CoreUrl } from '@singletons/url'; +import { CoreWait } from '@singletons/wait'; import { CoreWindow } from '@singletons/window'; import { CoreCache } from '@classes/cache'; import { CoreDelegate } from '@classes/delegate'; @@ -314,6 +315,7 @@ export class CoreCompileProvider { instance['CoreText'] = CoreText; instance['CoreTime'] = CoreTime; instance['CoreUrl'] = CoreUrl; + instance['CoreWait'] = CoreWait; instance['CoreWindow'] = CoreWindow; instance['CoreCache'] = CoreCache; // @deprecated since 4.4, plugins should use plain objects instead. instance['CoreDelegate'] = CoreDelegate; diff --git a/src/core/features/course/components/course-index/course-index.ts b/src/core/features/course/components/course-index/course-index.ts index 804846ee3..6fb8c8ca3 100644 --- a/src/core/features/course/components/course-index/course-index.ts +++ b/src/core/features/course/components/course-index/course-index.ts @@ -24,7 +24,7 @@ import { CoreCourseFormatDelegate } from '@features/course/services/format-deleg import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; import { CoreCoursesHelper } from '@features/courses/services/courses-helper'; import { CoreSites } from '@services/sites'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreWait } from '@singletons/wait'; import { ModalController } from '@singletons'; import { CoreDom } from '@singletons/dom'; @@ -123,11 +123,11 @@ export class CoreCourseCourseIndexComponent implements OnInit { this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course); // Wait a bit to render the data, otherwise the modal takes a while to appear in big courses or slow devices. - await CoreUtils.wait(400); + await CoreWait.wait(400); this.loaded = true; - await CoreUtils.nextTick(); + await CoreWait.nextTick(); CoreDom.scrollToElement( this.elementRef.nativeElement, diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts index af1ef4e35..b4e5c50d7 100644 --- a/src/core/features/course/pages/contents/contents.ts +++ b/src/core/features/course/pages/contents/contents.ts @@ -42,6 +42,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreRefreshContext, CORE_REFRESH_CONTEXT } from '@/core/utils/refresh-context'; import { CoreCoursesHelper } from '@features/courses/services/courses-helper'; import { CoreSites } from '@services/sites'; +import { CoreWait } from '@singletons/wait'; /** * Page that displays the contents of a course. @@ -411,7 +412,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon this.changeDetectorRef.detectChanges(); if (scrollTop > 0) { - await CoreUtils.nextTick(); + await CoreWait.nextTick(); this.content?.scrollToPoint(0, scrollTop, 0); } } diff --git a/src/core/features/course/pages/index/index.ts b/src/core/features/course/pages/index/index.ts index dbb693220..16955652f 100644 --- a/src/core/features/course/pages/index/index.ts +++ b/src/core/features/course/pages/index/index.ts @@ -30,6 +30,7 @@ import { CoreCoursesHelper, CoreCourseWithImageAndColor } from '@features/course import { CoreColors } from '@singletons/colors'; import { CorePath } from '@singletons/path'; import { CoreSites } from '@services/sites'; +import { CoreWait } from '@singletons/wait'; /** * Page that displays the list of courses the user is enrolled in. @@ -209,7 +210,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { // Select the tab if needed. this.firstTabName = undefined; if (tabToLoad) { - await CoreUtils.nextTick(); + await CoreWait.nextTick(); this.tabsComponent?.selectByIndex(tabToLoad); } diff --git a/src/core/features/courses/pages/my/my.ts b/src/core/features/courses/pages/my/my.ts index ddac3c1ed..f4f1ff63b 100644 --- a/src/core/features/courses/pages/my/my.ts +++ b/src/core/features/courses/pages/my/my.ts @@ -31,6 +31,7 @@ import { CoreCourses } from '../../services/courses'; import { CoreTime } from '@singletons/time'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { Translate } from '@singletons'; +import { CoreWait } from '@singletons/wait'; /** * Page that shows a my courses. @@ -129,7 +130,7 @@ export class CoreCoursesMyPage implements OnInit, OnDestroy, AsyncDirective { this.loadedBlock = blocks.mainBlocks.concat(blocks.sideBlocks).find((block) => block.name == 'myoverview'); this.hasSideBlocks = supportsMyParam && CoreBlockDelegate.hasSupportedBlock(blocks.sideBlocks); - await CoreUtils.nextTicks(2); + await CoreWait.nextTicks(2); this.myOverviewBlock = this.block?.dynamicComponent?.instance as AddonBlockMyOverviewComponent; diff --git a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts index 44182ac56..ced2b6dcf 100644 --- a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts +++ b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts @@ -46,6 +46,7 @@ import { SwiperOptions } from 'swiper/types'; import { ContextLevel } from '@/core/constants'; import { CoreSwiper } from '@singletons/swiper'; import { CoreTextUtils } from '@services/utils/text'; +import { CoreWait } from '@singletons/wait'; /** * Component to display a rich text editor if enabled. @@ -369,7 +370,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * @returns Blank height in px. Will be negative if no blank space. */ protected async getBlankHeightInContent(): Promise { - await CoreUtils.nextTicks(5); // Ensure content is completely loaded in the DOM. + await CoreWait.nextTicks(5); // Ensure content is completely loaded in the DOM. let content: Element | null = this.element.closest('ion-content'); const contentHeight = await CoreDomUtils.getContentHeight(this.content); @@ -485,7 +486,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, this.textareaElement?.removeAttribute('hidden'); } - await CoreUtils.nextTick(); + await CoreWait.nextTick(); this.focusRTE(event); } @@ -826,7 +827,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, this.toolbarArrows = true; } - await CoreUtils.nextTick(); + await CoreWait.nextTick(); this.toolbarSlides.update(); diff --git a/src/core/features/emulator/services/local-notifications.ts b/src/core/features/emulator/services/local-notifications.ts index 7e68a2f65..7c9b4c61b 100644 --- a/src/core/features/emulator/services/local-notifications.ts +++ b/src/core/features/emulator/services/local-notifications.ts @@ -15,7 +15,7 @@ import { CoreError } from '@classes/errors/error'; import { ILocalNotification, ILocalNotificationAction, LocalNotifications } from '@awesome-cordova-plugins/local-notifications/ngx'; import { Observable, Subject } from 'rxjs'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreWait } from '@singletons/wait'; import { CorePlatform } from '@services/platform'; /** @@ -340,7 +340,7 @@ export class LocalNotificationsMock extends LocalNotifications { // In some testing environments, Notification.requestPermission gets stuck and never returns. // Given that we don't actually need browser notifications to work in Behat tests, we can just // continue if the permissions haven't been granted after 1 second. - permissionRequests.push(CoreUtils.wait(1000).then(() => 'granted')); + permissionRequests.push(CoreWait.wait(1000).then(() => 'granted')); } const permission = await Promise.race(permissionRequests); diff --git a/src/core/features/login/components/site-help/site-help.ts b/src/core/features/login/components/site-help/site-help.ts index 6bee64115..75e9efb56 100644 --- a/src/core/features/login/components/site-help/site-help.ts +++ b/src/core/features/login/components/site-help/site-help.ts @@ -21,6 +21,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { SubPartial } from '@/core/utils/types'; import { CoreSharedModule } from '@/core/shared.module'; +import { CoreWait } from '@singletons/wait'; /** * Component that displays help to connect to a site. @@ -118,7 +119,7 @@ export class CoreLoginSiteHelpComponent implements AfterViewInit, OnDestroy { const answers = Array.from(this.el.nativeElement.querySelectorAll('.core-login-site-help--answer')); await Promise.all(answers.map(async answer => { - await this.track(CoreUtils.waitFor(() => answer.clientHeight !== 0)); + await this.track(CoreWait.waitFor(() => answer.clientHeight !== 0)); await this.track(CoreDomUtils.waitForImages(answer)); answer.style.setProperty('--height', `${answer.clientHeight}px`); diff --git a/src/core/features/mainmenu/pages/menu/menu.ts b/src/core/features/mainmenu/pages/menu/menu.ts index f75066f04..1c62bf7ad 100644 --- a/src/core/features/mainmenu/pages/menu/menu.ts +++ b/src/core/features/mainmenu/pages/menu/menu.ts @@ -31,6 +31,7 @@ import { CoreSites } from '@services/sites'; import { CoreDom } from '@singletons/dom'; import { CoreLogger } from '@singletons/logger'; import { CorePlatform } from '@services/platform'; +import { CoreWait } from '@singletons/wait'; const ANIMATION_DURATION = 500; @@ -203,7 +204,7 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { if (this.loaded && (!mainMenuTab || removedHandlersPages.includes(mainMenuTab))) { // No tab selected or handler no longer available, select the first one. - await CoreUtils.nextTick(); + await CoreWait.nextTick(); const tabPage = this.tabs[0] ? this.tabs[0].page : this.morePageName; const tabPageParams = this.tabs[0] ? this.tabs[0].pageParams : {}; @@ -330,9 +331,9 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { * Notify that the menu visibility has been updated. */ protected async notifyVisibilityUpdated(): Promise { - await CoreUtils.nextTick(); - await CoreUtils.wait(ANIMATION_DURATION); - await CoreUtils.nextTick(); + await CoreWait.nextTick(); + await CoreWait.wait(ANIMATION_DURATION); + await CoreWait.nextTick(); CoreEvents.trigger(CoreMainMenuProvider.MAIN_MENU_VISIBILITY_UPDATED); } diff --git a/src/core/features/policy/pages/site-policy/site-policy.ts b/src/core/features/policy/pages/site-policy/site-policy.ts index aca9f223d..bcb986566 100644 --- a/src/core/features/policy/pages/site-policy/site-policy.ts +++ b/src/core/features/policy/pages/site-policy/site-policy.ts @@ -30,6 +30,7 @@ import { IonContent } from '@ionic/angular'; import { CoreScreen } from '@services/screen'; import { Subscription } from 'rxjs'; import { CoreDom } from '@singletons/dom'; +import { CoreWait } from '@singletons/wait'; /** * Page to accept a site policy. @@ -337,7 +338,7 @@ export class CorePolicySitePolicyPage implements OnInit, OnDestroy { * Check if the content has scroll. */ protected async checkScroll(): Promise { - await CoreUtils.wait(400); + await CoreWait.wait(400); const scrollElement = await this.content?.getScrollElement(); diff --git a/src/core/features/reminders/components/set-reminder-menu/set-reminder-menu.ts b/src/core/features/reminders/components/set-reminder-menu/set-reminder-menu.ts index 26e55c901..bdc6cb875 100644 --- a/src/core/features/reminders/components/set-reminder-menu/set-reminder-menu.ts +++ b/src/core/features/reminders/components/set-reminder-menu/set-reminder-menu.ts @@ -20,7 +20,7 @@ import { CoreReminderValueAndUnit, } from '@features/reminders/services/reminders'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreWait } from '@singletons/wait'; import { PopoverController } from '@singletons'; import { CoreRemindersSetReminderCustomComponent } from '../set-reminder-custom/set-reminder-custom'; @@ -175,7 +175,7 @@ export class CoreRemindersSetReminderMenuComponent implements OnInit { this.customLabel = CoreReminders.getUnitValueLabel(this.customValue, this.customUnits); // Let the dimissed popover to be removed. - await CoreUtils.nextTick(); + await CoreWait.nextTick(); PopoverController.dismiss({ timeBefore: Math.abs(this.customValue) * this.customUnits }); } diff --git a/src/core/features/usertours/services/user-tours.ts b/src/core/features/usertours/services/user-tours.ts index f497c07bb..463f79287 100644 --- a/src/core/features/usertours/services/user-tours.ts +++ b/src/core/features/usertours/services/user-tours.ts @@ -27,6 +27,7 @@ import { CoreSubscriptions } from '@singletons/subscriptions'; import { CoreUserToursUserTourComponent } from '../components/user-tour/user-tour'; import { APP_SCHEMA, CoreUserToursDBEntry, USER_TOURS_TABLE_NAME } from './database/user-tours'; import { CorePromisedValue } from '@classes/promised-value'; +import { CoreWait } from '@singletons/wait'; /** * Service to manage User Tours. @@ -113,7 +114,7 @@ export class CoreUserToursService { protected async show(options: CoreUserToursBasicOptions | CoreUserToursFocusedOptions): Promise { const { delay, ...componentOptions } = options; - await CoreUtils.wait(delay ?? 200); + await CoreWait.wait(delay ?? 200); options.after && await this.waitForUserTour(options.after, options.afterTimeout); diff --git a/src/core/services/error-accordion.ts b/src/core/services/error-accordion.ts index 33c137780..b87a46aba 100644 --- a/src/core/services/error-accordion.ts +++ b/src/core/services/error-accordion.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { Translate, makeSingleton } from '@singletons'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreWait } from '@singletons/wait'; import { CoreDom } from '@singletons/dom'; import { CoreForms } from '@singletons/form'; import { CoreLogger } from '@singletons/logger'; @@ -113,7 +113,7 @@ export class CoreErrorAccordionService { wrapper.style.setProperty('--description-height', `${description.clientHeight}px`); wrapper.classList.add('hydrated'); - await CoreUtils.nextTick(); + await CoreWait.nextTick(); hideText.style.display = 'revert'; } diff --git a/src/core/services/tests/sites.test.ts b/src/core/services/tests/sites.test.ts index 1e91991af..7a274b4f2 100644 --- a/src/core/services/tests/sites.test.ts +++ b/src/core/services/tests/sites.test.ts @@ -22,7 +22,7 @@ import { Http } from '@singletons'; import { of } from 'rxjs'; import { CoreSite } from '@classes/sites/site'; import { CoreHTMLClasses } from '@singletons/html-classes'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreWait } from '@singletons/wait'; describe('CoreSitesProvider', () => { @@ -74,7 +74,7 @@ describe('CoreSitesProvider', () => { CoreEvents.trigger(CoreEvents.LOGIN, {}, '42'); // Wait the event to be processed. - await CoreUtils.nextTick(); + await CoreWait.nextTick(); expect(document.documentElement.classList.contains('theme-site-'+themeName)).toBe(true); expect(document.documentElement.classList.contains('theme-site-'+themeName2)).toBe(false); @@ -86,7 +86,7 @@ describe('CoreSitesProvider', () => { CoreEvents.trigger(CoreEvents.SITE_UPDATED, site.infos , '42'); // Wait the event to be processed. - await CoreUtils.nextTick(); + await CoreWait.nextTick(); expect(document.documentElement.classList.contains('theme-site-'+themeName2)).toBe(true); expect(document.documentElement.classList.contains('theme-site-'+themeName)).toBe(false); @@ -99,7 +99,7 @@ describe('CoreSitesProvider', () => { CoreEvents.trigger(CoreEvents.SITE_ADDED, site.infos , '42'); // Wait the event to be processed. - await CoreUtils.nextTick(); + await CoreWait.nextTick(); expect(document.documentElement.classList.contains('theme-site-'+themeName2)).toBe(true); expect(document.documentElement.classList.contains('theme-site-'+themeName)).toBe(false); diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 38846b8e9..5f238e435 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -59,6 +59,7 @@ import { CorePasswordModalParams, CorePasswordModalResponse } from '@components/ import { CoreWSError } from '@classes/errors/wserror'; import { CoreErrorLogs } from '@singletons/error-logs'; import { CoreKeyboard } from '@singletons/keyboard'; +import { CoreWait } from '@singletons/wait'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -333,7 +334,7 @@ export class CoreDomUtilsProvider { elementToFocus.focus(); if (elementToFocus === document.activeElement || (isIonButton && element === document.activeElement)) { - await CoreUtils.nextTick(); + await CoreWait.nextTick(); if (CorePlatform.isAndroid() && this.supportsInputKeyboard(elementToFocus)) { // On some Android versions the keyboard doesn't open automatically. CoreKeyboard.open(); @@ -342,7 +343,7 @@ export class CoreDomUtilsProvider { } // @TODO Probably a Mutation Observer would get this working. - await CoreUtils.wait(50); + await CoreWait.wait(50); retries--; } } @@ -1762,7 +1763,7 @@ export class CoreDomUtilsProvider { } // Wait a bit and try again. - await CoreUtils.wait(50); + await CoreWait.wait(50); return this.waitForResizeDone(windowWidth, windowHeight, retries+1); } diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index cc05c35ae..ea2b2e1f4 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -41,6 +41,7 @@ import { CoreUrlUtils } from './url'; import { QRScanner } from '@features/native/plugins'; import { CoreArray } from '@singletons/array'; import { CoreText } from '@singletons/text'; +import { CoreWait, CoreWaitOptions } from '@singletons/wait'; export type TreeNode = T & { children: TreeNode[] }; @@ -1783,10 +1784,10 @@ export class CoreUtilsProvider { * Wait some time. * * @param milliseconds Number of milliseconds to wait. - * @returns Promise resolved after the time has passed. + * @deprecated since 4.5. Use CoreWait.wait instead. */ - wait(milliseconds: number): Promise { - return new Promise(resolve => setTimeout(resolve, milliseconds)); + async wait(milliseconds: number): Promise { + await CoreWait.wait(milliseconds); } /** @@ -1794,51 +1795,34 @@ export class CoreUtilsProvider { * * @param condition Condition. * @returns Cancellable promise. + * @deprecated since 4.5. Use CoreWait.waitFor instead. */ waitFor(condition: () => boolean): CoreCancellablePromise; - waitFor(condition: () => boolean, options: CoreUtilsWaitOptions): CoreCancellablePromise; + waitFor(condition: () => boolean, options: CoreWaitOptions): CoreCancellablePromise; waitFor(condition: () => boolean, interval: number): CoreCancellablePromise; - waitFor(condition: () => boolean, optionsOrInterval: CoreUtilsWaitOptions | number = {}): CoreCancellablePromise { + waitFor(condition: () => boolean, optionsOrInterval: CoreWaitOptions | number = {}): CoreCancellablePromise { const options = typeof optionsOrInterval === 'number' ? { interval: optionsOrInterval } : optionsOrInterval; - if (condition()) { - return CoreCancellablePromise.resolve(); - } - - const startTime = Date.now(); - let intervalId: number | undefined; - - return new CoreCancellablePromise( - async (resolve) => { - intervalId = window.setInterval(() => { - if (!condition() && (!options.timeout || (Date.now() - startTime < options.timeout))) { - return; - } - - resolve(); - window.clearInterval(intervalId); - }, options.interval ?? 50); - }, - () => window.clearInterval(intervalId), - ); + return CoreWait.waitFor(condition, options); } /** * Wait until the next tick. * - * @returns Promise resolved when tick has been done. + * @deprecated since 4.5. Use CoreWait.nextTick instead. */ - nextTick(): Promise { - return this.wait(0); + async nextTick(): Promise { + await CoreWait.nextTick(); } /** * Wait until several next ticks. + * + * @param numTicks Number of ticks to wait. + * @deprecated since 4.5. Use CoreWait.nextTicks instead. */ async nextTicks(numTicks = 0): Promise { - for (let i = 0; i < numTicks; i++) { - await this.wait(0); - } + await CoreWait.nextTicks(numTicks); } /** @@ -1916,11 +1900,10 @@ export type CoreUtilsOpenInAppOptions = InAppBrowserOptions & { /** * Options for waiting. + * + * @deprecated since 4.5. Use CoreWaitOptions instead. */ -export type CoreUtilsWaitOptions = { - interval?: number; - timeout?: number; -}; +export type CoreUtilsWaitOptions = CoreWaitOptions; /** * Possible default picker actions. diff --git a/src/core/singletons/directives-registry.ts b/src/core/singletons/directives-registry.ts index 63642e12d..50fc996b6 100644 --- a/src/core/singletons/directives-registry.ts +++ b/src/core/singletons/directives-registry.ts @@ -14,7 +14,7 @@ import { Directive } from '@angular/core'; import { AsyncDirective } from '@classes/async-directive'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreWait } from './wait'; import { CoreLogger } from './logger'; /** @@ -135,7 +135,7 @@ export class CoreDirectivesRegistry { })); // Wait for next tick to ensure directives are completely rendered. - await CoreUtils.nextTick(); + await CoreWait.nextTick(); // Check if there are new elements now that the found elements are ready (there could be nested elements). if (elements.length !== findElements().length) { @@ -181,7 +181,7 @@ export class CoreDirectivesRegistry { })); // Wait for next tick to ensure directives are completely rendered. - await CoreUtils.nextTick(); + await CoreWait.nextTick(); // Check if there are new elements now that the found elements are ready (there could be nested elements). const elementsAfterReady = directives.reduce((elements, directive) => { diff --git a/src/core/singletons/subscriptions.ts b/src/core/singletons/subscriptions.ts index efae69236..b0cea166d 100644 --- a/src/core/singletons/subscriptions.ts +++ b/src/core/singletons/subscriptions.ts @@ -13,7 +13,7 @@ // limitations under the License. import { EventEmitter } from '@angular/core'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreWait } from './wait'; import { Observable, Subscription } from 'rxjs'; /** @@ -52,7 +52,7 @@ export class CoreSubscriptions { }; const unsubscribe = async () => { // Subscription variable might not be set because we can receive a value immediately. Wait for next tick. - await CoreUtils.nextTick(); + await CoreWait.nextTick(); subscription?.unsubscribe(); }; diff --git a/src/core/singletons/wait.ts b/src/core/singletons/wait.ts new file mode 100644 index 000000000..d2a633464 --- /dev/null +++ b/src/core/singletons/wait.ts @@ -0,0 +1,94 @@ +// (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 { CoreCancellablePromise } from '@classes/cancellable-promise'; + +/** + * Singleton with helper functions to wait. + */ +export class CoreWait { + + /** + * Wait until the next tick. + * + * @returns Promise resolved when tick has been done. + */ + static async nextTick(): Promise { + return CoreWait.wait(0); + } + + /** + * Wait until several next ticks. + * + * @param numTicks Number of ticks to wait. + */ + static async nextTicks(numTicks = 0): Promise { + for (let i = 0; i < numTicks; i++) { + await CoreWait.wait(0); + } + } + + /** + * Wait some time. + * + * @param milliseconds Number of milliseconds to wait. + * @returns Promise resolved after the time has passed. + */ + static wait(milliseconds: number): Promise { + return new Promise(resolve => setTimeout(resolve, milliseconds)); + } + + /** + * Wait until a given condition is met. + * + * @param condition Condition. + * @returns Cancellable promise. + */ + static waitFor(condition: () => boolean): CoreCancellablePromise; + static waitFor(condition: () => boolean, options: CoreWaitOptions): CoreCancellablePromise; + static waitFor(condition: () => boolean, interval: number): CoreCancellablePromise; + static waitFor(condition: () => boolean, optionsOrInterval: CoreWaitOptions | number = {}): CoreCancellablePromise { + const options = typeof optionsOrInterval === 'number' ? { interval: optionsOrInterval } : optionsOrInterval; + + if (condition()) { + return CoreCancellablePromise.resolve(); + } + + const startTime = Date.now(); + let intervalId: number | undefined; + + return new CoreCancellablePromise( + async (resolve) => { + intervalId = window.setInterval(() => { + if (!condition() && (!options.timeout || (Date.now() - startTime < options.timeout))) { + return; + } + + resolve(); + window.clearInterval(intervalId); + }, options.interval ?? 50); + }, + () => window.clearInterval(intervalId), + ); + } + +} + +/** + * Options for waiting. + */ +export type CoreWaitOptions = { + interval?: number; + timeout?: number; +}; diff --git a/src/testing/services/behat-blocking.ts b/src/testing/services/behat-blocking.ts index 716ed4438..b58407b0d 100644 --- a/src/testing/services/behat-blocking.ts +++ b/src/testing/services/behat-blocking.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreWait } from '@singletons/wait'; import { makeSingleton, NgZone } from '@singletons'; import { BehatTestsWindow, TestingBehatRuntime } from './behat-runtime'; @@ -117,7 +117,7 @@ export class TestingBehatBlockingService { // https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers // "This API does not guarantee that timers will run exactly on schedule. // Delays due to CPU load, other tasks, etc, are to be expected." - await CoreUtils.nextTicks(10); + await CoreWait.nextTicks(10); } // Check there isn't a spinner... @@ -193,7 +193,7 @@ export class TestingBehatBlockingService { * (and if not, removes it). */ protected async checkUIBlocked(): Promise { - await CoreUtils.nextTick(); + await CoreWait.nextTick(); const blockingElements = Array.from( document.querySelectorAll('div.core-loading-container, ion-loading'), diff --git a/src/testing/services/behat-dom.ts b/src/testing/services/behat-dom.ts index 7c6acb55e..d053e331e 100644 --- a/src/testing/services/behat-dom.ts +++ b/src/testing/services/behat-dom.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CorePromisedValue } from '@classes/promised-value'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreWait } from '@singletons/wait'; import { makeSingleton, NgZone } from '@singletons'; import { TestingBehatElementLocator, TestingBehatFindOptions } from './behat-runtime'; @@ -756,7 +756,7 @@ export class TestingBehatDomUtilsService { // Pretend we have cut and pasted the new text. if (element.tagName !== 'ION-SELECT' && getValue() !== '') { - await CoreUtils.nextTick(); + await CoreWait.nextTick(); await setValue(''); element.dispatchEvent(new InputEvent('input', { @@ -768,7 +768,7 @@ export class TestingBehatDomUtilsService { } if (value !== '') { - await CoreUtils.nextTick(); + await CoreWait.nextTick(); await setValue(value); element.dispatchEvent(new InputEvent('input', { From 5bd549477e28a675de42c6ec4176b7b5b74a340f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 23 Jul 2024 16:05:43 +0200 Subject: [PATCH 4/5] MOBILE-4616 jest: Remove storybook settings from jest --- jest.config.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jest.config.js b/jest.config.js index 75f177efd..caad70e17 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,10 +7,9 @@ module.exports = { testMatch: ['**/?(*.)test.ts'], collectCoverageFrom: [ 'src/**/*.ts', - '!src/**/*.{test,stories}.ts', + '!src/**/*.test.ts', '!src/assets/**/*', '!src/testing/**/*', - '!src/storybook/**/*', '!src/core/initializers/index.ts', '!src/core/features/emulators/services/zip.ts', ], From 1186694c5fe7ddd66642a94d504971e37bc7caa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 16 Jul 2024 13:55:35 +0200 Subject: [PATCH 5/5] MOBILE-4616 url: Migrate all CoreUrlUtils to CoreUrl static singleton --- .github/workflows/testing.yml | 3 +- .../tests/behat/behat_app_helper.php | 2 +- .../activitymodules/activitymodules.ts | 4 +- src/addons/blog/pages/index/index.ts | 4 +- .../calendar/components/calendar/calendar.ts | 4 +- .../upcoming-events/upcoming-events.ts | 4 +- src/addons/calendar/pages/day/day.ts | 4 +- src/addons/calendar/services/calendar.ts | 8 +- .../pages/competency/competency.page.ts | 6 +- .../services/handlers/push-click.ts | 4 +- .../services/handlers/displayh5p.ts | 4 +- .../filter/mediaplugin/services/videojs.ts | 4 +- .../assign/services/handlers/push-click.ts | 4 +- .../mod/book/pages/contents/contents.ts | 4 +- .../mod/book/services/handlers/tag-area.ts | 4 +- src/addons/mod/data/components/index/index.ts | 4 +- .../feedback/services/handlers/push-click.ts | 4 +- src/addons/mod/forum/pages/search/search.ts | 4 +- src/addons/mod/forum/services/forum.ts | 4 +- .../mod/forum/services/handlers/push-click.ts | 4 +- src/addons/mod/lesson/pages/player/player.ts | 4 +- src/addons/mod/lesson/services/lesson-sync.ts | 4 +- src/addons/mod/lti/services/lti.ts | 4 +- .../mod/quiz/services/handlers/push-click.ts | 4 +- src/addons/mod/scorm/services/scorm.ts | 4 +- .../mod/url/services/handlers/module.ts | 6 +- src/addons/notes/pages/list/list.ts | 6 +- src/core/classes/sites/authenticated-site.ts | 27 +- src/core/classes/sites/site.ts | 6 +- .../classes/sites/unauthenticated-site.ts | 24 +- src/core/components/file/file.ts | 6 +- src/core/components/iframe/iframe.ts | 9 +- src/core/components/mod-icon/mod-icon.ts | 8 +- .../components/user-avatar/user-avatar.ts | 4 +- src/core/directives/external-content.ts | 8 +- src/core/directives/link.ts | 7 +- .../services/contentlinks-delegate.ts | 5 +- .../course/classes/main-resource-component.ts | 4 +- .../features/course/services/course-helper.ts | 4 +- .../rich-text-editor/rich-text-editor.ts | 4 +- .../features/grades/services/grades-helper.ts | 4 +- src/core/features/h5p/classes/player.ts | 6 +- .../h5p/components/h5p-iframe/h5p-iframe.ts | 4 +- .../h5p/components/h5p-player/h5p-player.ts | 4 +- src/core/features/h5p/services/h5p.ts | 4 +- .../h5p/services/handlers/pluginfile.ts | 4 +- src/core/features/login/pages/site/site.ts | 11 +- .../features/login/services/login-helper.ts | 13 +- .../policy/pages/site-policy/site-policy.ts | 6 +- .../classes/base-question-component.ts | 4 +- .../question/services/question-helper.ts | 4 +- .../pages/global-search/global-search.ts | 4 +- .../siteplugins/services/siteplugins-init.ts | 4 +- src/core/features/tag/pages/index/index.ts | 4 +- src/core/features/user/pages/about/about.ts | 4 +- src/core/features/user/services/user.ts | 4 +- .../initializers/prepare-inapp-browser.ts | 6 +- src/core/services/analytics.ts | 4 +- src/core/services/file-helper.ts | 4 +- src/core/services/filepool.ts | 31 +- src/core/services/navigator.ts | 4 +- src/core/services/sites.ts | 20 +- src/core/services/tests/utils/url.test.ts | 90 --- src/core/services/urlschemes.ts | 8 +- src/core/services/utils/dom.ts | 12 +- src/core/services/utils/iframe.ts | 19 +- src/core/services/utils/url.ts | 418 ++---------- src/core/services/utils/utils.ts | 8 +- src/core/singletons/tests/url.test.ts | 116 +++- src/core/singletons/url.ts | 604 +++++++++++++++++- src/core/singletons/window.ts | 6 +- 71 files changed, 998 insertions(+), 667 deletions(-) delete mode 100644 src/core/services/tests/utils/url.test.ts diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index a176115c2..f44b2ae89 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -69,8 +69,7 @@ jobs: cat circular-dependencies lines=$(cat circular-dependencies | wc -l) echo "Total circular dependencies: $lines" - test $lines -ge 138 - test $lines -le 148 + test $lines -eq 135 - name: JavaScript code compatibility run: | npx check-es-compat www/*.js --polyfills="\{Array,String,TypedArray\}.prototype.at,Object.hasOwn" diff --git a/local_moodleappbehat/tests/behat/behat_app_helper.php b/local_moodleappbehat/tests/behat/behat_app_helper.php index 6cf42e881..45dc62741 100644 --- a/local_moodleappbehat/tests/behat/behat_app_helper.php +++ b/local_moodleappbehat/tests/behat/behat_app_helper.php @@ -454,7 +454,7 @@ class behat_app_helper extends behat_base { $result = $this->zone_js("customUrlSchemes.handleCustomURL('$customurl')"); if ($result !== 'OK') { - throw new DriverException('Error handling url - ' . $result); + throw new DriverException('Error handling url - ' . $customurl . ' - '.$result); } if (!empty($successXPath)) { // Wait until the page appears. diff --git a/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts b/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts index 6bd07f093..a8c52eb53 100644 --- a/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts +++ b/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts @@ -22,7 +22,7 @@ import { Translate } from '@singletons'; import { CoreUtils } from '@services/utils/utils'; import { CoreNavigator } from '@services/navigator'; import { CoreCourseHelper } from '@features/course/services/course-helper'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreSharedModule } from '@/core/shared.module'; /** @@ -103,7 +103,7 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i brandedIcons[mod.modname] = mod.branded; // If this is not a theme image, leave it undefined to avoid having specific activity icons. - if (CoreUrlUtils.isThemeImageUrl(mod.modicon)) { + if (CoreUrl.isThemeImageUrl(mod.modicon)) { modIcons[mod.modname] = mod.modicon; } }); diff --git a/src/addons/blog/pages/index/index.ts b/src/addons/blog/pages/index/index.ts index 63e08dd3f..430e425b6 100644 --- a/src/addons/blog/pages/index/index.ts +++ b/src/addons/blog/pages/index/index.ts @@ -26,7 +26,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreArray } from '@singletons/array'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -79,7 +79,7 @@ export class AddonBlogIndexPage implements OnInit, OnDestroy { ...this.filter, category: 'blog', }, - url: CoreUrlUtils.addParamsToUrl('/blog/index.php', { + url: CoreUrl.addParamsToUrl('/blog/index.php', { ...this.filter, modid: this.filter.cmid, cmid: undefined, diff --git a/src/addons/calendar/components/calendar/calendar.ts b/src/addons/calendar/components/calendar/calendar.ts index 6fac088f5..e2df77771 100644 --- a/src/addons/calendar/components/calendar/calendar.ts +++ b/src/addons/calendar/components/calendar/calendar.ts @@ -50,7 +50,7 @@ import { import { CoreSwipeSlidesDynamicItemsManager } from '@classes/items-management/swipe-slides-dynamic-items-manager'; import moment from 'moment-timezone'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreTime } from '@singletons/time'; import { Translate } from '@singletons'; @@ -132,7 +132,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro ...params, category: 'calendar', }, - url: CoreUrlUtils.addParamsToUrl('/calendar/view.php?view=month', params), + url: CoreUrl.addParamsToUrl('/calendar/view.php?view=month', params), }); }); } diff --git a/src/addons/calendar/components/upcoming-events/upcoming-events.ts b/src/addons/calendar/components/upcoming-events/upcoming-events.ts index 862712285..753a38b55 100644 --- a/src/addons/calendar/components/upcoming-events/upcoming-events.ts +++ b/src/addons/calendar/components/upcoming-events/upcoming-events.ts @@ -26,7 +26,7 @@ import { AddonCalendarOffline } from '../../services/calendar-offline'; import { CoreCategoryData, CoreCourses } from '@features/courses/services/courses'; import { CoreConstants } from '@/core/constants'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreTime } from '@singletons/time'; import { Translate } from '@singletons'; @@ -103,7 +103,7 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On ...params, category: 'calendar', }, - url: CoreUrlUtils.addParamsToUrl('/calendar/view.php?view=upcoming', params), + url: CoreUrl.addParamsToUrl('/calendar/view.php?view=upcoming', params), }); }); } diff --git a/src/addons/calendar/pages/day/day.ts b/src/addons/calendar/pages/day/day.ts index 2a57565f9..1edf50164 100644 --- a/src/addons/calendar/pages/day/day.ts +++ b/src/addons/calendar/pages/day/day.ts @@ -47,7 +47,7 @@ import { import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { AddonCalendarEventsSource } from '@addons/calendar/classes/events-source'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreTime } from '@singletons/time'; /** @@ -201,7 +201,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { ...params, category: 'calendar', }, - url: CoreUrlUtils.addParamsToUrl('/calendar/view.php?view=day', params), + url: CoreUrl.addParamsToUrl('/calendar/view.php?view=day', params), }); }); } diff --git a/src/addons/calendar/services/calendar.ts b/src/addons/calendar/services/calendar.ts index db0bc8437..71817798b 100644 --- a/src/addons/calendar/services/calendar.ts +++ b/src/addons/calendar/services/calendar.ts @@ -18,7 +18,7 @@ import { CoreSite } from '@classes/sites/site'; import { CoreNetwork } from '@services/network'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreGroups } from '@services/groups'; import { CoreLocalNotifications } from '@services/local-notifications'; @@ -362,14 +362,14 @@ export class AddonCalendarProvider { // Add links to the days if needed. if (dayStart && (!seenDay || !moment(seenDay).isSame(start, 'day'))) { promises.push(this.getViewUrl('day', event.timestart, undefined, siteId).then((url) => { - dayStart = CoreUrlUtils.buildLink(url, dayStart); + dayStart = CoreUrl.buildLink(url, dayStart); return; })); } if (dayEnd && (!seenDay || !moment(seenDay).isSame(end, 'day'))) { promises.push(this.getViewUrl('day', end / 1000, undefined, siteId).then((url) => { - dayEnd = CoreUrlUtils.buildLink(url, dayEnd); + dayEnd = CoreUrl.buildLink(url, dayEnd); return; })); @@ -398,7 +398,7 @@ export class AddonCalendarProvider { // Add link to view the day. const url = await this.getViewUrl('day', event.timestart, undefined, siteId); - return CoreUrlUtils.buildLink(url, this.getDayRepresentation(start, useCommonWords)) + ', ' + time; + return CoreUrl.buildLink(url, this.getDayRepresentation(start, useCommonWords)) + ', ' + time; } /** diff --git a/src/addons/competency/pages/competency/competency.page.ts b/src/addons/competency/pages/competency/competency.page.ts index 615a146c2..cb94fde05 100644 --- a/src/addons/competency/pages/competency/competency.page.ts +++ b/src/addons/competency/pages/competency/competency.page.ts @@ -40,7 +40,7 @@ import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; import { AddonCompetencyCourseCompetenciesSource } from '@addons/competency/classes/competency-course-competencies-source'; import { CoreTime } from '@singletons/time'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; /** * Page that displays the competency information. @@ -306,7 +306,7 @@ export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy { planstatus: this.planStatus, userid: userId, }, - url: CoreUrlUtils.addParamsToUrl('/admin/tool/lp/user_competency_in_plan.php', { + url: CoreUrl.addParamsToUrl('/admin/tool/lp/user_competency_in_plan.php', { planid: source.PLAN_ID, userid: userId, competencyid: compId, @@ -328,7 +328,7 @@ export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy { courseid: source.COURSE_ID, userid: userId, }, - url: CoreUrlUtils.addParamsToUrl('/admin/tool/lp/user_competency_in_course.php', { + url: CoreUrl.addParamsToUrl('/admin/tool/lp/user_competency_in_course.php', { courseid: source.COURSE_ID, competencyid: compId, userid: userId, diff --git a/src/addons/competency/services/handlers/push-click.ts b/src/addons/competency/services/handlers/push-click.ts index b28423ebf..63e4b1a50 100644 --- a/src/addons/competency/services/handlers/push-click.ts +++ b/src/addons/competency/services/handlers/push-click.ts @@ -18,7 +18,7 @@ import { COURSE_PAGE_NAME } from '@features/course/constants'; import { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate'; import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications'; import { CoreNavigator } from '@services/navigator'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; import { AddonCompetency } from '../competency'; @@ -49,7 +49,7 @@ export class AddonCompetencyPushClickHandlerService implements CorePushNotificat * @inheritdoc */ async handleClick(notification: AddonCompetencyPushNotificationData): Promise { - const contextUrlParams = CoreUrlUtils.extractUrlParams(notification.contexturl); + const contextUrlParams = CoreUrl.extractUrlParams(notification.contexturl); if (notification.name == 'competencyplancomment') { // Open the learning plan. diff --git a/src/addons/filter/displayh5p/services/handlers/displayh5p.ts b/src/addons/filter/displayh5p/services/handlers/displayh5p.ts index 3949f2b15..e71a429fa 100644 --- a/src/addons/filter/displayh5p/services/handlers/displayh5p.ts +++ b/src/addons/filter/displayh5p/services/handlers/displayh5p.ts @@ -18,7 +18,7 @@ import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/def import { CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter'; import { makeSingleton } from '@singletons'; import { CoreH5PPlayerComponent } from '@features/h5p/components/h5p-player/h5p-player'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreH5PHelper } from '@features/h5p/classes/helper'; /** @@ -57,7 +57,7 @@ export class AddonFilterDisplayH5PHandlerService extends CoreFilterDefaultHandle embeddedH5PIframes.forEach((iframe) => { // Add the preventredirect param to allow authenticating if auto-login fails. - iframe.src = CoreUrlUtils.addParamsToUrl(iframe.src, { preventredirect: false }); + iframe.src = CoreUrl.addParamsToUrl(iframe.src, { preventredirect: false }); // Add resizer script so the H5P has the right height. CoreH5PHelper.addResizerScript(); diff --git a/src/addons/filter/mediaplugin/services/videojs.ts b/src/addons/filter/mediaplugin/services/videojs.ts index 2b5ea07e7..a51932900 100644 --- a/src/addons/filter/mediaplugin/services/videojs.ts +++ b/src/addons/filter/mediaplugin/services/videojs.ts @@ -17,7 +17,7 @@ import { CorePromisedValue } from '@classes/promised-value'; import { CoreExternalContentDirective } from '@directives/external-content'; import { CoreLang } from '@services/lang'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { makeSingleton } from '@singletons'; import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreEvents } from '@singletons/events'; @@ -107,7 +107,7 @@ export class AddonFilterMediaPluginVideoJSService { const dataSetupString = video.getAttribute('data-setup') || video.getAttribute('data-setup-lazy') || '{}'; const data = CoreTextUtils.parseJSON(dataSetupString, {}); - const youtubeUrl = data.techOrder?.[0] == 'youtube' && CoreUrlUtils.getYoutubeEmbedUrl(data.sources?.[0]?.src); + const youtubeUrl = data.techOrder?.[0] == 'youtube' && CoreUrl.getYoutubeEmbedUrl(data.sources?.[0]?.src); if (!youtubeUrl) { return; diff --git a/src/addons/mod/assign/services/handlers/push-click.ts b/src/addons/mod/assign/services/handlers/push-click.ts index af0d797e9..9388eb4d2 100644 --- a/src/addons/mod/assign/services/handlers/push-click.ts +++ b/src/addons/mod/assign/services/handlers/push-click.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreCourseHelper } from '@features/course/services/course-helper'; import { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate'; import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; import { AddonModAssign } from '../assign'; @@ -50,7 +50,7 @@ export class AddonModAssignPushClickHandlerService implements CorePushNotificati * @returns Promise resolved when done. */ async handleClick(notification: NotificationData): Promise { - const contextUrlParams = CoreUrlUtils.extractUrlParams(notification.contexturl); + const contextUrlParams = CoreUrl.extractUrlParams(notification.contexturl); const courseId = Number(notification.courseid); const moduleId = Number(contextUrlParams.id); diff --git a/src/addons/mod/book/pages/contents/contents.ts b/src/addons/mod/book/pages/contents/contents.ts index 653974be2..77b1f47ac 100644 --- a/src/addons/mod/book/pages/contents/contents.ts +++ b/src/addons/mod/book/pages/contents/contents.ts @@ -38,7 +38,7 @@ import { AddonModBookTocChapter, } from '../../services/book'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { ADDON_MOD_BOOK_COMPONENT, AddonModBookNavStyle } from '../../constants'; /** @@ -293,7 +293,7 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy { ws: 'mod_book_view_book', name: this.module.name, data: { id: this.module.instance, category: 'book', chapterid: chapterId }, - url: CoreUrlUtils.addParamsToUrl(`/mod/book/view.php?id=${this.module.id}`, { chapterid: chapterId }), + url: CoreUrl.addParamsToUrl(`/mod/book/view.php?id=${this.module.id}`, { chapterid: chapterId }), }); const currentChapterIndex = this.chapters.findIndex((chapter) => chapter.id == chapterId); diff --git a/src/addons/mod/book/services/handlers/tag-area.ts b/src/addons/mod/book/services/handlers/tag-area.ts index fd6da7c45..5bcc9c054 100644 --- a/src/addons/mod/book/services/handlers/tag-area.ts +++ b/src/addons/mod/book/services/handlers/tag-area.ts @@ -18,7 +18,7 @@ import { CoreTagFeedComponent } from '@features/tag/components/feed/feed'; import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate'; import { CoreTagFeedElement, CoreTagHelper } from '@features/tag/services/tag-helper'; import { CoreSitesReadingStrategy } from '@services/sites'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { makeSingleton } from '@singletons'; import { AddonModBook } from '../book'; @@ -51,7 +51,7 @@ export class AddonModBookTagAreaHandlerService implements CoreTagAreaHandler { // Find module ids of the returned books, they are needed by the link delegate. await Promise.all(items.map(async (item) => { - const params = item.url ? CoreUrlUtils.extractUrlParams(item.url) : {}; + const params = item.url ? CoreUrl.extractUrlParams(item.url) : {}; if (params.b && !params.id) { const bookId = parseInt(params.b, 10); diff --git a/src/addons/mod/data/components/index/index.ts b/src/addons/mod/data/components/index/index.ts index aaf630e75..c8c029718 100644 --- a/src/addons/mod/data/components/index/index.ts +++ b/src/addons/mod/data/components/index/index.ts @@ -40,7 +40,7 @@ import { AddonModDataHelper, AddonModDatDisplayFieldsOptions } from '../../servi import { AddonModDataAutoSyncData, AddonModDataSyncResult } from '../../services/data-sync'; import { AddonModDataPrefetchHandler } from '../../services/handlers/prefetch-lazy'; import { AddonModDataComponentsCompileModule } from '../components-compile.module'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreTime } from '@singletons/time'; import { ADDON_MOD_DATA_AUTO_SYNCED, @@ -568,7 +568,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp this.analyticsLogEvent('mod_data_search_entries', { data: params, - url: CoreUrlUtils.addParamsToUrl(`/mod/data/view.php?d=${this.database.id}`, params), + url: CoreUrl.addParamsToUrl(`/mod/data/view.php?d=${this.database.id}`, params), }); } diff --git a/src/addons/mod/feedback/services/handlers/push-click.ts b/src/addons/mod/feedback/services/handlers/push-click.ts index 02ad4aedb..d6ae6a840 100644 --- a/src/addons/mod/feedback/services/handlers/push-click.ts +++ b/src/addons/mod/feedback/services/handlers/push-click.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreCourseHelper } from '@features/course/services/course-helper'; import { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate'; import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; import { AddonModFeedbackHelper } from '../feedback-helper'; @@ -48,7 +48,7 @@ export class AddonModFeedbackPushClickHandlerService implements CorePushNotifica * @inheritdoc */ handleClick(notification: AddonModFeedbackPushNotificationData): Promise { - const contextUrlParams = CoreUrlUtils.extractUrlParams(notification.contexturl!); + const contextUrlParams = CoreUrl.extractUrlParams(notification.contexturl!); const courseId = Number(notification.courseid); const moduleId = Number(contextUrlParams.id); diff --git a/src/addons/mod/forum/pages/search/search.ts b/src/addons/mod/forum/pages/search/search.ts index b60bd9ec6..89c701de2 100644 --- a/src/addons/mod/forum/pages/search/search.ts +++ b/src/addons/mod/forum/pages/search/search.ts @@ -27,7 +27,7 @@ import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; @@ -116,7 +116,7 @@ export class AddonModForumSearchPage implements OnInit { query, filters: JSON.stringify(this.resultsSource.getFilters()), }, - url: CoreUrlUtils.addParamsToUrl('/search/index.php', { + url: CoreUrl.addParamsToUrl('/search/index.php', { q: query, }), }); diff --git a/src/addons/mod/forum/services/forum.ts b/src/addons/mod/forum/services/forum.ts index 21ad741b5..3cec9605b 100644 --- a/src/addons/mod/forum/services/forum.ts +++ b/src/addons/mod/forum/services/forum.ts @@ -24,7 +24,7 @@ import { CoreNetwork } from '@services/network'; import { CoreFileEntry } from '@services/file-helper'; import { CoreGroups } from '@services/groups'; import { CoreSitesCommonWSOptions, CoreSites, CoreSitesReadingStrategy } from '@services/sites'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning, CoreWSStoredFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; @@ -1312,7 +1312,7 @@ export class AddonModForumProvider { protected translateWSPost(post: AddonModForumWSPost): AddonModForumPost { (post as unknown as AddonModForumPost).tags = (post.tags || []).map((tag) => { const viewUrl = (tag.urls && tag.urls.view) || ''; - const params = CoreUrlUtils.extractUrlParams(viewUrl); + const params = CoreUrl.extractUrlParams(viewUrl); return { id: tag.tagid, diff --git a/src/addons/mod/forum/services/handlers/push-click.ts b/src/addons/mod/forum/services/handlers/push-click.ts index 34f9a9662..1b17316fc 100644 --- a/src/addons/mod/forum/services/handlers/push-click.ts +++ b/src/addons/mod/forum/services/handlers/push-click.ts @@ -19,7 +19,7 @@ import { AddonModForum } from '@addons/mod/forum/services/forum'; import { CoreNavigator } from '@services/navigator'; import { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate'; import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; @@ -56,7 +56,7 @@ export class AddonModForumPushClickHandlerService implements CorePushNotificatio * @returns Promise resolved when done. */ async handleClick(notification: NotificationData): Promise { - const contextUrlParams = CoreUrlUtils.extractUrlParams(notification.contexturl); + const contextUrlParams = CoreUrl.extractUrlParams(notification.contexturl); const data = notification.customdata || {}; const courseId = Number(notification.courseid); const discussionId = Number(contextUrlParams.d || data.discussionid); diff --git a/src/addons/mod/lesson/pages/player/player.ts b/src/addons/mod/lesson/pages/player/player.ts index a17c77825..6c8228e11 100644 --- a/src/addons/mod/lesson/pages/player/player.ts +++ b/src/addons/mod/lesson/pages/player/player.ts @@ -23,7 +23,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSync } from '@services/sync'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile } from '@services/ws'; import { ModalController, Translate } from '@singletons'; @@ -439,7 +439,7 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave { // Format review lesson if present. if (this.eolData.reviewlesson) { - const params = CoreUrlUtils.extractUrlParams( this.eolData.reviewlesson.value); + const params = CoreUrl.extractUrlParams( this.eolData.reviewlesson.value); if (!params || !params.pageid) { // No pageid in the URL, the user cannot review (probably didn't answer any question). diff --git a/src/addons/mod/lesson/services/lesson-sync.ts b/src/addons/mod/lesson/services/lesson-sync.ts index 22da5ab5a..04db999a1 100644 --- a/src/addons/mod/lesson/services/lesson-sync.ts +++ b/src/addons/mod/lesson/services/lesson-sync.ts @@ -23,7 +23,7 @@ import { CoreNetwork } from '@services/network'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSync, CoreSyncResult } from '@services/sync'; import { CoreTimeUtils } from '@services/utils/time'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton, Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; @@ -463,7 +463,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid // Mark the retake as finished in a sync if it can be reviewed. if (!ignoreBlock && response.data?.reviewlesson) { - const params = CoreUrlUtils.extractUrlParams( response.data.reviewlesson.value); + const params = CoreUrl.extractUrlParams( response.data.reviewlesson.value); if (params.pageid) { // The retake can be reviewed, mark it as finished. Don't block the user for this. this.setRetakeFinishedInSync(lessonId, retake.retake, Number(params.pageid), siteId); diff --git a/src/addons/mod/lti/services/lti.ts b/src/addons/mod/lti/services/lti.ts index ab3ca0547..f88d4c3b6 100644 --- a/src/addons/mod/lti/services/lti.ts +++ b/src/addons/mod/lti/services/lti.ts @@ -22,7 +22,7 @@ import { CoreFile } from '@services/file'; import { CorePlatform } from '@services/platform'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; @@ -244,7 +244,7 @@ export class AddonModLtiProvider { * @returns Promise resolved when the WS call is successful. */ async launch(url: string, params: AddonModLtiParam[]): Promise { - if (!CoreUrlUtils.isHttpURL(url)) { + if (!CoreUrl.isHttpURL(url)) { throw Translate.instant('addon.mod_lti.errorinvalidlaunchurl'); } diff --git a/src/addons/mod/quiz/services/handlers/push-click.ts b/src/addons/mod/quiz/services/handlers/push-click.ts index 3e196f289..22e7e10e4 100644 --- a/src/addons/mod/quiz/services/handlers/push-click.ts +++ b/src/addons/mod/quiz/services/handlers/push-click.ts @@ -17,7 +17,7 @@ import { Injectable } from '@angular/core'; import { CoreCourseHelper } from '@features/course/services/course-helper'; import { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate'; import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; import { AddonModQuiz } from '../quiz'; @@ -56,7 +56,7 @@ export class AddonModQuizPushClickHandlerService implements CorePushNotification * @returns Promise resolved when done. */ async handleClick(notification: AddonModQuizPushNotificationData): Promise { - const contextUrlParams = CoreUrlUtils.extractUrlParams(notification.contexturl || ''); + const contextUrlParams = CoreUrl.extractUrlParams(notification.contexturl || ''); const data = notification.customdata || {}; const courseId = Number(notification.courseid); diff --git a/src/addons/mod/scorm/services/scorm.ts b/src/addons/mod/scorm/services/scorm.ts index 927b59528..f0025d62e 100644 --- a/src/addons/mod/scorm/services/scorm.ts +++ b/src/addons/mod/scorm/services/scorm.ts @@ -23,7 +23,7 @@ import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@ import { CoreSync } from '@services/sync'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreWS, CoreWSExternalFile, CoreWSExternalWarning, CoreWSFile, CoreWSPreSets } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; @@ -1328,7 +1328,7 @@ export class AddonModScormProvider { protected isExternalLink(link: string): boolean { link = link.toLowerCase(); - if (link.match(/^https?:\/\//i) && !CoreUrlUtils.isLocalFileUrl(link)) { + if (link.match(/^https?:\/\//i) && !CoreUrl.isLocalFileUrl(link)) { return true; } else if (link.substring(0, 4) == 'www.') { return true; diff --git a/src/addons/mod/url/services/handlers/module.ts b/src/addons/mod/url/services/handlers/module.ts index 88c8bba52..cf4f0eae0 100644 --- a/src/addons/mod/url/services/handlers/module.ts +++ b/src/addons/mod/url/services/handlers/module.ts @@ -26,7 +26,7 @@ import { makeSingleton } from '@singletons'; import { AddonModUrl } from '../url'; import { AddonModUrlHelper } from '../url-helper'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { ADDON_MOD_URL_ADDON_NAME, ADDON_MOD_URL_MODNAME, ADDON_MOD_URL_PAGE_NAME } from '../../constants'; @@ -122,14 +122,14 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple return modIcon; } - const component = CoreUrlUtils.getThemeImageUrlParam(module.modicon, 'component'); + const component = CoreUrl.getThemeImageUrlParam(module.modicon, 'component'); if (component === this.modName) { return modIcon; } let icon: string | undefined; - let image = CoreUrlUtils.getThemeImageUrlParam(module.modicon, 'image'); + let image = CoreUrl.getThemeImageUrlParam(module.modicon, 'image'); if (image.startsWith('f/')) { // Remove prefix, and hyphen + numbered suffix. image = image.substring(2).replace(/-[0-9]+$/, ''); diff --git a/src/addons/notes/pages/list/list.ts b/src/addons/notes/pages/list/list.ts index 0976982c3..b31a6ac17 100644 --- a/src/addons/notes/pages/list/list.ts +++ b/src/addons/notes/pages/list/list.ts @@ -26,7 +26,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -313,7 +313,7 @@ export class AddonNotesListPage implements OnInit, OnDestroy { ws: 'core_notes_view_notes', name: Translate.instant('addon.notes.notes'), data: { courseid: this.courseId, userid: this.userId || 0, category: 'notes' }, - url: CoreUrlUtils.addParamsToUrl('/notes/index.php', { + url: CoreUrl.addParamsToUrl('/notes/index.php', { user: this.userId, course: this.courseId !== CoreSites.getCurrentSiteHomeId() ? this.courseId : undefined, }), @@ -329,7 +329,7 @@ export class AddonNotesListPage implements OnInit, OnDestroy { ws: 'core_notes_create_notes', name: Translate.instant('addon.notes.notes'), data: { courseid: this.courseId, userid: this.userId || 0, category: 'notes' }, - url: CoreUrlUtils.addParamsToUrl('/notes/edit.php', { + url: CoreUrl.addParamsToUrl('/notes/edit.php', { courseid: this.courseId, userid: this.userId, publishstate: this.type === 'personal' ? 'draft' : (this.type === 'course' ? 'public' : 'site'), diff --git a/src/core/classes/sites/authenticated-site.ts b/src/core/classes/sites/authenticated-site.ts index 25e98e451..20be3c9fe 100644 --- a/src/core/classes/sites/authenticated-site.ts +++ b/src/core/classes/sites/authenticated-site.ts @@ -39,7 +39,6 @@ import { CoreSiteError } from '@classes/errors/siteerror'; import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/support/authenticated-support-config'; import { CoreSiteInfo, CoreSiteInfoResponse, CoreSitePublicConfigResponse, CoreUnauthenticatedSite } from './unauthenticated-site'; import { Md5 } from 'ts-md5'; -import { CoreUrlUtils } from '@services/utils/url'; import { CoreSiteWSCacheRecord } from '@services/database/sites'; import { CoreErrorLogs } from '@singletons/error-logs'; import { CoreWait } from '@singletons/wait'; @@ -1268,11 +1267,33 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite { * * @param page Docs page to go to. * @returns Promise resolved with the Moodle docs URL. + * + * @deprecated since 4.5. Not needed anymore. */ - getDocsUrl(page?: string): Promise { + async getDocsUrl(page?: string): Promise { const release = this.infos?.release ? this.infos.release : undefined; + let docsUrl = 'https://docs.moodle.org/en/' + page; - return CoreUrlUtils.getDocsUrl(release, page); + if (release !== undefined) { + // Remove this part of the function if this file only uses CoreSites here. + const version = CoreSites.getMajorReleaseNumber(release).replace('.', ''); + + // Check is a valid number. + if (Number(version) >= 24) { + // Append release number. + docsUrl = docsUrl.replace('https://docs.moodle.org/', 'https://docs.moodle.org/' + version + '/'); + } + } + + try { + // Remove this part of the function if this file only uses CoreLang here. + let lang = CoreLang.getCurrentLanguageSync(CoreLangFormat.LMS); + lang = CoreLang.getParentLanguage() || lang; + + return docsUrl.replace('/en/', '/' + lang + '/'); + } catch { + return docsUrl; + } } /** diff --git a/src/core/classes/sites/site.ts b/src/core/classes/sites/site.ts index 80e6c07e3..f0bd5fe73 100644 --- a/src/core/classes/sites/site.ts +++ b/src/core/classes/sites/site.ts @@ -27,7 +27,7 @@ import { import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils, CoreUtilsOpenInBrowserOptions } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { SQLiteDB } from '@classes/sqlitedb'; @@ -380,7 +380,7 @@ export class CoreSite extends CoreAuthenticatedSite { const accessKey = this.tokenPluginFileWorks || this.tokenPluginFileWorks === undefined ? this.infos && this.infos.userprivateaccesskey : undefined; - return CoreUrlUtils.fixPluginfileURL(url, this.token || '', this.siteUrl, accessKey); + return CoreUrl.fixPluginfileURL(url, this.token || '', this.siteUrl, accessKey); } /** @@ -775,7 +775,7 @@ export class CoreSite extends CoreAuthenticatedSite { * @returns Promise resolved with boolean: whether it works or not. */ checkTokenPluginFile(url: string): Promise { - if (!CoreUrlUtils.canUseTokenPluginFile(url, this.siteUrl, this.infos && this.infos.userprivateaccesskey)) { + if (!CoreUrl.canUseTokenPluginFile(url, this.siteUrl, this.infos && this.infos.userprivateaccesskey)) { // Cannot use tokenpluginfile. return Promise.resolve(false); } else if (this.tokenPluginFileWorks !== undefined) { diff --git a/src/core/classes/sites/unauthenticated-site.ts b/src/core/classes/sites/unauthenticated-site.ts index 0f999406b..68ef3f975 100644 --- a/src/core/classes/sites/unauthenticated-site.ts +++ b/src/core/classes/sites/unauthenticated-site.ts @@ -17,7 +17,7 @@ import { CoreError } from '@classes/errors/error'; import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreSitesReadingStrategy } from '@services/sites'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreWS, CoreWSAjaxPreSets, CoreWSExternalWarning } from '@services/ws'; import { CorePath } from '@singletons/path'; @@ -37,7 +37,10 @@ export class CoreUnauthenticatedSite { * @param publicConfig Site public config. */ constructor(siteUrl: string, publicConfig?: CoreSitePublicConfigResponse) { - this.siteUrl = CoreUrlUtils.removeUrlParams(siteUrl); // Make sure the URL doesn't have params. + this.siteUrl = CoreUrl.removeUrlParts( + siteUrl, + [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment], + ); // Make sure the URL doesn't have params. if (publicConfig) { this.setPublicConfig(publicConfig); } @@ -143,7 +146,7 @@ export class CoreUnauthenticatedSite { * @returns URL with params. */ createSiteUrl(path: string, params?: Record, anchor?: string): string { - return CoreUrlUtils.addParamsToUrl(CorePath.concatenatePaths(this.siteUrl, path), params, anchor); + return CoreUrl.addParamsToUrl(CorePath.concatenatePaths(this.siteUrl, path), params, anchor); } /** @@ -157,8 +160,10 @@ export class CoreUnauthenticatedSite { return false; } - const siteUrl = CoreTextUtils.addEndingSlash(CoreUrlUtils.removeProtocolAndWWW(this.siteUrl)); - url = CoreTextUtils.addEndingSlash(CoreUrlUtils.removeProtocolAndWWW(url)); + const siteUrl = CoreTextUtils.addEndingSlash( + CoreUrl.removeUrlParts(this.siteUrl, [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain]), + ); + url = CoreTextUtils.addEndingSlash(CoreUrl.removeUrlParts(url, [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain])); return url.indexOf(siteUrl) == 0; } @@ -244,7 +249,10 @@ export class CoreUnauthenticatedSite { // Use the wwwroot returned by the server. if (config.httpswwwroot) { - this.siteUrl = CoreUrlUtils.removeUrlParams(config.httpswwwroot); // Make sure the URL doesn't have params. + this.siteUrl = CoreUrl.removeUrlParts( + config.httpswwwroot, + [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment], + ); // Make sure the URL doesn't have params. } return config; @@ -268,7 +276,7 @@ export class CoreUnauthenticatedSite { * @returns Whether it's a site file URL. */ isSitePluginFileUrl(url: string): boolean { - const isPluginFileUrl = CoreUrlUtils.isPluginFileUrl(url) || CoreUrlUtils.isTokenPluginFileUrl(url); + const isPluginFileUrl = CoreUrl.isPluginFileUrl(url) || CoreUrl.isTokenPluginFileUrl(url); if (!isPluginFileUrl) { return false; } @@ -283,7 +291,7 @@ export class CoreUnauthenticatedSite { * @returns Whether it's a site theme image URL. */ isSiteThemeImageUrl(url: string): boolean { - if (!CoreUrlUtils.isThemeImageUrl(url)) { + if (!CoreUrl.isThemeImageUrl(url)) { return false; } diff --git a/src/core/components/file/file.ts b/src/core/components/file/file.ts index 46dc1917d..a525a5ec2 100644 --- a/src/core/components/file/file.ts +++ b/src/core/components/file/file.ts @@ -20,7 +20,7 @@ import { CorePluginFileDelegate } from '@services/plugin-file-delegate'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils, CoreUtilsOpenFileOptions, OpenFileAction } from '@services/utils/utils'; import { CoreTextUtils } from '@services/utils/text'; import { DownloadStatus } from '@/core/constants'; @@ -195,10 +195,10 @@ export class CoreFileComponent implements OnInit, OnDestroy { if (!this.canDownload || !this.state || this.state === DownloadStatus.NOT_DOWNLOADABLE) { // File cannot be downloaded, just open it. - if (CoreUrlUtils.isLocalFileUrl(this.fileUrl)) { + if (CoreUrl.isLocalFileUrl(this.fileUrl)) { CoreUtils.openFile(this.fileUrl); } else { - CoreUtils.openOnlineFile(CoreUrlUtils.unfixPluginfileURL(this.fileUrl)); + CoreUtils.openOnlineFile(CoreUrl.unfixPluginfileURL(this.fileUrl)); } return; diff --git a/src/core/components/iframe/iframe.ts b/src/core/components/iframe/iframe.ts index 32b7fc156..5012f9128 100644 --- a/src/core/components/iframe/iframe.ts +++ b/src/core/components/iframe/iframe.ts @@ -19,7 +19,7 @@ import { SafeResourceUrl } from '@angular/platform-browser'; import { CoreFile } from '@services/file'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreIframeUtils } from '@services/utils/iframe'; import { CoreUtils } from '@services/utils/utils'; import { DomSanitizer, Router, StatusBar } from '@singletons'; @@ -29,7 +29,6 @@ import { Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; import { NavigationStart } from '@angular/router'; import { CoreSites } from '@services/sites'; -import { CoreUrl } from '@singletons/url'; @Component({ selector: 'core-iframe', @@ -118,7 +117,7 @@ export class CoreIframeComponent implements OnChanges, OnDestroy { } // Show loading only with external URLs. - this.loading = !this.src || !CoreUrlUtils.isLocalFileUrl(this.src); + this.loading = !this.src || !CoreUrl.isLocalFileUrl(this.src); if (this.loading) { setTimeout(() => { @@ -197,8 +196,8 @@ export class CoreIframeComponent implements OnChanges, OnDestroy { this.launchExternalLabel = undefined; - if (url && !CoreUrlUtils.isLocalFileUrl(url)) { - url = CoreUrlUtils.getYoutubeEmbedUrl(url) || url; + if (url && !CoreUrl.isLocalFileUrl(url)) { + url = CoreUrl.getYoutubeEmbedUrl(url) || url; this.displayHelp = CoreIframeUtils.shouldDisplayHelpForUrl(url); const currentSite = CoreSites.getCurrentSite(); diff --git a/src/core/components/mod-icon/mod-icon.ts b/src/core/components/mod-icon/mod-icon.ts index 0f693cdc7..7e1a26cb8 100644 --- a/src/core/components/mod-icon/mod-icon.ts +++ b/src/core/components/mod-icon/mod-icon.ts @@ -28,7 +28,7 @@ import { CoreCourse } from '@features/course/services/course'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; import { CoreSites } from '@services/sites'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; const assetsPath = 'assets/img/'; const fallbackModName = 'external-tool'; @@ -155,8 +155,8 @@ export class CoreModIconComponent implements OnInit, OnChanges { } // If it's an Moodle Theme icon, check if filtericon is set and use it. - if (CoreUrlUtils.isThemeImageUrl(this.iconUrl())) { - const filter = CoreUrlUtils.getThemeImageUrlParam(this.iconUrl(), 'filtericon'); + if (CoreUrl.isThemeImageUrl(this.iconUrl())) { + const filter = CoreUrl.getThemeImageUrlParam(this.iconUrl(), 'filtericon'); if (filter === '1') { this.brandedClass = false; @@ -233,7 +233,7 @@ export class CoreModIconComponent implements OnInit, OnChanges { * @returns Guessed modname. */ protected getComponentNameFromIconUrl(iconUrl: string): string { - const component = CoreUrlUtils.getThemeImageUrlParam(iconUrl, 'component'); + const component = CoreUrl.getThemeImageUrlParam(iconUrl, 'component'); // Some invalid components (others may be added later on). if (component === 'core' || component === 'theme') { diff --git a/src/core/components/user-avatar/user-avatar.ts b/src/core/components/user-avatar/user-avatar.ts index d54342cf8..cc85a0f4c 100644 --- a/src/core/components/user-avatar/user-avatar.ts +++ b/src/core/components/user-avatar/user-avatar.ts @@ -21,7 +21,7 @@ import { USER_PROFILE_PICTURE_UPDATED, CoreUserBasicData } from '@features/user/ import { CoreNavigator } from '@services/navigator'; import { CoreNetwork } from '@services/network'; import { CoreUserHelper } from '@features/user/services/user-helper'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreSiteInfo } from '@classes/sites/unauthenticated-site'; /** @@ -124,7 +124,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy { this.fullname = this.fullname || (this.user && (this.user.fullname || this.user.userfullname)); - if (this.avatarUrl && CoreUrlUtils.isThemeImageUrl(this.avatarUrl)) { + if (this.avatarUrl && CoreUrl.isThemeImageUrl(this.avatarUrl)) { this.avatarUrl = undefined; } diff --git a/src/core/directives/external-content.ts b/src/core/directives/external-content.ts index dbd917e92..94c02458b 100644 --- a/src/core/directives/external-content.ts +++ b/src/core/directives/external-content.ts @@ -26,7 +26,7 @@ import { import { CoreFile, CoreFileProvider } from '@services/file'; import { CoreFilepool, CoreFilepoolFileActions, CoreFilepoolFileEventData } from '@services/filepool'; import { CoreSites } from '@services/sites'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreLogger } from '@singletons/logger'; import { CoreError } from '@classes/errors/error'; @@ -206,8 +206,8 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O const site = await CoreUtils.ignoreErrors(CoreSites.getSite(this.siteId)); const isSiteFile = site?.isSitePluginFileUrl(url); - if (!url || !url.match(/^https?:\/\//i) || CoreUrlUtils.isLocalFileUrl(url) || - (tagName === 'A' && !(isSiteFile || site?.isSiteThemeImageUrl(url) || CoreUrlUtils.isGravatarUrl(url)))) { + if (!url || !url.match(/^https?:\/\//i) || CoreUrl.isLocalFileUrl(url) || + (tagName === 'A' && !(isSiteFile || site?.isSiteThemeImageUrl(url) || CoreUrl.isGravatarUrl(url)))) { this.logger.debug('Ignoring non-downloadable URL: ' + url); @@ -393,7 +393,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O finalUrl = CoreFile.convertFileSrc(finalUrl); } - if (!CoreUrlUtils.isLocalFileUrl(finalUrl) && !finalUrl.includes('#') && tagName !== 'A') { + if (!CoreUrl.isLocalFileUrl(finalUrl) && !finalUrl.includes('#') && tagName !== 'A') { /* In iOS, if we use the same URL in embedded file and background download then the download only downloads a few bytes (cached ones). Add an anchor to the URL so both URLs are different. Don't add this anchor if the URL already has an anchor, otherwise other anchors might not work. diff --git a/src/core/directives/link.ts b/src/core/directives/link.ts index ca6eeca1d..0c7e72553 100644 --- a/src/core/directives/link.ts +++ b/src/core/directives/link.ts @@ -19,7 +19,7 @@ import { IonContent } from '@ionic/angular'; import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreTextUtils } from '@services/utils/text'; import { CoreConstants } from '@/core/constants'; @@ -27,7 +27,6 @@ import { CoreContentLinksHelper } from '@features/contentlinks/services/contentl import { CoreCustomURLSchemes } from '@services/urlschemes'; import { DomSanitizer } from '@singletons'; import { CoreFilepool } from '@services/filepool'; -import { CoreUrl } from '@singletons/url'; import { CoreDom } from '@singletons/dom'; /** @@ -87,7 +86,7 @@ export class CoreLinkDirective implements OnInit { href = href || this.element.getAttribute('href') || this.element.getAttribute('xlink:href'); - if (!href || CoreUrlUtils.getUrlScheme(href) === 'javascript') { + if (!href || CoreUrl.getUrlProtocol(href) === 'javascript') { return; } @@ -116,7 +115,7 @@ export class CoreLinkDirective implements OnInit { */ protected async navigate(href: string, openIn?: string | null): Promise { - if (CoreUrlUtils.isLocalFileUrl(href)) { + if (CoreUrl.isLocalFileUrl(href)) { return this.openLocalFile(href); } diff --git a/src/core/features/contentlinks/services/contentlinks-delegate.ts b/src/core/features/contentlinks/services/contentlinks-delegate.ts index 0c2fa6e22..c76527243 100644 --- a/src/core/features/contentlinks/services/contentlinks-delegate.ts +++ b/src/core/features/contentlinks/services/contentlinks-delegate.ts @@ -15,11 +15,10 @@ import { Injectable } from '@angular/core'; import { CoreLogger } from '@singletons/logger'; import { CoreSites } from '@services/sites'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; import { CoreText } from '@singletons/text'; -import { CoreUrl } from '@singletons/url'; /** * Interface that all handlers must implement. @@ -174,7 +173,7 @@ export class CoreContentLinksDelegateService { const linkActions: CoreContentLinksHandlerActions[] = []; const promises: Promise[] = []; - const params = CoreUrlUtils.extractUrlParams(url); + const params = CoreUrl.extractUrlParams(url); const relativeUrl = CoreText.addStartingSlash(CoreUrl.toRelativeURL(site.getURL(), url)); for (const name in this.handlers) { diff --git a/src/core/features/course/classes/main-resource-component.ts b/src/core/features/course/classes/main-resource-component.ts index 27fd778b0..24de44d23 100644 --- a/src/core/features/course/classes/main-resource-component.ts +++ b/src/core/features/course/classes/main-resource-component.ts @@ -31,7 +31,7 @@ import { CoreCourseHelper, CoreCourseModuleData } from '../services/course-helpe import { CoreCourseModuleDelegate, CoreCourseModuleMainComponent } from '../services/module-delegate'; import { CoreCourseModulePrefetchDelegate } from '../services/module-prefetch-delegate'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreTime } from '@singletons/time'; /** @@ -506,7 +506,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, url = options.url; } else if (this.pluginName) { // Use default value. - url = CoreUrlUtils.addParamsToUrl(`/mod/${this.pluginName}/view.php?id=${this.module.id}`, options.data); + url = CoreUrl.addParamsToUrl(`/mod/${this.pluginName}/view.php?id=${this.module.id}`, options.data); } } diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 0636e5048..b3e31546f 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -59,7 +59,7 @@ import { CoreFileHelper } from '@services/file-helper'; import { CoreNetwork } from '@services/network'; import { CoreSite } from '@classes/sites/site'; import { CoreFile } from '@services/file'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreFilterHelper } from '@features/filter/services/filter-helper'; @@ -710,7 +710,7 @@ export class CoreCourseHelperProvider { options, ); - if (CoreUrlUtils.isLocalFileUrl(result.path)) { + if (CoreUrl.isLocalFileUrl(result.path)) { return CoreUtils.openFile(result.path, options); } diff --git a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts index ced2b6dcf..c611b2e4d 100644 --- a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts +++ b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts @@ -31,7 +31,7 @@ import { Subscription } from 'rxjs'; import { CoreSites } from '@services/sites'; import { CoreFilepool } from '@services/filepool'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreEventFormActionData, CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEditorOffline } from '../../services/editor-offline'; @@ -514,7 +514,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, const url = el.src; - if (!url || !CoreUrlUtils.isDownloadableUrl(url) || (!canDownloadFiles && site?.isSitePluginFileUrl(url))) { + if (!url || !CoreUrl.isDownloadableUrl(url) || (!canDownloadFiles && site?.isSitePluginFileUrl(url))) { // Nothing to treat. return; } diff --git a/src/core/features/grades/services/grades-helper.ts b/src/core/features/grades/services/grades-helper.ts index 4a836cca5..ab09a7bb1 100644 --- a/src/core/features/grades/services/grades-helper.ts +++ b/src/core/features/grades/services/grades-helper.ts @@ -34,7 +34,7 @@ import { CoreGradesTableRow, } from '@features/grades/services/grades'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreMenuItem, CoreUtils } from '@services/utils/utils'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreNavigator } from '@services/navigator'; @@ -416,7 +416,7 @@ export class CoreGradesHelperProvider { const matches = row.itemname.content.match(regex); if (matches && matches.length) { - const hrefParams = CoreUrlUtils.extractUrlParams(matches[1]); + const hrefParams = CoreUrl.extractUrlParams(matches[1]); return hrefParams && parseInt(hrefParams.id) === moduleId; } diff --git a/src/core/features/h5p/classes/player.ts b/src/core/features/h5p/classes/player.ts index 917c0fd3b..3dcdadb60 100644 --- a/src/core/features/h5p/classes/player.ts +++ b/src/core/features/h5p/classes/player.ts @@ -14,7 +14,7 @@ import { CoreFile } from '@services/file'; import { CoreSites } from '@services/sites'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreH5P } from '../services/h5p'; import { CoreH5PCore, CoreH5PDisplayOptions, CoreH5PContentData, CoreH5PDependenciesFiles } from './core'; @@ -51,7 +51,7 @@ export class CoreH5PPlayer { params.component = component; } - return CoreUrlUtils.addParamsToUrl(CorePath.concatenatePaths(siteUrl, '/h5p/embed.php'), params); + return CoreUrl.addParamsToUrl(CorePath.concatenatePaths(siteUrl, '/h5p/embed.php'), params); } /** @@ -303,7 +303,7 @@ export class CoreH5PPlayer { params.customCssUrl = customCssUrl; } - return CoreUrlUtils.addParamsToUrl(path, params); + return CoreUrl.addParamsToUrl(path, params); } /** diff --git a/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts b/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts index 1aef7bcd9..0fd4d67b3 100644 --- a/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts +++ b/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts @@ -22,7 +22,7 @@ import { CoreFilepool } from '@services/filepool'; import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreH5P } from '@features/h5p/services/h5p'; import { DownloadStatus } from '@/core/constants'; import { CoreSite } from '@classes/sites/site'; @@ -140,7 +140,7 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy { ); // Add the preventredirect param so the user can authenticate. - this.iframeSrc = CoreUrlUtils.addParamsToUrl(src, { preventredirect: false }); + this.iframeSrc = CoreUrl.addParamsToUrl(src, { preventredirect: false }); } } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error loading H5P package.', true); diff --git a/src/core/features/h5p/components/h5p-player/h5p-player.ts b/src/core/features/h5p/components/h5p-player/h5p-player.ts index cf6770813..49227a1f9 100644 --- a/src/core/features/h5p/components/h5p-player/h5p-player.ts +++ b/src/core/features/h5p/components/h5p-player/h5p-player.ts @@ -18,7 +18,7 @@ import { CoreNetwork } from '@services/network'; import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CorePluginFileDelegate } from '@services/plugin-file-delegate'; import { DownloadStatus } from '@/core/constants'; import { CoreSite } from '@classes/sites/site'; @@ -168,7 +168,7 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy { */ protected async checkCanDownload(): Promise { this.observer && this.observer.off(); - this.urlParams = CoreUrlUtils.extractUrlParams(this.src || ''); + this.urlParams = CoreUrl.extractUrlParams(this.src || ''); if (this.src && this.siteCanDownload && CoreH5P.canGetTrustedH5PFileInSite() && this.site.containsUrl(this.src)) { this.calculateState(); diff --git a/src/core/features/h5p/services/h5p.ts b/src/core/features/h5p/services/h5p.ts index de53844e0..5681020ad 100644 --- a/src/core/features/h5p/services/h5p.ts +++ b/src/core/features/h5p/services/h5p.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; import { CoreWSExternalWarning, CoreWSExternalFile, CoreWSFile } from '@services/ws'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreQueueRunner } from '@classes/queue-runner'; import { CoreSite } from '@classes/sites/site'; @@ -246,7 +246,7 @@ export class CoreH5PProvider { url = url.replace('/webservice/pluginfile', '/pluginfile'); } - return CoreUrlUtils.removeUrlParams(url); + return CoreUrl.removeUrlParts(url, [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment]); } } diff --git a/src/core/features/h5p/services/handlers/pluginfile.ts b/src/core/features/h5p/services/handlers/pluginfile.ts index a370b7cbb..3c3f6a513 100644 --- a/src/core/features/h5p/services/handlers/pluginfile.ts +++ b/src/core/features/h5p/services/handlers/pluginfile.ts @@ -19,7 +19,7 @@ import { CoreFilepoolOnProgressCallback } from '@services/filepool'; import { CorePluginFileDownloadableResult, CorePluginFileHandler } from '@services/plugin-file-delegate'; import { CoreSites } from '@services/sites'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSFile } from '@services/ws'; import { CoreH5P } from '../h5p'; @@ -80,7 +80,7 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler { const urls: string[] = []; for (let i = 0; i < iframes.length; i++) { - const params = CoreUrlUtils.extractUrlParams(iframes[i].src); + const params = CoreUrl.extractUrlParams(iframes[i].src); if (params.url) { urls.push(params.url); diff --git a/src/core/features/login/pages/site/site.ts b/src/core/features/login/pages/site/site.ts index 9702f05c4..b127025c0 100644 --- a/src/core/features/login/pages/site/site.ts +++ b/src/core/features/login/pages/site/site.ts @@ -28,8 +28,7 @@ import { import { CoreError } from '@classes/errors/error'; import { CoreConstants } from '@/core/constants'; import { Translate } from '@singletons'; -import { CoreUrl } from '@singletons/url'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreNavigator } from '@services/navigator'; import { CoreCustomURLSchemes, CoreCustomURLSchemesHandleError } from '@services/urlschemes'; import { CoreTextUtils } from '@services/utils/text'; @@ -206,7 +205,9 @@ export class CoreLoginSitePage implements OnInit { */ protected extendCoreLoginSiteInfo(sites: CoreLoginSiteInfoExtended[]): CoreLoginSiteInfoExtended[] { return sites.map((site) => { - site.noProtocolUrl = this.siteFinderSettings.displayurl && site.url ? CoreUrl.removeProtocol(site.url) : ''; + site.noProtocolUrl = this.siteFinderSettings.displayurl && site.url + ? CoreUrl.removeUrlParts(site.url, CoreUrlPartNames.Protocol) + : ''; const name = this.siteFinderSettings.displaysitename ? site.name : ''; const alias = this.siteFinderSettings.displayalias && site.alias ? site.alias : ''; @@ -510,7 +511,7 @@ export class CoreLoginSitePage implements OnInit { name: 'connect', title: '', location: '', - noProtocolUrl: CoreUrl.removeProtocol(search), + noProtocolUrl: CoreUrl.removeUrlParts(search, CoreUrlPartNames.Protocol), }; } else { this.enteredSiteUrl = undefined; @@ -563,7 +564,7 @@ export class CoreLoginSitePage implements OnInit { } // Not a custom URL scheme, check if it's a URL scheme to another app. - const scheme = CoreUrlUtils.getUrlProtocol(text); + const scheme = CoreUrl.getUrlProtocol(text); if (scheme && scheme != 'http' && scheme != 'https') { CoreDomUtils.showErrorModal(Translate.instant('core.errorurlschemeinvalidscheme', { $a: text })); diff --git a/src/core/features/login/services/login-helper.ts b/src/core/features/login/services/login-helper.ts index 5a9917de1..856d729ca 100644 --- a/src/core/features/login/services/login-helper.ts +++ b/src/core/features/login/services/login-helper.ts @@ -23,7 +23,6 @@ import { CoreSites, CoreLoginSiteInfo, CoreSiteBasicInfo } from '@services/sites import { CoreWS, CoreWSExternalWarning } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlParams, CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { CoreSite } from '@classes/sites/site'; @@ -31,7 +30,7 @@ import { CoreError } from '@classes/errors/error'; import { CoreWSError } from '@classes/errors/wserror'; import { DomSanitizer, makeSingleton, Translate } from '@singletons'; import { CoreLogger } from '@singletons/logger'; -import { CoreUrl } from '@singletons/url'; +import { CoreUrl, CoreUrlParams } from '@singletons/url'; import { CoreNavigator, CoreRedirectPayload } from '@services/navigator'; import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreCustomURLSchemes } from '@services/urlschemes'; @@ -356,7 +355,7 @@ export class CoreLoginHelperProvider { if (siteConfig.identityproviders && siteConfig.identityproviders.length) { siteConfig.identityproviders.forEach((provider) => { - const urlParams = CoreUrlUtils.extractUrlParams(provider.url); + const urlParams = CoreUrl.extractUrlParams(provider.url); if ( provider.url && @@ -397,7 +396,7 @@ export class CoreLoginHelperProvider { if (siteConfig.identityproviders && siteConfig.identityproviders.length) { siteConfig.identityproviders.forEach((provider) => { - const urlParams = CoreUrlUtils.extractUrlParams(provider.url); + const urlParams = CoreUrl.extractUrlParams(provider.url); if (provider.url && (provider.url.indexOf(httpsUrl) != -1 || provider.url.indexOf(httpUrl) != -1) && !site.isFeatureDisabled(IDENTITY_PROVIDER_FEATURE_NAME_PREFIX + urlParams.id)) { @@ -642,7 +641,7 @@ export class CoreLoginHelperProvider { return false; } - const params = CoreUrlUtils.extractUrlParams(provider.url); + const params = CoreUrl.extractUrlParams(provider.url); if (!params.id) { return false; @@ -830,7 +829,7 @@ export class CoreLoginHelperProvider { loginUrl += '&urlscheme=' + CoreConstants.CONFIG.customurlscheme; if (urlParams) { - loginUrl = CoreUrlUtils.addParamsToUrl(loginUrl, urlParams); + loginUrl = CoreUrl.addParamsToUrl(loginUrl, urlParams); } // Store the siteurl and passport in CoreConfigProvider for persistence. @@ -1334,7 +1333,7 @@ export class CoreLoginHelperProvider { } } else if (text) { // Not a custom URL scheme, check if it's a URL scheme to another app. - const scheme = CoreUrlUtils.getUrlProtocol(text); + const scheme = CoreUrl.getUrlProtocol(text); if (scheme && scheme != 'http' && scheme != 'https') { CoreDomUtils.showErrorModal(Translate.instant('core.errorurlschemeinvalidscheme', { $a: text })); diff --git a/src/core/features/policy/pages/site-policy/site-policy.ts b/src/core/features/policy/pages/site-policy/site-policy.ts index bcb986566..46e65efb1 100644 --- a/src/core/features/policy/pages/site-policy/site-policy.ts +++ b/src/core/features/policy/pages/site-policy/site-policy.ts @@ -25,7 +25,7 @@ import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { Translate } from '@singletons'; import { CorePolicy, CorePolicyAgreementStyle, CorePolicySitePolicy } from '@features/policy/services/policy'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { IonContent } from '@ionic/angular'; import { CoreScreen } from '@services/screen'; import { Subscription } from 'rxjs'; @@ -223,7 +223,7 @@ export class CorePolicySitePolicyPage implements OnInit, OnDestroy { ws: 'tool_policy_get_user_acceptances', name: this.currentPolicy.name, data: analyticsParams, - url: CoreUrlUtils.addParamsToUrl('/admin/tool/policy/view.php', analyticsParams), + url: CoreUrl.addParamsToUrl('/admin/tool/policy/view.php', analyticsParams), }); } @@ -236,7 +236,7 @@ export class CorePolicySitePolicyPage implements OnInit, OnDestroy { ws: 'tool_policy_get_user_acceptances', name: Translate.instant('core.policy.consentpagetitle'), data: {}, - url: CoreUrlUtils.addParamsToUrl('/admin/tool/policy/index.php'), + url: CoreUrl.addParamsToUrl('/admin/tool/policy/index.php'), }); } diff --git a/src/core/features/question/classes/base-question-component.ts b/src/core/features/question/classes/base-question-component.ts index 2bcfcc5ba..09642ba35 100644 --- a/src/core/features/question/classes/base-question-component.ts +++ b/src/core/features/question/classes/base-question-component.ts @@ -18,7 +18,7 @@ import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreWSFile } from '@services/ws'; import { CoreIonicColorNames } from '@singletons/colors'; import { CoreLogger } from '@singletons/logger'; @@ -367,7 +367,7 @@ export class CoreQuestionBaseComponent -1) { + if (CoreUrl.isThemeImageUrl(fileUrl) && fileUrl.indexOf('flagged') > -1) { // Ignore flag images. return; } diff --git a/src/core/features/search/pages/global-search/global-search.ts b/src/core/features/search/pages/global-search/global-search.ts index bd5b28e7e..c67d2abfc 100644 --- a/src/core/features/search/pages/global-search/global-search.ts +++ b/src/core/features/search/pages/global-search/global-search.ts @@ -19,7 +19,7 @@ import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { Translate } from '@singletons'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreEvents, CoreEventObserver } from '@singletons/events'; import { CoreSearchGlobalSearchResult, @@ -115,7 +115,7 @@ export class CoreSearchGlobalSearchPage implements OnInit, OnDestroy, AfterViewI query, filters: JSON.stringify(this.resultsSource.getFilters()), }, - url: CoreUrlUtils.addParamsToUrl('/search/index.php', { + url: CoreUrl.addParamsToUrl('/search/index.php', { q: query, }), }); diff --git a/src/core/features/siteplugins/services/siteplugins-init.ts b/src/core/features/siteplugins/services/siteplugins-init.ts index fc4854919..e85dd2902 100644 --- a/src/core/features/siteplugins/services/siteplugins-init.ts +++ b/src/core/features/siteplugins/services/siteplugins-init.ts @@ -84,7 +84,7 @@ import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/class import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler'; import { CoreObject } from '@singletons/object'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CorePath } from '@singletons/path'; import { CoreEnrolAction, CoreEnrolDelegate } from '@features/enrol/services/enrol-delegate'; import { CoreSitePluginsEnrolHandler } from '../classes/handlers/enrol-handler'; @@ -170,7 +170,7 @@ export class CoreSitePluginsInitService { // Make sure it's an absolute URL. Do not use toAbsoluteURL because it can change the behaviour and break plugin styles. let url = handlerSchema.styles?.url; - if (url && !CoreUrlUtils.isAbsoluteURL(url)) { + if (url && !CoreUrl.isAbsoluteURL(url)) { url = CorePath.concatenatePaths(site.getURL(), url); } diff --git a/src/core/features/tag/pages/index/index.ts b/src/core/features/tag/pages/index/index.ts index fba315863..bc944e34a 100644 --- a/src/core/features/tag/pages/index/index.ts +++ b/src/core/features/tag/pages/index/index.ts @@ -21,7 +21,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreTime } from '@singletons/time'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { Translate } from '@singletons'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; /** * Page that displays the tag index. @@ -62,7 +62,7 @@ export class CoreTagIndexPage implements OnInit { ws: 'core_tag_get_tagindex_per_area', name: this.tagName || Translate.instant('core.tag.tag'), data: { id: this.tagId || undefined, ...params, category: 'tag' }, - url: CoreUrlUtils.addParamsToUrl('/tag/index.php', params), + url: CoreUrl.addParamsToUrl('/tag/index.php', params), }); }); } diff --git a/src/core/features/user/pages/about/about.ts b/src/core/features/user/pages/about/about.ts index 4315f0843..e5d3c71ef 100644 --- a/src/core/features/user/pages/about/about.ts +++ b/src/core/features/user/pages/about/about.ts @@ -32,7 +32,7 @@ import { CoreSite } from '@classes/sites/site'; import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuploader-helper'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { Translate } from '@singletons'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; /** * Page that displays info about a user. @@ -247,7 +247,7 @@ export class CoreUserAboutPage implements OnInit, OnDestroy { return 'undefined'; } - if (CoreUrlUtils.isThemeImageUrl(avatarUrl, this.site?.siteUrl)) { + if (CoreUrl.isThemeImageUrl(avatarUrl, this.site?.siteUrl)) { return 'default'; } diff --git a/src/core/features/user/services/user.ts b/src/core/features/user/services/user.ts index 9ac7bcd63..e837294d5 100644 --- a/src/core/features/user/services/user.ts +++ b/src/core/features/user/services/user.ts @@ -26,7 +26,7 @@ import { CoreEvents, CoreEventSiteData, CoreEventUserDeletedData, CoreEventUserS import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws'; import { CoreError } from '@classes/errors/error'; import { USERS_TABLE_NAME, CoreUserDBRecord } from './database/user'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site'; import { CoreConstants } from '@/core/constants'; @@ -668,7 +668,7 @@ export class CoreUserProvider { } // Do not prefetch when initials are set and image is default. - if (imageUrl && CoreUrlUtils.isThemeImageUrl(imageUrl)) { + if (imageUrl && CoreUrl.isThemeImageUrl(imageUrl)) { return; } diff --git a/src/core/initializers/prepare-inapp-browser.ts b/src/core/initializers/prepare-inapp-browser.ts index 18be057f6..e3c393eb7 100644 --- a/src/core/initializers/prepare-inapp-browser.ts +++ b/src/core/initializers/prepare-inapp-browser.ts @@ -20,7 +20,7 @@ import { CorePlatform } from '@services/platform'; import { CoreSites } from '@services/sites'; import { CoreCustomURLSchemes } from '@services/urlschemes'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; @@ -34,9 +34,9 @@ export default function(): void { // Check URLs loaded in any InAppBrowser. CoreEvents.on(CoreEvents.IAB_LOAD_START, async (event) => { // URLs with a custom scheme can be prefixed with "http://" or "https://", we need to remove this. - const protocol = CoreUrlUtils.getUrlProtocol(event.url); + const protocol = CoreUrl.getUrlProtocol(event.url); const url = event.url.replace(/^https?:\/\//, ''); - const urlScheme = CoreUrlUtils.getUrlProtocol(url); + const urlScheme = CoreUrl.getUrlProtocol(url); const isExternalApp = urlScheme && urlScheme !== 'file' && urlScheme !== 'cdvfile'; if (CoreCustomURLSchemes.isCustomURL(url)) { diff --git a/src/core/services/analytics.ts b/src/core/services/analytics.ts index 0fb057e96..91b6520e5 100644 --- a/src/core/services/analytics.ts +++ b/src/core/services/analytics.ts @@ -20,8 +20,8 @@ import { CoreEvents } from '@singletons/events'; import { CoreSites } from './sites'; import { CoreConfig, CoreConfigProvider } from './config'; import { CoreConstants } from '../constants'; -import { CoreUrlUtils } from './utils/url'; import { CoreTextUtils } from '@services/utils/text'; +import { CoreUrl } from '@singletons/url'; /** * Helper service to support analytics. @@ -107,7 +107,7 @@ export class CoreAnalyticsService extends CoreDelegate { } if ('url' in treatedEvent && treatedEvent.url) { - if (!CoreUrlUtils.isAbsoluteURL(treatedEvent.url)) { + if (!CoreUrl.isAbsoluteURL(treatedEvent.url)) { treatedEvent.url = site.createSiteUrl(treatedEvent.url); } else if (!site.containsUrl(treatedEvent.url)) { // URL belongs to a different site, ignore the event. diff --git a/src/core/services/file-helper.ts b/src/core/services/file-helper.ts index d756ef177..91163f2ad 100644 --- a/src/core/services/file-helper.ts +++ b/src/core/services/file-helper.ts @@ -21,7 +21,7 @@ import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; import { CoreWS, CoreWSFile } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils, CoreUtilsOpenFileOptions, OpenFileAction } from '@services/utils/utils'; import { CoreConstants, DownloadStatus } from '@/core/constants'; import { CoreError } from '@classes/errors/error'; @@ -93,7 +93,7 @@ export class CoreFileHelperProvider { return; } - if (!CoreUrlUtils.isLocalFileUrl(url)) { + if (!CoreUrl.isLocalFileUrl(url)) { /* In iOS, if we use the same URL in embedded browser and background download then the download only downloads a few bytes (cached ones). Add a hash to the URL so both URLs are different. */ url = url + '#moodlemobile-embedded'; diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 9455dfbfb..7e0b3f450 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -26,7 +26,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreUtils, CoreUtilsOpenFileOptions } from '@services/utils/utils'; import { CoreError } from '@classes/errors/error'; import { DownloadStatus } from '@/core/constants'; @@ -51,7 +51,6 @@ import { QUEUE_TABLE_PRIMARY_KEYS, } from '@services/database/filepool'; import { CoreFileHelper } from './file-helper'; -import { CoreUrl } from '@singletons/url'; import { CoreDatabaseTable } from '@classes/database/database-table'; import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy'; import { lazyMap, LazyMap } from '../utils/lazy-map'; @@ -782,7 +781,7 @@ export class CoreFilepoolProvider { CoreAnalytics.logEvent({ type: CoreAnalyticsEventType.DOWNLOAD_FILE, - fileUrl: CoreUrlUtils.unfixPluginfileURL(fileUrl, site.getURL()), + fileUrl: CoreUrl.unfixPluginfileURL(fileUrl, site.getURL()), }); // Add the anchor again to the local URL. @@ -1126,14 +1125,14 @@ export class CoreFilepoolProvider { const element = elements[i]; const url = 'href' in element ? element.href : element.src; - if (url && CoreUrlUtils.isDownloadableUrl(url) && urls.indexOf(url) == -1) { + if (url && CoreUrl.isDownloadableUrl(url) && urls.indexOf(url) == -1) { urls.push(url); } // Treat video poster. if (element.tagName == 'VIDEO' && element.getAttribute('poster')) { const poster = element.getAttribute('poster'); - if (poster && CoreUrlUtils.isDownloadableUrl(poster) && urls.indexOf(poster) == -1) { + if (poster && CoreUrl.isDownloadableUrl(poster) && urls.indexOf(poster) == -1) { urls.push(poster); } } @@ -1363,7 +1362,7 @@ export class CoreFilepoolProvider { } // Remove the anchor. - url = CoreUrl.removeUrlAnchor(url); + url = CoreUrl.removeUrlParts(url, CoreUrlPartNames.Fragment); // Try to guess the filename the target file should have. // We want to keep the original file name so people can easily identify the files after the download. @@ -1535,7 +1534,7 @@ export class CoreFilepoolProvider { return DownloadStatus.NOT_DOWNLOADABLE; } - fileUrl = CoreUrl.removeUrlAnchor(CoreFileHelper.getFileUrl(file)); + fileUrl = CoreUrl.removeUrlParts(CoreFileHelper.getFileUrl(file), CoreUrlPartNames.Fragment); timemodified = file.timemodified ?? timemodified; revision = revision ?? this.getRevisionFromUrl(fileUrl); const fileId = this.getFileIdByUrl(fileUrl); @@ -1914,7 +1913,7 @@ export class CoreFilepoolProvider { * @returns The args found, undefined if not a pluginfile. */ protected getPluginFileArgs(url: string): string[] | undefined { - if (!CoreUrlUtils.isPluginFileUrl(url)) { + if (!CoreUrl.isPluginFileUrl(url)) { // Not pluginfile, return. return; } @@ -2160,27 +2159,27 @@ export class CoreFilepoolProvider { if (fileUrl.indexOf('/webservice/pluginfile') !== -1) { // It's a pluginfile URL. Search for the 'file' param to extract the name. - const params = CoreUrlUtils.extractUrlParams(fileUrl); + const params = CoreUrl.extractUrlParams(fileUrl); if (params.file) { filename = params.file.substring(params.file.lastIndexOf('/') + 1); } else { // 'file' param not found. Extract what's after the last '/' without params. - filename = CoreUrlUtils.getLastFileWithoutParams(fileUrl); + filename = CoreUrl.getLastFileWithoutParams(fileUrl); } - } else if (CoreUrlUtils.isGravatarUrl(fileUrl)) { + } else if (CoreUrl.isGravatarUrl(fileUrl)) { // Extract gravatar ID. - filename = 'gravatar_' + CoreUrlUtils.getLastFileWithoutParams(fileUrl); - } else if (CoreUrlUtils.isThemeImageUrl(fileUrl)) { + filename = 'gravatar_' + CoreUrl.getLastFileWithoutParams(fileUrl); + } else if (CoreUrl.isThemeImageUrl(fileUrl)) { // Extract user ID. const matches = fileUrl.match(/\/core\/([^/]*)\//); if (matches && matches[1]) { filename = matches[1]; } // Attach a constant and the image type. - filename = 'default_' + filename + '_' + CoreUrlUtils.getLastFileWithoutParams(fileUrl); + filename = 'default_' + filename + '_' + CoreUrl.getLastFileWithoutParams(fileUrl); } else { // Another URL. Just get what's after the last /. - filename = CoreUrlUtils.getLastFileWithoutParams(fileUrl); + filename = CoreUrl.getLastFileWithoutParams(fileUrl); } // If there are hashes in the URL, extract them. @@ -3005,7 +3004,7 @@ export class CoreFilepoolProvider { try { let fileUrl = absoluteUrl; - if (!CoreUrlUtils.isLocalFileUrl(absoluteUrl)) { + if (!CoreUrl.isLocalFileUrl(absoluteUrl)) { // Not a local file, download it. fileUrl = await this.downloadUrl( siteId, diff --git a/src/core/services/navigator.ts b/src/core/services/navigator.ts index 82c5a906d..5bb2c0be2 100644 --- a/src/core/services/navigator.ts +++ b/src/core/services/navigator.ts @@ -23,7 +23,7 @@ import { CoreMainMenu } from '@features/mainmenu/services/mainmenu'; import { CoreObject } from '@singletons/object'; import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreTextUtils } from '@services/utils/text'; import { makeSingleton, NavController, Router } from '@singletons'; import { CoreScreen } from './screen'; @@ -262,7 +262,7 @@ export class CoreNavigatorService { * @returns Current path. */ getCurrentPath(): string { - return CoreUrlUtils.removeUrlParams(Router.url); + return CoreUrl.removeUrlParts(Router.url, [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment]); } /** diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index e2d80c06f..8433311e9 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -21,7 +21,7 @@ import { CoreEvents } from '@singletons/events'; import { CoreWS } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { @@ -286,9 +286,9 @@ export class CoreSitesProvider { */ async checkSite(siteUrl: string, protocol: string = 'https://'): Promise { // The formatURL function adds the protocol if is missing. - siteUrl = CoreUrlUtils.formatURL(siteUrl); + siteUrl = CoreUrl.formatURL(siteUrl); - if (!CoreUrlUtils.isHttpURL(siteUrl)) { + if (!CoreUrl.isHttpURL(siteUrl)) { throw new CoreError(Translate.instant('core.login.invalidsite')); } @@ -350,7 +350,7 @@ export class CoreSitesProvider { } // Try to add or remove 'www'. - temporarySite.setURL(CoreUrlUtils.addOrRemoveWWW(temporarySite.getURL())); + temporarySite.setURL(CoreUrl.addOrRemoveWWW(temporarySite.getURL())); try { config = await temporarySite.getPublicConfig(); @@ -546,7 +546,7 @@ export class CoreSitesProvider { // We only allow one retry (to avoid loops). if (!retry && data.errorcode == 'requirecorrectaccess') { - siteUrl = CoreUrlUtils.addOrRemoveWWW(siteUrl); + siteUrl = CoreUrl.addOrRemoveWWW(siteUrl); return this.getUserToken(siteUrl, username, password, service, true); } @@ -1697,7 +1697,7 @@ export class CoreSitesProvider { // Check if URL has http(s) protocol. if (!url.match(/^https?:\/\//i)) { // URL doesn't have http(s) protocol. Check if it has any protocol. - if (CoreUrlUtils.isAbsoluteURL(url)) { + if (CoreUrl.isAbsoluteURL(url)) { // It has some protocol. Return empty array. return []; } @@ -1943,11 +1943,13 @@ export class CoreSitesProvider { const site = await this.getSite(siteIds[0]); const siteUrl = CoreText.removeEndingSlash( - CoreUrlUtils.removeProtocolAndWWW(site.getURL()), + CoreUrl.removeUrlParts(site.getURL(), [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain]), + ); + const treatedUrl = CoreText.removeEndingSlash( + CoreUrl.removeUrlParts(url, [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain]), ); - const treatedUrl = CoreText.removeEndingSlash(CoreUrlUtils.removeProtocolAndWWW(url)); - if (siteUrl == treatedUrl) { + if (siteUrl === treatedUrl) { result.site = site; } diff --git a/src/core/services/tests/utils/url.test.ts b/src/core/services/tests/utils/url.test.ts deleted file mode 100644 index fe3ee07b3..000000000 --- a/src/core/services/tests/utils/url.test.ts +++ /dev/null @@ -1,90 +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 { CoreUrlUtilsProvider } from '@services/utils/url'; - -describe('CoreUrlUtilsProvider', () => { - - let urlUtils: CoreUrlUtilsProvider; - - beforeEach(() => { - urlUtils = new CoreUrlUtilsProvider(); - }); - - it('adds www if missing', () => { - const originalUrl = 'https://moodle.org'; - const url = urlUtils.addOrRemoveWWW(originalUrl); - - expect(url).toEqual('https://www.moodle.org'); - }); - - it('removes www if present', () => { - const originalUrl = 'https://www.moodle.org'; - const url = urlUtils.addOrRemoveWWW(originalUrl); - - expect(url).toEqual('https://moodle.org'); - }); - - it('adds params to URL without params', () => { - const originalUrl = 'https://moodle.org'; - const params = { - first: '1', - second: '2', - }; - const url = urlUtils.addParamsToUrl(originalUrl, params); - - expect(url).toEqual('https://moodle.org?first=1&second=2'); - }); - - it('adds params to URL with existing params', () => { - const originalUrl = 'https://moodle.org?existing=1'; - const params = { - first: '1', - second: '2', - }; - const url = urlUtils.addParamsToUrl(originalUrl, params); - - expect(url).toEqual('https://moodle.org?existing=1&first=1&second=2'); - }); - - it('doesn\'t change URL if no params supplied', () => { - const originalUrl = 'https://moodle.org'; - const url = urlUtils.addParamsToUrl(originalUrl); - - expect(url).toEqual(originalUrl); - }); - - it('doesn\'t add undefined or null params', () => { - const originalUrl = 'https://moodle.org'; - const url = urlUtils.addParamsToUrl(originalUrl, { - foo: undefined, - bar: null, - baz: 1, - }); - - expect(url).toEqual('https://moodle.org?baz=1'); - }); - - it('adds anchor to URL', () => { - const originalUrl = 'https://moodle.org'; - const params = { - first: '1', - second: '2', - }; - const url = urlUtils.addParamsToUrl(originalUrl, params, 'myanchor'); - - expect(url).toEqual('https://moodle.org?first=1&second=2#myanchor'); - }); - -}); diff --git a/src/core/services/urlschemes.ts b/src/core/services/urlschemes.ts index 780739f3e..b70a5542a 100644 --- a/src/core/services/urlschemes.ts +++ b/src/core/services/urlschemes.ts @@ -28,7 +28,7 @@ import { CoreNavigator, CoreRedirectPayload } from './navigator'; import { CoreSiteCheckResponse, CoreSites } from './sites'; import { CoreDomUtils } from './utils/dom'; import { CoreTextErrorObject, CoreTextUtils } from './utils/text'; -import { CoreUrlUtils } from './utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from './utils/utils'; /* @@ -240,13 +240,13 @@ export class CoreCustomURLSchemesProvider { url = this.removeCustomURLScheme(url); // Detect if there's a user specified. - const username = CoreUrlUtils.getUsernameFromUrl(url); + const username = CoreUrl.getUsernameFromUrl(url); if (username) { url = url.replace(username + '@', ''); // Remove the username from the URL. } // Get the params of the URL. - const params = CoreUrlUtils.extractUrlParams(url); + const params = CoreUrl.extractUrlParams(url); // Remove the params to get the site URL. if (url.indexOf('?') != -1) { @@ -293,7 +293,7 @@ export class CoreCustomURLSchemesProvider { url = this.removeCustomURLLinkScheme(url); // Detect if there's a user specified. - const username = CoreUrlUtils.getUsernameFromUrl(url); + const username = CoreUrl.getUsernameFromUrl(url); if (username) { url = url.replace(username + '@', ''); // Remove the username from the URL. } diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 5f238e435..c00e2f88d 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -21,7 +21,7 @@ import { CoreConfig } from '@services/config'; import { CoreFile } from '@services/file'; import { CoreWSExternalWarning } from '@services/ws'; import { CoreTextUtils, CoreTextErrorObject } from '@services/utils/text'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { CoreIonLoadingElement } from '@classes/ion-loading'; @@ -710,7 +710,10 @@ export class CoreDomUtilsProvider { media.forEach((media: HTMLElement) => { const currentSrc = media.getAttribute('src'); const newSrc = currentSrc ? - paths[CoreUrlUtils.removeUrlParams(CoreTextUtils.decodeURIComponent(currentSrc))] : + paths[CoreUrl.removeUrlParts( + CoreTextUtils.decodeURIComponent(currentSrc), + [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment], + )] : undefined; if (newSrc !== undefined) { @@ -732,7 +735,10 @@ export class CoreDomUtilsProvider { anchors.forEach((anchor: HTMLElement) => { const currentHref = anchor.getAttribute('href'); const newHref = currentHref ? - paths[CoreUrlUtils.removeUrlParams(CoreTextUtils.decodeURIComponent(currentHref))] : + paths[CoreUrl.removeUrlParts( + CoreTextUtils.decodeURIComponent(currentHref), + [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment], + )] : undefined; if (newHref !== undefined) { diff --git a/src/core/services/utils/iframe.ts b/src/core/services/utils/iframe.ts index 93e9a0933..39975ce21 100644 --- a/src/core/services/utils/iframe.ts +++ b/src/core/services/utils/iframe.ts @@ -21,12 +21,11 @@ import { CoreFile } from '@services/file'; import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton, NgZone, Translate } from '@singletons'; import { CoreLogger } from '@singletons/logger'; -import { CoreUrl } from '@singletons/url'; import { CoreWindow } from '@singletons/window'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; import { CorePath } from '@singletons/path'; @@ -68,7 +67,7 @@ export class CoreIframeUtilsProvider { checkOnlineFrameInOffline(element: CoreFrameElement, isSubframe?: boolean): boolean { const src = 'src' in element ? element.src : element.data; - if (src && src != 'about:blank' && !CoreUrlUtils.isLocalFileUrl(src) && !CoreNetwork.isOnline()) { + if (src && src != 'about:blank' && !CoreUrl.isLocalFileUrl(src) && !CoreNetwork.isOnline()) { if (element.classList.contains('core-iframe-offline-disabled')) { // Iframe already hidden, stop. return true; @@ -234,7 +233,7 @@ export class CoreIframeUtilsProvider { */ getContentWindowAndDocument(element: CoreFrameElement): { window: Window | null; document: Document | null } { const src = 'src' in element ? element.src : element.data; - if (src !== 'about:blank' && !CoreUrlUtils.isLocalFileUrl(src)) { + if (src !== 'about:blank' && !CoreUrl.isLocalFileUrl(src)) { // No permissions to access the iframe. return { window: null, document: null }; } @@ -423,7 +422,7 @@ export class CoreIframeUtilsProvider { * @returns Promise resolved when done. */ protected async windowOpen(url: string, name: string, element?: CoreFrameElement): Promise { - const scheme = CoreUrlUtils.getUrlScheme(url); + const scheme = CoreUrl.getUrlProtocol(url); if (!scheme) { // It's a relative URL, use the frame src to create the full URL. const src = element @@ -488,12 +487,12 @@ export class CoreIframeUtilsProvider { const urlParts = CoreUrl.parse(link.href); const originalHref = 'getAttribute' in link ? link.getAttribute('href') : link.originalHref; - if (!link.href || !originalHref || originalHref == '#' || !urlParts || urlParts.protocol == 'javascript') { + if (!link.href || !originalHref || originalHref == '#' || !urlParts || urlParts.protocol === 'javascript') { // Links with no URL and Javascript links are ignored. return; } - if (urlParts.protocol && !CoreUrlUtils.isLocalFileUrlScheme(urlParts.protocol, urlParts.domain || '')) { + if (urlParts.protocol && !CoreUrl.isLocalFileUrlScheme(urlParts.protocol, urlParts.domain || '')) { // Scheme suggests it's an external resource. event && event.preventDefault(); @@ -503,7 +502,7 @@ export class CoreIframeUtilsProvider { if ( element && frameSrc && - !CoreUrlUtils.isLocalFileUrl(frameSrc) && + !CoreUrl.isLocalFileUrl(frameSrc) && (!link.target || link.target == '_self') ) { // Load the link inside the frame itself. @@ -574,7 +573,7 @@ export class CoreIframeUtilsProvider { * @returns Promise resolved when done. */ async fixIframeCookies(url: string): Promise { - if (!CorePlatform.isIOS() || !url || CoreUrlUtils.isLocalFileUrl(url)) { + if (!CorePlatform.isIOS() || !url || CoreUrl.isLocalFileUrl(url)) { // No need to fix cookies. return; } @@ -613,7 +612,7 @@ export class CoreIframeUtilsProvider { * @returns Boolean. */ shouldDisplayHelpForUrl(url: string): boolean { - return this.shouldDisplayHelp() && !CoreUrlUtils.isLocalFileUrl(url); + return this.shouldDisplayHelp() && !CoreUrl.isLocalFileUrl(url); } /** diff --git a/src/core/services/utils/url.ts b/src/core/services/utils/url.ts index e05a8725b..d00d4e62e 100644 --- a/src/core/services/utils/url.ts +++ b/src/core/services/utils/url.ts @@ -14,18 +14,12 @@ import { Injectable } from '@angular/core'; -import { CoreLang, CoreLangFormat } from '@services/lang'; -import { CoreTextUtils } from '@services/utils/text'; -import { CoreConstants } from '@/core/constants'; import { makeSingleton } from '@singletons'; -import { CoreUrl } from '@singletons/url'; -import { CoreSites } from '@services/sites'; -import { CorePath } from '@singletons/path'; -import { CorePlatform } from '@services/platform'; -import { CoreMedia } from '@singletons/media'; +import { CoreUrl, CoreUrlParams as CoreUrlParamsNew, CoreUrlPartNames } from '@singletons/url'; /* * "Utils" service with helper functions for URLs. + * @deprecated since 4.5. Use CoreUrl instead. */ @Injectable({ providedIn: 'root' }) export class CoreUrlUtilsProvider { @@ -35,19 +29,10 @@ export class CoreUrlUtilsProvider { * * @param url URL to modify. * @returns Modified URL. + * @deprecated since 4.5. Use CoreUrl.addOrRemoveWWW instead. */ addOrRemoveWWW(url: string): string { - if (url) { - if (url.match(/http(s)?:\/\/www\./)) { - // Already has www. Remove it. - url = url.replace('www.', ''); - } else { - url = url.replace('https://', 'https://www.'); - url = url.replace('http://', 'http://www.'); - } - } - - return url; + return CoreUrl.addOrRemoveWWW(url); } /** @@ -58,43 +43,10 @@ export class CoreUrlUtilsProvider { * @param anchor Anchor text if needed. * @param boolToNumber Whether to convert bools to 1 or 0. * @returns URL with params. + * @deprecated since 4.5. Use CoreUrl.addParamsToUrl instead. */ addParamsToUrl(url: string, params?: Record, anchor?: string, boolToNumber?: boolean): string { - // Remove any existing anchor to add the params before it. - const urlAndAnchor = url.split('#'); - url = urlAndAnchor[0]; - - let separator = url.indexOf('?') !== -1 ? '&' : '?'; - - for (const key in params) { - let value = params[key]; - - if (boolToNumber && typeof value === 'boolean') { - // Convert booleans to 1 or 0. - value = value ? '1' : '0'; - } - - // Ignore objects and undefined. - if (typeof value !== 'object' && value !== undefined) { - url += separator + key + '=' + value; - separator = '&'; - } - } - - // Re-add the anchor if any. - if (urlAndAnchor.length > 1) { - // Remove the URL from the array. - urlAndAnchor.shift(); - - // Use a join in case there is more than one #. - url += '#' + urlAndAnchor.join('#'); - } - - if (anchor) { - url += '#' + anchor; - } - - return url; + return CoreUrl.addParamsToUrl(url, params, anchor, boolToNumber); } /** @@ -103,9 +55,10 @@ export class CoreUrlUtilsProvider { * @param url URL. * @param text Text of the link. * @returns Link. + * @deprecated since 4.5. Use CoreUrl.buildLink instead. */ buildLink(url: string, text: string): string { - return '' + text + ''; + return CoreUrl.buildLink(url, text); } /** @@ -115,14 +68,10 @@ export class CoreUrlUtilsProvider { * @param siteUrl The URL of the site the URL belongs to. * @param accessKey User access key for tokenpluginfile. * @returns Whether tokenpluginfile.php can be used. + * @deprecated since 4.5. Use CoreUrl.canUseTokenPluginFile instead. */ canUseTokenPluginFile(url: string, siteUrl: string, accessKey?: string): boolean { - // Do not use tokenpluginfile if site doesn't use slash params, the URL doesn't work. - // Also, only use it for "core" pluginfile endpoints. Some plugins can implement their own endpoint (like customcert). - return !CoreConstants.CONFIG.disableTokenFile && !!accessKey && !url.match(/[&?]file=/) && ( - url.indexOf(CorePath.concatenatePaths(siteUrl, 'pluginfile.php')) === 0 || - url.indexOf(CorePath.concatenatePaths(siteUrl, 'webservice/pluginfile.php')) === 0) && - !CoreMedia.sourceUsesJavascriptPlayer({ src: url }); + return CoreUrl.canUseTokenPluginFile(url, siteUrl, accessKey); } /** @@ -130,43 +79,10 @@ export class CoreUrlUtilsProvider { * * @param url URL to treat. * @returns Object with the params. + * @deprecated since 4.5. Use CoreUrl.extractUrlParams instead. */ - extractUrlParams(url: string): CoreUrlParams { - const regex = /[?&]+([^=&]+)=?([^&]*)?/gi; - const subParamsPlaceholder = '@@@SUBPARAMS@@@'; - const params: CoreUrlParams = {}; - const urlAndHash = url.split('#'); - const questionMarkSplit = urlAndHash[0].split('?'); - let subParams: string; - - if (questionMarkSplit.length > 2) { - // There is more than one question mark in the URL. This can happen if any of the params is a URL with params. - // We only want to treat the first level of params, so we'll remove this second list of params and restore it later. - questionMarkSplit.splice(0, 2); - - subParams = '?' + questionMarkSplit.join('?'); - urlAndHash[0] = urlAndHash[0].replace(subParams, subParamsPlaceholder); - } - - urlAndHash[0].replace(regex, (match: string, key: string, value: string): string => { - params[key] = value !== undefined ? CoreTextUtils.decodeURIComponent(value) : ''; - - if (subParams) { - params[key] = params[key].replace(subParamsPlaceholder, subParams); - } - - return match; - }); - - if (urlAndHash.length > 1) { - // Remove the URL from the array. - urlAndHash.shift(); - - // Add the hash as a param with a special name. Use a join in case there is more than one #. - params.urlHash = urlAndHash.join('#'); - } - - return params; + extractUrlParams(url: string): CoreUrlParamsNew { + return CoreUrl.extractUrlParams(url); } /** @@ -179,40 +95,10 @@ export class CoreUrlUtilsProvider { * @param siteUrl The URL of the site the URL belongs to. * @param accessKey User access key for tokenpluginfile. * @returns Fixed URL. + * @deprecated since 4.5. Use CoreUrl.fixPluginfileURL instead. */ fixPluginfileURL(url: string, token: string, siteUrl: string, accessKey?: string): string { - if (!url) { - return ''; - } - - url = url.replace(/&/g, '&'); - - const canUseTokenPluginFile = accessKey && this.canUseTokenPluginFile(url, siteUrl, accessKey); - - // First check if we need to fix this url or is already fixed. - if (!canUseTokenPluginFile && url.indexOf('token=') != -1) { - return url; - } - - // Check if is a valid URL (contains the pluginfile endpoint) and belongs to the site. - if (!this.isPluginFileUrl(url) || url.indexOf(CoreTextUtils.addEndingSlash(siteUrl)) !== 0) { - return url; - } - - if (canUseTokenPluginFile) { - // Use tokenpluginfile.php. - url = url.replace(/(\/webservice)?\/pluginfile\.php/, '/tokenpluginfile.php/' + accessKey); - } else { - // Use pluginfile.php. Some webservices returns directly the correct download url, others not. - if (url.indexOf(CorePath.concatenatePaths(siteUrl, 'pluginfile.php')) === 0) { - url = url.replace('/pluginfile', '/webservice/pluginfile'); - } - - url = this.addParamsToUrl(url, { token }); - } - - // Always send offline=1 (it's for external repositories). - return this.addParamsToUrl(url, { offline: '1', lang: CoreLang.getCurrentLanguageSync(CoreLangFormat.LMS) }); + return CoreUrl.fixPluginfileURL(url, token, siteUrl, accessKey); } /** @@ -220,54 +106,26 @@ export class CoreUrlUtilsProvider { * * @param url The url to be formatted. * @returns Fromatted url. + * @deprecated since 4.5. Use CoreUrl.formatURL instead. */ formatURL(url: string): string { - url = url.trim(); - - // Check if the URL starts by http or https. - if (! /^http(s)?:\/\/.*/i.test(url)) { - // Test first allways https. - url = 'https://' + url; - } - - // http always in lowercase. - url = url.replace(/^http/i, 'http'); - url = url.replace(/^https/i, 'https'); - - // Replace last slash. - url = url.replace(/\/$/, ''); - - return url; + return CoreUrl.formatURL(url); } /** * Returns the URL to the documentation of the app, based on Moodle version and current language. * - * @param release Moodle release. + * The URL has been simplified and always returns the English version of the latest version of Moodle + * to simplify the circular dependencies. + * + * @param release Moodle release. Unused. * @param page Docs page to go to. * @returns Promise resolved with the Moodle docs URL. + * + * @deprecated since 4.5. You can use CoreAuthenticatedSite.getDocsUrl but is also deprecated. */ async getDocsUrl(release?: string, page: string = 'Mobile_app'): Promise { - let docsUrl = 'https://docs.moodle.org/en/' + page; - - if (release !== undefined) { - const version = CoreSites.getMajorReleaseNumber(release).replace('.', ''); - - // Check is a valid number. - if (Number(version) >= 24) { - // Append release number. - docsUrl = docsUrl.replace('https://docs.moodle.org/', 'https://docs.moodle.org/' + version + '/'); - } - } - - try { - let lang = await CoreLang.getCurrentLanguage(CoreLangFormat.LMS); - lang = CoreLang.getParentLanguage() || lang; - - return docsUrl.replace('/en/', '/' + lang + '/'); - } catch (error) { - return docsUrl; - } + return 'https://docs.moodle.org/en/' + page; } /** @@ -275,53 +133,10 @@ export class CoreUrlUtilsProvider { * * @param url URL * @returns Youtube Embed Video URL or undefined if not found. + * @deprecated since 4.5. Use CoreUrl.getYoutubeEmbedUrl instead. */ getYoutubeEmbedUrl(url?: string): string | void { - if (!url) { - return; - } - - let videoId = ''; - const params: CoreUrlParams = {}; - - url = CoreTextUtils.decodeHTML(url); - - // Get the video ID. - let match = url.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/); - - if (match && match[2].length === 11) { - videoId = match[2]; - } - - // No videoId, do not continue. - if (!videoId) { - return; - } - - // Now get the playlist (if any). - match = url.match(/[?&]list=([^#&?]+)/); - - if (match && match[1]) { - params.list = match[1]; - } - - // Now get the start time (if any). - match = url.match(/[?&]start=(\d+)/); - - if (match && match[1]) { - params.start = parseInt(match[1], 10).toString(); - } else { - // No start param, but it could have a time param. - match = url.match(/[?&]t=(\d+h)?(\d+m)?(\d+s)?/); - if (match) { - const start = (match[1] ? parseInt(match[1], 10) * 3600 : 0) + - (match[2] ? parseInt(match[2], 10) * 60 : 0) + - (match[3] ? parseInt(match[3], 10) : 0); - params.start = start.toString(); - } - } - - return this.addParamsToUrl('https://www.youtube.com/embed/' + videoId, params); + return CoreUrl.getYoutubeEmbedUrl(url); } /** @@ -331,15 +146,10 @@ export class CoreUrlUtilsProvider { * * @param url URL to treat. * @returns Last file without params. + * @deprecated since 4.5. Use CoreUrl.getLastFileWithoutParams instead. */ getLastFileWithoutParams(url: string): string { - const parsedUrl = CoreUrl.parse(url); - if (!parsedUrl) { - return ''; - } - const path = parsedUrl.path ?? ''; - - return path.split('/').pop() ?? ''; + return CoreUrl.getLastFileWithoutParams(url); } /** @@ -348,17 +158,10 @@ export class CoreUrlUtilsProvider { * * @param url URL to treat. * @returns Protocol, undefined if no protocol found. - * @todo Use CoreUrl.parse + * @deprecated since 4.5. Use CoreUrl.getUrlProtocol instead. */ getUrlProtocol(url: string): string | void { - if (!url) { - return; - } - - const matches = url.match(/^([^/:.?]*):\/\//); - if (matches && matches[1]) { - return matches[1]; - } + return CoreUrl.getUrlProtocol(url); } /** @@ -367,36 +170,21 @@ export class CoreUrlUtilsProvider { * * @param url URL to treat. * @returns Scheme, undefined if no scheme found. + * @deprecated since 4.5. Use CoreUrl.getUrlProtocol instead. */ getUrlScheme(url: string): string | void { - if (!url) { - return; - } - - const matches = url.match(/^([a-z][a-z0-9+\-.]*):/); - if (matches && matches[1]) { - return matches[1]; - } + return CoreUrl.getUrlProtocol(url); } - /* + /** * Gets a username from a URL like: user@mysite.com. * * @param url URL to treat. * @returns Username. Undefined if no username found. - * @todo Use CoreUrl.parse + * @deprecated since 4.5. Use CoreUrl.getUsernameFromUrl instead. */ getUsernameFromUrl(url: string): string | undefined { - if (url.indexOf('@') > -1) { - // Get URL without protocol. - const withoutProtocol = url.replace(/^[^?@/]*:\/\//, ''); - const matches = withoutProtocol.match(/[^@]*/); - - // Make sure that @ is at the start of the URL, not in a param at the end. - if (matches && matches.length && !matches[0].match(/[/|?]/)) { - return matches[0]; - } - } + return CoreUrl.getUsernameFromUrl(url); } /** @@ -404,9 +192,10 @@ export class CoreUrlUtilsProvider { * * @param url The url to test against the pattern. * @returns Whether the url is absolute. + * @deprecated since 4.5. Use CoreUrl.isAbsoluteURL instead. */ isAbsoluteURL(url: string): boolean { - return /^[^:]{2,}:\/\//i.test(url) || /^(tel:|mailto:|geo:)/.test(url); + return CoreUrl.isAbsoluteURL(url); } /** @@ -414,9 +203,10 @@ export class CoreUrlUtilsProvider { * * @param url The URL to test. * @returns Whether the URL is downloadable. + * @deprecated since 4.5. Use CoreUrl.isDownloadableUrl instead. */ isDownloadableUrl(url: string): boolean { - return this.isPluginFileUrl(url) || this.isTokenPluginFileUrl(url) || this.isThemeImageUrl(url) || this.isGravatarUrl(url); + return CoreUrl.isDownloadableUrl(url); } /** @@ -424,9 +214,10 @@ export class CoreUrlUtilsProvider { * * @param url The URL to test. * @returns Whether the URL is a gravatar URL. + * @deprecated since 4.5. Use CoreUrl.isGravatarUrl instead. */ isGravatarUrl(url: string): boolean { - return url?.indexOf('gravatar.com/avatar') !== -1; + return CoreUrl.isGravatarUrl(url); } /** @@ -434,10 +225,10 @@ export class CoreUrlUtilsProvider { * * @param url The url to test. * @returns Whether the url uses http or https protocol. - * @todo Use CoreUrl.parse + * @deprecated since 4.5. Use CoreUrl.isHttpURL instead. */ isHttpURL(url: string): boolean { - return /^https?:\/\/.+/i.test(url); + return CoreUrl.isHttpURL(url); } /** @@ -445,11 +236,10 @@ export class CoreUrlUtilsProvider { * * @param url URL to check. * @returns Whether the URL belongs to a local file. + * @deprecated since 4.5. Use CoreUrl.isLocalFileUrl instead. */ isLocalFileUrl(url: string): boolean { - const urlParts = CoreUrl.parse(url); - - return this.isLocalFileUrlScheme(urlParts?.protocol || '', urlParts?.domain || ''); + return CoreUrl.isLocalFileUrl(url); } /** @@ -457,18 +247,10 @@ export class CoreUrlUtilsProvider { * * @param scheme Scheme to check. * @returns Whether the scheme belongs to a local file. + * @deprecated since 4.5. Use CoreUrl.isLocalFileUrlScheme instead. */ isLocalFileUrlScheme(scheme: string, domain: string): boolean { - if (!scheme) { - return false; - } - scheme = scheme.toLowerCase(); - - return scheme == 'cdvfile' || - scheme == 'file' || - scheme == 'filesystem' || - scheme == CoreConstants.CONFIG.ioswebviewscheme || - (CorePlatform.isMobile() && scheme === 'http' && domain === 'localhost'); // @todo Get served domain from ENV. + return CoreUrl.isLocalFileUrlScheme(scheme, domain); } /** @@ -476,9 +258,10 @@ export class CoreUrlUtilsProvider { * * @param url The URL to test. * @returns Whether the URL is a pluginfile URL. + * @deprecated since 4.5. Use CoreUrl.isPluginFileUrl instead. */ isPluginFileUrl(url: string): boolean { - return url.indexOf('/pluginfile.php') !== -1; + return CoreUrl.isPluginFileUrl(url); } /** @@ -486,9 +269,10 @@ export class CoreUrlUtilsProvider { * * @param url The URL to test. * @returns Whether the URL is a tokenpluginfile URL. + * @deprecated since 4.5. Use CoreUrl.isTokenPluginFileUrl instead. */ isTokenPluginFileUrl(url: string): boolean { - return url.indexOf('/tokenpluginfile.php') !== -1; + return CoreUrl.isTokenPluginFileUrl(url); } /** @@ -497,13 +281,10 @@ export class CoreUrlUtilsProvider { * @param imageUrl The URL to test. * @param siteUrl The Site Url. * @returns Whether the URL is a theme image URL. + * @deprecated since 4.5. Use CoreUrl.isThemeImageUrl instead. */ isThemeImageUrl(imageUrl: string, siteUrl?: string): boolean { - if (siteUrl) { - return imageUrl.startsWith(`${siteUrl}/theme/image.php`); - } - - return imageUrl?.indexOf('/theme/image.php') !== -1; + return CoreUrl.isThemeImageUrl(imageUrl, siteUrl); } /** @@ -513,57 +294,10 @@ export class CoreUrlUtilsProvider { * @param param Param to get from the URL. * @param siteUrl Site URL. * @returns Param from the URL. + * @deprecated since 4.5. Use CoreUrl.getThemeImageUrlParam instead. */ getThemeImageUrlParam(imageUrl: string, param: string, siteUrl?: string): string { - if (!this.isThemeImageUrl(imageUrl, siteUrl)) { - // Cannot be guessed. - return ''; - } - - const matches = imageUrl.match('/theme/image.php/(.*)'); - if (matches?.[1]) { - // Slash arguments found. - const slasharguments = matches[1].split('/'); - - if (slasharguments.length < 4) { - // Image not found, malformed URL. - return ''; - } - - // Join from the third element to the end. - const image = slasharguments.slice(3).join('/'); - switch (param) { - case 'theme': - return slasharguments[0]; - case 'component': - return slasharguments[1]; - case 'rev': - return slasharguments[2]; - case 'image': - // Remove possible url params. - return CoreUrlUtils.removeUrlParams(image); - default: - return CoreUrlUtils.extractUrlParams(image)[param] || ''; - } - - } - - // URL arguments found. - const iconParams = CoreUrlUtils.extractUrlParams(imageUrl); - - switch (param) { - case 'theme': - return iconParams[param] || 'standard'; - case 'component': - return iconParams[param] || 'core'; - case 'rev': - return iconParams[param] || '-1'; - case 'svg': - return iconParams[param] || '1'; - case 'image': - default: - return iconParams[param] || ''; - } + return CoreUrl.getThemeImageUrlParam(imageUrl, param, siteUrl); } /** @@ -571,14 +305,10 @@ export class CoreUrlUtilsProvider { * * @param url URL to treat. * @returns Treated URL. + * @deprecated since 4.5. Use CoreUrl.removeUrlParts(url, [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain]) instead. */ removeProtocolAndWWW(url: string): string { - // Remove protocol. - url = url.replace(/^.*?:\/\//, ''); - // Remove www. - url = url.replace(/^www./, ''); - - return url; + return CoreUrl.removeUrlParts(url, [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain]); } /** @@ -586,11 +316,10 @@ export class CoreUrlUtilsProvider { * * @param url URL to treat. * @returns URL without params. + * @deprecated since 4.5. Use CoreUrl.removeUrlParts(url, [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment]) instead. */ removeUrlParams(url: string): string { - const matches = url.match(/^[^?]+/); - - return matches ? matches[0] : ''; + return CoreUrl.removeUrlParts(url, [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment]); } /** @@ -599,33 +328,16 @@ export class CoreUrlUtilsProvider { * @param url The url to be fixed. * @param siteUrl The URL of the site the URL belongs to. * @returns Modified URL. + * @deprecated since 4.5. Use CoreUrl.unfixPluginfileURL instead. */ unfixPluginfileURL(url: string, siteUrl?: string): string { - if (!url) { - return ''; - } - - url = url.replace(/&/g, '&'); - - // It site URL is supplied, check if the URL belongs to the site. - if (siteUrl && url.indexOf(CoreTextUtils.addEndingSlash(siteUrl)) !== 0) { - return url; - } - - // Check tokenpluginfile first. - url = url.replace(/\/tokenpluginfile\.php\/[^/]+\//, '/pluginfile.php/'); - - // Treat webservice/pluginfile case. - url = url.replace(/\/webservice\/pluginfile\.php\//, '/pluginfile.php/'); - - // Make sure the URL doesn't contain the token. - url = url.replace(/([?&])token=[^&]*&?/, '$1'); - - return url; + return CoreUrl.unfixPluginfileURL(url, siteUrl); } } - export const CoreUrlUtils = makeSingleton(CoreUrlUtilsProvider); -export type CoreUrlParams = {[key: string]: string}; +/** + * @deprecated since 4.5. Use CoreUrlParams on CoreUrl instead. + */ +export type CoreUrlParams = CoreUrlParamsNew; diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index ea2b2e1f4..cdced27db 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -37,7 +37,7 @@ import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreUrlUtils } from './url'; +import { CoreUrl } from '@singletons/url'; import { QRScanner } from '@features/native/plugins'; import { CoreArray } from '@singletons/array'; import { CoreText } from '@singletons/text'; @@ -1114,7 +1114,7 @@ export class CoreUtilsProvider { CoreAnalytics.logEvent({ type: CoreAnalyticsEventType.OPEN_LINK, - link: CoreUrlUtils.unfixPluginfileURL(options.originalUrl ?? url), + link: CoreUrl.unfixPluginfileURL(options.originalUrl ?? url), }); return this.iabInstance; @@ -1172,7 +1172,7 @@ export class CoreUtilsProvider { */ async openInBrowser(url: string, options: CoreUtilsOpenInBrowserOptions = {}): Promise { // eslint-disable-next-line deprecation/deprecation - const originaUrl = CoreUrlUtils.unfixPluginfileURL(options.originalUrl ?? options.browserWarningUrl ?? url); + const originaUrl = CoreUrl.unfixPluginfileURL(options.originalUrl ?? options.browserWarningUrl ?? url); if (options.showBrowserWarning || options.showBrowserWarning === undefined) { try { await CoreWindow.confirmOpenBrowserIfNeeded(originaUrl); @@ -1217,7 +1217,7 @@ export class CoreUtilsProvider { CoreAnalytics.logEvent({ type: CoreAnalyticsEventType.OPEN_LINK, - link: CoreUrlUtils.unfixPluginfileURL(url), + link: CoreUrl.unfixPluginfileURL(url), }); return; diff --git a/src/core/singletons/tests/url.test.ts b/src/core/singletons/tests/url.test.ts index 228fb7cf8..08410360b 100644 --- a/src/core/singletons/tests/url.test.ts +++ b/src/core/singletons/tests/url.test.ts @@ -14,10 +14,75 @@ import { mock } from '@/testing/utils'; import { CoreSite } from '@classes/sites/site'; -import { CoreUrl } from '@singletons/url'; +import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; describe('CoreUrl singleton', () => { + it('adds www if missing', () => { + const originalUrl = 'https://moodle.org'; + const url = CoreUrl.addOrRemoveWWW(originalUrl); + + expect(url).toEqual('https://www.moodle.org'); + }); + + it('removes www if present', () => { + const originalUrl = 'https://www.moodle.org'; + const url = CoreUrl.addOrRemoveWWW(originalUrl); + + expect(url).toEqual('https://moodle.org'); + }); + + it('adds params to URL without params', () => { + const originalUrl = 'https://moodle.org'; + const params = { + first: '1', + second: '2', + }; + const url = CoreUrl.addParamsToUrl(originalUrl, params); + + expect(url).toEqual('https://moodle.org?first=1&second=2'); + }); + + it('adds params to URL with existing params', () => { + const originalUrl = 'https://moodle.org?existing=1'; + const params = { + first: '1', + second: '2', + }; + const url = CoreUrl.addParamsToUrl(originalUrl, params); + + expect(url).toEqual('https://moodle.org?existing=1&first=1&second=2'); + }); + + it('doesn\'t change URL if no params supplied', () => { + const originalUrl = 'https://moodle.org'; + const url = CoreUrl.addParamsToUrl(originalUrl); + + expect(url).toEqual(originalUrl); + }); + + it('doesn\'t add undefined or null params', () => { + const originalUrl = 'https://moodle.org'; + const url = CoreUrl.addParamsToUrl(originalUrl, { + foo: undefined, + bar: null, + baz: 1, + }); + + expect(url).toEqual('https://moodle.org?baz=1'); + }); + + it('adds anchor to URL', () => { + const originalUrl = 'https://moodle.org'; + const params = { + first: '1', + second: '2', + }; + const url = CoreUrl.addParamsToUrl(originalUrl, params, 'myanchor'); + + expect(url).toEqual('https://moodle.org?first=1&second=2#myanchor'); + }); + it('parses standard urls', () => { expect(CoreUrl.parse('https://u1:pw1@my.subdomain.com/path/?query=search#hash')).toEqual({ protocol: 'https', @@ -83,9 +148,34 @@ describe('CoreUrl singleton', () => { }); it('removes protocol', () => { - expect(CoreUrl.removeProtocol('https://school.edu')).toEqual('school.edu'); - expect(CoreUrl.removeProtocol('ftp://school.edu')).toEqual('school.edu'); - expect(CoreUrl.removeProtocol('school.edu')).toEqual('school.edu'); + expect(CoreUrl.removeUrlParts('https://school.edu', CoreUrlPartNames.Protocol)).toEqual('school.edu'); + expect(CoreUrl.removeUrlParts('ftp://school.edu', CoreUrlPartNames.Protocol)).toEqual('school.edu'); + expect(CoreUrl.removeUrlParts('school.edu', CoreUrlPartNames.Protocol)).toEqual('school.edu'); + }); + + it('removes protocol and www', () => { + expect(CoreUrl.removeUrlParts('https://www.school.edu', [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain])) + .toEqual('school.edu'); + expect(CoreUrl.removeUrlParts('ftp://school.edu', [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain])) + .toEqual('school.edu'); + expect(CoreUrl.removeUrlParts('www.school.edu', [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain])) + .toEqual('school.edu'); + // Test that it works in a different order. + expect(CoreUrl.removeUrlParts('https://www.school.edu', [CoreUrlPartNames.WWWInDomain, CoreUrlPartNames.Protocol])) + .toEqual('school.edu'); + expect(CoreUrl.removeUrlParts('ftp://school.edu', [CoreUrlPartNames.WWWInDomain, CoreUrlPartNames.Protocol])) + .toEqual('school.edu'); + expect(CoreUrl.removeUrlParts('www.school.edu', [CoreUrlPartNames.WWWInDomain, CoreUrlPartNames.Protocol])) + .toEqual('school.edu'); + }); + + it('removes params', () => { + expect(CoreUrl.removeUrlParts('https://www.school.edu?blabla#a', [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment])) + .toEqual('https://www.school.edu'); + expect(CoreUrl.removeUrlParts('ftp://school.edu?blabla=r#a', [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment])) + .toEqual('ftp://school.edu'); + expect(CoreUrl.removeUrlParts('www.school.edu?blabla=5&gg=3', [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment])) + .toEqual('www.school.edu'); }); it('compares domains and paths', () => { @@ -108,9 +198,21 @@ describe('CoreUrl singleton', () => { }); it('removes the anchor of a URL', () => { - expect(CoreUrl.removeUrlAnchor('https://school.edu#foo')).toEqual('https://school.edu'); - expect(CoreUrl.removeUrlAnchor('https://school.edu#foo#bar')).toEqual('https://school.edu'); - expect(CoreUrl.removeUrlAnchor('https://school.edu')).toEqual('https://school.edu'); + expect(CoreUrl.removeUrlParts('https://school.edu#foo', CoreUrlPartNames.Fragment)).toEqual('https://school.edu'); + expect(CoreUrl.removeUrlParts('https://school.edu#foo#bar', CoreUrlPartNames.Fragment)).toEqual('https://school.edu'); + expect(CoreUrl.removeUrlParts('https://school.edu', CoreUrlPartNames.Fragment)).toEqual('https://school.edu'); + }); + + it('gets the username from a URL', () => { + expect(CoreUrl.getUsernameFromUrl( + 'https://username@domain.com?token=TOKEN&privatetoken=PRIVATETOKEN&redirect=http://domain.com/course/view.php?id=2', + )).toEqual('username'); + expect(CoreUrl.getUsernameFromUrl( + 'https://username:password@domain.com?token=TOKEN&privatetoken=PRIVATETOKEN&redirect=http://domain.com/course/', + )).toEqual('username'); + expect(CoreUrl.getUsernameFromUrl( + 'https://domain.com?token=TOKEN&privatetoken=PRIVATETOKEN&redirect=http://domain.com/course/view.php?id=2', + )).toEqual(undefined); }); it('converts to absolute URLs', () => { diff --git a/src/core/singletons/url.ts b/src/core/singletons/url.ts index dea4022c1..d033268dc 100644 --- a/src/core/singletons/url.ts +++ b/src/core/singletons/url.ts @@ -16,6 +16,12 @@ import { CoreSite } from '@classes/sites/site'; import { CorePath } from './path'; import { CoreText } from './text'; +import { CorePlatform } from '@services/platform'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreConstants } from '../constants'; +import { CoreMedia } from './media'; +import { CoreLang, CoreLangFormat } from '@services/lang'; + /** * Parts contained within a url. */ @@ -68,6 +74,13 @@ interface UrlParts { } +export const enum CoreUrlPartNames { + Protocol = 'protocol', + WWWInDomain = 'www', // Will remove starting www from domain. + Query = 'query', + Fragment = 'fragment', +} + /** * Singleton with helper functions for urls. */ @@ -85,8 +98,9 @@ export class CoreUrl { * @returns Url parts. */ static parse(url: string): UrlParts | null { + url = url.trim(); // Parse url with regular expression taken from RFC 3986: https://tools.ietf.org/html/rfc3986#appendix-B. - const match = url.trim().match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/); + const match = url.match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/); if (!match) { return null; @@ -120,8 +134,12 @@ export class CoreUrl { * @returns Assembled URL. */ static assemble(parts: UrlParts): string { - return (parts.protocol ? `${parts.protocol}://` : '') + - (parts.credentials ? `${parts.credentials}@` : '') + + const protocol = parts.protocol; + const credentials = parts.credentials || + (parts.password ? `${parts.username}:${parts.password}` : parts.username); + + return (protocol ? `${protocol}://` : '') + + (credentials ? `${credentials}@` : '') + (parts.domain ?? '') + (parts.port ? `:${parts.port}` : '') + (parts.path ?? '') + @@ -190,9 +208,10 @@ export class CoreUrl { * * @param url Site url. * @returns Url without protocol. + * @deprecated since 4.5. Use CoreUrl.removeUrlParts(url, CoreUrlPartNames.Protocol) instead. */ static removeProtocol(url: string): string { - return url.replace(/^[a-zA-Z]+:\/\//i, ''); + return CoreUrl.removeUrlParts(url, CoreUrlPartNames.Protocol); } /** @@ -229,12 +248,9 @@ export class CoreUrl { * @returns Anchor, undefined if no anchor. */ static getUrlAnchor(url: string): string | undefined { - const firstAnchorIndex = url.indexOf('#'); - if (firstAnchorIndex === -1) { - return; - } + const urlParts = CoreUrl.parse(url); - return url.substring(firstAnchorIndex); + return urlParts?.fragment ? `#${urlParts.fragment}` : undefined; } /** @@ -242,11 +258,11 @@ export class CoreUrl { * * @param url URL. * @returns URL without anchor if any. + * + * @deprecated since 4.5. Use CoreUrl.removeUrlParts(url, CoreUrlPartNames.Fragment) instead. */ static removeUrlAnchor(url: string): string { - const urlAndAnchor = url.split('#'); - - return urlAndAnchor[0]; + return CoreUrl.removeUrlParts(url, CoreUrlPartNames.Fragment); } /** @@ -290,13 +306,13 @@ export class CoreUrl { * @returns Relative URL. */ static toRelativeURL(parentUrl: string, url: string): string { - parentUrl = CoreUrl.removeProtocol(parentUrl); + parentUrl = CoreUrl.removeUrlParts(parentUrl, CoreUrlPartNames.Protocol); if (!url.includes(parentUrl)) { return url; // Already relative URL. } - return CoreText.removeStartingSlash(CoreUrl.removeProtocol(url).replace(parentUrl, '')); + return CoreText.removeStartingSlash(CoreUrl.removeUrlParts(url, CoreUrlPartNames.Protocol).replace(parentUrl, '')); } /** @@ -344,4 +360,564 @@ export class CoreUrl { return newUrl; } + /** + * Add or remove 'www' from a URL. The url needs to have http or https protocol. + * + * @param url URL to modify. + * @returns Modified URL. + */ + static addOrRemoveWWW(url: string): string { + if (url) { + if (url.match(/http(s)?:\/\/www\./)) { + // Already has www. Remove it. + url = url.replace('www.', ''); + } else { + url = url.replace('https://', 'https://www.'); + url = url.replace('http://', 'http://www.'); + } + } + + return url; + } + + /** + * Add params to a URL. + * + * @param url URL to add the params to. + * @param params Object with the params to add. + * @param anchor Anchor text if needed. + * @param boolToNumber Whether to convert bools to 1 or 0. + * @returns URL with params. + */ + static addParamsToUrl(url: string, params?: Record, anchor?: string, boolToNumber?: boolean): string { + // Remove any existing anchor to add the params before it. + const urlAndAnchor = url.split('#'); + url = urlAndAnchor[0]; + + let separator = url.indexOf('?') !== -1 ? '&' : '?'; + + for (const key in params) { + let value = params[key]; + + if (boolToNumber && typeof value === 'boolean') { + // Convert booleans to 1 or 0. + value = value ? '1' : '0'; + } + + // Ignore objects and undefined. + if (typeof value !== 'object' && value !== undefined) { + url += separator + key + '=' + value; + separator = '&'; + } + } + + // Re-add the anchor if any. + if (urlAndAnchor.length > 1) { + // Remove the URL from the array. + urlAndAnchor.shift(); + + // Use a join in case there is more than one #. + url += '#' + urlAndAnchor.join('#'); + } + + if (anchor) { + url += '#' + anchor; + } + + return url; + } + + /** + * Given a URL and a text, return an HTML link. + * + * @param url URL. + * @param text Text of the link. + * @returns Link. + */ + static buildLink(url: string, text: string): string { + return '' + text + ''; + } + + /** + * Check whether we can use tokenpluginfile.php endpoint for a certain URL. + * + * @param url URL to check. + * @param siteUrl The URL of the site the URL belongs to. + * @param accessKey User access key for tokenpluginfile. + * @returns Whether tokenpluginfile.php can be used. + */ + static canUseTokenPluginFile(url: string, siteUrl: string, accessKey?: string): boolean { + // Do not use tokenpluginfile if site doesn't use slash params, the URL doesn't work. + // Also, only use it for "core" pluginfile endpoints. Some plugins can implement their own endpoint (like customcert). + return !CoreConstants.CONFIG.disableTokenFile && !!accessKey && !url.match(/[&?]file=/) && ( + url.indexOf(CorePath.concatenatePaths(siteUrl, 'pluginfile.php')) === 0 || + url.indexOf(CorePath.concatenatePaths(siteUrl, 'webservice/pluginfile.php')) === 0) && + !CoreMedia.sourceUsesJavascriptPlayer({ src: url }); + } + + /** + * Extracts the parameters from a URL and stores them in an object. + * + * @param url URL to treat. + * @returns Object with the params. + */ + static extractUrlParams(url: string): CoreUrlParams { + const regex = /[?&]+([^=&]+)=?([^&]*)?/gi; + const subParamsPlaceholder = '@@@SUBPARAMS@@@'; + const params: CoreUrlParams = {}; + const urlAndHash = url.split('#'); + const questionMarkSplit = urlAndHash[0].split('?'); + let subParams: string; + + if (questionMarkSplit.length > 2) { + // There is more than one question mark in the URL. This can happen if any of the params is a URL with params. + // We only want to treat the first level of params, so we'll remove this second list of params and restore it later. + questionMarkSplit.splice(0, 2); + + subParams = '?' + questionMarkSplit.join('?'); + urlAndHash[0] = urlAndHash[0].replace(subParams, subParamsPlaceholder); + } + + urlAndHash[0].replace(regex, (match: string, key: string, value: string): string => { + params[key] = value !== undefined ? CoreTextUtils.decodeURIComponent(value) : ''; + + if (subParams) { + params[key] = params[key].replace(subParamsPlaceholder, subParams); + } + + return match; + }); + + if (urlAndHash.length > 1) { + // Remove the URL from the array. + urlAndHash.shift(); + + // Add the hash as a param with a special name. Use a join in case there is more than one #. + params.urlHash = urlAndHash.join('#'); + } + + return params; + } + + /** + * Generic function for adding the wstoken to Moodle urls and for pointing to the correct script. + * For download remote files from Moodle we need to use the special /webservice/pluginfile passing + * the ws token as a get parameter. + * + * @param url The url to be fixed. + * @param token Token to use. + * @param siteUrl The URL of the site the URL belongs to. + * @param accessKey User access key for tokenpluginfile. + * @returns Fixed URL. + */ + static fixPluginfileURL(url: string, token: string, siteUrl: string, accessKey?: string): string { + if (!url) { + return ''; + } + + url = url.replace(/&/g, '&'); + + const canUseTokenPluginFile = accessKey && CoreUrl.canUseTokenPluginFile(url, siteUrl, accessKey); + + // First check if we need to fix this url or is already fixed. + if (!canUseTokenPluginFile && url.indexOf('token=') != -1) { + return url; + } + + // Check if is a valid URL (contains the pluginfile endpoint) and belongs to the site. + if (!CoreUrl.isPluginFileUrl(url) || url.indexOf(CoreTextUtils.addEndingSlash(siteUrl)) !== 0) { + return url; + } + + if (canUseTokenPluginFile) { + // Use tokenpluginfile.php. + url = url.replace(/(\/webservice)?\/pluginfile\.php/, '/tokenpluginfile.php/' + accessKey); + } else { + // Use pluginfile.php. Some webservices returns directly the correct download url, others not. + if (url.indexOf(CorePath.concatenatePaths(siteUrl, 'pluginfile.php')) === 0) { + url = url.replace('/pluginfile', '/webservice/pluginfile'); + } + + url = CoreUrl.addParamsToUrl(url, { token }); + } + + // Always send offline=1 (it's for external repositories). + return CoreUrl.addParamsToUrl(url, { offline: '1', lang: CoreLang.getCurrentLanguageSync(CoreLangFormat.LMS) }); + } + + /** + * Formats a URL, trim, lowercase, etc... + * + * @param url The url to be formatted. + * @returns Fromatted url. + */ + static formatURL(url: string): string { + url = url.trim(); + + // Check if the URL starts by http or https. + if (! /^http(s)?:\/\/.*/i.test(url)) { + // Test first allways https. + url = 'https://' + url; + } + + // http always in lowercase. + url = url.replace(/^http/i, 'http'); + url = url.replace(/^https/i, 'https'); + + // Replace last slash. + url = url.replace(/\/$/, ''); + + return url; + } + + /** + * Returns the Youtube Embed Video URL or undefined if not found. + * + * @param url URL + * @returns Youtube Embed Video URL or undefined if not found. + */ + static getYoutubeEmbedUrl(url?: string): string | void { + if (!url) { + return; + } + + let videoId = ''; + const params: CoreUrlParams = {}; + + url = CoreTextUtils.decodeHTML(url); + + // Get the video ID. + let match = url.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/); + + if (match && match[2].length === 11) { + videoId = match[2]; + } + + // No videoId, do not continue. + if (!videoId) { + return; + } + + // Now get the playlist (if any). + match = url.match(/[?&]list=([^#&?]+)/); + + if (match && match[1]) { + params.list = match[1]; + } + + // Now get the start time (if any). + match = url.match(/[?&]start=(\d+)/); + + if (match && match[1]) { + params.start = parseInt(match[1], 10).toString(); + } else { + // No start param, but it could have a time param. + match = url.match(/[?&]t=(\d+h)?(\d+m)?(\d+s)?/); + if (match) { + const start = (match[1] ? parseInt(match[1], 10) * 3600 : 0) + + (match[2] ? parseInt(match[2], 10) * 60 : 0) + + (match[3] ? parseInt(match[3], 10) : 0); + params.start = start.toString(); + } + } + + return CoreUrl.addParamsToUrl('https://www.youtube.com/embed/' + videoId, params); + } + + /** + * Given a URL, returns what's after the last '/' without params. + * Example: + * http://mysite.com/a/course.html?id=1 -> course.html + * + * @param url URL to treat. + * @returns Last file without params. + */ + static getLastFileWithoutParams(url: string): string { + const parsedUrl = CoreUrl.parse(url); + if (!parsedUrl) { + return ''; + } + const path = parsedUrl.path ?? ''; + + return path.split('/').pop() ?? ''; + } + + /** + * Get the protocol from a URL. + * E.g. http://www.google.com returns 'http'. + * + * @param url URL to treat. + * @returns Protocol, undefined if no protocol found. + */ + static getUrlProtocol(url: string): string | void { + return CoreUrl.parse(url)?.protocol; + } + + /** + * Gets a username from a URL like: user@mysite.com. + * + * @param url URL to treat. + * @returns Username. Undefined if no username found. + * @todo Use CoreUrl.parse. It cannot use it right now because it won't detect username on custom URL with double protocol. + */ + static getUsernameFromUrl(url: string): string | undefined { + if (url.indexOf('@') < 0) { + return; + } + + // Get URL without protocol. + const withoutProtocol = url.replace(/^[^?@/]*:\/\//, ''); + const matches = withoutProtocol.match(/[^@]*/); + + // Make sure that @ is at the start of the URL, not in a param at the end. + if (matches && matches.length && !matches[0].match(/[/|?]/)) { + const credentials = matches[0]; + + return credentials.split(':')[0]; + } + } + + /** + * Returns if a URL has any protocol (not a relative URL). + * + * @param url The url to test against the pattern. + * @returns Whether the url is absolute. + */ + static isAbsoluteURL(url: string): boolean { + return /^[^:]{2,}:\/\//i.test(url) || /^(tel:|mailto:|geo:)/.test(url); + } + + /** + * Returns if a URL is downloadable: plugin file OR theme/image.php OR gravatar. + * + * @param url The URL to test. + * @returns Whether the URL is downloadable. + */ + static isDownloadableUrl(url: string): boolean { + return CoreUrl.isPluginFileUrl(url) || + CoreUrl.isTokenPluginFileUrl(url) || + CoreUrl.isThemeImageUrl(url) || + CoreUrl.isGravatarUrl(url); + } + + /** + * Returns if a URL is a gravatar URL. + * + * @param url The URL to test. + * @returns Whether the URL is a gravatar URL. + */ + static isGravatarUrl(url: string): boolean { + return url?.indexOf('gravatar.com/avatar') !== -1; + } + + /** + * Check if a URL uses http or https protocol. + * + * @param url The url to test. + * @returns Whether the url uses http or https protocol. + * @todo Use CoreUrl.parse + */ + static isHttpURL(url: string): boolean { + return /^https?:\/\/.+/i.test(url); + } + + /** + * Check whether an URL belongs to a local file. + * + * @param url URL to check. + * @returns Whether the URL belongs to a local file. + */ + static isLocalFileUrl(url: string): boolean { + const urlParts = CoreUrl.parse(url); + + return CoreUrl.isLocalFileUrlScheme(urlParts?.protocol || '', urlParts?.domain || ''); + } + + /** + * Check whether a URL scheme belongs to a local file. + * + * @param scheme Scheme to check. + * @returns Whether the scheme belongs to a local file. + */ + static isLocalFileUrlScheme(scheme: string, domain: string): boolean { + if (!scheme) { + return false; + } + scheme = scheme.toLowerCase(); + + return scheme === 'cdvfile' || + scheme === 'file' || + scheme === 'filesystem' || + scheme === CoreConstants.CONFIG.ioswebviewscheme || + (CorePlatform.isMobile() && scheme === 'http' && domain === 'localhost'); // @todo Get served domain from ENV. + } + + /** + * Returns if a URL is a pluginfile URL. + * + * @param url The URL to test. + * @returns Whether the URL is a pluginfile URL. + */ + static isPluginFileUrl(url: string): boolean { + return url.indexOf('/pluginfile.php') !== -1; + } + + /** + * Returns if a URL is a tokenpluginfile URL. + * + * @param url The URL to test. + * @returns Whether the URL is a tokenpluginfile URL. + */ + static isTokenPluginFileUrl(url: string): boolean { + return url.indexOf('/tokenpluginfile.php') !== -1; + } + + /** + * Returns if a URL is a theme image URL. + * + * @param imageUrl The URL to test. + * @param siteUrl The Site Url. + * @returns Whether the URL is a theme image URL. + */ + static isThemeImageUrl(imageUrl: string, siteUrl?: string): boolean { + if (siteUrl) { + return imageUrl.startsWith(`${siteUrl}/theme/image.php`); + } + + return imageUrl?.indexOf('/theme/image.php') !== -1; + } + + /** + * Returns an specific param from an image URL. + * + * @param imageUrl Image Url + * @param param Param to get from the URL. + * @param siteUrl Site URL. + * @returns Param from the URL. + */ + static getThemeImageUrlParam(imageUrl: string, param: string, siteUrl?: string): string { + if (!CoreUrl.isThemeImageUrl(imageUrl, siteUrl)) { + // Cannot be guessed. + return ''; + } + + const matches = imageUrl.match('/theme/image.php/(.*)'); + if (matches?.[1]) { + // Slash arguments found. + const slasharguments = matches[1].split('/'); + + if (slasharguments.length < 4) { + // Image not found, malformed URL. + return ''; + } + + // Join from the third element to the end. + const image = slasharguments.slice(3).join('/'); + switch (param) { + case 'theme': + return slasharguments[0]; + case 'component': + return slasharguments[1]; + case 'rev': + return slasharguments[2]; + case 'image': + // Remove possible url params. + return CoreUrl.removeUrlParts(image, [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment]); + default: + return CoreUrl.extractUrlParams(image)[param] || ''; + } + + } + + // URL arguments found. + const iconParams = CoreUrl.extractUrlParams(imageUrl); + + switch (param) { + case 'theme': + return iconParams[param] || 'standard'; + case 'component': + return iconParams[param] || 'core'; + case 'rev': + return iconParams[param] || '-1'; + case 'svg': + return iconParams[param] || '1'; + case 'image': + default: + return iconParams[param] || ''; + } + } + + /** + * Returns the URL without the desired parts. + * + * @param url URL to treat. + * @param parts Parts to remove. + * @returns URL without the parts. + */ + static removeUrlParts(url: string, parts: CoreUrlPartNames | CoreUrlPartNames[]): string { + if (!url) { + return url; + } + + if (!Array.isArray(parts)) { + parts = [parts]; + } + + parts.forEach((part) => { + switch (part) { + case CoreUrlPartNames.WWWInDomain: + // Remove www, no protocol. + url = url.replace(/^www./, ''); + // Remove www, with protocol. + url = url.replace(/\/\/www./, '//'); + break; + case CoreUrlPartNames.Protocol: + // Remove the protocol from url + url = url.replace(/^.*?:\/\//, ''); + break; + case CoreUrlPartNames.Query: + url = url.match(/^[^?]+/)?.[0] || ''; + break; + case CoreUrlPartNames.Fragment: + url = url.split('#')[0]; + break; + } + }); + + return url; + } + + /** + * Modifies a pluginfile URL to use the default pluginfile script instead of the webservice one. + * + * @param url The url to be fixed. + * @param siteUrl The URL of the site the URL belongs to. + * @returns Modified URL. + */ + static unfixPluginfileURL(url: string, siteUrl?: string): string { + if (!url) { + return ''; + } + + url = url.replace(/&/g, '&'); + + // It site URL is supplied, check if the URL belongs to the site. + if (siteUrl && url.indexOf(CoreTextUtils.addEndingSlash(siteUrl)) !== 0) { + return url; + } + + // Check tokenpluginfile first. + url = url.replace(/\/tokenpluginfile\.php\/[^/]+\//, '/pluginfile.php/'); + + // Treat webservice/pluginfile case. + url = url.replace(/\/webservice\/pluginfile\.php\//, '/pluginfile.php/'); + + // Make sure the URL doesn't contain the token. + url = url.replace(/([?&])token=[^&]*&?/, '$1'); + + return url; + } + } + +export type CoreUrlParams = {[key: string]: string}; diff --git a/src/core/singletons/window.ts b/src/core/singletons/window.ts index e3323e20b..c520b9741 100644 --- a/src/core/singletons/window.ts +++ b/src/core/singletons/window.ts @@ -18,7 +18,7 @@ import { CoreConfig } from '@services/config'; import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreConstants } from '../constants'; @@ -40,7 +40,7 @@ export class CoreWindow { * @returns Promise resolved if confirmed, rejected if rejected. */ static async confirmOpenBrowserIfNeeded(url: string): Promise { - if (!CoreUrlUtils.isHttpURL(url)) { + if (!CoreUrl.isHttpURL(url)) { // Only ask confirm for http(s), other cases usually launch external apps. return; } @@ -76,7 +76,7 @@ export class CoreWindow { * @returns Promise resolved when done. */ static async open(url: string, name?: string): Promise { - if (CoreUrlUtils.isLocalFileUrl(url)) { + if (CoreUrl.isLocalFileUrl(url)) { const filename = url.substring(url.lastIndexOf('/') + 1); if (!CoreFileHelper.isOpenableInApp({ filename })) {