diff --git a/scripts/langindex.json b/scripts/langindex.json index aba8a3680..93ec25362 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -40,10 +40,10 @@ "addon.block_learningplans.pluginname": "block_lp", "addon.block_myoverview.all": "block_myoverview", "addon.block_myoverview.allincludinghidden": "block_myoverview", - "addon.block_myoverview.aria:favourites": "block_myoverview", - "addon.block_myoverview.aria:hiddencourses": "block_myoverview", "addon.block_myoverview.card": "block_myoverview", + "addon.block_myoverview.favourites": "block_myoverview", "addon.block_myoverview.future": "block_myoverview", + "addon.block_myoverview.hiddencourses": "block_myoverview", "addon.block_myoverview.inprogress": "block_myoverview", "addon.block_myoverview.lastaccessed": "block_myoverview", "addon.block_myoverview.list": "block_myoverview", @@ -1451,7 +1451,6 @@ "core.allparticipants": "moodle", "core.answer": "moodle", "core.answered": "quiz", - "core.applyfilters": "user", "core.areyousure": "moodle", "core.back": "moodle", "core.block.blocks": "moodle", diff --git a/src/addons/block/myoverview/components/components.module.ts b/src/addons/block/myoverview/components/components.module.ts index 49e53a2f4..c1829bd61 100644 --- a/src/addons/block/myoverview/components/components.module.ts +++ b/src/addons/block/myoverview/components/components.module.ts @@ -17,12 +17,10 @@ import { NgModule } from '@angular/core'; import { CoreSharedModule } from '@/core/shared.module'; import { CoreCoursesComponentsModule } from '@features/courses/components/components.module'; import { AddonBlockMyOverviewComponent } from './myoverview/myoverview'; -import { AddonBlockMyOverviewFilterOptionsComponent } from './filteroptions/filteroptions'; @NgModule({ declarations: [ AddonBlockMyOverviewComponent, - AddonBlockMyOverviewFilterOptionsComponent, ], imports: [ CoreSharedModule, diff --git a/src/addons/block/myoverview/components/filteroptions/filteroptions.html b/src/addons/block/myoverview/components/filteroptions/filteroptions.html deleted file mode 100644 index 2a6caf36b..000000000 --- a/src/addons/block/myoverview/components/filteroptions/filteroptions.html +++ /dev/null @@ -1,58 +0,0 @@ - - - -

{{ 'core.courses.filtermycourses' | translate }}

-
- - - - - -
-
- - - - - {{'addon.block_myoverview.all' | translate}} - - - - {{'addon.block_myoverview.inprogress' | translate}} - - - - {{'addon.block_myoverview.future' | translate}} - - - - {{'addon.block_myoverview.past' | translate}} - - - - - - - - {{customOption.name}} - - - - - - - {{ 'addon.block_myoverview.aria:favourites' | translate }} - - - - - {{ 'addon.block_myoverview.aria:hiddencourses' | translate }} - - - - - - - {{ 'core.applyfilters' | translate }} - - diff --git a/src/addons/block/myoverview/components/filteroptions/filteroptions.ts b/src/addons/block/myoverview/components/filteroptions/filteroptions.ts deleted file mode 100644 index 93b9d753a..000000000 --- a/src/addons/block/myoverview/components/filteroptions/filteroptions.ts +++ /dev/null @@ -1,44 +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 { Component, Input } from '@angular/core'; -import { ModalController } from '@singletons'; -import { AddonBlockMyOverviewFilterOptions } from '../myoverview/myoverview'; - -/** - * Component to render a my overview filter options. - */ -@Component({ - selector: 'addon-block-myoverview-filter-options', - templateUrl: 'filteroptions.html', -}) -export class AddonBlockMyOverviewFilterOptionsComponent { - - @Input() options!: AddonBlockMyOverviewFilterOptions; - - /** - * Appl filters. - */ - apply(): void { - ModalController.dismiss(this.options); - } - - /** - * Close modal. - */ - closeModal(): void { - ModalController.dismiss(); - } - -} diff --git a/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html b/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html index 3718ea8e7..d4328e235 100644 --- a/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html +++ b/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html @@ -22,16 +22,55 @@ + + + + + + + - + + + {{ 'addon.block_myoverview.allincludinghidden' | translate }} + + + {{ 'addon.block_myoverview.all' | translate }} + + + {{ 'addon.block_myoverview.inprogress' | translate }} + + + {{ 'addon.block_myoverview.future' | translate }} + + + {{ 'addon.block_myoverview.past' | translate }} + + + + + {{ customOption.name }} + + + + {{ 'addon.block_myoverview.favourites' | translate }} + + + {{ 'addon.block_myoverview.hiddencourses' | translate }} + + (ionCancel)="filterTextChanged($event.target)" [placeholder]="'core.courses.filtermycourses' | translate"> @@ -59,17 +98,14 @@ - - - - - - - + + + {{'core.courses.searchcourses' | translate}} + diff --git a/src/addons/block/myoverview/components/myoverview/myoverview.scss b/src/addons/block/myoverview/components/myoverview/myoverview.scss index 9c2cb0d56..06c764187 100644 --- a/src/addons/block/myoverview/components/myoverview/myoverview.scss +++ b/src/addons/block/myoverview/components/myoverview/myoverview.scss @@ -1,21 +1,35 @@ :host { ion-row.addon-block-myoverview-filter { - padding: 4px 8px 0px 8px; + margin: 8px; + padding: 0; ion-col { - padding: 0 2px; + padding: 0; } ion-button, core-combobox ::ng-deep ion-button { + --border-width: 0; + --a11y-min-target-size: 40px; margin: 0; + + .select-icon { + display: none; + } ion-icon { font-size: 20px; } } - ::ng-deep ion-searchbar { + core-combobox ::ng-deep ion-select { + margin: 0; + --a11y-min-target-size: 40px; + } + + ion-searchbar { padding: 0; + --height: 40px; } } + } diff --git a/src/addons/block/myoverview/components/myoverview/myoverview.ts b/src/addons/block/myoverview/components/myoverview/myoverview.ts index 8c97fa6c6..14bb2c6ff 100644 --- a/src/addons/block/myoverview/components/myoverview/myoverview.ts +++ b/src/addons/block/myoverview/components/myoverview/myoverview.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Component, OnInit, OnDestroy } from '@angular/core'; -import { ModalOptions } from '@ionic/core'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; @@ -27,11 +26,12 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion'; -import { AddonBlockMyOverviewFilterOptionsComponent } from '../filteroptions/filteroptions'; import { IonSearchbar } from '@ionic/angular'; import moment from 'moment'; +import { CoreNavigator } from '@services/navigator'; -const FILTER_PRIORITY: AddonBlockMyOverviewTimeFilters[] = ['all', 'inprogress', 'future', 'past']; +const FILTER_PRIORITY: AddonBlockMyOverviewTimeFilters[] = + ['all', 'inprogress', 'future', 'past', 'favourite', 'allincludinghidden', 'hidden']; /** * Component to render a my overview block. @@ -58,6 +58,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem filters: AddonBlockMyOverviewFilterOptions = { enabled: false, show: { // Options are visible, disabled, hidden. + allincludinghidden: true, all: true, past: true, inprogress: true, @@ -67,14 +68,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem custom: false, }, timeFilterSelected: 'inprogress', - favouriteSelected: false, - hiddenSelected: false, customFilters: [], - count: 0, - }; - - filterModalOptions: ModalOptions = { - component: AddonBlockMyOverviewFilterOptionsComponent, }; isLayoutSwitcherAvailable = false; @@ -88,6 +82,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem textFilter = ''; hasCourses = false; + searchEnabled = false; protected currentSite!: CoreSite; protected allCourses: CoreEnrolledCourseDataWithOptions[] = []; @@ -98,6 +93,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem protected fetchContentDefaultError = 'Error getting my overview data.'; protected gradePeriodAfter = 0; protected gradePeriodBefore = 0; + protected today = 0; constructor() { super('AddonBlockMyOverviewComponent'); @@ -110,12 +106,13 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem // Refresh the enabled flags if enabled. this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite(); this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite(); + this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite(); // Refresh the enabled flags if site is updated. this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite(); this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite(); - + this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite(); }, CoreSites.getCurrentSiteId()); this.coursesObserver = CoreEvents.on( @@ -148,43 +145,11 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem return; })); - // Wait for the migration. - await this.currentSite.getLocalSiteConfig( + promises.push(this.currentSite.getLocalSiteConfig( 'AddonBlockMyOverviewFilter', this.filters.timeFilterSelected, - ).then(async (value) => { - if (FILTER_PRIORITY.includes(value as AddonBlockMyOverviewTimeFilters)) { - this.filters.timeFilterSelected = value as AddonBlockMyOverviewTimeFilters; - - return; - } - - // Migrate setting. - this.filters.hiddenSelected = value == 'allincludinghidden' || value == 'hidden'; - - if (value == 'favourite') { - this.filters.favouriteSelected = true; - } else { - this.filters.favouriteSelected = false; - } - - return await this.saveFilters('all'); - }); - - promises.push(this.currentSite.getLocalSiteConfig( - 'AddonBlockMyOverviewFavouriteFilter', - this.filters.favouriteSelected ? 1 : 0, ).then((value) => { - this.filters.favouriteSelected = value == 1; - - return; - })); - - promises.push(this.currentSite.getLocalSiteConfig( - 'AddonBlockMyOverviewHiddenFilter', - this.filters.hiddenSelected ? 1 : 0, - ).then((value) => { - this.filters.hiddenSelected = value == 1; + this.filters.timeFilterSelected = value; return; })); @@ -312,6 +277,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem config?.displaygroupingallincludinghidden?.value == '1' || sampleCourse.hidden !== undefined && (!config || config.displaygroupinghidden?.value == '1'); + this.filters.show.allincludinghidden = !config || config.displaygroupingallincludinghidden?.value == '1'; this.filters.show.all = !config || config.displaygroupingall?.value == '1'; this.filters.show.inprogress = !config || config.displaygroupinginprogress?.value == '1'; this.filters.show.past = !config || config.displaygroupingpast?.value == '1'; @@ -335,10 +301,6 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem this.saveFilters('all'); } - this.filterModalOptions.componentProps = { - options: Object.assign({}, this.filters), - }; - this.filterCourses(); } @@ -472,81 +434,66 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem * Set selected courses filter. */ protected async filterCourses(): Promise { - this.filters.count = 0; - let timeFilter = this.filters.timeFilterSelected; - // Filter is not active, take the first active or all. - if (!this.filters.show[timeFilter]) { - timeFilter = FILTER_PRIORITY.find((name) => this.filters.show[name]) || 'all'; - - this.saveFilters(timeFilter); - } - - if (timeFilter !== 'all') { - this.filters.count++; - } - this.filteredCourses = this.allCourses; - const customFilterName = this.block.configsRecord?.customfiltergrouping.value; - const customFilterValue = this.filters.customSelected; - if (customFilterName && this.filters.show.custom && customFilterValue !== undefined) { - this.filters.count++; + if (this.filters.show.custom && timeFilter.startsWith('custom-')) { + // Custom filter. + const customFilterName = this.block.configsRecord?.customfiltergrouping.value; + const customFilterValue = this.filters.customFilters[timeFilter.substring(7)]?.value; - this.loaded = false; - try { - const courses = await CoreCourses.getEnrolledCoursesByCustomField(customFilterName, customFilterValue); + if (customFilterName !== undefined && customFilterValue !== undefined) { + this.loaded = false; + try { + const courses = await CoreCourses.getEnrolledCoursesByCustomField(customFilterName, customFilterValue); - const courseIds = courses.map((course) => course.id); + // Get the courses information from allincludinghidden to get the max info about the course. + const courseIds = courses.map((course) => course.id); - this.filteredCourses = this.filteredCourses.filter((course) => courseIds.includes(course.id)); - } catch (error) { - CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError); - } finally { - this.loaded = true; - } - } - - const onlyFavourite = this.filters.show.favourite && this.filters.favouriteSelected; - if (onlyFavourite) { - this.filters.count++; - } - - const showHidden = this.filters.show.hidden && this.filters.hiddenSelected; - if (showHidden) { - this.filters.count++; - } - - // Time filter, favourite and hidden. - const today = CoreTimeUtils.timestamp(); - - this.filteredCourses = this.filteredCourses.filter((course) => { - let include = timeFilter == 'all'; - - if (!include) { - if ((course.enddate && this.courseClassifyEndDate(course.enddate) < today) || course.completed) { - // Courses that have already ended. - include = timeFilter == 'past'; - } else if (course.startdate && this.courseClassifyStartDate(course.startdate) > today) { - // Courses that have not started yet. - include = timeFilter == 'future'; - } else { - // Courses still in progress. - include = timeFilter == 'inprogress'; + this.filteredCourses = this.filteredCourses.filter((course) => courseIds.includes(course.id)); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError); + } finally { + this.loaded = true; } } - - if (onlyFavourite) { - include = include && !!course.isfavourite; + } else { + // Filter is not active, take the first active or all. Custom is never saved. + if (!this.filters.show[timeFilter]) { + timeFilter = FILTER_PRIORITY.find((name) => this.filters.show[name]) || 'all'; } + this.saveFilters(timeFilter); - if (!showHidden) { - include = include && !course.hidden; + // Update today date. + this.today = Date.now(); + + // Apply filters. + switch(timeFilter) { + case 'allincludinghidden': + // No nothing, it's all courses. + break; + case 'all': + this.filteredCourses = this.filteredCourses.filter((course) => !course.hidden); + break; + case 'inprogress': + this.filteredCourses = this.filteredCourses.filter((course) => + !course.hidden && !this.isPastCourse(course) && !this.isFutureCourse(course)); + break; + case 'future': + this.filteredCourses = this.filteredCourses.filter((course) => !course.hidden && this.isFutureCourse(course)); + break; + case 'past': + this.filteredCourses = this.filteredCourses.filter((course) => !course.hidden && this.isPastCourse(course)); + break; + case 'favourite': + this.filteredCourses = this.filteredCourses.filter((course) => !course.hidden && course.isfavourite); + break; + case 'hidden': + this.filteredCourses = this.filteredCourses.filter((course) => course.hidden); + break; } - - return include; - }); + } // Text filter. const value = this.textFilter.trim().toLowerCase(); @@ -569,23 +516,41 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem } /** - * This function calculates the end date to use for display classification purposes, incorporating the grace period, if any. + * Calculates if course date is past. * - * @param endDate Course end date. - * @return The new enddate. + * @param course Course Object. + * @return Wether the course is past. */ - protected courseClassifyEndDate(endDate: number): number { - return moment(endDate).add(this.gradePeriodAfter, 'days').valueOf(); + protected isPastCourse(course: CoreEnrolledCourseDataWithOptions): boolean { + if (course.completed) { + return true; + } + + if (!course.enddate) { + return false; + } + + // Calculate the end date to use for display classification purposes, incorporating the grace period, if any. + const endDate = moment(course.enddate * 1000).add(this.gradePeriodAfter, 'days').valueOf(); + + return endDate < this.today; } /** - * This function calculates the start date to use for display classification purposes, incorporating the grace period, if any. + * Calculates if course date is future. * - * @param startDate Course start date. - * @return The new startdate. + * @param course Course Object. + * @return Wether the course is future. */ - protected courseClassifyStartDate(startDate: number): number { - return moment(startDate).subtract(this.gradePeriodBefore, 'days').valueOf(); + protected isFutureCourse(course: CoreEnrolledCourseDataWithOptions): boolean { + if (this.isPastCourse(course) || !course.startdate) { + return false; + } + + // Calculate the start date to use for display classification purposes, incorporating the grace period, if any. + const startDate = moment(course.startdate * 1000).subtract(this.gradePeriodBefore, 'days').valueOf(); + + return startDate > this.today; } /** @@ -627,18 +592,9 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem * @param timeFilter New time filter. * @return Promise resolved when done. */ - async saveFilters(timeFilter: AddonBlockMyOverviewTimeFilters): Promise { + async saveFilters(timeFilter: string): Promise { this.filters.timeFilterSelected = timeFilter; - - this.filterModalOptions.componentProps = { - options: Object.assign({}, this.filters), - }; - - await Promise.all([ - this.currentSite.setLocalSiteConfig('AddonBlockMyOverviewFilter', this.filters.timeFilterSelected), - this.currentSite.setLocalSiteConfig('AddonBlockMyOverviewFavouriteFilter', this.filters.favouriteSelected ? 1 : 0), - this.currentSite.setLocalSiteConfig('AddonBlockMyOverviewHiddenFilter', this.filters.hiddenSelected ? 1 : 0), - ]); + await this.currentSite.setLocalSiteConfig('AddonBlockMyOverviewFilter', timeFilter); } /** @@ -666,16 +622,23 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem } /** - * Opens display Options modal. + * Option selected save and apply filter. * + * @param selected Option selected. * @return Promise resolved when done. */ - filterOptionsChanged(modalData: AddonBlockMyOverviewFilterOptions): void { - this.filters = modalData; - this.saveFilters(this.filters.timeFilterSelected); + async filterOptionsChanged(selected: AddonBlockMyOverviewTimeFilters): Promise { + this.filters.timeFilterSelected = selected; this.filterCourses(); } + /** + * Go to search courses. + */ + async openSearch(): Promise { + CoreNavigator.navigateToSitePath('courses/list', { params : { mode: 'search' } }); + } + /** * @inheritdoc */ @@ -688,11 +651,12 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem } type AddonBlockMyOverviewLayouts = 'card'|'list'; -type AddonBlockMyOverviewTimeFilters = 'all'|'inprogress'|'future'|'past'; +type AddonBlockMyOverviewTimeFilters = 'allincludinghidden'|'all'|'inprogress'|'future'|'past'|'favourite'|'hidden'; export type AddonBlockMyOverviewFilterOptions = { enabled: boolean; show: { + allincludinghidden: boolean; all: boolean; inprogress: boolean; future: boolean; @@ -701,15 +665,11 @@ export type AddonBlockMyOverviewFilterOptions = { hidden: boolean; custom: boolean; }; - timeFilterSelected: AddonBlockMyOverviewTimeFilters; - favouriteSelected: boolean; - hiddenSelected: boolean; + timeFilterSelected: string; customFilters: { name: string; value: string; }[]; - customSelected?: string; - count: number; }; type AddonBlockMyOverviewSortOptions = { diff --git a/src/addons/block/myoverview/lang.json b/src/addons/block/myoverview/lang.json index 8030a7813..40c94300b 100644 --- a/src/addons/block/myoverview/lang.json +++ b/src/addons/block/myoverview/lang.json @@ -1,10 +1,10 @@ { "all": "All", "allincludinghidden": "All (including archived)", - "aria:favourites": "Show starred courses only", - "aria:hiddencourses": "Show archived courses", "card": "Card", + "favourites": "Starred", "future": "Future", + "hiddencourses": "Archived", "inprogress": "In progress", "lastaccessed": "Last accessed", "list": "List", diff --git a/src/addons/messages/pages/contacts/contacts.html b/src/addons/messages/pages/contacts/contacts.html index 8a3cacd95..0715dea69 100644 --- a/src/addons/messages/pages/contacts/contacts.html +++ b/src/addons/messages/pages/contacts/contacts.html @@ -24,7 +24,7 @@ - + @@ -62,7 +62,7 @@ - + diff --git a/src/addons/messages/pages/discussion/discussion.scss b/src/addons/messages/pages/discussion/discussion.scss index 2db2276f0..f3a030dc3 100644 --- a/src/addons/messages/pages/discussion/discussion.scss +++ b/src/addons/messages/pages/discussion/discussion.scss @@ -27,24 +27,28 @@ @include position(0, 0, null, null); } - ion-header ion-toolbar h1 { - display: flex; - align-items: center; + ion-header ion-toolbar ion-title { padding: 0; - .core-bar-button-image { - @include margin-horizontal(null, 6px); - } + h1 { + display: flex; + align-items: center; + padding: 0; - core-format-text { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex-shrink: 1; - } + .core-bar-button-image { + @include margin-horizontal(null, 6px); + } - ion-icon { - @include margin-horizontal(6px, null); + core-format-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex-shrink: 1; + } + + ion-icon { + @include margin-horizontal(6px, null); + } } } } diff --git a/src/addons/mod/resource/components/index/addon-mod-resource-index.html b/src/addons/mod/resource/components/index/addon-mod-resource-index.html index 6291b1a09..020ba6eea 100644 --- a/src/addons/mod/resource/components/index/addon-mod-resource-index.html +++ b/src/addons/mod/resource/components/index/addon-mod-resource-index.html @@ -24,7 +24,7 @@ -
+
diff --git a/src/core/components/combobox/combobox.ts b/src/core/components/combobox/combobox.ts index 32e56a904..83d1f4877 100644 --- a/src/core/components/combobox/combobox.ts +++ b/src/core/components/combobox/combobox.ts @@ -53,7 +53,6 @@ export class CoreComboboxComponent { // Additional options when interface modal is selected. @Input() icon?: string; // Icon for modal interface. - @Input() badge?: number; // Badge number to show near the icon. @Input() modalOptions?: ModalOptions; // Will emit an event the value changed. @Input() listboxId = ''; diff --git a/src/core/components/combobox/core-combobox.html b/src/core/components/combobox/core-combobox.html index baa897fa3..02b0cec27 100644 --- a/src/core/components/combobox/core-combobox.html +++ b/src/core/components/combobox/core-combobox.html @@ -1,7 +1,6 @@ - {{badge}} @@ -14,7 +13,6 @@ - {{badge}} {{ label }}:
{{selection}} diff --git a/src/core/components/empty-box/empty-box.scss b/src/core/components/empty-box/empty-box.scss index 812dde4ca..5f333fb36 100644 --- a/src/core/components/empty-box/empty-box.scss +++ b/src/core/components/empty-box/empty-box.scss @@ -13,6 +13,8 @@ padding: 16px; --image-size: 120px; + height: 100%; + ion-icon { font-size: var(--image-size); } diff --git a/src/core/components/spacer/spacer.ts b/src/core/components/spacer/spacer.ts index e2196dd81..0bedc96bf 100644 --- a/src/core/components/spacer/spacer.ts +++ b/src/core/components/spacer/spacer.ts @@ -22,8 +22,9 @@ import { Component } from '@angular/core'; */ @Component({ selector: 'core-spacer', - template: '', - styles: [':host {--item-divider-min-height: 30px;}'], + template: '', + styles: [':host { display: block; margin: var(--spacer-vertical) var(--spacer-horizontal); \ + border-bottom: 1px solid var(--spacer-color);}'], }) export class CoreSpacerComponent { diff --git a/src/core/directives/collapsible-footer.ts b/src/core/directives/collapsible-footer.ts index f0b7c90ad..e90806092 100644 --- a/src/core/directives/collapsible-footer.ts +++ b/src/core/directives/collapsible-footer.ts @@ -50,7 +50,8 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { protected endContentScrollListener?: EventListener; protected resizeListener?: CoreEventObserver; protected slotPromise?: CoreCancellablePromise; - protected calcPending = false; + protected viewportPromise?: CoreCancellablePromise; + protected loadingHeight = false; protected pageDidEnterListener?: EventListener; protected page?: HTMLElement; @@ -85,13 +86,14 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { * Calculate the height of the footer. */ protected async calculateHeight(): Promise { - if (!CoreDom.isElementVisible(this.element)) { - this.calcPending = true; - + if (this.loadingHeight) { + // Already calculating, return. return; } + this.loadingHeight = true; - this.calcPending = false; + this.viewportPromise = CoreDom.waitToBeInViewport(this.element); + await this.viewportPromise; this.element.classList.remove('is-active'); await CoreUtils.nextTick(); @@ -110,6 +112,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { this.element.classList.add('is-active'); this.setBarHeight(this.initialHeight); + this.loadingHeight = false; } /** @@ -175,9 +178,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { this.page?.addEventListener( 'ionViewDidEnter', this.pageDidEnterListener = () => { - if (this.calcPending) { - this.calculateHeight(); - } + this.calculateHeight(); }, ); } @@ -255,6 +256,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { this.resizeListener?.off(); this.slotPromise?.cancel(); + this.viewportPromise?.cancel(); } } diff --git a/src/core/directives/collapsible-header.ts b/src/core/directives/collapsible-header.ts index 196a2b1ce..887918f05 100644 --- a/src/core/directives/collapsible-header.ts +++ b/src/core/directives/collapsible-header.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChange } from '@angular/core'; -import { CorePromisedValue } from '@classes/promised-value'; +import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreTabsOutletComponent } from '@components/tabs-outlet/tabs-outlet'; import { CoreTabsComponent } from '@components/tabs/tabs'; @@ -69,17 +69,15 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest protected content?: HTMLIonContentElement; protected contentScrollListener?: EventListener; protected endContentScrollListener?: EventListener; - protected pageDidEnterListener?: EventListener; protected resizeListener?: CoreEventObserver; protected floatingTitle?: HTMLHeadingElement; protected scrollingHeight?: number; protected subscriptions: Subscription[] = []; protected enabled = true; protected isWithinContent = false; - protected enteredPromise = new CorePromisedValue(); protected mutationObserver?: MutationObserver; - protected firstEnter = true; - protected initPending = false; + protected loadingFloatingTitle = false; + protected visiblePromise?: CoreCancellablePromise; constructor(el: ElementRef) { this.collapsedHeader = el.nativeElement; @@ -89,6 +87,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest * @inheritdoc */ ngOnInit(): void { + this.collapsible = !CoreUtils.isFalseOrZero(this.collapsible); this.init(); } @@ -105,10 +104,9 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest await Promise.all([ this.initializeCollapsedHeader(), this.initializeExpandedHeader(), - await this.enteredPromise, ]); - this.initializeFloatingTitle(); + await this.initializeFloatingTitle(); this.initializeContent(); } @@ -116,8 +114,9 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest * @inheritdoc */ async ngOnChanges(changes: {[name: string]: SimpleChange}): Promise { - if (changes.collapsible) { - this.enabled = !CoreUtils.isFalseOrZero(changes.collapsible.currentValue); + if (changes.collapsible && !changes.collapsible.firstChange) { + this.collapsible = !CoreUtils.isFalseOrZero(changes.collapsible.currentValue); + this.enabled = this.collapsible; await this.init(); @@ -139,47 +138,16 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest if (this.content && this.endContentScrollListener) { this.content.removeEventListener('ionScrollEnd', this.endContentScrollListener); } - if (this.page && this.pageDidEnterListener) { - this.page.removeEventListener('ionViewDidEnter', this.pageDidEnterListener); - } this.resizeListener?.off(); this.mutationObserver?.disconnect(); + this.visiblePromise?.cancel(); } /** - * Search the page element, initialize it, and wait until it's ready for the transition to trigger on scroll. + * Listen to changing events. */ - protected initializePage(): void { - if (!this.collapsedHeader.parentElement) { - throw new Error('[collapsible-header] Couldn\'t get page'); - } - - // Find element and prepare classes. - this.page = this.collapsedHeader.parentElement; - this.page.classList.add('collapsible-header-page'); - - this.page.addEventListener( - 'ionViewDidEnter', - this.pageDidEnterListener = () => { - if (this.firstEnter) { - this.firstEnter = false; - clearTimeout(timeout); - this.enteredPromise.resolve(); - } else if (this.initPending) { - this.initializeFloatingTitle(); - } - }, - ); - - // Timeout in case event is never fired. - const timeout = window.setTimeout(() => { - if (this.firstEnter) { - this.firstEnter = false; - this.enteredPromise.reject(new Error('[collapsible-header] Waiting for ionViewDidEnter timeout reached')); - } - }, 5000); - + protected listenEvents(): void { this.resizeListener = CoreDom.onWindowResize(() => { this.initializeFloatingTitle(); }, 50); @@ -214,6 +182,19 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest }); } + /** + * Search the page element, initialize it, and wait until it's ready for the transition to trigger on scroll. + */ + protected initializePage(): void { + if (!this.collapsedHeader.parentElement) { + throw new Error('[collapsible-header] Couldn\'t get page'); + } + + // Find element and prepare classes. + this.page = this.collapsedHeader.parentElement; + this.page.classList.add('collapsible-header-page'); + } + /** * Search the collapsed header element, initialize it, and wait until it's ready for the transition to trigger on scroll. */ @@ -251,6 +232,8 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest return; } + this.listenEvents(); + // Initialize from tabs. const tabs = CoreComponentsRegistry.resolve(this.page.querySelector('core-tabs-outlet'), CoreTabsOutletComponent); @@ -282,21 +265,22 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest /** * Initialize a floating title to mimic transitioning the title from one state to the other. */ - protected initializeFloatingTitle(): void { + protected async initializeFloatingTitle(): Promise { if (!this.page || !this.expandedHeader) { - throw new Error('[collapsible-header] Couldn\'t create floating title'); - } - - if (!CoreDom.isElementVisible(this.expandedHeader)) { - this.initPending = true; - return; } - this.initPending = false; + if (this.loadingFloatingTitle) { + // Already calculating, return. + return; + } + this.loadingFloatingTitle = true; + + this.visiblePromise = CoreDom.waitToBeVisible(this.expandedHeader); + await this.visiblePromise; this.page.classList.remove('collapsible-header-page-is-active'); - CoreUtils.nextTick(); + await CoreUtils.nextTick(); // Add floating title and measure initial position. const collapsedHeaderTitle = this.collapsedHeader.querySelector('h1') as HTMLHeadingElement; @@ -368,6 +352,8 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest this.collapsedFontStyles = collapsedFontStyles; this.expandedFontStyles = expandedFontStyles; this.expandedHeaderHeight = expandedHeaderHeight; + + this.loadingFloatingTitle = false; } /** diff --git a/src/core/directives/collapsible-item.ts b/src/core/directives/collapsible-item.ts index 6e95242f2..1a4950c8b 100644 --- a/src/core/directives/collapsible-item.ts +++ b/src/core/directives/collapsible-item.ts @@ -55,8 +55,9 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy { protected resizeListener?: CoreEventObserver; protected darkModeListener?: Subscription; protected domPromise?: CoreCancellablePromise; + protected visiblePromise?: CoreCancellablePromise; protected uniqueId: string; - protected calcPending = false; + protected loadingHeight = false; protected pageDidEnterListener?: EventListener; protected page?: HTMLElement; @@ -99,9 +100,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy { this.page?.addEventListener( 'ionViewDidEnter', this.pageDidEnterListener = () => { - if (this.calcPending) { - this.calculateHeight(); - } + this.calculateHeight(); }, ); @@ -143,20 +142,20 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy { * Calculate the height and check if we need to display show more or not. */ protected async calculateHeight(): Promise { + if (this.loadingHeight) { + // Already calculating, return. + return; + } + this.loadingHeight = true; + + this.visiblePromise = CoreDom.waitToBeVisible(this.element); + await this.visiblePromise; + // Remove max-height (if any) to calculate the real height. this.element.classList.add('collapsible-loading-height'); await this.waitFormatTextsRendered(); - if (!this.element.clientHeight) { - this.calcPending = true; - this.element.classList.remove('collapsible-loading-height'); - - return; - } - - this.calcPending = false; - this.expandedHeight = this.element.getBoundingClientRect().height; // Restore the max height now. @@ -167,6 +166,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy { this.setExpandButtonEnabled(enable); this.setGradientColor(); + this.loadingHeight = false; } /** @@ -298,6 +298,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy { this.resizeListener?.off(); this.darkModeListener?.unsubscribe(); this.domPromise?.cancel(); + this.visiblePromise?.cancel(); if (this.page && this.pageDidEnterListener) { this.page.removeEventListener('ionViewDidEnter', this.pageDidEnterListener); diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index f134eddd6..a6f0d2f9d 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -256,7 +256,8 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo button.classList.add('hidden'); button.setAttribute('aria-label', label); // Add an ion-icon item to apply the right styles, but the ion-icon component won't be executed. - button.innerHTML = '