diff --git a/scripts/langindex.json b/scripts/langindex.json index 5dd3147e9..a271644f8 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -143,6 +143,8 @@ "addon.calendar.mon": "calendar", "addon.calendar.monday": "calendar", "addon.calendar.monthlyview": "calendar", + "addon.calendar.monthnext": "calendar", + "addon.calendar.monthprev": "calendar", "addon.calendar.newevent": "calendar", "addon.calendar.noevents": "local_moodlemobileapp", "addon.calendar.nopermissiontoupdatecalendar": "error", @@ -2405,6 +2407,7 @@ "core.search.allcategories": "local_moodlemobileapp", "core.search.allcourses": "search", "core.search.empty": "local_moodlemobileapp", + "core.search.err_minlength": "form", "core.search.filtercategories": "local_moodlemobileapp", "core.search.filtercourses": "local_moodlemobileapp", "core.search.filterheader": "search", diff --git a/src/addons/block/tags/components/tags/tags.scss b/src/addons/block/tags/components/tags/tags.scss index 2282396da..2de71afde 100644 --- a/src/addons/block/tags/components/tags/tags.scss +++ b/src/addons/block/tags/components/tags/tags.scss @@ -1,3 +1,5 @@ +@use "theme/globals" as *; + :host .core-block-content ::ng-deep { ion-label { max-width: 100%; @@ -31,6 +33,7 @@ vertical-align: baseline; text-decoration: none; border-radius: var(--mdl-shape-borderRadius-xs); + @include core-focus-outline(); } .s20 { font-size: 2.7em; diff --git a/src/addons/block/timeline/components/events/addon-block-timeline-events.html b/src/addons/block/timeline/components/events/addon-block-timeline-events.html index 52aa0092c..415bc6636 100644 --- a/src/addons/block/timeline/components/events/addon-block-timeline-events.html +++ b/src/addons/block/timeline/components/events/addon-block-timeline-events.html @@ -16,48 +16,43 @@ - - - - - {{event.timesort * 1000 | coreFormatDate:"strftimetime24" }} - - - -

- - - - {{ 'addon.block_timeline.overdue' | translate }} - -

-

- - - -

-

- - - -

-
-
+ + + {{event.timesort * 1000 | coreFormatDate:"strftimetime24" }} + - - - {{event.action.name}} - - {{event.action.itemcount}} + +

+ + + + {{ 'addon.block_timeline.overdue' | translate }} - +

+

+ + + +

+

+ + + +

+
+ + {{event.action.name}} + + {{event.action.itemcount}} + + +
diff --git a/src/addons/block/timeline/components/events/events.scss b/src/addons/block/timeline/components/events/events.scss index 21fae2d8d..709f503bd 100644 --- a/src/addons/block/timeline/components/events/events.scss +++ b/src/addons/block/timeline/components/events/events.scss @@ -27,26 +27,22 @@ h4.core-bold { --margin-end: 0.5rem; --margin-vertical: 0; } + + ion-label { + display: flex; + flex-direction: column; + + .addon-block-timeline-activity-action { + display: flex; + justify-content: flex-end; + } + } } .addon-block-timeline-activity-time { flex-grow: 0; } -.addon-block-timeline-activity-action { - display: flex; - justify-content: flex-end; -} - -.addon-block-timeline-activity-main, -.addon-block-timeline-activity-name { - flex-grow: 1; - p { - overflow: hidden; - text-overflow: ellipsis; - } -} - .addon-block-timeline-activity-name { flex-grow: 1; overflow: hidden; diff --git a/src/addons/blog/pages/edit-entry/edit-entry.html b/src/addons/blog/pages/edit-entry/edit-entry.html index 842cf3400..2c516a75a 100644 --- a/src/addons/blog/pages/edit-entry/edit-entry.html +++ b/src/addons/blog/pages/edit-entry/edit-entry.html @@ -54,7 +54,7 @@
@if (associationsExpanded) { - + @if (associatedModule) { @if (showMyEntriesToggle) { - + {{ 'addon.blog.showonlyyourentries' | translate }} @@ -65,34 +65,32 @@
- -
-
- -
- - @if (tagsEnabled && entry.tags && entry.tags!.length > 0) { - - -
{{ 'core.tag.tags' | translate }}:
- -
-
- } - - @for (file of entry.attachmentfiles; track $index) { - - } - - @if (entry.uniquehash) { - - {{ 'addon.blog.linktooriginalentry' | translate }} - - } - +
+
+
- + + @if (tagsEnabled && entry.tags && entry.tags!.length > 0) { + + +
{{ 'core.tag.tags' | translate }}:
+ +
+
+ } + + @for (file of entry.attachmentfiles; track $index) { + + } + + @if (entry.uniquehash) { + + {{ 'addon.blog.linktooriginalentry' | translate }} + + } + +
@if (entry.lastmodified > entry.created || (entry.userid === currentUserId && entry.publishstate !== 'draft')) { diff --git a/src/addons/blog/pages/index/index.scss b/src/addons/blog/pages/index/index.scss index 16cd7210f..c610046a4 100644 --- a/src/addons/blog/pages/index/index.scss +++ b/src/addons/blog/pages/index/index.scss @@ -7,7 +7,7 @@ } .entry { - border-top: 1px solid var(--stroke); + border-bottom: 1px solid var(--stroke); &-visibility-permission { display: flex; diff --git a/src/addons/blog/pages/index/index.ts b/src/addons/blog/pages/index/index.ts index 46ba2bba6..8f2cb9329 100644 --- a/src/addons/blog/pages/index/index.ts +++ b/src/addons/blog/pages/index/index.ts @@ -28,6 +28,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; +import { CoreArray } from '@singletons/array'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreTime } from '@singletons/time'; @@ -218,7 +219,7 @@ export class AddonBlogIndexPage implements OnInit, OnDestroy { if (refresh) { this.entries = result.entries; } else { - this.entries = CoreUtils.uniqueArray(this.entries + this.entries = CoreArray.unique(this.entries .concat(result.entries), 'id') .sort((a, b) => b.created - a.created); } diff --git a/src/addons/calendar/components/calendar/addon-calendar-calendar.html b/src/addons/calendar/components/calendar/addon-calendar-calendar.html index 2d543551b..83438a0e6 100644 --- a/src/addons/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addons/calendar/components/calendar/addon-calendar-calendar.html @@ -12,7 +12,7 @@ - + @@ -23,7 +23,7 @@ - + @@ -55,7 +55,8 @@ "weekend": day.isweekend, "duration_finish": day.haslastdayofevent }' [class.addon-calendar-event-past-day]="month.isPastMonth || day.ispast" role="cell" - (ariaButtonClick)="dayClicked(day.mday)" [tabindex]="activeView ? 0 : -1"> + (ariaButtonClick)="dayClicked(day.mday)" [tabindex]="activeView ? 0 : -1" + [attr.aria-current]="month.isCurrentMonth && day.istoday ? 'date' : null">

{{ day.periodName | translate }} diff --git a/src/addons/calendar/components/filter/filter.html b/src/addons/calendar/components/filter/filter.html index 5d746163a..ba83242d3 100644 --- a/src/addons/calendar/components/filter/filter.html +++ b/src/addons/calendar/components/filter/filter.html @@ -9,7 +9,7 @@ - +

{{ (showCalendar ? 'addon.calendar.calendarevents' : 'addon.calendar.upcomingevents') | translate }}

+

{{ 'addon.calendar.calendar' | translate }}

+

{{ 'addon.calendar.upcomingevents' | translate }}

diff --git a/src/addons/calendar/pages/index/index.ts b/src/addons/calendar/pages/index/index.ts index ac6828c74..0943286ae 100644 --- a/src/addons/calendar/pages/index/index.ts +++ b/src/addons/calendar/pages/index/index.ts @@ -159,7 +159,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { } /** - * View loaded. + * @inheritdoc */ ngOnInit(): void { this.loadUpcoming = !!CoreNavigator.getRouteBooleanParam('upcoming'); diff --git a/src/addons/calendar/tests/behat/create_events.feature b/src/addons/calendar/tests/behat/create_events.feature index 9b301653c..8a334d941 100755 --- a/src/addons/calendar/tests/behat/create_events.feature +++ b/src/addons/calendar/tests/behat/create_events.feature @@ -43,7 +43,7 @@ Feature: Test creation of calendar events in app And I set the field "Description" to "This is User Event 01 description." in the app And I set the field "Location" to "Barcelona" in the app And I press "Save" in the app - Then I should find "Calendar events" in the app + Then I should find "Calendar" in the app # Verify that event was created right. When I open the calendar for "4" "2025" in the app diff --git a/src/addons/messageoutput/airnotifier/pages/devices/devices.html b/src/addons/messageoutput/airnotifier/pages/devices/devices.html index f37297344..a1d79e116 100644 --- a/src/addons/messageoutput/airnotifier/pages/devices/devices.html +++ b/src/addons/messageoutput/airnotifier/pages/devices/devices.html @@ -21,7 +21,8 @@ - +

{{ device.name }} {{ device.model }} ({{platform.platform}} {{ device.version }}) diff --git a/src/addons/messages/pages/discussion/discussion.html b/src/addons/messages/pages/discussion/discussion.html index db3c27112..b31c90905 100644 --- a/src/addons/messages/pages/discussion/discussion.html +++ b/src/addons/messages/pages/discussion/discussion.html @@ -24,16 +24,26 @@ [content]="'addon.messages.info' | translate" (action)="viewInfo()" iconAction="fas-circle-info" /> - + + + + - + + + + - +

{{ 'addon.messages.contacts' | translate }}

diff --git a/src/addons/messages/pages/group-conversations/group-conversations.html b/src/addons/messages/pages/group-conversations/group-conversations.html index 313ebb0b5..db43ceb5c 100644 --- a/src/addons/messages/pages/group-conversations/group-conversations.html +++ b/src/addons/messages/pages/group-conversations/group-conversations.html @@ -21,119 +21,55 @@ - + - + - - - -
- - - - - -

{{ 'addon.messages.nofavourites' | translate }}

-
-
-
- - - - - - - - - -
- - - - - -

{{ 'addon.messages.nogroupconversations' | translate }}

-
-
-
- - - - - - - - -
- - - - - -

{{ 'addon.messages.noindividualconversations' | translate }}

-
-
-
- - - - - - + + + + +

{{ option.titleString | translate }} ({{ option.count }})

+
+ + {{ option.unread }} + +
+
+ + + + + + +

{{ option.emptyString| translate }}

+
+
+
+ + + + + +
+
+
diff --git a/src/addons/messages/pages/group-conversations/group-conversations.ts b/src/addons/messages/pages/group-conversations/group-conversations.ts index c27a34d7d..20fbedc11 100644 --- a/src/addons/messages/pages/group-conversations/group-conversations.ts +++ b/src/addons/messages/pages/group-conversations/group-conversations.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; -import { IonContent } from '@ionic/angular'; +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { AccordionGroupChangeEventDetail, IonAccordionGroup, IonContent } from '@ionic/angular'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreSites } from '@services/sites'; import { @@ -21,6 +21,8 @@ import { AddonMessagesConversationFormatted, AddonMessages, AddonMessagesNewMessagedEventData, + AddonMessagesUnreadConversationCountsEventData, + AddonMessagesUpdateConversationAction, } from '../../services/messages'; import { AddonMessagesOffline, @@ -40,6 +42,12 @@ import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-lin import { CorePlatform } from '@services/platform'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; +const enum AddonMessagesGroupConversationOptionNames { + FAVOURITES = 'favourites', + GROUP = 'group', + INDIVIDUAL = 'individual', +} + /** * Page that displays the list of conversations, including group conversations. */ @@ -51,43 +59,49 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view'; export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; - @ViewChild(IonContent) content?: IonContent; - @ViewChild('favlist') favListEl?: ElementRef; - @ViewChild('grouplist') groupListEl?: ElementRef; - @ViewChild('indlist') indListEl?: ElementRef; + @ViewChild('accordionGroup', { static: true }) accordionGroup!: IonAccordionGroup; loaded = false; loadingMessage: string; selectedConversationId?: number; selectedUserId?: number; contactRequestsCount = 0; - favourites: AddonMessagesGroupConversationOption = { - type: undefined, - favourites: true, - count: 0, - unread: 0, - conversations: [], - }; - group: AddonMessagesGroupConversationOption = { - type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP, - favourites: false, - count: 0, - unread: 0, - conversations: [], - }; - - individual: AddonMessagesGroupConversationOption = { - type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, - favourites: false, - count: 0, - unread: 0, - conversations: [], - }; + groupConversations: AddonMessagesGroupConversationOption[] = [ + { + optionName: AddonMessagesGroupConversationOptionNames.FAVOURITES, + titleString: 'core.favourites', + emptyString: 'addon.messages.nofavourites', + type: undefined, + favourites: true, + count: 0, + unread: 0, + conversations: [], + }, + { + optionName: AddonMessagesGroupConversationOptionNames.GROUP, + titleString: 'addon.messages.groupconversations', + emptyString: 'addon.messages.nogroupconversations', + type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP, + favourites: false, + count: 0, + unread: 0, + conversations: [], + }, + { + optionName: AddonMessagesGroupConversationOptionNames.INDIVIDUAL, + titleString: 'addon.messages.individualconversations', + emptyString: 'addon.messages.noindividualconversations', + type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + favourites: false, + count: 0, + unread: 0, + conversations: [], + }, + ]; typeGroup = AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP; - currentListEl?: HTMLElement; protected siteId: string; protected currentUserId: number; @@ -100,6 +114,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { protected updateConversationListObserver: CoreEventObserver; protected contactRequestsCountObserver: CoreEventObserver; protected memberInfoObserver: CoreEventObserver; + protected firstExpand = false; constructor( protected route: ActivatedRoute, @@ -114,9 +129,9 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { (data) => { // Check if the new message belongs to the option that is currently expanded. const expandedOption = this.getExpandedOption(); - const messageOption = this.getConversationOption(data); + const messageOptionName = this.getConversationOptionName(data); - if (expandedOption != messageOption) { + if (expandedOption?.optionName !== messageOptionName) { return; // Message doesn't belong to current list, stop. } @@ -155,8 +170,9 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { } // Sort the affected list. - const option = this.getConversationOption(conversation); - option.conversations = AddonMessages.sortConversations(option.conversations || []); + const optionName = this.getConversationOptionName(conversation); + const option = this.getConversationGroupByName(optionName); + option.conversations = AddonMessages.sortConversations(option.conversations); if (isNewer) { // The last message is newer than the previous one, scroll to top to keep viewing the conversation. @@ -209,11 +225,11 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { this.updateConversationListObserver = CoreEvents.on( AddonMessagesProvider.UPDATE_CONVERSATION_LIST_EVENT, (data) => { - if (data && data.action == 'mute') { - // If the conversation is displayed, change its muted value. + if (data?.action === AddonMessagesUpdateConversationAction.MUTE) { + // If the conversation is displayed, change its muted value. const expandedOption = this.getExpandedOption(); - if (expandedOption && expandedOption.conversations) { + if (expandedOption?.conversations) { const conversation = this.findConversation(data.conversationId, undefined, expandedOption); if (conversation) { conversation.ismuted = !!data.value; @@ -233,7 +249,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { this.pushObserver = CorePushNotificationsDelegate.on('receive') .subscribe((notification) => { // New message received. If it's from current site, refresh the data. - if (CoreUtils.isFalseOrZero(notification.notif) && notification.site == this.siteId) { + if (CoreUtils.isFalseOrZero(notification.notif) && notification.site === this.siteId) { // Don't refresh unread counts, it's refreshed from the main menu handler in this case. this.refreshData(undefined, false); } @@ -243,9 +259,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { this.cronObserver = CoreEvents.on( AddonMessagesProvider.UNREAD_CONVERSATION_COUNTS_EVENT, (data) => { - this.favourites.unread = data.favourites; - this.individual.unread = data.individual + data.self; // Self is only returned if it's not favourite. - this.group.unread = data.group; + this.setCounts(data, 'unread'); }, this.siteId, ); @@ -269,15 +283,14 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { } const expandedOption = this.getExpandedOption(); - if (expandedOption == this.individual || expandedOption == this.favourites) { - if (!expandedOption.conversations || expandedOption.conversations.length <= 0) { - return; - } + if (expandedOption?.optionName === AddonMessagesGroupConversationOptionNames.GROUP || + !expandedOption?.conversations.length) { + return; + } - const conversation = this.findConversation(undefined, data.userId, expandedOption); - if (conversation) { - conversation.isblocked = data.userBlocked; - } + const conversation = this.findConversation(undefined, data.userId, expandedOption); + if (conversation) { + conversation.isblocked = data.userBlocked; } }, this.siteId, @@ -285,7 +298,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { } /** - * Component loaded. + * @inheritdoc */ async ngOnInit(): Promise { this.route.queryParams.subscribe(async (params) => { @@ -305,15 +318,11 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { if (!this.selectedConversationId && !this.selectedUserId && CoreScreen.isTablet) { // Load the first conversation. - let conversation: AddonMessagesConversationForList; const expandedOption = this.getExpandedOption(); - if (expandedOption && expandedOption.conversations.length) { - conversation = expandedOption.conversations[0]; - - if (conversation) { - await this.gotoConversation(conversation.id); - } + const conversation = expandedOption?.conversations[0]; + if (conversation) { + await this.gotoConversation(conversation.id); } } @@ -341,22 +350,19 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { try { await Promise.all(promises); - // The expanded status hasn't been initialized. Do it now. - if (this.favourites.expanded === undefined && (this.selectedConversationId || this.selectedUserId)) { + if (!this.firstExpand && (this.selectedConversationId || this.selectedUserId)) { // A certain conversation should be opened. // We don't know which option it belongs to, so we need to fetch the data for all of them. - const promises: Promise[] = []; - - promises.push(this.fetchDataForOption(this.favourites, false)); - promises.push(this.fetchDataForOption(this.group, false)); - promises.push(this.fetchDataForOption(this.individual, false)); + const promises = this.groupConversations.map((option) => + this.fetchDataForOption(option, false)); await Promise.all(promises); // All conversations have been loaded, find the one we need to load and expand its option. const conversation = this.findConversation(this.selectedConversationId, this.selectedUserId); if (conversation) { - const option = this.getConversationOption(conversation); + const optionName = this.getConversationOptionName(conversation); + const option = this.getConversationGroupByName(optionName); await this.expandOption(option); @@ -376,18 +382,24 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { /** * Fetch data for the expanded option. - * - * @returns Promise resolved when done. */ protected async fetchDataForExpandedOption(): Promise { - if (this.favourites.expanded === undefined) { + if (!this.firstExpand) { // Calculate which option should be expanded initially. - this.favourites.expanded = this.favourites.count != 0 && !this.group.unread && !this.individual.unread; - this.group.expanded = !this.favourites.expanded && this.group.count != 0 && !this.individual.unread; - this.individual.expanded = !this.favourites.expanded && !this.group.expanded; - } + let expandOption = this.groupConversations.find((option) => option.unread); - this.loadCurrentListElement(); + if (!expandOption) { + expandOption = this.groupConversations.find((option) => option.count > 0); + } + + if (!expandOption) { + expandOption = this.getConversationGroupByName(AddonMessagesGroupConversationOptionNames.INDIVIDUAL); + } + + this.accordionGroup.value = expandOption.optionName; + + this.firstExpand = true; + } const expandedOption = this.getExpandedOption(); @@ -418,8 +430,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { conversations: [], canLoadMore: false, }; - let offlineMessages: - AddonMessagesOfflineAnyMessagesFormatted[] = []; + let offlineMessages: AddonMessagesOfflineAnyMessagesFormatted[] = []; // Get the conversations and, if needed, the offline messages. Always try to get the latest data. promises.push(AddonMessages.invalidateConversations(this.siteId).then(async () => { @@ -469,9 +480,36 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { await AddonMessages.invalidateConversationCounts(this.siteId); const counts = await AddonMessages.getConversationCounts(this.siteId); - this.favourites.count = counts.favourites; - this.individual.count = counts.individual + counts.self; // Self is only returned if it's not favourite. - this.group.count = counts.group; + this.setCounts(counts); + } + + /** + * Set conversation counts. + * + * @param counts Counts to set. + * @param valueToSet Value to set count or unread. + */ + protected setCounts( + counts: AddonMessagesUnreadConversationCountsEventData, + valueToSet: 'count' | 'unread' = 'count', + ): void { + this.getConversationGroupByName(AddonMessagesGroupConversationOptionNames.FAVOURITES)[valueToSet] = counts.favourites; + this.getConversationGroupByName(AddonMessagesGroupConversationOptionNames.INDIVIDUAL)[valueToSet] = + counts.individual + counts.self; // Self is only returned if it's not favourite. + this.getConversationGroupByName(AddonMessagesGroupConversationOptionNames.GROUP)[valueToSet] = counts.group; + } + + /** + * Get a conversation group by its name. + * + * @param name Name of the group. + * @returns The conversation group. + */ + protected getConversationGroupByName(name: AddonMessagesGroupConversationOptionNames): AddonMessagesGroupConversationOption { + const option = this.groupConversations.find((group) => group.optionName === name); + + // Option should always be defined. + return option ?? this.groupConversations[0]; } /** @@ -491,16 +529,19 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { if (conversationId) { const conversations: AddonMessagesConversationForList[] = option ? option.conversations - : (this.favourites.conversations.concat(this.group.conversations).concat(this.individual.conversations)); + : this.groupConversations.flatMap((option) => option.conversations); - return conversations.find((conv) => conv.id == conversationId); + return conversations.find((conv) => conv.id === conversationId); } - const conversations = option - ? option.conversations - : this.favourites.conversations.concat(this.individual.conversations); + let conversations = option?.conversations; + if (!conversations) { + // Only check on favourites and individual conversations. + conversations = this.getConversationGroupByName(AddonMessagesGroupConversationOptionNames.FAVOURITES).conversations + .concat(this.getConversationGroupByName(AddonMessagesGroupConversationOptionNames.INDIVIDUAL).conversations); + } - return conversations.find((conv) => conv.userid == userId); + return conversations.find((conv) => conv.userid === userId); } /** @@ -509,12 +550,8 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * @returns Option currently expanded. */ protected getExpandedOption(): AddonMessagesGroupConversationOption | undefined { - if (this.favourites.expanded) { - return this.favourites; - } else if (this.group.expanded) { - return this.group; - } else if (this.individual.expanded) { - return this.individual; + if (this.accordionGroup.value) { + return this.getConversationGroupByName(this.accordionGroup.value as AddonMessagesGroupConversationOptionNames); } } @@ -572,7 +609,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { option.loadMoreError = true; } - infiniteComplete && infiniteComplete(); + infiniteComplete?.(); } /** @@ -617,13 +654,13 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { imageurl: message.conversation?.imageurl || '', }; - if (this.getConversationOption(conversation) == option) { + if (this.getConversationOptionName(conversation) === option.optionName) { // Message belongs to current option, add the conversation. this.addLastOfflineMessage(conversation, message); - this.addOfflineConversation(conversation); + this.addOfflineConversation(conversation, option); } } - } else if (option.type == AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) { + } else if (option.type === AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) { // It's a new conversation. Check if we already created it (there is more than one message for the same user). const conversation = this.findConversation(undefined, message.touserid, option); @@ -655,7 +692,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { }; this.addLastOfflineMessage(conversation, message); - this.addOfflineConversation(conversation); + this.addOfflineConversation(conversation, option); return; })); @@ -670,9 +707,12 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * Add an offline conversation into the right list of conversations. * * @param conversation Offline conversation to add. + * @param option Option where to add the conversation. */ - protected addOfflineConversation(conversation: AddonMessagesConversationForList): void { - const option = this.getConversationOption(conversation); + protected addOfflineConversation( + conversation: AddonMessagesConversationForList, + option: AddonMessagesGroupConversationOption, + ): void { option.conversations.unshift(conversation); } @@ -693,23 +733,23 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { } /** - * Given a conversation, return its option (favourites, group, individual). + * Given a conversation, return its option name. * * @param conversation Conversation to check. - * @returns Option object. + * @returns Option name. */ - protected getConversationOption( + protected getConversationOptionName( conversation: AddonMessagesConversationForList | AddonMessagesNewMessagedEventData, - ): AddonMessagesGroupConversationOption { + ): AddonMessagesGroupConversationOptionNames { if (conversation.isfavourite) { - return this.favourites; + return AddonMessagesGroupConversationOptionNames.FAVOURITES; } - if (conversation.type == AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP) { - return this.group; + if (conversation.type === AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP) { + return AddonMessagesGroupConversationOptionNames.GROUP; } - return this.individual; + return AddonMessagesGroupConversationOptionNames.INDIVIDUAL; } /** @@ -727,9 +767,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { try { await this.fetchData(refreshUnreadCounts); } finally { - if (refresher) { - refresher?.complete(); - } + refresher?.complete(); } } } @@ -737,19 +775,20 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { /** * Toogle the visibility of an option (expand/collapse). * - * @param option The option to expand/collapse. + * @param ev The event of the accordion. */ - toggle(option: AddonMessagesGroupConversationOption): void { - if (option.expanded) { - // Already expanded, close it. - option.expanded = false; - this.loadCurrentListElement(); - } else { - // Pass getCounts=true to update the counts everytime the user expands an option. - this.expandOption(option, true).catch((error) => { - CoreDomUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true); - }); + accordionGroupChange(ev: AccordionGroupChangeEventDetail): void { + const optionName = ev.value as AddonMessagesGroupConversationOptionNames; + if (!optionName) { + return; } + + const option = this.getConversationGroupByName(optionName); + + // Pass getCounts=true to update the counts everytime the user expands an option. + this.expandOption(option, true).catch((error) => { + CoreDomUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true); + }); } /** @@ -761,40 +800,18 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { */ protected async expandOption(option: AddonMessagesGroupConversationOption, getCounts = false): Promise { // Collapse all and expand the right one. - this.favourites.expanded = false; - this.group.expanded = false; - this.individual.expanded = false; - - option.expanded = true; option.loading = true; + this.accordionGroup.value = option.optionName; try { await this.fetchDataForOption(option, false, getCounts); - - this.loadCurrentListElement(); } catch (error) { - option.expanded = false; + this.accordionGroup.value = undefined; throw error; } finally { option.loading = false; } - - } - - /** - * Load the current list element based on the expanded list. - */ - protected loadCurrentListElement(): void { - if (this.favourites.expanded) { - this.currentListEl = this.favListEl && this.favListEl.nativeElement; - } else if (this.group.expanded) { - this.currentListEl = this.groupListEl && this.groupListEl.nativeElement; - } else if (this.individual.expanded) { - this.currentListEl = this.indListEl && this.indListEl.nativeElement; - } else { - this.currentListEl = undefined; - } } /** @@ -805,7 +822,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { } /** - * Page destroyed. + * @inheritdoc */ ngOnDestroy(): void { this.newMessagesObserver?.off(); @@ -825,11 +842,13 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * Conversation options. */ export type AddonMessagesGroupConversationOption = { + optionName: AddonMessagesGroupConversationOptionNames; + titleString: string; + emptyString: string; type?: number; // Option type. favourites: boolean; // Whether it contains favourites conversations. count: number; // Number of conversations. unread?: number; // Number of unread conversations. - expanded?: boolean; // Whether the option is currently expanded. loading?: boolean; // Whether the option is being loaded. canLoadMore?: boolean; // Whether it can load more data. loadMoreError?: boolean; // Whether there was an error loading more conversations. diff --git a/src/addons/messages/pages/search/search.html b/src/addons/messages/pages/search/search.html index 60fa3c1ca..30d66eff8 100644 --- a/src/addons/messages/pages/search/search.html +++ b/src/addons/messages/pages/search/search.html @@ -28,7 +28,7 @@
+ icon="fas-magnifying-glass" [message]="'core.noresults' | translate" role="alert" /> diff --git a/src/addons/messages/pages/settings/settings.html b/src/addons/messages/pages/settings/settings.html index c638b6e6b..e4d5a634c 100644 --- a/src/addons/messages/pages/settings/settings.html +++ b/src/addons/messages/pages/settings/settings.html @@ -21,7 +21,7 @@

{{ 'core.settings.general' | translate }}

- + {{ 'addon.messages.useentertosend' | translate }} @@ -31,7 +31,7 @@ - + {{ 'addon.messages.blocknoncontacts' | translate }} @@ -95,7 +95,7 @@ - +

{{ 'core.settings.' + state | translate }}

@@ -131,7 +131,7 @@ - +

{{ processor.displayname }}

diff --git a/src/addons/messages/services/messages.ts b/src/addons/messages/services/messages.ts index 158756a45..f95558d21 100644 --- a/src/addons/messages/services/messages.ts +++ b/src/addons/messages/services/messages.ts @@ -35,8 +35,6 @@ import { CoreWSError } from '@classes/errors/wserror'; import { AddonNotificationsPreferencesNotificationProcessorState } from '@addons/notifications/services/notifications'; import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site'; -const ROOT_CACHE_KEY = 'mmaMessages:'; - declare module '@singletons/events' { /** @@ -57,12 +55,20 @@ declare module '@singletons/events' { } +export const enum AddonMessagesUpdateConversationAction { + MUTE = 'mute', + FAVOURITE = 'favourite', + DELETE = 'delete', +} + /** * Service to handle messages. */ @Injectable({ providedIn: 'root' }) export class AddonMessagesProvider { + protected static readonly ROOT_CACHE_KEY = 'mmaMessages:'; + static readonly NEW_MESSAGE_EVENT = 'addon_messages_new_message_event'; static readonly READ_CHANGED_EVENT = 'addon_messages_read_changed_event'; static readonly OPEN_CONVERSATION_EVENT = 'addon_messages_open_conversation_event'; // Notify a conversation should be opened. @@ -396,7 +402,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForBlockedContacts(userId: number): string { - return ROOT_CACHE_KEY + 'blockedContacts:' + userId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'blockedContacts:' + userId; } /** @@ -405,7 +411,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForContacts(): string { - return ROOT_CACHE_KEY + 'contacts'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'contacts'; } /** @@ -414,7 +420,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForUserContacts(): string { - return ROOT_CACHE_KEY + 'userContacts'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'userContacts'; } /** @@ -423,7 +429,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForContactRequests(): string { - return ROOT_CACHE_KEY + 'contactRequests'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'contactRequests'; } /** @@ -432,7 +438,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForContactRequestsCount(): string { - return ROOT_CACHE_KEY + 'contactRequestsCount'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'contactRequestsCount'; } /** @@ -442,7 +448,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ getCacheKeyForDiscussion(userId: number): string { - return ROOT_CACHE_KEY + 'discussion:' + userId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'discussion:' + userId; } /** @@ -452,7 +458,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForMessageCount(userId: number): string { - return ROOT_CACHE_KEY + 'count:' + userId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'count:' + userId; } /** @@ -461,7 +467,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForUnreadConversationCounts(): string { - return ROOT_CACHE_KEY + 'unreadConversationCounts'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'unreadConversationCounts'; } /** @@ -470,7 +476,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForDiscussions(): string { - return ROOT_CACHE_KEY + 'discussions'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'discussions'; } /** @@ -481,7 +487,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForConversation(userId: number, conversationId: number): string { - return ROOT_CACHE_KEY + 'conversation:' + userId + ':' + conversationId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'conversation:' + userId + ':' + conversationId; } /** @@ -492,7 +498,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForConversationBetweenUsers(userId: number, otherUserId: number): string { - return ROOT_CACHE_KEY + 'conversationBetweenUsers:' + userId + ':' + otherUserId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'conversationBetweenUsers:' + userId + ':' + otherUserId; } /** @@ -503,7 +509,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForConversationMembers(userId: number, conversationId: number): string { - return ROOT_CACHE_KEY + 'conversationMembers:' + userId + ':' + conversationId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'conversationMembers:' + userId + ':' + conversationId; } /** @@ -514,7 +520,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForConversationMessages(userId: number, conversationId: number): string { - return ROOT_CACHE_KEY + 'conversationMessages:' + userId + ':' + conversationId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'conversationMessages:' + userId + ':' + conversationId; } /** @@ -535,7 +541,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForConversationCounts(): string { - return ROOT_CACHE_KEY + 'conversationCounts'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'conversationCounts'; } /** @@ -546,7 +552,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForMemberInfo(userId: number, otherUserId: number): string { - return ROOT_CACHE_KEY + 'memberInfo:' + userId + ':' + otherUserId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'memberInfo:' + userId + ':' + otherUserId; } /** @@ -556,7 +562,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForSelfConversation(userId: number): string { - return ROOT_CACHE_KEY + 'selfconversation:' + userId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'selfconversation:' + userId; } /** @@ -575,7 +581,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getRootCacheKeyForConversations(): string { - return ROOT_CACHE_KEY + 'conversations:'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'conversations:'; } /** @@ -1132,7 +1138,7 @@ export class AddonMessagesProvider { const result = await site.read( 'core_message_get_conversation_counts', - { }, + {}, preSets, ); @@ -1388,7 +1394,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getMessagePreferencesCacheKey(): string { - return ROOT_CACHE_KEY + 'messagePreferences'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'messagePreferences'; } /** @@ -2706,7 +2712,7 @@ export class AddonMessagesProvider { * @param conversations Array of conversations. * @returns Conversations sorted with most recent last. */ - sortConversations(conversations: AddonMessagesConversationFormatted[]): AddonMessagesConversationFormatted[] { + sortConversations(conversations: AddonMessagesConversationFormatted[] = []): AddonMessagesConversationFormatted[] { return conversations.sort((a, b) => { const timeA = Number(a.lastmessagedate); const timeB = Number(b.lastmessagedate); @@ -3684,7 +3690,7 @@ export type AddonMessagesNewMessagedEventData = { */ export type AddonMessagesUpdateConversationListEventData = { conversationId: number; - action: string; + action: AddonMessagesUpdateConversationAction; value?: boolean; }; diff --git a/src/addons/messages/tests/behat/snapshots/test-basic-usage-of-messages-in-app-view-recent-conversations-and-contacts_22.png b/src/addons/messages/tests/behat/snapshots/test-basic-usage-of-messages-in-app-view-recent-conversations-and-contacts_22.png index d6eada1dc..fca711d9c 100644 Binary files a/src/addons/messages/tests/behat/snapshots/test-basic-usage-of-messages-in-app-view-recent-conversations-and-contacts_22.png and b/src/addons/messages/tests/behat/snapshots/test-basic-usage-of-messages-in-app-view-recent-conversations-and-contacts_22.png differ diff --git a/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html b/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html index 0cf93f8f1..13de238a2 100644 --- a/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html +++ b/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html @@ -345,7 +345,7 @@
- +

{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}

{{ 'addon.mod_assign.applytoteam' | translate }}

@@ -371,7 +371,7 @@

- +

{{ 'addon.mod_assign.addattempt' | translate }}

diff --git a/src/addons/mod/assign/tests/behat/snapshots/test-basic-usage-of-assignment-activity-in-app-editadd-submission-online-text--add-new-attempt-from-previous-submission--submit-for-grading_8.png b/src/addons/mod/assign/tests/behat/snapshots/test-basic-usage-of-assignment-activity-in-app-editadd-submission-online-text--add-new-attempt-from-previous-submission--submit-for-grading_8.png index 0145f0af5..3c302f51c 100644 Binary files a/src/addons/mod/assign/tests/behat/snapshots/test-basic-usage-of-assignment-activity-in-app-editadd-submission-online-text--add-new-attempt-from-previous-submission--submit-for-grading_8.png and b/src/addons/mod/assign/tests/behat/snapshots/test-basic-usage-of-assignment-activity-in-app-editadd-submission-online-text--add-new-attempt-from-previous-submission--submit-for-grading_8.png differ diff --git a/src/addons/mod/book/pages/contents/contents.scss b/src/addons/mod/book/pages/contents/contents.scss index 87e3729a3..c4e7488f9 100644 --- a/src/addons/mod/book/pages/contents/contents.scss +++ b/src/addons/mod/book/pages/contents/contents.scss @@ -4,4 +4,8 @@ flex: none; } } + + .has-collapsible-footer ::ng-deep swiper-container::part(container) { + padding-bottom: 50px; + } } diff --git a/src/addons/mod/chat/pages/sessions/sessions.html b/src/addons/mod/chat/pages/sessions/sessions.html index c85c3d68c..6b307bfe8 100644 --- a/src/addons/mod/chat/pages/sessions/sessions.html +++ b/src/addons/mod/chat/pages/sessions/sessions.html @@ -16,7 +16,7 @@ - + {{ 'addon.mod_chat.showincompletesessions' | translate }} diff --git a/src/addons/mod/data/components/search/search.html b/src/addons/mod/data/components/search/search.html index 479fb2eb3..34b019cc4 100644 --- a/src/addons/mod/data/components/search/search.html +++ b/src/addons/mod/data/components/search/search.html @@ -11,7 +11,7 @@ - + {{ 'addon.mod_data.advancedsearch' | translate }} diff --git a/src/addons/mod/forum/pages/new-discussion/new-discussion.html b/src/addons/mod/forum/pages/new-discussion/new-discussion.html index 4971fdd2b..5ce021178 100644 --- a/src/addons/mod/forum/pages/new-discussion/new-discussion.html +++ b/src/addons/mod/forum/pages/new-discussion/new-discussion.html @@ -38,7 +38,7 @@
- + {{ 'addon.mod_forum.posttomygroups' | translate }} @@ -54,12 +54,12 @@ - + {{ 'addon.mod_forum.discussionsubscription' | translate }} - + {{ 'addon.mod_forum.discussionpinned' | translate }} diff --git a/src/addons/mod/forum/pages/search/search.html b/src/addons/mod/forum/pages/search/search.html index c31a0b312..18ed2b35d 100644 --- a/src/addons/mod/forum/pages/search/search.html +++ b/src/addons/mod/forum/pages/search/search.html @@ -31,7 +31,7 @@ - +

{{ 'core.search.empty' | translate }}

{{ 'core.search.noresults' | translate: { $a: resultsSource.getQuery() } }}

diff --git a/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_14.png b/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_14.png index c09abb97e..2c3a9cfe5 100644 Binary files a/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_14.png and b/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_14.png differ diff --git a/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_9.png b/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_9.png index cc30d26c8..537c64e4c 100644 Binary files a/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_9.png and b/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_9.png differ diff --git a/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html b/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html index fc3f19726..10ff6ad2e 100644 --- a/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html +++ b/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html @@ -24,7 +24,7 @@ - + @@ -65,7 +65,7 @@ + [message]="'addon.mod_glossary.noentriesfound' | translate" [attr.role]="hasSearched ? 'alert' : null" /> diff --git a/src/addons/mod/glossary/pages/edit/edit.html b/src/addons/mod/glossary/pages/edit/edit.html index 6cde74e13..ca1ad36b2 100644 --- a/src/addons/mod/glossary/pages/edit/edit.html +++ b/src/addons/mod/glossary/pages/edit/edit.html @@ -53,17 +53,17 @@

{{ 'addon.mod_glossary.linking' | translate }}

- + {{ 'addon.mod_glossary.entryusedynalink' | translate }} - + {{ 'addon.mod_glossary.casesensitive' | translate }} - + {{ 'addon.mod_glossary.fullmatch' | translate }} diff --git a/src/addons/mod/h5pactivity/services/h5pactivity-sync.ts b/src/addons/mod/h5pactivity/services/h5pactivity-sync.ts index 1189c3d3e..d9132942c 100644 --- a/src/addons/mod/h5pactivity/services/h5pactivity-sync.ts +++ b/src/addons/mod/h5pactivity/services/h5pactivity-sync.ts @@ -36,6 +36,7 @@ import { CoreTextUtils } from '@services/utils/text'; import { CoreXAPIIRI } from '@features/xapi/classes/iri'; import { CoreXAPIItemAgent } from '@features/xapi/classes/item-agent'; import { CoreWSError } from '@classes/errors/wserror'; +import { CoreArray } from '@singletons/array'; /** * Service to sync H5P activities. @@ -76,7 +77,7 @@ export class AddonModH5PActivitySyncProvider extends CoreCourseActivitySyncBaseP ]); const entries = (<(CoreXAPIStatementDBRecord|CoreXAPIStateDBRecord)[]> statements).concat(states); - const contextIds = CoreUtils.uniqueArray(entries.map(entry => 'contextid' in entry ? entry.contextid : entry.itemid)); + const contextIds = CoreArray.unique(entries.map(entry => 'contextid' in entry ? entry.contextid : entry.itemid)); // Sync all activities. const promises = contextIds.map(async (contextId) => { diff --git a/src/addons/mod/quiz/pages/player/player.html b/src/addons/mod/quiz/pages/player/player.html index 1e0753e79..78c954671 100644 --- a/src/addons/mod/quiz/pages/player/player.html +++ b/src/addons/mod/quiz/pages/player/player.html @@ -47,7 +47,7 @@
+ class="ion-text-wrap ion-margin-start addon-mod_quiz-question-note">

{{question.status}}

diff --git a/src/addons/mod/quiz/pages/player/player.scss b/src/addons/mod/quiz/pages/player/player.scss index 417040e35..fb6ba1e5f 100644 --- a/src/addons/mod/quiz/pages/player/player.scss +++ b/src/addons/mod/quiz/pages/player/player.scss @@ -6,6 +6,8 @@ $quiz-timer-iterations: 15 !default; :host { .addon-mod_quiz-question-note p { + font-weight: normal; + font-size: var(--mdl-typography-fontSize-md); margin-top: 2px; margin-bottom: 2px; } diff --git a/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_27.png b/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_27.png index 256874df3..4dee92729 100644 Binary files a/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_27.png and b/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_27.png differ diff --git a/src/addons/mod/workshop/pages/submission/submission.html b/src/addons/mod/workshop/pages/submission/submission.html index 12c878730..9369bd485 100644 --- a/src/addons/mod/workshop/pages/submission/submission.html +++ b/src/addons/mod/workshop/pages/submission/submission.html @@ -99,7 +99,7 @@

{{ 'addon.mod_workshop.feedbackauthor' | translate }}

- +

{{ 'addon.mod_workshop.publishsubmission' | translate }}

{{ 'addon.mod_workshop.publishsubmission_help' | translate }}

diff --git a/src/addons/notes/services/notes-sync.ts b/src/addons/notes/services/notes-sync.ts index 494ad931c..8d20bcb8d 100644 --- a/src/addons/notes/services/notes-sync.ts +++ b/src/addons/notes/services/notes-sync.ts @@ -24,6 +24,7 @@ import { CoreEvents } from '@singletons/events'; import { AddonNotesDBRecord, AddonNotesDeletedDBRecord } from './database/notes'; import { AddonNotes, AddonNotesCreateNoteData } from './notes'; import { AddonNotesOffline } from './notes-offline'; +import { CoreArray } from '@singletons/array'; /** * Service to sync notes. @@ -67,7 +68,7 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider note.courseid)); }); - CoreUtils.uniqueArray(courseIds); + CoreArray.unique(courseIds); // Sync all courses. const promises = courseIds.map(async (courseId) => { diff --git a/src/addons/notifications/pages/settings/settings.html b/src/addons/notifications/pages/settings/settings.html index 0348dcf1c..f14d84f42 100644 --- a/src/addons/notifications/pages/settings/settings.html +++ b/src/addons/notifications/pages/settings/settings.html @@ -21,12 +21,12 @@ - +

{{ 'addon.notifications.allownotifications' | translate }}

- +

{{ 'addon.notifications.playsound' | translate }}

@@ -79,7 +79,7 @@ - + diff --git a/src/addons/qtype/match/component/addon-qtype-match.html b/src/addons/qtype/match/component/addon-qtype-match.html index 53fb6478d..d03327294 100644 --- a/src/addons/qtype/match/component/addon-qtype-match.html +++ b/src/addons/qtype/match/component/addon-qtype-match.html @@ -1,18 +1,20 @@
- + - + -
- +
+
+ +
diff --git a/src/addons/qtype/match/component/match.scss b/src/addons/qtype/match/component/match.scss index a938ad1dd..3d7daeb1a 100644 --- a/src/addons/qtype/match/component/match.scss +++ b/src/addons/qtype/match/component/match.scss @@ -15,11 +15,14 @@ flex-grow: 1; } - div.flew-row { + div.flex-row { width: 100%; - display: flex; - flex-direction: row; justify-content: space-between; align-items: center; } + + ion-item.question-rows { + --inner-padding-top: var(--mdl-spacing-2); + --inner-padding-bottom: var(--mdl-spacing-2); + } } diff --git a/src/addons/storagemanager/pages/courses-storage/courses-storage.html b/src/addons/storagemanager/pages/courses-storage/courses-storage.html index 892a08ab1..40ccc995e 100644 --- a/src/addons/storagemanager/pages/courses-storage/courses-storage.html +++ b/src/addons/storagemanager/pages/courses-storage/courses-storage.html @@ -37,8 +37,9 @@ {{ totalSize | coreBytesToSize }} - + [disabled]="completelyDownloadedCourses.length === 0" color="danger" fill="clear" + [attr.aria-label]="'addon.storagemanager.deletecourses' | translate"> + - - + + diff --git a/src/core/components/combobox/combobox.scss b/src/core/components/combobox/combobox.scss index c4964c9e7..5889ee710 100644 --- a/src/core/components/combobox/combobox.scss +++ b/src/core/components/combobox/combobox.scss @@ -26,12 +26,25 @@ } } - ion-select, ion-button { --icon-margin: 0 4px; --background: var(--core-combobox-background); + --background-hover: var(--ion-text-color); + --background-activated: var(--ion-text-color); + --background-focused: var(--ion-text-color); + --background-hover-opacity: .04; + + &.md { + --background-activated-opacity: 0; + --background-focused-opacity: .12; + } + + &.ios { + --background-activated-opacity: .12; + --background-focused-opacity: .15; + } --border-color: var(--core-combobox-border-color); --border-style: solid; @@ -53,10 +66,7 @@ overflow: hidden; box-shadow: var(--box-shadow); - &:focus, - &:focus-within { - @include core-focus-style(); - } + --highlight-color: transparent !important; } ion-select { @@ -65,6 +75,7 @@ border-width: var(--border-width); border-radius: var(--core-combobox-radius); margin: 8px; + width: auto; &.combobox-icon-only { &::part(text) { @@ -80,6 +91,21 @@ &::part(icon) { margin: var(--icon-margin); opacity: 1; + --highlight-color: currentColor; + } + + &:hover { + --background: rgba(var(--ion-text-color-rgb), var(--background-hover-opacity)); + } + + &:focus, + &:focus-visible, + &.ion-focused { + --background: rgba(var(--ion-text-color-rgb), var(--background-focused-opacity)); + } + + &.ion-activated { + --background: rgba(var(--ion-text-color-rgb), var(--background-activated-opacity)); } } @@ -89,21 +115,6 @@ --color-focused: currentcolor; --color-hover: currentcolor; - --background-hover: black; - --background-activated: black; - --background-focused: black; - --background-hover-opacity: .04; - - &.md { - --background-activated-opacity: 0; - --background-focused-opacity: .12; - } - - &.ios { - --background-activated-opacity: .12; - --background-focused-opacity: .15; - } - border-radius: var(--core-combobox-radius); margin: 4px 8px; diff --git a/src/core/components/context-menu/core-context-menu-popover.html b/src/core/components/context-menu/core-context-menu-popover.html index 9e844194a..8e2c4ca4e 100644 --- a/src/core/components/context-menu/core-context-menu-popover.html +++ b/src/core/components/context-menu/core-context-menu-popover.html @@ -3,9 +3,9 @@ {{title}} -

diff --git a/src/core/components/mod-icon/mod-icon.ts b/src/core/components/mod-icon/mod-icon.ts index b9f3c1b7c..1b4bcc553 100644 --- a/src/core/components/mod-icon/mod-icon.ts +++ b/src/core/components/mod-icon/mod-icon.ts @@ -55,7 +55,7 @@ export class CoreModIconComponent implements OnInit, OnChanges { @HostBinding('attr.role') get getRole(): string | null { - return !this.showAlt ? 'presentation' : null; + return this.showAlt ? 'img' : 'presentation'; } @HostBinding('attr.aria-label') diff --git a/src/core/components/swipe-slides/swipe-slides.html b/src/core/components/swipe-slides/swipe-slides.html index bcfc05ac8..aeeff91a2 100644 --- a/src/core/components/swipe-slides/swipe-slides.html +++ b/src/core/components/swipe-slides/swipe-slides.html @@ -1,5 +1,6 @@ - - + + diff --git a/src/core/components/swipe-slides/swipe-slides.ts b/src/core/components/swipe-slides/swipe-slides.ts index fa3864c38..49690e299 100644 --- a/src/core/components/swipe-slides/swipe-slides.ts +++ b/src/core/components/swipe-slides/swipe-slides.ts @@ -72,7 +72,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe protected hostElement: HTMLElement; protected unsubscribe?: () => void; protected resizeListener: CoreEventObserver; - protected activeSlideIndexes: number[] = []; + protected activeSlideIndex?: number; protected onReadyPromise = new CorePromisedValue(); constructor( @@ -112,7 +112,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe * @returns Whether the slide is active. */ isActive(index: number): boolean { - return this.activeSlideIndexes.includes(index); + return this.activeSlideIndex === index; } /** @@ -153,7 +153,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe item: items[initialIndex], }; - this.activeSlideIndexes = [initialIndex]; + this.activeSlideIndex = initialIndex; this.manager.setSelectedItem(items[initialIndex]); this.onWillChange.emit(initialItemData); @@ -268,7 +268,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe return; } - this.activeSlideIndexes.push(currentItemData.index); + this.activeSlideIndex = undefined; this.manager?.setSelectedItem(currentItemData.item); this.onWillChange.emit(currentItemData); @@ -283,12 +283,12 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe async slideDidChange(): Promise { const currentItemData = await this.getCurrentSlideItemData(); if (!currentItemData) { - this.activeSlideIndexes = []; + this.activeSlideIndex = undefined; return; } - this.activeSlideIndexes = [currentItemData.index]; + this.activeSlideIndex = currentItemData.index; this.onDidChange.emit(currentItemData); @@ -348,7 +348,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe return; } - this.swiper?.update(); + this.swiper.update(); // We need to ensure the slides are updated before continuing. await CoreUtils.nextTicks(2); diff --git a/src/core/components/tabs-outlet/core-tabs-outlet.html b/src/core/components/tabs-outlet/core-tabs-outlet.html index 1819ef577..f1af5a514 100644 --- a/src/core/components/tabs-outlet/core-tabs-outlet.html +++ b/src/core/components/tabs-outlet/core-tabs-outlet.html @@ -8,8 +8,8 @@ - + this.performAction(event)); + async ngOnInit(): Promise { + let hasNativeButton = false; + if ('componentOnReady' in this.element) { + await this.element.componentOnReady(); + + // Native buttons may be already accessible and does not neet to set TabIndex and role. + hasNativeButton = !!this.element.shadowRoot?.querySelector('.button-native'); + } + + CoreDom.initializeClickableElementA11y(this.element, (event) => this.performAction(event), !hasNativeButton); } /** @@ -79,7 +87,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 || CoreUrlUtils.getUrlScheme(href) === 'javascript') { return; } diff --git a/src/core/features/course/components/course-format/course-format.html b/src/core/features/course/components/course-format/course-format.html index 8a837aa6a..423cf7031 100644 --- a/src/core/features/course/components/course-format/course-format.html +++ b/src/core/features/course/components/course-format/course-format.html @@ -54,12 +54,10 @@ diff --git a/src/core/features/course/components/module/module.scss b/src/core/features/course/components/module/module.scss index d46bf5a78..a57402960 100644 --- a/src/core/features/course/components/module/module.scss +++ b/src/core/features/course/components/module/module.scss @@ -126,7 +126,7 @@ } .activity-extrabadges { - color: var(--gray-700); + color: var(--medium); } .activity-description-availabilityinfo { diff --git a/src/core/features/courses/pages/list/list.html b/src/core/features/courses/pages/list/list.html index 47b406bc2..b896132fb 100644 --- a/src/core/features/courses/pages/list/list.html +++ b/src/core/features/courses/pages/list/list.html @@ -26,9 +26,9 @@ + searchArea="CoreCoursesSearch" [lengthCheck]="1" /> - + @@ -45,7 +45,7 @@ + [message]="'core.courses.nosearchresults' | translate" role="alert" /> diff --git a/src/core/features/courses/pages/list/list.ts b/src/core/features/courses/pages/list/list.ts index 0a3d460ec..1993496cb 100644 --- a/src/core/features/courses/pages/list/list.ts +++ b/src/core/features/courses/pages/list/list.ts @@ -49,6 +49,7 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { coursesLoaded = 0; canLoadMore = false; loadMoreError = false; + loadingMessage = Translate.instant('core.loading'); showOnlyEnrolled = false; @@ -176,6 +177,8 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { * @returns Promise resolved when done. */ protected async loadCourses(clearTheList = false): Promise { + this.loadingMessage = Translate.instant('core.loading'); + this.loadMoreError = false; try { @@ -249,9 +252,10 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { this.searchTotal = 0; this.logSearch = CoreTime.once(() => this.performLogSearch()); - const modal = await CoreDomUtils.showModalLoading('core.searching', true); + this.loaded = false; await this.searchCourses().finally(() => { - modal.dismiss(); + this.loaded = true; + }); } @@ -310,6 +314,7 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { */ protected async searchCourses(): Promise { this.loadMoreError = false; + this.loadingMessage = Translate.instant('core.searching'); try { const response = await CoreCourses.search(this.searchText, this.searchPage, undefined, this.showOnlyEnrolled); diff --git a/src/core/features/courses/pages/my/my.scss b/src/core/features/courses/pages/my/my.scss index e073b4273..df09390a0 100644 --- a/src/core/features/courses/pages/my/my.scss +++ b/src/core/features/courses/pages/my/my.scss @@ -11,6 +11,5 @@ core-block ::ng-deep ion-card.addon-block-myoverview { --border-width: 0; - --background: transparent; margin: 0; } diff --git a/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html b/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html index 029b97c3d..6b476e077 100644 --- a/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html +++ b/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html @@ -1,8 +1,7 @@

+ (blur)="hideToolbar($event)" (keydown)="onKeyDown($event)">
Promise; - protected selectionChangeFunction?: () => void; + protected selectionChangeFunction = (): void => this.updateToolbarStyles(); protected languageChangedSubscription?: Subscription; protected resizeListener?: CoreEventObserver; protected domPromise?: CoreCancellablePromise; @@ -226,6 +227,15 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, } ionItem.classList.add('item-rte'); + if (this.editorElement) { + const debounceMutation = CoreUtils.debounce(() => { + this.onChange(); + }, 20); + + this.contentObserver = new MutationObserver(debounceMutation); + this.contentObserver.observe(this.editorElement, { childList: true, subtree: true, characterData: true }); + } + const label = ionItem.querySelector('ion-label'); if (!label) { @@ -253,7 +263,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, protected setListeners(): void { // Listen for changes on the control to update the editor (if it is updated from outside of this component). this.valueChangeSubscription = this.control?.valueChanges.subscribe((newValue) => { - if (this.draftWasRestored && this.originalContent == newValue) { + if (this.draftWasRestored && this.originalContent === newValue) { // A draft was restored and the content hasn't changed in the site. Use the draft value instead of this one. this.control?.setValue(this.lastDraft, { emitEvent: false }); @@ -282,7 +292,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, this.windowResized(); }, 50); - document.addEventListener('selectionchange', this.selectionChangeFunction = () => this.updateToolbarStyles()); + document.addEventListener('selectionchange', this.selectionChangeFunction); this.keyboardObserver = CoreEvents.on(CoreEvents.KEYBOARD_CHANGE, () => { // Opening or closing the keyboard also calls the resize function, but sometimes the resize is called too soon. @@ -304,8 +314,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * @param event Event */ onKeyDown(event: KeyboardEvent): void { - this.onChange(); - const shortcutId = this.getShortcutId(event); const commands = this.getShortcutCommands(); const command = commands[shortcutId]; @@ -364,7 +372,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, // Get first children with content, not fixed. let scrollContentHeight = 0; - while (scrollContentHeight == 0 && content?.children) { + while (scrollContentHeight === 0 && content?.children) { const children = Array.from(content.children) .filter((element) => element.slot !== 'fixed' && !element.classList.contains('core-loading-container')); @@ -489,7 +497,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * @param event The event. */ async toggleEditor(event: Event): Promise { - if (event.type == 'keyup' && !this.isValidKeyboardKey(event)) { + if (event.type === 'keyup' && !this.isValidKeyboardKey(event)) { return; } @@ -581,7 +589,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * @returns If value is null only a white space. */ protected isNullOrWhiteSpace(value: string | null | undefined): boolean { - if (value == null || value === undefined) { + if (value === null || value === undefined) { return true; } @@ -602,10 +610,17 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, } if (this.isNullOrWhiteSpace(value)) { - this.editorElement.innerHTML = '

'; + // Avoid loops. + if (this.editorElement.innerHTML !== '

') { + this.editorElement.innerHTML = '

'; + } this.textarea.value = ''; } else { - this.editorElement.innerHTML = value || ''; + value = value || ''; + // Avoid loops. + if (this.editorElement.innerHTML !== value) { + this.editorElement.innerHTML = value; + } this.textarea.value = value; this.treatExternalContent(); } @@ -637,7 +652,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * toolbar styles button when set. */ buttonAction(event: Event, command: string, parameters?: string): void { - if (event.type == 'keyup' && !this.isValidKeyboardKey(event)) { + if (event.type === 'keyup' && !this.isValidKeyboardKey(event)) { return; } @@ -659,7 +674,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * @param command.parameters Command parameters. */ protected executeCommand({ name: command, parameters }: EditorCommand): void { - if (parameters == 'block') { + if (parameters === 'block') { // eslint-disable-next-line deprecation/deprecation document.execCommand('formatBlock', false, '<' + command + '>'); @@ -676,7 +691,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, // Modern browsers are using non a11y tags, so replace them. if (command === 'bold') { this.replaceTags(['b'], ['strong']); - } else if (command == 'italic') { + } else if (command === 'italic') { this.replaceTags(['i'], ['em']); } } @@ -715,14 +730,14 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * @param event Event. * @param force If true it will not check the target of the event. */ - hideToolbar(event: Event, force = false): void { + hideToolbar(event: FocusEvent | KeyboardEvent | MouseEvent, force = false): void { if (!force && event.target && this.element.contains(event.target as HTMLElement)) { // Do not hide if clicked inside the editor area, except forced. return; } - if (event.type == 'keyup' && !this.isValidKeyboardKey(event)) { + if (event.type === 'keyup' && !this.isValidKeyboardKey(event)) { return; } @@ -748,7 +763,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, /** * Show the toolbar. */ - showToolbar(event: Event): void { + showToolbar(event: FocusEvent): void { this.updateToolbarButtons(); this.element.classList.add('ion-touched'); @@ -779,14 +794,14 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * @param event Event. */ downAction(event: Event): void { - if (event.type == 'keydown' && !this.isValidKeyboardKey(event)) { + if (event.type === 'keydown' && !this.isValidKeyboardKey(event)) { return; } const selection = window.getSelection()?.toString(); // When RTE is focused with a whole paragraph in desktop the stopBubble will not fire click. - if (CorePlatform.isMobile() || !this.rteEnabled || document.activeElement != this.editorElement || selection == '') { + if (CorePlatform.isMobile() || !this.rteEnabled || document.activeElement != this.editorElement || selection === '') { this.stopBubble(event); } } @@ -795,7 +810,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * Method that shows the next toolbar buttons. */ async toolbarNext(event: Event): Promise { - if (event.type == 'keyup' && !this.isValidKeyboardKey(event)) { + if (event.type === 'keyup' && !this.isValidKeyboardKey(event)) { return; } @@ -813,7 +828,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * Method that shows the previous toolbar buttons. */ async toolbarPrev(event: Event): Promise { - if (event.type == 'keyup' && !this.isValidKeyboardKey(event)) { + if (event.type === 'keyup' && !this.isValidKeyboardKey(event)) { return; } @@ -831,7 +846,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * Update the number of toolbar buttons displayed. */ async updateToolbarButtons(): Promise { - if (!this.isCurrentView || !this.toolbar || !this.toolbarSlides || this.element.offsetParent == null) { + if (!this.isCurrentView || !this.toolbar || !this.toolbarSlides || this.element.offsetParent === null) { // Don't calculate if component isn't in current view, the calculations are wrong. return; } @@ -879,15 +894,18 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, */ updateToolbarStyles(): void { const node = window.getSelection()?.focusNode; - if (!node) { + + if (!node || !this.element.contains(node)) { return; } - let element = node.nodeType == 1 ? node as HTMLElement : node.parentElement; + let element = node.nodeType === 1 ? node as HTMLElement : node.parentElement; + const styles = {}; - while (element != null && element !== this.editorElement) { + while (element !== null && element !== this.editorElement) { const tagName = element.tagName.toLowerCase(); + if (this.toolbarStyles[tagName]) { styles[tagName] = 'true'; } @@ -906,7 +924,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, /** * Check if should auto save drafts. * - * @returns {boolean} Whether it should auto save drafts. + * @returns Whether it should auto save drafts. */ protected shouldAutoSaveDrafts(): boolean { return !!CoreSites.getCurrentSite() && @@ -943,8 +961,8 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, let draftText = entry.drafttext || ''; // Revert untouched editor contents to an empty string. - if (draftText == '

' || draftText == '


' || draftText == '
' || - draftText == '

 

' || draftText == '


 

') { + if (draftText === '

' || draftText === '


' || draftText === '
' || + draftText === '

 

' || draftText === '


 

') { draftText = ''; } @@ -977,7 +995,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, const newText = this.control.value ?? ''; - if (this.lastDraft == newText) { + if (this.lastDraft === newText) { // Text hasn't changed, nothing to save. return; } @@ -996,7 +1014,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, // Draft saved, notify the user. this.lastDraft = newText; this.showMessage('core.editor.autosavesucceeded', this.SAVE_MESSAGE_CLEAR_TIME); - } catch (error) { + } catch { // Error saving draft. } }, this.DRAFT_AUTOSAVE_FREQUENCY); @@ -1009,7 +1027,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, this.resetObserver = CoreEvents.on(CoreEvents.FORM_ACTION, async (data: CoreEventFormActionData) => { const form = this.element.closest('form'); - if (data.form && form && data.form == form) { + if (data.form && form && data.form === form) { try { await CoreEditorOffline.deleteDraft( this.contextLevel || ContextLevel.SYSTEM, @@ -1048,7 +1066,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * @returns Promise resolved when done. */ async scanQR(event: Event): Promise { - if (event.type == 'keyup' && !this.isValidKeyboardKey(event)) { + if (event.type === 'keyup' && !this.isValidKeyboardKey(event)) { return; } @@ -1097,14 +1115,20 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, ngOnDestroy(): void { this.valueChangeSubscription?.unsubscribe(); this.languageChangedSubscription?.unsubscribe(); - this.selectionChangeFunction && document.removeEventListener('selectionchange', this.selectionChangeFunction); + + document.removeEventListener('selectionchange', this.selectionChangeFunction); + clearInterval(this.initHeightInterval); clearInterval(this.autoSaveInterval); clearTimeout(this.hideMessageTimeout); + this.resetObserver?.off(); this.keyboardObserver?.off(); - this.labelObserver?.disconnect(); this.resizeListener?.off(); + + this.labelObserver?.disconnect(); + this.contentObserver?.disconnect(); + this.domPromise?.cancel(); this.buttonsDomPromise?.cancel(); } diff --git a/src/core/features/enrol/services/enrol-helper.ts b/src/core/features/enrol/services/enrol-helper.ts index 54d3dd03b..72ef71821 100644 --- a/src/core/features/enrol/services/enrol-helper.ts +++ b/src/core/features/enrol/services/enrol-helper.ts @@ -17,6 +17,7 @@ import { makeSingleton } from '@singletons'; import { CoreEnrolAction, CoreEnrolDelegate, CoreEnrolInfoIcon } from './enrol-delegate'; import { CoreUtils } from '@services/utils/utils'; import { CoreEnrol, CoreEnrolEnrolmentMethod } from './enrol'; +import { CoreArray } from '@singletons/array'; /** * Service that provides helper functions for enrolment plugins. @@ -32,7 +33,7 @@ export class CoreEnrolHelperService { * @returns Enrolment icons to show. */ async getEnrolmentIcons(methodTypes: string[], courseId: number): Promise { - methodTypes = CoreUtils.uniqueArray(methodTypes); + methodTypes = CoreArray.unique(methodTypes); let enrolmentIcons: CoreEnrolInfoIcon[] = []; let addBrowserOption = false; diff --git a/src/core/features/fileuploader/services/handlers/album.ts b/src/core/features/fileuploader/services/handlers/album.ts index e61ddf6de..6f457a7a1 100644 --- a/src/core/features/fileuploader/services/handlers/album.ts +++ b/src/core/features/fileuploader/services/handlers/album.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CorePlatform } from '@services/platform'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreArray } from '@singletons/array'; import { makeSingleton } from '@singletons'; import { CoreFileUploaderHandler, CoreFileUploaderHandlerData, CoreFileUploaderHandlerResult } from '../fileuploader-delegate'; import { CoreFileUploaderHelper } from '../fileuploader-helper'; @@ -41,7 +41,7 @@ export class CoreFileUploaderAlbumHandlerService implements CoreFileUploaderHand */ getSupportedMimetypes(mimetypes: string[]): string[] { // Album allows picking images and videos. - return CoreUtils.filterByRegexp(mimetypes, /^(image|video)\//); + return CoreArray.filterByRegexp(mimetypes, /^(image|video)\//); } /** diff --git a/src/core/features/fileuploader/services/handlers/audio.ts b/src/core/features/fileuploader/services/handlers/audio.ts index 2a2e93725..a5e5dc573 100644 --- a/src/core/features/fileuploader/services/handlers/audio.ts +++ b/src/core/features/fileuploader/services/handlers/audio.ts @@ -16,10 +16,11 @@ import { Injectable } from '@angular/core'; import { CoreApp } from '@services/app'; import { CorePlatform } from '@services/platform'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreArray } from '@singletons/array'; import { makeSingleton } from '@singletons'; import { CoreFileUploaderHandler, CoreFileUploaderHandlerData, CoreFileUploaderHandlerResult } from '../fileuploader-delegate'; import { CoreFileUploaderHelper } from '../fileuploader-helper'; + /** * Handler to record an audio to upload it. */ @@ -42,10 +43,10 @@ export class CoreFileUploaderAudioHandlerService implements CoreFileUploaderHand getSupportedMimetypes(mimetypes: string[]): string[] { if (CorePlatform.isIOS()) { // In iOS it's recorded as WAV. - return CoreUtils.filterByRegexp(mimetypes, /^audio\/wav$/); + return CoreArray.filterByRegexp(mimetypes, /^audio\/wav$/); } else if (CorePlatform.isAndroid()) { // In Android we don't know the format the audio will be recorded, so accept any audio mimetype. - return CoreUtils.filterByRegexp(mimetypes, /^audio\//); + return CoreArray.filterByRegexp(mimetypes, /^audio\//); } else { // In browser, support audio formats that are supported by MediaRecorder. if (MediaRecorder) { diff --git a/src/core/features/fileuploader/services/handlers/camera.ts b/src/core/features/fileuploader/services/handlers/camera.ts index 80cae2cdd..373b338fa 100644 --- a/src/core/features/fileuploader/services/handlers/camera.ts +++ b/src/core/features/fileuploader/services/handlers/camera.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreApp } from '@services/app'; import { CorePlatform } from '@services/platform'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreArray } from '@singletons/array'; import { makeSingleton } from '@singletons'; import { CoreFileUploaderHandler, CoreFileUploaderHandlerData, CoreFileUploaderHandlerResult } from '../fileuploader-delegate'; import { CoreFileUploaderHelper } from '../fileuploader-helper'; @@ -42,7 +42,7 @@ export class CoreFileUploaderCameraHandlerService implements CoreFileUploaderHan */ getSupportedMimetypes(mimetypes: string[]): string[] { // Camera only supports JPEG and PNG. - return CoreUtils.filterByRegexp(mimetypes, /^image\/(jpeg|png)$/); + return CoreArray.filterByRegexp(mimetypes, /^image\/(jpeg|png)$/); } /** diff --git a/src/core/features/fileuploader/services/handlers/video.ts b/src/core/features/fileuploader/services/handlers/video.ts index 7d5b72476..ac655b580 100644 --- a/src/core/features/fileuploader/services/handlers/video.ts +++ b/src/core/features/fileuploader/services/handlers/video.ts @@ -16,10 +16,11 @@ import { Injectable } from '@angular/core'; import { CoreApp } from '@services/app'; import { CorePlatform } from '@services/platform'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreArray } from '@singletons/array'; import { makeSingleton } from '@singletons'; import { CoreFileUploaderHandler, CoreFileUploaderHandlerData, CoreFileUploaderHandlerResult } from '../fileuploader-delegate'; import { CoreFileUploaderHelper } from '../fileuploader-helper'; + /** * Handler to record a video to upload it. */ @@ -42,10 +43,10 @@ export class CoreFileUploaderVideoHandlerService implements CoreFileUploaderHand getSupportedMimetypes(mimetypes: string[]): string[] { if (CorePlatform.isIOS()) { // In iOS it's recorded as MOV. - return CoreUtils.filterByRegexp(mimetypes, /^video\/quicktime$/); + return CoreArray.filterByRegexp(mimetypes, /^video\/quicktime$/); } else if (CorePlatform.isAndroid()) { // In Android we don't know the format the video will be recorded, so accept any video mimetype. - return CoreUtils.filterByRegexp(mimetypes, /^video\//); + return CoreArray.filterByRegexp(mimetypes, /^video\//); } else { // In browser, support video formats that are supported by MediaRecorder. if (MediaRecorder) { diff --git a/src/core/features/grades/pages/courses/courses.html b/src/core/features/grades/pages/courses/courses.html index 9a0f31c22..1c7eb2f8d 100644 --- a/src/core/features/grades/pages/courses/courses.html +++ b/src/core/features/grades/pages/courses/courses.html @@ -21,7 +21,9 @@ [attr.aria-current]="courses.getItemAriaCurrent(course)" class="ion-text-wrap" button [detail]="true" (click)="courses.select(course)"> - +

+ +

diff --git a/src/core/features/h5p/classes/content-validator.ts b/src/core/features/h5p/classes/content-validator.ts index 3dec4b47b..df57b88d1 100644 --- a/src/core/features/h5p/classes/content-validator.ts +++ b/src/core/features/h5p/classes/content-validator.ts @@ -17,6 +17,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreH5P } from '@features/h5p/services/h5p'; import { Translate } from '@singletons'; import { CoreH5PCore, CoreH5PLibraryData, CoreH5PLibraryAddonData, CoreH5PContentDepsTreeDependency } from './core'; +import { CoreArray } from '@singletons/array'; const ALLOWED_STYLEABLE_TAGS = ['span', 'p', 'div', 'h1', 'h2', 'h3', 'td']; @@ -131,7 +132,7 @@ export class CoreH5PContentValidator { tags.push('s'); } - tags = CoreUtils.uniqueArray(tags); + tags = CoreArray.unique(tags); // Determine allowed style tags const stylePatterns: RegExp[] = []; @@ -372,7 +373,7 @@ export class CoreH5PContentValidator { if (semantics.extraAttributes) { validKeys = validKeys.concat(semantics.extraAttributes); } - validKeys = CoreUtils.uniqueArray(validKeys); + validKeys = CoreArray.unique(validKeys); this.filterParams(file, validKeys); @@ -556,7 +557,7 @@ export class CoreH5PContentValidator { let validKeys = ['library', 'params', 'subContentId', 'metadata']; if (semantics.extraAttributes) { - validKeys = CoreUtils.uniqueArray(validKeys.concat(semantics.extraAttributes)); + validKeys = CoreArray.unique(validKeys.concat(semantics.extraAttributes)); } this.filterParams(value, validKeys); diff --git a/src/core/features/login/pages/site/site.ts b/src/core/features/login/pages/site/site.ts index 64f6eaa9d..3e93bc137 100644 --- a/src/core/features/login/pages/site/site.ts +++ b/src/core/features/login/pages/site/site.ts @@ -235,7 +235,7 @@ export class CoreLoginSitePage implements OnInit { /** * Validate Url. * - * @returns {ValidatorFn} Validation results. + * @returns Validation results. */ protected moodleUrlValidator(): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { diff --git a/src/core/features/mainmenu/pages/menu/menu.scss b/src/core/features/mainmenu/pages/menu/menu.scss index 768a558a9..22f2007d9 100644 --- a/src/core/features/mainmenu/pages/menu/menu.scss +++ b/src/core/features/mainmenu/pages/menu/menu.scss @@ -41,48 +41,53 @@ } ion-tab-bar { - height: var(--menutabbar-size); + --background: var(--core-bottom-tabs-background); + --color: var(--core-bottom-tabs-color); + --color-selected: var(--core-bottom-tabs-color-selected); + --background-selected: var(--core-bottom-tabs-background-selected); - core-user-menu-button { - align-items: center; - display: flex; - justify-content: center; + height: var(--menutabbar-size); +} + +core-user-menu-button { + align-items: center; + display: flex; + justify-content: center; +} + +ion-tab-button { + &.tab-selected { + background: var(--background-selected); } - ion-tab-button { - &.tab-selected { - background: var(--background-selected); - } + ion-icon.core-tab-icon { + text-overflow: unset; + overflow: visible; + text-align: center; + font-size: var(--mdl-typography-icon-fontSize-lg); + } - ion-icon.core-tab-icon { - text-overflow: unset; - overflow: visible; - text-align: center; - font-size: var(--mdl-typography-icon-fontSize-lg); - } + ion-badge.core-tab-badge { + font-size: 12px; + font-weight: bold; + border-radius: 10px; + padding-left: 6px; + padding-right: 6px; + line-height: 14px; + --background: var(--core-bottom-tabs-badge-color); + --color: var(--core-bottom-tabs-badge-text-color); + } - ion-badge.core-tab-badge { - font-size: 12px; - font-weight: bold; - border-radius: 10px; - padding-left: 6px; - padding-right: 6px; - line-height: 14px; - --background: var(--core-bottom-tabs-badge-color); - --color: var(--core-bottom-tabs-badge-text-color); - } - - ion-icon.core-tab-badge { - color: var(--core-bottom-tabs-badge-color); - padding: 3px 6px 2px; - @include position(8px, null, null, calc(50% + 6px)); - min-width: 12px; - font-size: 8px; - font-weight: normal; - box-sizing: border-box; - position: absolute; - z-index: 1; - } + ion-icon.core-tab-badge { + color: var(--core-bottom-tabs-badge-color); + padding: 3px 6px 2px; + @include position(8px, null, null, calc(50% + 6px)); + min-width: 12px; + font-size: 8px; + font-weight: normal; + box-sizing: border-box; + position: absolute; + z-index: 1; } } diff --git a/src/core/features/search/components/search-box/core-search-box.html b/src/core/features/search/components/search-box/core-search-box.html index 054ab491a..c10f9d2ea 100644 --- a/src/core/features/search/components/search-box/core-search-box.html +++ b/src/core/features/search/components/search-box/core-search-box.html @@ -2,8 +2,7 @@ - + + + + {{ 'core.search.err_minlength' | translate : {'$a': {'format': lengthCheck} } }} + + diff --git a/src/core/features/search/components/search-box/search-box.scss b/src/core/features/search/components/search-box/search-box.scss index 8afdc8c3d..fa22c8742 100644 --- a/src/core/features/search/components/search-box/search-box.scss +++ b/src/core/features/search/components/search-box/search-box.scss @@ -35,8 +35,6 @@ } } - - .core-search-history { max-height: calc(-120px + 80vh); overflow-y: auto; diff --git a/src/core/features/search/components/search-box/search-box.ts b/src/core/features/search/components/search-box/search-box.ts index c013928d3..1db5989c8 100644 --- a/src/core/features/search/components/search-box/search-box.ts +++ b/src/core/features/search/components/search-box/search-box.ts @@ -62,6 +62,7 @@ export class CoreSearchBoxComponent implements OnInit { searchText = ''; history: CoreSearchHistoryDBRecord[] = []; historyShown = false; + showLengthAlert = false; constructor() { this.onSubmit = new EventEmitter(); @@ -86,14 +87,17 @@ export class CoreSearchBoxComponent implements OnInit { * @param e Event. */ submitForm(e?: Event): void { - e && e.preventDefault(); - e && e.stopPropagation(); + e?.preventDefault(); + e?.stopPropagation(); if (this.searchText.length < this.lengthCheck) { - // The view should handle this case, but we check it here too just in case. + this.showLengthAlert = true; + return; } + this.showLengthAlert = false; + if (this.searchArea) { this.saveSearchToHistory(this.searchText); } @@ -147,6 +151,7 @@ export class CoreSearchBoxComponent implements OnInit { clearForm(): void { this.searched = ''; this.searchText = ''; + this.showLengthAlert = false; this.onClear.emit(); } diff --git a/src/core/features/search/lang.json b/src/core/features/search/lang.json index 5a17626cd..d1fbb00cd 100644 --- a/src/core/features/search/lang.json +++ b/src/core/features/search/lang.json @@ -8,5 +8,6 @@ "globalsearch": "Global search", "noresults": "No results for \"{{$a}}\"", "noresultshelp": "Check for typos or try using different keywords", + "err_minlength": "You must enter at least {{$a.format}} characters here.", "resultby": "By {{$a}}" } diff --git a/src/core/features/search/pages/global-search/global-search.html b/src/core/features/search/pages/global-search/global-search.html index 61f677773..ebdb674d9 100644 --- a/src/core/features/search/pages/global-search/global-search.html +++ b/src/core/features/search/pages/global-search/global-search.html @@ -35,7 +35,7 @@ [error]="loadMoreError" /> -

{{ 'core.search.empty' | translate }}

+

{{ 'core.search.empty' | translate }}

{{ 'core.search.noresults' | translate: { $a: resultsSource.getQuery() } }}

{{ 'core.search.noresultshelp' | translate }}

diff --git a/src/core/features/settings/pages/dev/dev.html b/src/core/features/settings/pages/dev/dev.html index 37484811f..3de3e2c67 100644 --- a/src/core/features/settings/pages/dev/dev.html +++ b/src/core/features/settings/pages/dev/dev.html @@ -17,13 +17,13 @@ - +

Change text direction

{{ direction }}

- +

Force safe area margins

@@ -34,13 +34,13 @@
- +

Enable remote styles {{remoteStylesCount}}

- +

Enable site plugin styles {{pluginStylesCount}}

diff --git a/src/core/features/settings/pages/deviceinfo/deviceinfo.html b/src/core/features/settings/pages/deviceinfo/deviceinfo.html index 98a838f0f..bda6b9467 100644 --- a/src/core/features/settings/pages/deviceinfo/deviceinfo.html +++ b/src/core/features/settings/pages/deviceinfo/deviceinfo.html @@ -18,7 +18,7 @@ - +