|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -16,48 +16,43 @@
|
|||
<ion-item class="addon-block-timeline-activity" [detail]="false" (click)="action($event, event.url)" [attr.aria-label]="event.name"
|
||||
button lines="full">
|
||||
<ion-label>
|
||||
<ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding">
|
||||
<ion-col class="addon-block-timeline-activity-main ion-no-padding">
|
||||
<ion-row class="ion-justify-content-between ion-align-items-center ion-nowrap ion-no-padding">
|
||||
<ion-col class="addon-block-timeline-activity-time ion-no-padding ion-text-nowrap">
|
||||
<small>{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</small>
|
||||
<core-mod-icon *ngIf="event.iconUrl" [modicon]="event.iconUrl" [componentId]="event.instance"
|
||||
[modname]="event.modulename" [purpose]="event.purpose" [colorize]="colorizeIcons"
|
||||
[isBranded]="event.branded" />
|
||||
</ion-col>
|
||||
<ion-col class="addon-block-timeline-activity-name ion-no-padding">
|
||||
<p class="item-heading">
|
||||
<span>
|
||||
<core-format-text [text]="event.activityname || event.name" contextLevel="module"
|
||||
[contextInstanceId]="event.id" [courseId]="event.course?.id" />
|
||||
</span>
|
||||
<ion-badge *ngIf="event.overdue" color="danger">{{ 'addon.block_timeline.overdue' | translate }}
|
||||
</ion-badge>
|
||||
</p>
|
||||
<p *ngIf="showInlineCourse && event.course">
|
||||
<span>
|
||||
<core-format-text [text]="event.course.fullnamedisplay" contextLevel="course"
|
||||
[contextInstanceId]="event.course.id" />
|
||||
</span>
|
||||
</p>
|
||||
<p *ngIf="event.activitystr">
|
||||
<span>
|
||||
<core-format-text *ngIf="event.activitystr" [text]="event.activitystr" contextLevel="module"
|
||||
[contextInstanceId]="event.id" />
|
||||
</span>
|
||||
</p>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-row class="ion-justify-content-between ion-align-items-center ion-nowrap ion-no-padding">
|
||||
<ion-col class="addon-block-timeline-activity-time ion-no-padding ion-text-nowrap">
|
||||
<small>{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</small>
|
||||
<core-mod-icon *ngIf="event.iconUrl" [modicon]="event.iconUrl" [componentId]="event.instance"
|
||||
[modname]="event.modulename" [purpose]="event.purpose" [colorize]="colorizeIcons" [isBranded]="event.branded" />
|
||||
</ion-col>
|
||||
<ion-col class="addon-block-timeline-activity-action ion-no-padding" *ngIf="event.action && event.action.actionable">
|
||||
<ion-button fill="outline" (click)="action($event, event.action.url)" [title]="event.action.name" class="chip">
|
||||
{{event.action.name}}
|
||||
<ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount">
|
||||
{{event.action.itemcount}}
|
||||
<ion-col class="addon-block-timeline-activity-name ion-no-padding">
|
||||
<p class="item-heading">
|
||||
<span>
|
||||
<core-format-text [text]="event.activityname || event.name" contextLevel="module"
|
||||
[contextInstanceId]="event.id" [courseId]="event.course?.id" />
|
||||
</span>
|
||||
<ion-badge *ngIf="event.overdue" color="danger">{{ 'addon.block_timeline.overdue' | translate }}
|
||||
</ion-badge>
|
||||
</ion-button>
|
||||
</p>
|
||||
<p *ngIf="showInlineCourse && event.course">
|
||||
<span>
|
||||
<core-format-text [text]="event.course.fullnamedisplay" contextLevel="course"
|
||||
[contextInstanceId]="event.course.id" />
|
||||
</span>
|
||||
</p>
|
||||
<p *ngIf="event.activitystr">
|
||||
<span>
|
||||
<core-format-text *ngIf="event.activitystr" [text]="event.activitystr" contextLevel="module"
|
||||
[contextInstanceId]="event.id" />
|
||||
</span>
|
||||
</p>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<div class="addon-block-timeline-activity-action" *ngIf="event.action && event.action.actionable">
|
||||
<ion-button fill="outline" (click)="action($event, event.action.url)" [title]="event.action.name" class="chip">
|
||||
{{event.action.name}}
|
||||
<ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount">
|
||||
{{event.action.itemcount}}
|
||||
</ion-badge>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
</ion-item>
|
||||
<div id="addon-blog-associations">
|
||||
@if (associationsExpanded) {
|
||||
<ion-item>
|
||||
<ion-item class="ion-no-validation">
|
||||
@if (associatedModule) {
|
||||
<ion-toggle formControlName="associateWithModule">
|
||||
<core-format-text [text]="'addon.blog.associatewithmodule' | translate: {
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
<core-loading [hideUntil]="loaded">
|
||||
@if (showMyEntriesToggle) {
|
||||
<ion-item>
|
||||
<ion-item class="ion-no-validation">
|
||||
<ion-toggle [(ngModel)]="onlyMyEntries" (ionChange)="onlyMyEntriesToggleChanged(onlyMyEntries)">
|
||||
{{ 'addon.blog.showonlyyourentries' | translate }}
|
||||
</ion-toggle>
|
||||
|
@ -65,34 +65,32 @@
|
|||
</div>
|
||||
|
||||
|
||||
<ion-label>
|
||||
<div class="entry-summary" [ngClass]="{ 'border-bottom': entry.lastmodified <= entry.created }" [collapsible-item]="64">
|
||||
<div class="ion-margin-bottom">
|
||||
<core-format-text [text]="entry.summary" [component]="component" [componentId]="entry.id"
|
||||
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="entry.courseid" />
|
||||
</div>
|
||||
|
||||
@if (tagsEnabled && entry.tags && entry.tags!.length > 0) {
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<div slot="start">{{ 'core.tag.tags' | translate }}:</div>
|
||||
<core-tag-list [tags]="entry.tags" />
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
}
|
||||
|
||||
@for (file of entry.attachmentfiles; track $index) {
|
||||
<core-file [file]="file" [component]="this.component" [componentId]="entry.id" />
|
||||
}
|
||||
|
||||
@if (entry.uniquehash) {
|
||||
<ion-item [href]="entry.uniquehash" core-link [detail]="true">
|
||||
<ion-label>{{ 'addon.blog.linktooriginalentry' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
}
|
||||
|
||||
<div class="entry-summary" [ngClass]="{ 'border-bottom': entry.lastmodified <= entry.created }" [collapsible-item]="64">
|
||||
<div class="ion-margin-bottom">
|
||||
<core-format-text [text]="entry.summary" [component]="component" [componentId]="entry.id" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="contextInstanceId" [courseId]="entry.courseid" />
|
||||
</div>
|
||||
</ion-label>
|
||||
|
||||
@if (tagsEnabled && entry.tags && entry.tags!.length > 0) {
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<div slot="start">{{ 'core.tag.tags' | translate }}:</div>
|
||||
<core-tag-list [tags]="entry.tags" />
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
}
|
||||
|
||||
@for (file of entry.attachmentfiles; track $index) {
|
||||
<core-file [file]="file" [component]="this.component" [componentId]="entry.id" />
|
||||
}
|
||||
|
||||
@if (entry.uniquehash) {
|
||||
<ion-item [href]="entry.uniquehash" core-link [detail]="true">
|
||||
<ion-label>{{ 'addon.blog.linktooriginalentry' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
@if (entry.lastmodified > entry.created || (entry.userid === currentUserId && entry.publishstate !== 'draft')) {
|
||||
<ion-item class="entry-last-modification">
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
}
|
||||
|
||||
.entry {
|
||||
border-top: 1px solid var(--stroke);
|
||||
border-bottom: 1px solid var(--stroke);
|
||||
|
||||
&-visibility-permission {
|
||||
display: flex;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<ion-grid class="ion-no-padding addon-calendar-navigation">
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col class="ion-text-start" *ngIf="canNavigate">
|
||||
<ion-button fill="clear" (click)="loadPrevious()" [attr.aria-label]="'core.previous' | translate">
|
||||
<ion-button fill="clear" (click)="loadPrevious()" [attr.aria-label]="'addon.calendar.monthprev' | translate">
|
||||
<ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true" />
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
|
@ -23,7 +23,7 @@
|
|||
</h2>
|
||||
</ion-col>
|
||||
<ion-col class="ion-text-end" *ngIf="canNavigate">
|
||||
<ion-button fill="clear" (click)="loadNext()" [attr.aria-label]="'core.next' | translate">
|
||||
<ion-button fill="clear" (click)="loadNext()" [attr.aria-label]="'addon.calendar.monthnext' | translate">
|
||||
<ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true" />
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
|
@ -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">
|
||||
<p class="addon-calendar-day-number">
|
||||
<span aria-hidden="true">{{ day.mday }}</span>
|
||||
<span class="sr-only">{{ day.periodName | translate }}</span>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</ion-header>
|
||||
<ion-content [fullscreen]="true">
|
||||
<ion-list>
|
||||
<ion-item *ngFor="let type of types" class="addon-calendar-event" [ngClass]="['addon-calendar-eventtype-'+type]">
|
||||
<ion-item *ngFor="let type of types" class="addon-calendar-event ion-no-validation" [ngClass]="['addon-calendar-eventtype-'+type]">
|
||||
<ion-icon [name]="typeIcons[type]" slot="start" aria-hidden="true" />
|
||||
<ion-toggle [(ngModel)]="filter[type]" (ionChange)="onChange()">
|
||||
{{ 'addon.calendar.' + type + 'events' | translate}}
|
||||
|
|
|
@ -39,6 +39,8 @@
|
|||
"mon": "Mon",
|
||||
"monday": "Monday",
|
||||
"monthlyview": "Monthly view",
|
||||
"monthnext": "Next month",
|
||||
"monthprev": "Previous month",
|
||||
"newevent": "New event",
|
||||
"noevents": "There are no events",
|
||||
"nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event.",
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
<ion-back-button [text]="'core.back' | translate" />
|
||||
</ion-buttons>
|
||||
<ion-title>
|
||||
<h1>{{ (showCalendar ? 'addon.calendar.calendarevents' : 'addon.calendar.upcomingevents') | translate }}</h1>
|
||||
<h1>{{ 'addon.calendar.calendar' | translate }}</h1>
|
||||
<p *ngIf="!showCalendar" class="subheading">{{ 'addon.calendar.upcomingevents' | translate }}</p>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button fill="clear" (click)="openFilter()" [attr.aria-label]="'core.filter' | translate">
|
||||
|
|
|
@ -159,7 +159,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* View loaded.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.loadUpcoming = !!CoreNavigator.getRouteBooleanParam('upcoming');
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
</ion-item-divider>
|
||||
<ion-card>
|
||||
<ion-list>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let device of platform.devices" [class.item-current]="device.current">
|
||||
<ion-item class="ion-text-wrap ion-no-validation" *ngFor="let device of platform.devices"
|
||||
[class.item-current]="device.current">
|
||||
<ion-label>
|
||||
<p class="item-heading" id="device-{{device.id}}">
|
||||
<strong>{{ device.name }} {{ device.model }}</strong> ({{platform.platform}} {{ device.version }})
|
||||
|
|
|
@ -24,16 +24,26 @@
|
|||
[content]="'addon.messages.info' | translate" (action)="viewInfo()" iconAction="fas-circle-info" />
|
||||
<core-context-menu-item [hidden]="isSelf || !showInfo || !isGroup" [priority]="1000"
|
||||
[content]="'addon.messages.groupinfo' | translate" (action)="viewInfo()" iconAction="fas-circle-info" />
|
||||
<core-context-menu-item [hidden]="!groupMessagingEnabled || !conversation" [priority]="800" (action)="changeFavourite($event)"
|
||||
[closeOnClick]="false" [content]="(conversation && conversation.isfavourite ? 'addon.messages.removefromfavourites' :
|
||||
'addon.messages.addtofavourites') | translate" [iconAction]="favouriteIcon" [iconSlash]="favouriteIconSlash" />
|
||||
|
||||
<core-context-menu-item [hidden]="!groupMessagingEnabled || !conversation || conversation.isfavourite" [priority]="800"
|
||||
(action)="changeFavourite($event)" [closeOnClick]="false" [content]="'addon.messages.addtofavourites' | translate"
|
||||
[iconAction]="favouriteIcon" />
|
||||
<core-context-menu-item [hidden]="!groupMessagingEnabled || !conversation || !conversation.isfavourite" [priority]="800"
|
||||
(action)="changeFavourite($event)" [closeOnClick]="false" [content]="'addon.messages.removefromfavourites' | translate"
|
||||
[iconAction]="favouriteIcon" [iconSlash]="true" />
|
||||
|
||||
<core-context-menu-item [hidden]="isSelf || !otherMember || otherMember.isblocked" [priority]="700"
|
||||
[content]="'addon.messages.blockuser' | translate" (action)="blockUser()" [iconAction]="blockIcon" />
|
||||
<core-context-menu-item [hidden]="isSelf || !otherMember || !otherMember.isblocked" [priority]="700"
|
||||
[content]="'addon.messages.unblockuser' | translate" (action)="unblockUser()" [iconAction]="blockIcon" />
|
||||
<core-context-menu-item [hidden]="isSelf || !muteEnabled || !conversation" [priority]="600" (action)="changeMute($event)"
|
||||
[closeOnClick]="false" [content]="(conversation && conversation.ismuted ? 'addon.messages.unmuteconversation' :
|
||||
'addon.messages.muteconversation') | translate" [iconAction]="muteIcon" />
|
||||
|
||||
<core-context-menu-item [hidden]="isSelf || !muteEnabled || !conversation || conversation.ismuted" [priority]="600"
|
||||
(action)="changeMute($event)" [closeOnClick]="false" [content]="'addon.messages.muteconversation' | translate"
|
||||
[iconAction]="muteIcon" />
|
||||
<core-context-menu-item [hidden]="isSelf || !muteEnabled || !conversation || !conversation.ismuted" [priority]="600"
|
||||
(action)="changeMute($event)" [closeOnClick]="false" [content]="'addon.messages.unmuteconversation' | translate"
|
||||
[iconAction]="muteIcon" />
|
||||
|
||||
<core-context-menu-item [hidden]="!canDelete || !messages || !messages.length" [priority]="400"
|
||||
[content]="'addon.messages.showdeletemessages' | translate" iconAction="toggle" [(toggle)]="showDelete" />
|
||||
<core-context-menu-item [hidden]="!groupMessagingEnabled || !conversationId || isGroup || !messages || !messages.length"
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
AddonMessages,
|
||||
AddonMessagesConversationMessageFormatted,
|
||||
AddonMessagesSendMessageResults,
|
||||
AddonMessagesUpdateConversationAction,
|
||||
} from '../../services/messages';
|
||||
import { AddonMessagesOffline, AddonMessagesOfflineMessagesDBRecordFormatted } from '../../services/messages-offline';
|
||||
import { AddonMessagesSync, AddonMessagesSyncProvider } from '../../services/messages-sync';
|
||||
|
@ -97,7 +98,6 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
|
|||
blockIcon = 'fas-user-lock';
|
||||
addRemoveIcon = 'fas-user-plus';
|
||||
muteIcon = 'fas-bell-slash';
|
||||
favouriteIconSlash = false;
|
||||
muteEnabled = false;
|
||||
otherMember?: AddonMessagesConversationMember; // Other member information (individual conversations only).
|
||||
footerType: 'message' | 'blocked' | 'requiresContact' | 'requestSent' | 'requestReceived' | 'unable' = 'unable';
|
||||
|
@ -594,7 +594,6 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
|
|||
this.conversationImage = this.conversation.imageurl;
|
||||
this.isGroup = this.conversation.type == AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP;
|
||||
this.favouriteIcon = 'fas-star';
|
||||
this.favouriteIconSlash = this.conversation.isfavourite;
|
||||
this.muteIcon = this.conversation.ismuted ? 'fas-bell' : 'fas-bell-slash';
|
||||
if (!this.isGroup) {
|
||||
this.userId = this.conversation.userid;
|
||||
|
@ -1300,14 +1299,13 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
|
|||
|
||||
CoreEvents.trigger(AddonMessagesProvider.UPDATE_CONVERSATION_LIST_EVENT, {
|
||||
conversationId: this.conversation.id,
|
||||
action: 'favourite',
|
||||
action: AddonMessagesUpdateConversationAction.FAVOURITE,
|
||||
value: this.conversation.isfavourite,
|
||||
}, this.siteId);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error changing favourite state.');
|
||||
} finally {
|
||||
this.favouriteIcon = 'fas-star';
|
||||
this.favouriteIconSlash = this.conversation.isfavourite;
|
||||
done && done();
|
||||
}
|
||||
}
|
||||
|
@ -1333,7 +1331,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
|
|||
|
||||
CoreEvents.trigger(AddonMessagesProvider.UPDATE_CONVERSATION_LIST_EVENT, {
|
||||
conversationId: this.conversation.id,
|
||||
action: 'mute',
|
||||
action: AddonMessagesUpdateConversationAction.MUTE,
|
||||
value: this.conversation.ismuted,
|
||||
}, this.siteId);
|
||||
|
||||
|
@ -1449,7 +1447,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
|
|||
AddonMessagesProvider.UPDATE_CONVERSATION_LIST_EVENT,
|
||||
{
|
||||
conversationId: this.conversation.id,
|
||||
action: 'delete',
|
||||
action: AddonMessagesUpdateConversationAction.DELETE,
|
||||
},
|
||||
this.siteId,
|
||||
);
|
||||
|
|
|
@ -27,8 +27,8 @@
|
|||
|
||||
<ion-list class="ion-no-margin">
|
||||
|
||||
<ion-item class="ion-text-wrap addon-message-discussion" (click)="gotoContacts()"
|
||||
[attr.aria-label]="'addon.messages.contacts' | translate" [detail]="true" button>
|
||||
<ion-item class="ion-text-wrap" (click)="gotoContacts()" [attr.aria-label]="'addon.messages.contacts' | translate"
|
||||
[detail]="true" button>
|
||||
<ion-icon name="fas-address-book" slot="start" aria-hidden="true" />
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'addon.messages.contacts' | translate }}</p>
|
||||
|
|
|
@ -21,119 +21,55 @@
|
|||
</ion-header>
|
||||
<ion-content>
|
||||
<core-split-view>
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded || !currentListEl" (ionRefresh)="refreshData($event.target)">
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" />
|
||||
</ion-refresher>
|
||||
|
||||
<core-loading [hideUntil]="loaded" [message]="loadingMessage">
|
||||
<ion-list>
|
||||
<ion-item class="ion-text-wrap addon-message-discussion" (click)="gotoContacts()" [detail]="true" button>
|
||||
<ion-item class="ion-text-wrap" (click)="gotoContacts()" [detail]="true" button>
|
||||
<ion-icon name="fas-address-book" slot="start" aria-hidden="true" />
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.messages.contacts' | translate }}</h2>
|
||||
<p class="item-heading">{{ 'addon.messages.contacts' | translate }}</p>
|
||||
</ion-label>
|
||||
<ion-badge *ngIf="contactRequestsCount > 0" slot="end" aria-hidden="true">{{contactRequestsCount}}</ion-badge>
|
||||
<span *ngIf="contactRequestsCount > 0" class="sr-only">
|
||||
{{ 'addon.messages.pendingcontactrequests' | translate:{$a: contactRequestsCount} }}
|
||||
</span>
|
||||
<ion-badge *ngIf="contactRequestsCount > 0" slot="end"
|
||||
[attr.aria-label]="'addon.messages.pendingcontactrequests' | translate:{$a: contactRequestsCount}">
|
||||
{{contactRequestsCount}}
|
||||
</ion-badge>
|
||||
</ion-item>
|
||||
<!-- Favourite conversations. -->
|
||||
<ion-item button class="ion-text-wrap divider" (click)="toggle(favourites)"
|
||||
[attr.aria-label]="(favourites.expanded ? 'core.collapse' : 'core.expand') | translate"
|
||||
[attr.aria-expanded]="favourites.expanded" aria-controls="addon-messages-groupconversations-favourite" role="heading"
|
||||
[detail]="false">
|
||||
<ion-icon name="fas-chevron-right" flip-rtl slot="start" aria-hidden="true" class="expandable-status-icon"
|
||||
[class.expandable-status-icon-expanded]="favourites.expanded" />
|
||||
<ion-label>
|
||||
<h2>{{ 'core.favourites' | translate }} ({{ favourites.count }})</h2>
|
||||
</ion-label>
|
||||
<ion-badge slot="end" *ngIf="favourites.unread" aria-hidden="true">{{ favourites.unread }}</ion-badge>
|
||||
<span *ngIf="favourites.unread" class="sr-only">
|
||||
{{ 'addon.messages.unreadconversations' | translate:{$a: favourites.unread} }}
|
||||
</span>
|
||||
</ion-item>
|
||||
<div [hidden]="!favourites.conversations || !favourites.expanded || favourites.loading" #favlist
|
||||
id="addon-messages-groupconversations-favourite">
|
||||
<ng-container *ngTemplateOutlet="conversationsTemplate; context: {conversations: favourites.conversations}" />
|
||||
<!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. -->
|
||||
<core-infinite-loading [enabled]="favourites.canLoadMore" (action)="loadMoreConversations(favourites, $event)"
|
||||
[error]="favourites.loadMoreError" />
|
||||
<ion-item class="ion-text-wrap" *ngIf="favourites.conversations && favourites.conversations.length === 0">
|
||||
<ion-label>
|
||||
<p>{{ 'addon.messages.nofavourites' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
<ion-item class="ion-text-center" *ngIf="favourites.loading">
|
||||
<ion-label>
|
||||
<ion-spinner [attr.aria-label]="'core.loading' | translate" />
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- Group conversations. -->
|
||||
<ion-item button class="divider ion-text-wrap" (click)="toggle(group)"
|
||||
[attr.aria-label]="(group.expanded ? 'core.collapse' : 'core.expand') | translate" [attr.aria-expanded]="group.expanded"
|
||||
aria-controls="addon-messages-groupconversations-group" role="heading" [detail]="false">
|
||||
<ion-icon name="fas-chevron-right" flip-rtl slot="start" aria-hidden="true" class="expandable-status-icon"
|
||||
[class.expandable-status-icon-expanded]="group.expanded" />
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.messages.groupconversations' | translate }} ({{ group.count }})</h2>
|
||||
</ion-label>
|
||||
<ion-badge slot="end" *ngIf="group.unread" aria-hidden="true">{{ group.unread }}</ion-badge>
|
||||
<span *ngIf="group.unread" class="sr-only">
|
||||
{{ 'addon.messages.unreadconversations' | translate:{$a: group.unread} }}
|
||||
</span>
|
||||
</ion-item>
|
||||
<div [hidden]="!group.conversations || !group.expanded || group.loading" #grouplist
|
||||
id="addon-messages-groupconversations-group">
|
||||
<ng-container *ngTemplateOutlet="conversationsTemplate; context: {conversations: group.conversations}" />
|
||||
<!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. -->
|
||||
<core-infinite-loading [enabled]="group.canLoadMore" (action)="loadMoreConversations(group, $event)"
|
||||
[error]="group.loadMoreError" />
|
||||
<ion-item class="ion-text-wrap" *ngIf="group.conversations && group.conversations.length === 0">
|
||||
<ion-label>
|
||||
<p>{{ 'addon.messages.nogroupconversations' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
<ion-item class="ion-text-center" *ngIf="group.loading">
|
||||
<ion-label>
|
||||
<ion-spinner [attr.aria-label]="'core.loading' | translate" />
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item button class="divider ion-text-wrap" (click)="toggle(individual)"
|
||||
[attr.aria-label]="(individual.expanded ? 'core.collapse' : 'core.expand') | translate"
|
||||
[attr.aria-expanded]="individual.expanded" aria-controls="addon-messages-groupconversations-individual" role="heading"
|
||||
[detail]="false">
|
||||
<ion-icon name="fas-chevron-right" flip-rtl slot="start" aria-hidden="true" class="expandable-status-icon"
|
||||
[class.expandable-status-icon-expanded]="individual.expanded" />
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.messages.individualconversations' | translate }} ({{ individual.count }})</h2>
|
||||
</ion-label>
|
||||
<ion-badge slot="end" *ngIf="individual.unread" aria-hidden="true">{{ individual.unread }}</ion-badge>
|
||||
<span *ngIf="individual.unread" class="sr-only">
|
||||
{{ 'addon.messages.unreadconversations' | translate:{$a: individual.unread} }}
|
||||
</span>
|
||||
</ion-item>
|
||||
<div [hidden]="!individual.conversations || !individual.expanded || individual.loading" #indlist
|
||||
id="addon-messages-groupconversations-individual">
|
||||
<ng-container *ngTemplateOutlet="conversationsTemplate; context: {conversations: individual.conversations}" />
|
||||
<!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. -->
|
||||
<core-infinite-loading [enabled]="individual.canLoadMore" (action)="loadMoreConversations(individual, $event)"
|
||||
[error]="individual.loadMoreError" />
|
||||
<ion-item class="ion-text-wrap" *ngIf="individual.conversations && individual.conversations.length === 0">
|
||||
<ion-label>
|
||||
<p>{{ 'addon.messages.noindividualconversations' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
<ion-item class="ion-text-center" *ngIf="individual.loading">
|
||||
<ion-label>
|
||||
<ion-spinner [attr.aria-label]="'core.loading' | translate" />
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-accordion-group (ionChange)="accordionGroupChange($event.detail)" #accordionGroup>
|
||||
<ion-accordion *ngFor="let option of groupConversations" [value]="option.optionName" toggleIconSlot="start">
|
||||
<ion-item slot="header" class="ion-text-wrap divider">
|
||||
<ion-label>
|
||||
<h2>{{ option.titleString | translate }} ({{ option.count }})</h2>
|
||||
</ion-label>
|
||||
<ion-badge slot="end" *ngIf="option.unread"
|
||||
[attr.aria-label]="'addon.messages.unreadconversations' | translate:{$a: option.unread}">
|
||||
{{ option.unread }}
|
||||
</ion-badge>
|
||||
</ion-item>
|
||||
<div slot="content">
|
||||
<ng-container *ngIf="!option.loading">
|
||||
<ng-container *ngTemplateOutlet="conversationsTemplate;
|
||||
context: {conversations: option.conversations}" />
|
||||
<!-- The infinite loading cannot be inside the ng-template,
|
||||
it fails because it doesn't find ion-content. -->
|
||||
<core-infinite-loading [enabled]="option.canLoadMore" (action)="loadMoreConversations(option, $event)"
|
||||
[error]="option.loadMoreError" />
|
||||
<ion-item class="ion-text-wrap" *ngIf="option.conversations && option.conversations.length === 0">
|
||||
<ion-label>
|
||||
<p>{{ option.emptyString| translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ion-item class="ion-text-center" *ngIf="option.loading">
|
||||
<ion-label>
|
||||
<ion-spinner [attr.aria-label]="'core.loading' | translate" />
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ion-accordion>
|
||||
</ion-accordion-group>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
</core-split-view>
|
||||
|
|
|
@ -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<CorePushNotificationsNotificationBasicData>('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<void> {
|
||||
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<void>[] = [];
|
||||
|
||||
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<void> {
|
||||
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: <AddonMessagesConversationForList[]> [],
|
||||
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<void> {
|
||||
// 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.
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
</ion-list>
|
||||
|
||||
<core-empty-box *ngIf="displayResults && !contacts.results.length && !nonContacts.results.length && !messages.results.length"
|
||||
icon="fas-magnifying-glass" [message]="'core.noresults' | translate" />
|
||||
icon="fas-magnifying-glass" [message]="'core.noresults' | translate" role="alert" />
|
||||
</core-loading>
|
||||
</core-split-view>
|
||||
</ion-content>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<h2>{{ 'core.settings.general' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-item class="ion-text-wrap ion-no-validation">
|
||||
<ion-toggle [(ngModel)]="sendOnEnter" (ngModelChange)="sendOnEnterChanged()">
|
||||
{{ 'addon.messages.useentertosend' | translate }}
|
||||
</ion-toggle>
|
||||
|
@ -31,7 +31,7 @@
|
|||
|
||||
<!-- Contactable privacy. -->
|
||||
<ion-card>
|
||||
<ion-item *ngIf="!advancedContactable" class="ion-text-wrap">
|
||||
<ion-item *ngIf="!advancedContactable" class="ion-text-wrap ion-no-validation">
|
||||
<ion-toggle [(ngModel)]="contactablePrivacy" (ngModelChange)="saveContactablePrivacy(contactablePrivacy)">
|
||||
{{ 'addon.messages.blocknoncontacts' | translate }}
|
||||
</ion-toggle>
|
||||
|
@ -95,7 +95,7 @@
|
|||
</ion-item-divider>
|
||||
<!-- If notifications not disabled, show toggles.
|
||||
If notifications are disabled, show "Disabled" instead of toggle. -->
|
||||
<ion-item *ngFor="let state of ['loggedin', 'loggedoff']" class="ion-text-wrap">
|
||||
<ion-item *ngFor="let state of ['loggedin', 'loggedoff']" class="ion-text-wrap ion-no-validation">
|
||||
<ion-label>
|
||||
<p>{{ 'core.settings.' + state | translate }}</p>
|
||||
</ion-label>
|
||||
|
@ -131,7 +131,7 @@
|
|||
</ion-item-divider>
|
||||
<ng-container *ngFor="let processor of notification.processors">
|
||||
<!-- If group messaging is enabled, display a simplified view. -->
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-item class="ion-text-wrap ion-no-validation">
|
||||
<ion-label>
|
||||
<p>{{ processor.displayname }}</p>
|
||||
</ion-label>
|
||||
|
|
|
@ -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<AddonMessagesGetConversationCountsWSResponse>(
|
||||
'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;
|
||||
};
|
||||
|
||||
|
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
@ -345,7 +345,7 @@
|
|||
</ion-item>
|
||||
|
||||
<!--- Apply grade to all team members. -->
|
||||
<ion-item class="ion-text-wrap" *ngIf="assign!.teamsubmission && canSaveGrades">
|
||||
<ion-item class="ion-text-wrap ion-no-validation" *ngIf="assign!.teamsubmission && canSaveGrades">
|
||||
<ion-toggle [(ngModel)]="grade.applyToAll">
|
||||
<p class="item-heading">{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</p>
|
||||
<p>{{ 'addon.mod_assign.applytoteam' | translate }}</p>
|
||||
|
@ -371,7 +371,7 @@
|
|||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="canSaveGrades && allowAddAttempt">
|
||||
<ion-item *ngIf="canSaveGrades && allowAddAttempt" class="ion-no-validation">
|
||||
<ion-toggle [(ngModel)]="grade.addAttempt">
|
||||
<p>{{ 'addon.mod_assign.addattempt' | translate }}</p>
|
||||
</ion-toggle>
|
||||
|
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
@ -4,4 +4,8 @@
|
|||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
.has-collapsible-footer ::ng-deep swiper-container::part(container) {
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<core-loading [hideUntil]="sessions.loaded">
|
||||
<core-group-selector [groupInfo]="groupInfo" [(selected)]="groupId" (selectedChange)="reloadSessions()" [courseId]="courseId" />
|
||||
|
||||
<ion-item>
|
||||
<ion-item class="ion-no-validation">
|
||||
<ion-toggle [(ngModel)]="showAll" (ionChange)="reloadSessions()">
|
||||
{{ 'addon.mod_chat.showincompletesessions' | translate }}
|
||||
</ion-toggle>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-item>
|
||||
<ion-item class="ion-no-validation">
|
||||
<ion-toggle [(ngModel)]="search.searchingAdvanced">
|
||||
{{ 'addon.mod_data.advancedsearch' | translate }}
|
||||
</ion-toggle>
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
</ion-label>
|
||||
</ion-item>
|
||||
<div *ngIf="advanced" id="addon-mod-forum-new-discussion-advanced">
|
||||
<ion-item *ngIf="showGroups && groupIds.length > 1 && accessInfo.cancanposttomygroups">
|
||||
<ion-item *ngIf="showGroups && groupIds.length > 1 && accessInfo.cancanposttomygroups" class="ion-no-validation">
|
||||
<ion-toggle [(ngModel)]="newDiscussion.postToAllGroups" name="postallgroups">
|
||||
{{ 'addon.mod_forum.posttomygroups' | translate }}
|
||||
</ion-toggle>
|
||||
|
@ -54,12 +54,12 @@
|
|||
</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-item class="ion-text-wrap ion-no-validation">
|
||||
<ion-toggle [(ngModel)]="newDiscussion.subscribe" name="subscribe">
|
||||
{{ 'addon.mod_forum.discussionsubscription' | translate }}
|
||||
</ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="canPin" class="ion-text-wrap">
|
||||
<ion-item *ngIf="canPin" class="ion-text-wrap ion-no-validation">
|
||||
<ion-toggle [(ngModel)]="newDiscussion.pin" name="pin">
|
||||
{{ 'addon.mod_forum.discussionpinned' | translate }}
|
||||
</ion-toggle>
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
<core-infinite-loading [enabled]="resultsSource.isLoaded() && !resultsSource.isCompleted()" (action)="loadMoreResults($event)"
|
||||
[error]="loadMoreError" />
|
||||
|
||||
<core-empty-box *ngIf="resultsSource.isEmpty()" icon="fas-magnifying-glass" [dimmed]="!resultsSource.isLoaded()">
|
||||
<core-empty-box *ngIf="resultsSource.isEmpty()" icon="fas-magnifying-glass" [dimmed]="!resultsSource.isLoaded()" role="alert">
|
||||
<p *ngIf="!resultsSource.isLoaded()">{{ 'core.search.empty' | translate }}</p>
|
||||
<ng-container *ngIf="resultsSource.isLoaded()">
|
||||
<p><strong>{{ 'core.search.noresults' | translate: { $a: resultsSource.getQuery() } }}</strong></p>
|
||||
|
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
@ -24,7 +24,7 @@
|
|||
<core-search-box *ngIf="isSearch" (onSubmit)="search($event)" [placeholder]="'addon.mod_glossary.searchquery' | translate"
|
||||
[autoFocus]="true" [lengthCheck]="2" (onClear)="toggleSearch()" searchArea="AddonModGlossary-{{module.id}}" />
|
||||
|
||||
<core-loading [hideUntil]="!showLoading">
|
||||
<core-loading [hideUntil]="!showLoading" [message]="loadingMessage">
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="!isSearch" [module]="module" [description]="description" [component]="component"
|
||||
[componentId]="componentId" [courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()" />
|
||||
|
@ -65,7 +65,7 @@
|
|||
</ion-list>
|
||||
|
||||
<core-empty-box *ngIf="(!entries || !entries.hasEntries) && (!isSearch || hasSearched)" icon="fas-list"
|
||||
[message]="'addon.mod_glossary.noentriesfound' | translate" />
|
||||
[message]="'addon.mod_glossary.noentriesfound' | translate" [attr.role]="hasSearched ? 'alert' : null" />
|
||||
|
||||
<core-infinite-loading [enabled]="entries && !entries.completed && (!isSearch || hasSearched)" [error]="loadMoreError"
|
||||
(action)="loadMoreEntries($event)" />
|
||||
|
|
|
@ -53,17 +53,17 @@
|
|||
<h2>{{ 'addon.mod_glossary.linking' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-item class="ion-text-wrap ion-no-validation">
|
||||
<ion-toggle [(ngModel)]="data.usedynalink" name="usedynalink">
|
||||
{{ 'addon.mod_glossary.entryusedynalink' | translate }}
|
||||
</ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-item class="ion-text-wrap ion-no-validation">
|
||||
<ion-toggle [disabled]="!data.usedynalink" [(ngModel)]="data.casesensitive" name="casesensitive">
|
||||
{{ 'addon.mod_glossary.casesensitive' | translate }}
|
||||
</ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-item class="ion-text-wrap ion-no-validation">
|
||||
<ion-toggle [disabled]="!data.usedynalink" [(ngModel)]="data.fullmatch" name="fullmatch">
|
||||
{{ 'addon.mod_glossary.fullmatch' | translate }}
|
||||
</ion-toggle>
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
</h2>
|
||||
</ion-label>
|
||||
<div *ngIf="question.status || question.readableMark" slot="end"
|
||||
class="ion-text-wrap ion-margin-horizontal addon-mod_quiz-question-note">
|
||||
class="ion-text-wrap ion-margin-start addon-mod_quiz-question-note">
|
||||
<p *ngIf="question.status" class="block">{{question.status}}</p>
|
||||
<p *ngIf="question.readableMark" [innerHTML]="question.readableMark"></p>
|
||||
</div>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
@ -99,7 +99,7 @@
|
|||
<h3 class="item-heading">{{ 'addon.mod_workshop.feedbackauthor' | translate }}</h3>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="access.canpublishsubmissions">
|
||||
<ion-item class="ion-text-wrap ion-no-validation" *ngIf="access.canpublishsubmissions">
|
||||
<ion-toggle formControlName="published">
|
||||
<p class="item-heading">{{ 'addon.mod_workshop.publishsubmission' | translate }}</p>
|
||||
<p>{{ 'addon.mod_workshop.publishsubmission_help' | translate }}</p>
|
||||
|
|
|
@ -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<AddonNotesSyncR
|
|||
courseIds = courseIds.concat(notes.map((note) => note.courseid));
|
||||
});
|
||||
|
||||
CoreUtils.uniqueArray(courseIds);
|
||||
CoreArray.unique(courseIds);
|
||||
|
||||
// Sync all courses.
|
||||
const promises = courseIds.map(async (courseId) => {
|
||||
|
|
|
@ -21,12 +21,12 @@
|
|||
</ion-refresher>
|
||||
<core-loading [hideUntil]="preferencesLoaded">
|
||||
<ion-card>
|
||||
<ion-item class="ion-text-wrap" *ngIf="preferences">
|
||||
<ion-item class="ion-text-wrap ion-no-validation" *ngIf="preferences">
|
||||
<ion-toggle [(ngModel)]="preferences.enableall" (ngModelChange)="enableAll(preferences.enableall)">
|
||||
<p class="item-heading">{{ 'addon.notifications.allownotifications' | translate }}</p>
|
||||
</ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="canChangeSound">
|
||||
<ion-item class="ion-text-wrap ion-no-validation" *ngIf="canChangeSound">
|
||||
<ion-toggle [(ngModel)]="notificationSound" (ngModelChange)="changeNotificationSound(notificationSound)">
|
||||
<p class="item-heading">{{ 'addon.notifications.playsound' | translate }}</p>
|
||||
</ion-toggle>
|
||||
|
@ -79,7 +79,7 @@
|
|||
</ion-card-header>
|
||||
<ng-container *ngFor="let notification of component.notifications">
|
||||
<!-- Tablet view -->
|
||||
<ion-item class="ion-text-wrap ion-hide-md-down addon-notifications-table-content only-links">
|
||||
<ion-item class="ion-text-wrap ion-hide-md-down addon-notifications-table-content only-links ion-no-validation">
|
||||
<ion-label>
|
||||
<ion-row class="ion-no-padding ion-align-items-center">
|
||||
<ion-col class="ion-margin-horizontal ion-no-padding">
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
<section class="addon-qtype-match-container" *ngIf="question && question.loaded">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-item class="ion-text-wrap question-text">
|
||||
<ion-label>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="contextInstanceId" [courseId]="courseId" />
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let row of question.rows">
|
||||
<ion-item class="ion-text-wrap question-rows" *ngFor="let row of question.rows">
|
||||
|
||||
<ion-select id="{{row.id}}" [name]="row.name" [(ngModel)]="row.selected" interface="action-sheet" [disabled]="row.disabled"
|
||||
[cancelText]="'core.cancel' | translate"
|
||||
[ngClass]="{'addon-qtype-match-correct': row.isCorrect === 1,'addon-qtype-match-incorrect': row.isCorrect === 0}">
|
||||
<div slot="label" class="flew-row">
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="row.text" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="contextInstanceId" [courseId]="courseId" />
|
||||
<div slot="label" class="flex-row ion-text-wrap">
|
||||
<div>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="row.text" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="contextInstanceId" [courseId]="courseId" />
|
||||
</div>
|
||||
<label class="accesshide" for="{{row.id}}" *ngIf="row.accessibilityLabel">
|
||||
{{ row.accessibilityLabel }}
|
||||
</label>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,8 +37,9 @@
|
|||
<ion-badge color="light">{{ totalSize | coreBytesToSize }}</ion-badge>
|
||||
</ion-label>
|
||||
<ion-button slot="end" (click)="deleteCompletelyDownloadedCourses($event)"
|
||||
[disabled]="completelyDownloadedCourses.length === 0" color="danger" fill="clear">
|
||||
<ion-icon name="fas-trash" slot="icon-only" ariaLabel="{{ 'addon.storagemanager.deletecourses' | translate }}" />
|
||||
[disabled]="completelyDownloadedCourses.length === 0" color="danger" fill="clear"
|
||||
[attr.aria-label]="'addon.storagemanager.deletecourses' | translate">
|
||||
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true" />
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<ion-item *ngFor="let course of downloadedCourses" class="course" (click)="openCourse(course.id, course.title)" button
|
||||
|
@ -54,9 +55,10 @@
|
|||
{{ course.totalSize | coreBytesToSize }}
|
||||
</ion-badge>
|
||||
</ion-label>
|
||||
<ion-button slot="end" (click)="deleteCourse($event, course)" [disabled]="course.isDownloading" color="danger" fill="clear">
|
||||
<ion-icon name="fas-trash" slot="icon-only" [attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate:
|
||||
{ name: course.title }" />
|
||||
<ion-button slot="end" (click)="deleteCourse($event, course)" [disabled]="course.isDownloading" color="danger" fill="clear"
|
||||
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate:
|
||||
{ name: course.title }">
|
||||
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true" />
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
<ion-list-header *ngIf="title">
|
||||
<ion-label>{{title}}</ion-label>
|
||||
</ion-list-header>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let item of items" core-link [capture]="item.captureLink" [autoLogin]="item.autoLogin"
|
||||
[href]="item.href" (click)="itemClicked($event, item)" [attr.aria-label]="item.ariaAction" [hidden]="item.hidden"
|
||||
[detail]="!!(item.href && !item.iconAction)" role="menuitem" [button]="!!(item.href && !item.iconAction)"
|
||||
<ion-item class="ion-text-wrap ion-no-validation" *ngFor="let item of items" core-link [capture]="item.captureLink"
|
||||
[autoLogin]="item.autoLogin" [href]="item.href" (click)="itemClicked($event, item)" [attr.aria-label]="item.ariaAction"
|
||||
[hidden]="item.hidden" [detail]="!!(item.href && !item.iconAction)" role="menuitem" [button]="!!(item.href && !item.iconAction)"
|
||||
[showBrowserWarning]="item.showBrowserWarning">
|
||||
<ion-toggle *ngIf="item.iconAction === 'toggle'" [(ngModel)]="item.toggle" (ionChange)="item.toggleChanged($event)">
|
||||
<p class="item-heading">
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<swiper-container #swiperRef *ngIf="loaded">
|
||||
<swiper-slide *ngFor="let item of items; index as index" [attr.aria-hidden]="!isActive(index)">
|
||||
<swiper-container #swiperRef *ngIf="loaded" [attr.aria-busy]="activeSlideIndex ? 'true' : null">
|
||||
<swiper-slide *ngFor="let item of items; index as index" [attr.aria-hidden]="!isActive(index)"
|
||||
[attr.tabindex]="!isActive(index) ? -1 : null">
|
||||
<ng-container *ngIf="template" [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{item: item, active: isActive(index)}" />
|
||||
</swiper-slide>
|
||||
</swiper-container>
|
||||
|
|
|
@ -72,7 +72,7 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
|||
protected hostElement: HTMLElement;
|
||||
protected unsubscribe?: () => void;
|
||||
protected resizeListener: CoreEventObserver;
|
||||
protected activeSlideIndexes: number[] = [];
|
||||
protected activeSlideIndex?: number;
|
||||
protected onReadyPromise = new CorePromisedValue<void>();
|
||||
|
||||
constructor(
|
||||
|
@ -112,7 +112,7 @@ export class CoreSwipeSlidesComponent<Item = unknown> 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<Item = unknown> 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<Item = unknown> 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<Item = unknown> implements OnChanges, OnDe
|
|||
async slideDidChange(): Promise<void> {
|
||||
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<Item = unknown> implements OnChanges, OnDe
|
|||
return;
|
||||
}
|
||||
|
||||
this.swiper?.update();
|
||||
this.swiper.update();
|
||||
|
||||
// We need to ensure the slides are updated before continuing.
|
||||
await CoreUtils.nextTicks(2);
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
</ion-button>
|
||||
<swiper-container #swiperRef [slidesPerView]="swiperOpts.slidesPerView" role="tablist" [attr.aria-label]="description">
|
||||
<ng-container *ngFor="let tab of tabs">
|
||||
<swiper-slide *ngIf="tab.id" role="presentation" [id]="tab.id + '-tab'" tabindex="-1"
|
||||
[class.selected]="selected === tab.id" class="{{tab.class}}">
|
||||
<swiper-slide *ngIf="tab.id" role="presentation" [id]="tab.id + '-tab'" [class.selected]="selected === tab.id"
|
||||
class="{{tab.class}}">
|
||||
<ion-tab-button (ionTabButtonClick)="selectTab(tab.id, $event)" (keydown)="tabAction.keyDown(tab.id, $event)"
|
||||
(keyup)="tabAction.keyUp(tab.id, $event)" [tab]="tab.page" [layout]="layout" role="tab"
|
||||
[attr.aria-controls]="tab.id" [attr.aria-selected]="selected === tab.id"
|
||||
|
|
|
@ -57,7 +57,9 @@
|
|||
overflow: hidden;
|
||||
|
||||
ion-tab-button {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
ion-label {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
|
|
@ -40,6 +40,7 @@ import { CoreDirectivesRegistry } from '@singletons/directives-registry';
|
|||
import { CorePromisedValue } from '@classes/promised-value';
|
||||
import { CorePlatform } from '@services/platform';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreArray } from '@singletons/array';
|
||||
|
||||
/**
|
||||
* Directive to handle external content.
|
||||
|
@ -279,7 +280,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
|
|||
return;
|
||||
}
|
||||
|
||||
const urls = CoreUtils.uniqueArray(Array.from(inlineStyles.match(/https?:\/\/[^"') ;]*/g) ?? []));
|
||||
const urls = CoreArray.unique(Array.from(inlineStyles.match(/https?:\/\/[^"') ;]*/g) ?? []));
|
||||
if (!urls.length) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ export class CoreLinkDirective implements OnInit {
|
|||
@Input() autoLogin: boolean | string = true; // Whether to try to use auto-login. Values yes/no/check are deprecated.
|
||||
@Input() showBrowserWarning = true; // Whether to show a warning before opening browser. Defaults to true.
|
||||
|
||||
protected element: HTMLElement;
|
||||
protected element: HTMLElement | HTMLIonFabButtonElement | HTMLIonButtonElement | HTMLIonItemElement;
|
||||
|
||||
constructor(
|
||||
element: ElementRef,
|
||||
|
@ -54,10 +54,18 @@ export class CoreLinkDirective implements OnInit {
|
|||
}
|
||||
|
||||
/**
|
||||
* Function executed when the component is initialized.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
CoreDom.initializeClickableElementA11y(this.element, (event) => this.performAction(event));
|
||||
async ngOnInit(): Promise<void> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -54,12 +54,10 @@
|
|||
<ion-fab-button size="small" *ngIf="communicationRoomUrl" [href]="communicationRoomUrl" core-link capture="false"
|
||||
[attr.aria-label]="'core.course.communicationroomlink' | translate">
|
||||
<ion-icon name="far-comments" aria-hidden="true" />
|
||||
<span class="sr-only">{{'core.course.communicationroomlink' | translate }}</span>
|
||||
</ion-fab-button>
|
||||
<ion-fab-button *ngIf="displayCourseIndex" (click)="openCourseIndex()" [userTour]="courseIndexTour"
|
||||
[attr.aria-label]="'core.course.courseindex' | translate" color="secondary">
|
||||
<ion-icon name="fas-list-ul" aria-hidden="true" />
|
||||
<span class="sr-only">{{'core.course.courseindex' | translate }}</span>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
|
||||
|
|
|
@ -126,7 +126,7 @@
|
|||
}
|
||||
|
||||
.activity-extrabadges {
|
||||
color: var(--gray-700);
|
||||
color: var(--medium);
|
||||
}
|
||||
|
||||
.activity-description-availabilityinfo {
|
||||
|
|
|
@ -26,9 +26,9 @@
|
|||
|
||||
<core-search-box *ngIf="searchEnabled" (onSubmit)="search($event)" (onClear)="clearSearch()"
|
||||
[placeholder]="'core.courses.search' | translate" [searchLabel]="'core.courses.search' | translate" [autoFocus]="searchMode"
|
||||
searchArea="CoreCoursesSearch" />
|
||||
searchArea="CoreCoursesSearch" [lengthCheck]="1" />
|
||||
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<core-loading [hideUntil]="loaded" [message]="loadingMessage">
|
||||
<ng-container *ngIf="searchMode && searchTotal > 0">
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
|
@ -45,7 +45,7 @@
|
|||
|
||||
|
||||
<core-empty-box *ngIf="searchMode && !courses.length" icon="fas-magnifying-glass"
|
||||
[message]="'core.courses.nosearchresults' | translate" />
|
||||
[message]="'core.courses.nosearchresults' | translate" role="alert" />
|
||||
|
||||
<core-empty-box *ngIf="!searchMode && !courses.length" icon="fas-graduation-cap" [message]="'core.courses.nocourses' | translate" />
|
||||
|
||||
|
|
|
@ -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<void> {
|
||||
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<void> {
|
||||
this.loadMoreError = false;
|
||||
this.loadingMessage = Translate.instant('core.searching');
|
||||
|
||||
try {
|
||||
const response = await CoreCourses.search(this.searchText, this.searchPage, undefined, this.showOnlyEnrolled);
|
||||
|
|
|
@ -11,6 +11,5 @@
|
|||
|
||||
core-block ::ng-deep ion-card.addon-block-myoverview {
|
||||
--border-width: 0;
|
||||
--background: transparent;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
<div class="core-rte-editor-container" (click)="focusRTE()" [class.toolbar-hidden]="toolbarHidden">
|
||||
<div [hidden]="!rteEnabled" #editor class="core-rte-editor" role="textbox" contenteditable="true"
|
||||
[attr.aria-labelledby]="ariaLabelledBy" [attr.data-placeholder-text]="placeholder" (focus)="showToolbar($event)"
|
||||
(blur)="hideToolbar($event)" (keydown)="onKeyDown($event)" (keyup)="onChange()" (change)="onChange()" (paste)="onChange()"
|
||||
(input)="onChange()">
|
||||
(blur)="hideToolbar($event)" (keydown)="onKeyDown($event)">
|
||||
</div>
|
||||
|
||||
<ion-textarea [hidden]="rteEnabled" #textarea class="core-textarea" role="textbox" [attr.name]="name" ngControl="control"
|
||||
|
|
|
@ -23,6 +23,18 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--background);
|
||||
border: 1px solid var(--stroke);
|
||||
border-radius: var(--mdl-shape-borderRadius-md);
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--a11y-focus-color);
|
||||
border-width: 2px;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.core-rte-editor, .core-textarea, textarea {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.core-rte-editor-container {
|
||||
max-height: calc(100% - 46px);
|
||||
|
@ -39,7 +51,6 @@
|
|||
border-top: 1px solid var(--info);
|
||||
background: var(--background);
|
||||
flex-shrink: 1;
|
||||
font-size: 1.1rem;
|
||||
|
||||
.icon {
|
||||
color: var(--info);
|
||||
|
@ -105,7 +116,6 @@
|
|||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--toobar-background);
|
||||
padding-top: 5px;
|
||||
border-top: 1px solid var(--stroke);
|
||||
|
||||
swiper-container {
|
||||
|
@ -122,7 +132,7 @@
|
|||
height: 36px;
|
||||
padding-right: 6px;
|
||||
padding-left: 6px;
|
||||
margin: 0 auto;
|
||||
margin: 2px auto;
|
||||
font-size: 18px;
|
||||
background-color: var(--toobar-background);
|
||||
border-radius: var(--mdl-shape-borderRadius-xs);
|
||||
|
|
|
@ -115,6 +115,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
protected keyboardObserver?: CoreEventObserver;
|
||||
protected resetObserver?: CoreEventObserver;
|
||||
protected labelObserver?: MutationObserver;
|
||||
protected contentObserver?: MutationObserver;
|
||||
protected initHeightInterval?: number;
|
||||
protected isCurrentView = true;
|
||||
protected toolbarButtonWidth = 44;
|
||||
|
@ -126,7 +127,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
protected draftWasRestored = false;
|
||||
protected originalContent?: string;
|
||||
protected resizeFunction?: () => Promise<number>;
|
||||
protected selectionChangeFunction?: () => void;
|
||||
protected selectionChangeFunction = (): void => this.updateToolbarStyles();
|
||||
protected languageChangedSubscription?: Subscription;
|
||||
protected resizeListener?: CoreEventObserver;
|
||||
protected domPromise?: CoreCancellablePromise<void>;
|
||||
|
@ -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<void> {
|
||||
if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
|
||||
if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>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 = '<p></p>';
|
||||
// Avoid loops.
|
||||
if (this.editorElement.innerHTML !== '<p></p>') {
|
||||
this.editorElement.innerHTML = '<p></p>';
|
||||
}
|
||||
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(<KeyboardEvent>event)) {
|
||||
if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>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(<KeyboardEvent>event)) {
|
||||
if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>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(<KeyboardEvent>event)) {
|
||||
if (event.type === 'keydown' && !this.isValidKeyboardKey(<KeyboardEvent>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<void> {
|
||||
if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
|
||||
if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -813,7 +828,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
* Method that shows the previous toolbar buttons.
|
||||
*/
|
||||
async toolbarPrev(event: Event): Promise<void> {
|
||||
if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
|
||||
if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -831,7 +846,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
|
|||
* Update the number of toolbar buttons displayed.
|
||||
*/
|
||||
async updateToolbarButtons(): Promise<void> {
|
||||
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 == '<p></p>' || draftText == '<p><br></p>' || draftText == '<br>' ||
|
||||
draftText == '<p> </p>' || draftText == '<p><br> </p>') {
|
||||
if (draftText === '<p></p>' || draftText === '<p><br></p>' || draftText === '<br>' ||
|
||||
draftText === '<p> </p>' || draftText === '<p><br> </p>') {
|
||||
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<void> {
|
||||
if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
|
||||
if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>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();
|
||||
}
|
||||
|
|
|
@ -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<CoreEnrolInfoIcon[]> {
|
||||
methodTypes = CoreUtils.uniqueArray(methodTypes);
|
||||
methodTypes = CoreArray.unique(methodTypes);
|
||||
|
||||
let enrolmentIcons: CoreEnrolInfoIcon[] = [];
|
||||
let addBrowserOption = false;
|
||||
|
|
|
@ -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)\//);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)$/);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -21,7 +21,9 @@
|
|||
[attr.aria-current]="courses.getItemAriaCurrent(course)" class="ion-text-wrap" button [detail]="true"
|
||||
(click)="courses.select(course)">
|
||||
<ion-label>
|
||||
<core-format-text [text]="course.courseFullName" [contextInstanceId]="course.courseid" contextLevel="course" />
|
||||
<p class="item-heading">
|
||||
<core-format-text [text]="course.courseFullName" [contextInstanceId]="course.courseid" contextLevel="course" />
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-badge slot="end" color="light">
|
||||
<span class="sr-only" *ngIf="course.grade && course.grade !== '-'">
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
<ion-input [attr.aria-label]="placeholder" type="search" name="search" [(ngModel)]="searchText" [placeholder]="placeholder"
|
||||
[autocorrect]="autocorrect" [spellcheck]="spellcheck" [core-auto-focus]="autoFocus" [disabled]="disabled" role="searchbox"
|
||||
(ionFocus)="focus($event)">
|
||||
<ion-button slot="end" fill="clear" type="submit" [attr.aria-label]="searchLabel"
|
||||
[disabled]="disabled || !searchText || (searchText.length < lengthCheck)">
|
||||
<ion-button slot="end" fill="clear" type="submit" [attr.aria-label]="searchLabel" [disabled]="disabled || !searchText">
|
||||
<ion-icon name="fas-magnifying-glass" slot="icon-only" aria-hidden="true" />
|
||||
</ion-button>
|
||||
<ion-button *ngIf="showClear" slot="end" fill="clear" [attr.aria-label]="'core.clearsearch' | translate"
|
||||
|
@ -11,6 +10,11 @@
|
|||
<ion-icon name="fas-delete-left" slot="icon-only" aria-hidden="true" flip-rtl />
|
||||
</ion-button>
|
||||
</ion-input>
|
||||
<ion-item *ngIf="showLengthAlert" class="core-search-alert text-danger ion-text-wrap" role="alert">
|
||||
<ion-label>
|
||||
{{ 'core.search.err_minlength' | translate : {'$a': {'format': lengthCheck} } }}
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-list class="core-search-history" [hidden]="!historyShown">
|
||||
<ion-item button class="ion-text-wrap" *ngFor="let item of history" (click)="historyClicked($event, item.searchedtext)" tabindex="0"
|
||||
[detail]="true">
|
||||
|
|
|
@ -35,8 +35,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.core-search-history {
|
||||
max-height: calc(-120px + 80vh);
|
||||
overflow-y: auto;
|
||||
|
|
|
@ -62,6 +62,7 @@ export class CoreSearchBoxComponent implements OnInit {
|
|||
searchText = '';
|
||||
history: CoreSearchHistoryDBRecord[] = [];
|
||||
historyShown = false;
|
||||
showLengthAlert = false;
|
||||
|
||||
constructor() {
|
||||
this.onSubmit = new EventEmitter<string>();
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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}}"
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
[error]="loadMoreError" />
|
||||
|
||||
<core-empty-box *ngIf="resultsSource.isEmpty()" icon="fas-magnifying-glass" [dimmed]="!resultsSource.isLoaded()">
|
||||
<p *ngIf="!resultsSource.isLoaded()">{{ 'core.search.empty' | translate }}</p>
|
||||
<p *ngIf="!resultsSource.isLoaded()" role="alert">{{ 'core.search.empty' | translate }}</p>
|
||||
<ng-container *ngIf="resultsSource.isLoaded()">
|
||||
<p><strong>{{ 'core.search.noresults' | translate: { $a: resultsSource.getQuery() } }}</strong></p>
|
||||
<p><small>{{ 'core.search.noresultshelp' | translate }}</small></p>
|
||||
|
|
|
@ -17,13 +17,13 @@
|
|||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list class="list-item-limited-width">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-item class="ion-text-wrap ion-no-validation">
|
||||
<ion-toggle [(ngModel)]="rtl" (ionChange)="RTLChanged()">
|
||||
<p class="item-heading">Change text direction</p>
|
||||
<p>{{ direction }}</p>
|
||||
</ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-item class="ion-text-wrap ion-no-validation">
|
||||
<ion-toggle [(ngModel)]="forceSafeAreaMargins" (ionChange)="safeAreaChanged()">
|
||||
<p class="item-heading">Force safe area margins</p>
|
||||
</ion-toggle>
|
||||
|
@ -34,13 +34,13 @@
|
|||
</ion-toggle>
|
||||
</ion-item>
|
||||
<ng-container *ngIf="siteId">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-item class="ion-text-wrap ion-no-validation">
|
||||
<ion-toggle [(ngModel)]="remoteStyles" (ionChange)="remoteStylesChanged()">
|
||||
<p class="item-heading">Enable remote styles <ion-badge>{{remoteStylesCount}}</ion-badge>
|
||||
</p>
|
||||
</ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-item class="ion-text-wrap ion-no-validation">
|
||||
<ion-toggle [(ngModel)]="pluginStyles" (ionChange)="pluginStylesChanged()">
|
||||
<p class="item-heading">Enable site plugin styles <ion-badge>{{pluginStylesCount}}</ion-badge>
|
||||
</p>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
<ion-content>
|
||||
<ion-list class="list-item-limited-width">
|
||||
<ion-item *ngIf="showDevOptions" [detail]="true" (click)="gotoDevOptions()">
|
||||
<ion-item *ngIf="showDevOptions" [detail]="true" (click)="gotoDevOptions()" button>
|
||||
<ion-icon name="fas-terminal" slot="start" aria-hidden="true" />
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p class="item-heading">{{ 'core.settings.developeroptions' | translate }}</p>
|
||||
|
|
|
@ -11,16 +11,16 @@
|
|||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list class="list-item-limited-width">
|
||||
<ion-item class="ion-text-wrap" lines="none">
|
||||
<ion-item class="ion-text-wrap ion-no-validation" lines="none">
|
||||
<ion-select [(ngModel)]="selectedLanguage" (ionChange)="languageChanged($event)" interface="action-sheet"
|
||||
[cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'core.settings.language' | translate}">
|
||||
<div slot="label" class="item-heading">{{ 'core.settings.language' | translate }}</div>
|
||||
<div slot="label" class="item-heading ion-text-wrap">{{ 'core.settings.language' | translate }}</div>
|
||||
<ion-select-option *ngFor="let entry of languages" [value]="entry.code">{{ entry.name }}</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap core-settings-general-font-size item-interactive" lines="none">
|
||||
<ion-item class="ion-text-wrap core-settings-general-font-size item-interactive ion-no-validation" lines="none">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.settings.fontsize' | translate }}</p>
|
||||
<p class="item-heading ion-text-wrap">{{ 'core.settings.fontsize' | translate }}</p>
|
||||
</ion-label>
|
||||
<ion-segment [(ngModel)]="selectedZoomLevel" color="primary">
|
||||
<ion-segment-button *ngFor=" let zoomLevel of zoomLevels" [value]="zoomLevel.value"
|
||||
|
@ -33,12 +33,12 @@
|
|||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap core-settings-general-color-scheme" *ngIf="colorSchemes.length > 0" lines="none">
|
||||
<ion-item class="ion-text-wrap core-settings-general-color-scheme ion-no-validation" *ngIf="colorSchemes.length > 0" lines="none">
|
||||
<ion-select [(ngModel)]="selectedScheme" (ionChange)="colorSchemeChanged($event)" interface="action-sheet"
|
||||
[cancelText]="'core.cancel' | translate" [disabled]="colorSchemeDisabled"
|
||||
[interfaceOptions]="{header: 'core.settings.colorscheme' | translate}">
|
||||
<div slot="label">
|
||||
<p class="item-heading">{{ 'core.settings.colorscheme' | translate }}</p>
|
||||
<p class="item-heading ion-text-wrap">{{ 'core.settings.colorscheme' | translate }}</p>
|
||||
<p *ngIf="colorSchemeDisabled" class="text-danger">{{ 'core.settings.forcedsetting' | translate }}</p>
|
||||
</div>
|
||||
<ion-select-option *ngFor="let scheme of colorSchemes" [value]="scheme">
|
||||
|
@ -47,34 +47,34 @@
|
|||
</ion-item>
|
||||
<ion-item *ngIf="colorSchemes.length> 0 && selectedScheme==='system' && isAndroid" lines="none">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p>{{ 'core.settings.colorscheme-system-notice' | translate }}</p>
|
||||
<p class="ion-text-wrap">{{ 'core.settings.colorscheme-system-notice' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-item class="ion-no-validation">
|
||||
<ion-toggle [(ngModel)]="richTextEditor" (ionChange)="richTextEditorChanged($event)">
|
||||
<p class="item-heading">{{ 'core.settings.enablerichtexteditor' | translate }}</p>
|
||||
<p>{{ 'core.settings.enablerichtexteditordescription' | translate }}</p>
|
||||
<p class="item-heading ion-text-wrap">{{ 'core.settings.enablerichtexteditor' | translate }}</p>
|
||||
<p class="ion-text-wrap">{{ 'core.settings.enablerichtexteditordescription' | translate }}</p>
|
||||
</ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="displayIframeHelp">
|
||||
<ion-item *ngIf="displayIframeHelp" class="ion-no-validation">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.settings.ioscookies' | translate }}</p>
|
||||
<p>{{ 'core.settings.ioscookiesdescription' | translate }}</p>
|
||||
<p class="item-heading ion-text-wrap">{{ 'core.settings.ioscookies' | translate }}</p>
|
||||
<p class="ion-text-wrap">{{ 'core.settings.ioscookiesdescription' | translate }}</p>
|
||||
<ion-button expand="block" (click)="openNativeSettings($event)">
|
||||
{{ 'core.opensettings' | translate }}
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-item class="ion-no-validation">
|
||||
<ion-toggle [(ngModel)]="debugDisplay" (ionChange)="debugDisplayChanged($event)">
|
||||
<p class="item-heading">{{ 'core.settings.debugdisplay' | translate }}</p>
|
||||
<p>{{ 'core.settings.debugdisplaydescription' | translate }}</p>
|
||||
<p class="item-heading ion-text-wrap">{{ 'core.settings.debugdisplay' | translate }}</p>
|
||||
<p class="ion-text-wrap">{{ 'core.settings.debugdisplaydescription' | translate }}</p>
|
||||
</ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="analyticsAvailable">
|
||||
<ion-item *ngIf="analyticsAvailable" class="ion-no-validation">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.settings.enableanalytics' | translate }}</p>
|
||||
<p>{{ 'core.settings.enableanalyticsdescription' | translate }}</p>
|
||||
<p class="item-heading ion-text-wrap">{{ 'core.settings.enableanalytics' | translate }}</p>
|
||||
<p class="ion-text-wrap">{{ 'core.settings.enableanalyticsdescription' | translate }}</p>
|
||||
</ion-label>
|
||||
<ion-toggle [(ngModel)]="analyticsEnabled" (ionChange)="analyticsEnabledChanged($event)" slot="end" />
|
||||
</ion-item>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
</ion-refresher>
|
||||
<core-loading [hideUntil]="handlers.loaded">
|
||||
<ion-list>
|
||||
<ion-item *ngFor="let handler of handlerItems" class="core-settings-handler" [ngClass]="handler.class"
|
||||
<ion-item *ngFor="let handler of handlerItems" class="core-settings-handler ion-no-validation" [ngClass]="handler.class"
|
||||
[attr.aria-label]="handler.title | translate" (click)="!handler.toggle && handlers.select(handler)"
|
||||
[button]="!handler.toggle" [detail]="!handler.toggle" [attr.aria-current]="handlers.getItemAriaCurrent(handler)">
|
||||
<ion-icon [name]="handler.icon" slot="start" *ngIf="handler.icon" aria-hidden="true" />
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<h2>{{ 'core.settings.syncsettings' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-item class="ion-text-wrap ion-no-validation">
|
||||
<ion-toggle [(ngModel)]="dataSaver" (ngModelChange)="syncOnlyOnWifiChanged()">
|
||||
{{ 'core.settings.syncdatasaver' | translate }}
|
||||
</ion-toggle>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<ion-row class="ion-no-padding ion-wrap">
|
||||
<ion-col class="ion-no-padding" size="12" size-sm="6" [attr.col-sm-6]="collections && collections.length > 1 ? '' : null">
|
||||
<core-search-box (onSubmit)="searchTags($event)" (onClear)="searchTags('')" [initialSearch]="query" [disabled]="searching"
|
||||
autocorrect="off" [spellcheck]="false" [autoFocus]="false" [lengthCheck]="0" searchArea="CoreTag" />
|
||||
autocorrect="off" [spellcheck]="false" [autoFocus]="false" [lengthCheck]="1" searchArea="CoreTag" />
|
||||
</ion-col>
|
||||
<ion-col class="ion-no-padding" size="12" size-sm="6" *ngIf="collections && collections.length > 1">
|
||||
<core-combobox [selection]="collectionId" (onChange)="searchTags(query, $event)" [disabled]="searching">
|
||||
|
@ -34,7 +34,7 @@
|
|||
</ion-grid>
|
||||
<core-loading [hideUntil]="loaded && !searching">
|
||||
<core-empty-box *ngIf="!cloud || !cloud!.tags || !cloud!.tags.length" icon="fas-tags"
|
||||
[message]="'core.tag.notagsfound' | translate: {$a: query}" />
|
||||
[message]="'core.tag.notagsfound' | translate: {$a: query}" role="alert" />
|
||||
|
||||
<ng-container *ngIf="cloud && cloud!.tags && cloud!.tags.length > 0">
|
||||
<div class="ion-text-center core-tag-cloud">
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
[message]="'core.user.noparticipants' | translate" />
|
||||
|
||||
<core-empty-box *ngIf="participants.empty && !searchInProgress && searchQuery" icon="fas-magnifying-glass"
|
||||
[message]="'core.noresults' | translate" />
|
||||
[message]="'core.noresults' | translate" [attr.role]="searchQuery ? 'alert' : null" />
|
||||
|
||||
<ion-list *ngIf="!participants.empty">
|
||||
<ion-item *ngFor="let participant of participants.items" class="ion-text-wrap"
|
||||
|
|
|
@ -127,6 +127,7 @@ export class CoreUserToursService {
|
|||
const tour = CoreDirectivesRegistry.require(element, CoreUserToursUserTourComponent);
|
||||
|
||||
viewContainer?.setAttribute('aria-hidden', 'true');
|
||||
viewContainer?.setAttribute('tabindex', '-1');
|
||||
|
||||
this.toursListeners[options.id]?.forEach(listener => listener.resolve());
|
||||
|
||||
|
@ -149,6 +150,8 @@ export class CoreUserToursService {
|
|||
const viewContainer = container.querySelector('ion-router-outlet, ion-nav, #ion-view-container-root');
|
||||
|
||||
viewContainer?.removeAttribute('aria-hidden');
|
||||
viewContainer?.removeAttribute('tabindex');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -299,24 +299,38 @@ export class CoreDomUtilsProvider {
|
|||
): Promise<void> {
|
||||
let retries = 10;
|
||||
|
||||
let focusElement = element;
|
||||
let elementToFocus = element;
|
||||
|
||||
if ('getInputElement' in focusElement) {
|
||||
// If it's an Ionic element get the right input to use.
|
||||
focusElement.componentOnReady && await focusElement.componentOnReady();
|
||||
focusElement = await focusElement.getInputElement();
|
||||
/**
|
||||
* See focusElement function on Ionic Framework utils/helpers.ts.
|
||||
*/
|
||||
if (elementToFocus.classList.contains('ion-focusable')) {
|
||||
const app = elementToFocus.closest('ion-app');
|
||||
if (app) {
|
||||
app.setFocus([elementToFocus]);
|
||||
}
|
||||
|
||||
if (document.activeElement === elementToFocus) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!focusElement || !focusElement.focus) {
|
||||
if ('getInputElement' in elementToFocus) {
|
||||
// If it's an Ionic element get the right input to use.
|
||||
elementToFocus.componentOnReady && await elementToFocus.componentOnReady();
|
||||
elementToFocus = await elementToFocus.getInputElement();
|
||||
}
|
||||
|
||||
if (!elementToFocus || !elementToFocus.focus) {
|
||||
throw new CoreError('Element to focus cannot be focused');
|
||||
}
|
||||
|
||||
while (retries > 0 && focusElement !== document.activeElement) {
|
||||
focusElement.focus();
|
||||
while (retries > 0 && elementToFocus !== document.activeElement) {
|
||||
elementToFocus.focus();
|
||||
|
||||
if (focusElement === document.activeElement) {
|
||||
if (elementToFocus === document.activeElement) {
|
||||
await CoreUtils.nextTick();
|
||||
if (CorePlatform.isAndroid() && this.supportsInputKeyboard(focusElement)) {
|
||||
if (CorePlatform.isAndroid() && this.supportsInputKeyboard(elementToFocus)) {
|
||||
// On some Android versions the keyboard doesn't open automatically.
|
||||
CoreApp.openKeyboard();
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ import { CoreCancellablePromise } from '@classes/cancellable-promise';
|
|||
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
||||
import { CoreUrlUtils } from './url';
|
||||
import { QRScanner } from '@features/native/plugins';
|
||||
import { CoreArray } from '@singletons/array';
|
||||
|
||||
export type TreeNode<T> = T & { children: TreeNode<T>[] };
|
||||
|
||||
|
@ -350,6 +351,7 @@ export class CoreUtilsProvider {
|
|||
* @param from Object to copy the properties from.
|
||||
* @param to Object where to store the properties.
|
||||
* @param clone Whether the properties should be cloned (so they are different instances).
|
||||
* @deprecated since 4.4. Not used anymore.
|
||||
*/
|
||||
copyProperties(from: Record<string, unknown>, to: Record<string, unknown>, clone: boolean = true): void {
|
||||
for (const name in from) {
|
||||
|
@ -387,6 +389,7 @@ export class CoreUtilsProvider {
|
|||
* Empties an array without losing its reference.
|
||||
*
|
||||
* @param array Array to empty.
|
||||
* @deprecated since 4.4. Not used anymore.
|
||||
*/
|
||||
emptyArray(array: unknown[]): void {
|
||||
array.length = 0; // Empty array without losing its reference.
|
||||
|
@ -396,6 +399,7 @@ export class CoreUtilsProvider {
|
|||
* Removes all properties from an object without losing its reference.
|
||||
*
|
||||
* @param object Object to remove the properties.
|
||||
* @deprecated since 4.4. Not used anymore.
|
||||
*/
|
||||
emptyObject(object: Record<string, unknown>): void {
|
||||
for (const key in object) {
|
||||
|
@ -482,17 +486,10 @@ export class CoreUtilsProvider {
|
|||
* @param array Array to filter.
|
||||
* @param regex RegExp to apply to each string.
|
||||
* @returns Filtered array.
|
||||
* @deprecated since 4.4. Use CoreArray.filterByRegexp instead.
|
||||
*/
|
||||
filterByRegexp(array: string[], regex: RegExp): string[] {
|
||||
if (!array || !array.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array.filter((entry) => {
|
||||
const matches = entry.match(regex);
|
||||
|
||||
return matches && matches.length;
|
||||
});
|
||||
return CoreArray.filterByRegexp(array, regex);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -956,7 +953,7 @@ export class CoreUtilsProvider {
|
|||
* @returns Merged array.
|
||||
*/
|
||||
mergeArraysWithoutDuplicates<T>(array1: T[], array2: T[], key?: string): T[] {
|
||||
return this.uniqueArray(array1.concat(array2), key) as T[];
|
||||
return CoreArray.unique(array1.concat(array2), key) as T[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1390,6 +1387,7 @@ export class CoreUtilsProvider {
|
|||
* @param data Object.
|
||||
* @param prefix Prefix to add.
|
||||
* @returns Prefixed object.
|
||||
* @deprecated since 4.4. Not used anymore.
|
||||
*/
|
||||
prefixKeys(data: Record<string, unknown>, prefix: string): Record<string, unknown> {
|
||||
const newObj = {};
|
||||
|
@ -1611,21 +1609,10 @@ export class CoreUtilsProvider {
|
|||
* @param array The array to treat.
|
||||
* @param [key] Key of the property that must be unique. If not specified, the whole entry.
|
||||
* @returns Array without duplicate values.
|
||||
* @deprecated since 4.4. Use CoreArray.unique instead.
|
||||
*/
|
||||
uniqueArray<T>(array: T[], key?: string): T[] {
|
||||
const unique = {}; // Use an object to make it faster to check if it's duplicate.
|
||||
|
||||
return array.filter(entry => {
|
||||
const value = key ? entry[key] : entry;
|
||||
|
||||
if (value in unique) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unique[value] = true;
|
||||
|
||||
return true;
|
||||
});
|
||||
return CoreArray.unique(array, key);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -66,4 +66,46 @@ export class CoreArray {
|
|||
return newArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array without duplicate values.
|
||||
*
|
||||
* @param array The array to treat.
|
||||
* @param [key] Key of the property that must be unique. If not specified, the whole entry.
|
||||
* @returns Array without duplicate values.
|
||||
*/
|
||||
static unique<T>(array: T[], key?: string): T[] {
|
||||
const unique = {}; // Use an object to make it faster to check if it's duplicate.
|
||||
|
||||
return array.filter(entry => {
|
||||
const value = key ? entry[key] : entry;
|
||||
|
||||
if (value in unique) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unique[value] = true;
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of strings, return only the ones that match a regular expression.
|
||||
*
|
||||
* @param array Array to filter.
|
||||
* @param regex RegExp to apply to each string.
|
||||
* @returns Filtered array.
|
||||
*/
|
||||
static filterByRegexp(array: string[], regex: RegExp): string[] {
|
||||
if (!array || !array.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array.filter((entry) => {
|
||||
const matches = entry.match(regex);
|
||||
|
||||
return matches && matches.length;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -537,10 +537,12 @@ export class CoreDom {
|
|||
*
|
||||
* @param element Element to listen to events.
|
||||
* @param callback Callback to call when clicked or the key is pressed.
|
||||
* @param setTabIndex Whether to set tabindex and role.
|
||||
*/
|
||||
static initializeClickableElementA11y(
|
||||
element: HTMLElement & {disabled?: boolean},
|
||||
callback: (event: MouseEvent | KeyboardEvent) => void,
|
||||
setTabIndex = true,
|
||||
): void {
|
||||
const enabled = () => !CoreUtils.isTrueOrOne(element.dataset.disabledA11yClicks ?? 'false');
|
||||
|
||||
|
@ -563,14 +565,14 @@ export class CoreDom {
|
|||
}
|
||||
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
callback(event);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
callback(event);
|
||||
}
|
||||
});
|
||||
|
||||
if (element.tagName !== 'BUTTON' && element.tagName !== 'A') {
|
||||
if (setTabIndex && element.tagName !== 'BUTTON' && element.tagName !== 'A') {
|
||||
// Set tabindex if not previously set.
|
||||
if (element.getAttribute('tabindex') === null) {
|
||||
element.setAttribute('tabindex', element.disabled ? '-1' : '0');
|
||||
|
|
|
@ -23,4 +23,18 @@ describe('CoreArray singleton', () => {
|
|||
expect(CoreArray.withoutItem(originalArray, 'not found')).toEqual(['foo', 'bar', 'baz']);
|
||||
});
|
||||
|
||||
it('gets unique array', () => {
|
||||
const originalArray = ['foo', 'bar', 'foo', 'baz'];
|
||||
|
||||
expect(CoreArray.unique(originalArray)).toEqual(['foo', 'bar', 'baz']);
|
||||
});
|
||||
|
||||
it('filters array by regexp', () => {
|
||||
const originalArray = ['foo', 'bar', 'baz', 'qux'];
|
||||
|
||||
expect(CoreArray.filterByRegexp(originalArray, /ba/)).toEqual(['bar', 'baz']);
|
||||
expect(CoreArray.filterByRegexp(originalArray, /foo/)).toEqual(['foo']);
|
||||
expect(CoreArray.filterByRegexp([], /foo/)).toEqual([]);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -496,18 +496,27 @@ export class TestingBehatRuntimeService {
|
|||
getHeader(): string {
|
||||
this.log('Action - Get header');
|
||||
|
||||
let titles = Array.from(document.querySelectorAll<HTMLElement>('.ion-page:not(.ion-page-hidden) > ion-header h1'));
|
||||
titles = titles.filter((title) => TestingBehatDomUtils.isElementVisible(title, document.body));
|
||||
let titles = Array.from(document.querySelectorAll<HTMLElement>('.ion-page:not(.ion-page-hidden) > ion-header h1'))
|
||||
.filter((title) => TestingBehatDomUtils.isElementVisible(title, document.body))
|
||||
.map((title) => title.innerText.trim());
|
||||
|
||||
// Collapsed title, get the floating title.
|
||||
if (titles.length < 0 || (titles.length === 1 && titles[0] === '')) {
|
||||
titles = Array.from(document.querySelectorAll<HTMLElement>(
|
||||
'.ion-page:not(.ion-page-hidden) h1.collapsible-header-floating-title',
|
||||
)).filter((title) => TestingBehatDomUtils.isElementVisible(title, document.body))
|
||||
.map((title) => title.innerText.trim());
|
||||
}
|
||||
|
||||
if (titles.length > 1) {
|
||||
return 'ERROR: Too many possible titles ('+titles.length+').';
|
||||
} else if (!titles.length) {
|
||||
return 'ERROR: No title found.';
|
||||
} else {
|
||||
const title = titles[0].innerText.trim();
|
||||
|
||||
return 'OK:' + title;
|
||||
return `ERROR: Too many possible titles (${titles.length}).`;
|
||||
}
|
||||
|
||||
if (!titles.length) {
|
||||
return 'ERROR: No title found.';
|
||||
}
|
||||
|
||||
return `OK: ${titles[0]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,6 +25,12 @@ body:not(.core-iframe-fullscreen) .collapsible-header-page {
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
&.collapsible-header-page-is-collapsed .collapsible-header-floating-title {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:not(.collapsible-header-page-is-collapsed) .collapsible-header-collapsed {
|
||||
--core-header-toolbar-border-width: 0;
|
||||
--core-header-buttons-background: var(--ion-background-color);
|
||||
|
@ -35,6 +41,9 @@ body:not(.core-iframe-fullscreen) .collapsible-header-page {
|
|||
|
||||
h1 {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -64,7 +73,9 @@ body:not(.core-iframe-fullscreen) .collapsible-header-page {
|
|||
|
||||
.collapsible-header-original-title {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
& > *:not(.collapsible-header-floating-title-wrapper),
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
--state-color-hover: rgb(40 40 40, 4%); // --gray-900 4%
|
||||
--state-color-pressed: rgb(40 40 40, 12%); // --gray-900 12%
|
||||
--state-color-focused: rgb(40 40 40, 12%); // --gray-900 12%
|
||||
|
||||
background: var(--background-color);
|
||||
border-radius: var(--mdl-shape-borderRadius-xs);
|
||||
|
@ -78,19 +77,12 @@
|
|||
transform: rotate(0);
|
||||
}
|
||||
|
||||
@include core-focus-background();
|
||||
|
||||
&:hover {
|
||||
background: var(--state-color-hover);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
background: var(--state-color-focused);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@include core-focus-style();
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--state-color-pressed);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ ion-card {
|
|||
|
||||
&::part(native) {
|
||||
--border-width: 0;
|
||||
|
||||
@include core-focus-over();
|
||||
}
|
||||
|
||||
ion-item:only-child {
|
||||
|
|
|
@ -11,11 +11,9 @@ ion-item.item.divider {
|
|||
|
||||
ion-label h2,
|
||||
ion-label p.item-heading {
|
||||
font-size: var(--item-divider-font-size);
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
font: var(--mdl-typography-subtitle-font-md);
|
||||
}
|
||||
ion-label h2.big {
|
||||
font-size: var(--item-divider-font-size-big);
|
||||
font: var(--mdl-typography-subtitle-font-lg);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
ion-item {
|
||||
ion-item.item {
|
||||
--detail-icon-color: var(--ion-item-detail-icon-color);
|
||||
--detail-icon-font-size: var(--ion-item-detail-icon-font-size);
|
||||
--detail-icon-opacity: var(--ion-item-detail-icon-opacity);
|
||||
|
||||
ion-input.in-item,
|
||||
&.item-label > ion-label.label-stacked {
|
||||
font-size: var(--mdl-typography-fontSize-lg);
|
||||
}
|
||||
|
||||
> ion-icon[slot] {
|
||||
color: var(--ion-item-icon-color);
|
||||
}
|
||||
|
@ -16,7 +21,7 @@ ion-item {
|
|||
&.ion-invalid {
|
||||
--inner-border-width: 0 0 1px 0;
|
||||
|
||||
&.ion-touched {
|
||||
&.ion-touched:not(.ion-no-validation) {
|
||||
&.ion-invalid {
|
||||
--ion-item-border-color: var(--highlight-color-invalid);
|
||||
--highlight-background: var(--ion-item-border-color);
|
||||
|
@ -30,11 +35,25 @@ ion-item {
|
|||
}
|
||||
}
|
||||
|
||||
&.ion-no-validation {
|
||||
--inner-border-width: 0 0 1px 0;
|
||||
}
|
||||
|
||||
// Hide details on items to align badges.
|
||||
&.hide-detail {
|
||||
--detail-icon-opacity: 0;
|
||||
}
|
||||
|
||||
&:not(.item-input) {
|
||||
--show-full-highlight: 0;
|
||||
--show-inset-highlight: 0;
|
||||
}
|
||||
|
||||
&.item-has-interactive-control:focus-within {
|
||||
@include core-focus-outline();
|
||||
}
|
||||
}
|
||||
|
||||
// Fake item.
|
||||
div.fake-ion-item {
|
||||
position: relative;
|
||||
|
@ -50,7 +69,7 @@ div.fake-ion-item {
|
|||
}
|
||||
|
||||
.md div.fake-ion-item {
|
||||
font-size: 16px;
|
||||
font-size: var(--text-size);
|
||||
font-weight: normal;
|
||||
text-transform: none;
|
||||
@include padding(null, 16px, null, 16px);
|
||||
|
@ -125,3 +144,175 @@ div.fake-ion-item {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
// Item Headings.
|
||||
// Some styles taken from ion-label
|
||||
ion-item.item.item-label > ion-label,
|
||||
.fake-ion-item > ion-label,
|
||||
ion-item .in-item {
|
||||
font-size: var(--text-size);
|
||||
|
||||
p {
|
||||
--color: var(--subdued-text-color);
|
||||
color: var(--color);
|
||||
@include margin(2px, 0);
|
||||
}
|
||||
|
||||
.item-heading {
|
||||
@include margin(2px, 0);
|
||||
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
|
||||
text-overflow: inherit;
|
||||
overflow: inherit;
|
||||
--color: initial;
|
||||
color: var(--color);
|
||||
|
||||
&.item-heading-secondary {
|
||||
@include margin(2px, 0);
|
||||
|
||||
font-size: var(--text-size);
|
||||
font-weight: normal;
|
||||
line-height: normal;
|
||||
|
||||
--color: var(--subdued-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Correctly inherit ion-text-wrap onto labels.
|
||||
.item > ion-label,
|
||||
.fake-ion-item,
|
||||
.item.ion-text-wrap > ion-checkbox::part(label),
|
||||
ion-checkbox.ion-text-wrap::part(label) {
|
||||
core-format-text,
|
||||
core-format-text > *:not(pre) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.item.ion-text-wrap > ion-label,
|
||||
ion-item > .in-item,
|
||||
.fake-ion-item.ion-text-wrap,
|
||||
.item.ion-text-wrap > ion-checkbox::part(label),
|
||||
ion-checkbox.ion-text-wrap::part(label) {
|
||||
core-format-text,
|
||||
core-format-text > *:not(pre) {
|
||||
white-space: normal;
|
||||
overflow: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.item.ion-text-wrap > ion-label,
|
||||
.item.ion-text-wrap ion-checkbox::part(label),
|
||||
ion-checkbox.ion-text-wrap::part(label),
|
||||
.item.ion-text-wrap ion-radio::part(label),
|
||||
ion-radio.ion-text-wrap::part(label) {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
ion-item .core-input-errors-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ion-item.item.item-file {
|
||||
ion-thumbnail {
|
||||
--size: 32px;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
|
||||
p.item-heading {
|
||||
font-size: var(--text-size);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--mdl-typography-fontSize-sm);
|
||||
}
|
||||
|
||||
ion-label {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
ion-button {
|
||||
--a11y-sizing-minTargetSize: 40px;
|
||||
}
|
||||
|
||||
&.item-directory ion-icon {
|
||||
@include margin-horizontal(0px, 16px);
|
||||
}
|
||||
|
||||
[slot=end] {
|
||||
@include margin-horizontal(16px, null);
|
||||
}
|
||||
}
|
||||
|
||||
.item-dimmed {
|
||||
opacity: 0.7;
|
||||
--background: var(--light);
|
||||
ion-item {
|
||||
--background: var(--light);
|
||||
}
|
||||
}
|
||||
|
||||
// Make links clickable when inside radio or checkbox items. Style part.
|
||||
@media (hover: hover) {
|
||||
ion-item.item-multiple-inputs:not(.item-rte):hover::part(native) {
|
||||
color: var(--color-hover);
|
||||
|
||||
&::after {
|
||||
background: var(--background-hover);
|
||||
opacity: var(--background-hover-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
ion-item.ion-color.item-multiple-inputs:hover::part(native) {
|
||||
color: #{current-color(contrast)};
|
||||
|
||||
&::after {
|
||||
background: #{current-color(contrast)};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It fixes the click on links where ion-ripple-effect is present.
|
||||
// Make links clickable when inside radio or checkbox items. Pointer and cursor part.
|
||||
ion-item.item-multiple-inputs:not(.only-links):not(.item-rte),
|
||||
ion-item.ion-activatable:not(.only-links) {
|
||||
cursor: pointer;
|
||||
ion-label {
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
|
||||
ion-anchor, a,
|
||||
ion-button, button,
|
||||
ion-item.ion-focusable,
|
||||
audio, video, select, input, iframe {
|
||||
pointer-events: visible;
|
||||
}
|
||||
}
|
||||
|
||||
ion-checkbox, ion-datetime, ion-radio, ion-select{
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
ion-item.item-multiple-inputs.only-links {
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
// Case with ion-input + ion-select inside.
|
||||
ion-item.item-input.item-multiple-inputs {
|
||||
.flex-row {
|
||||
width: 100%;
|
||||
ion-select {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
// Select.
|
||||
ion-select {
|
||||
&::part(text) {
|
||||
white-space: normal;
|
||||
}
|
||||
&::part(icon) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
ion-select-popover {
|
||||
ion-list ion-radio-group ion-item.select-interface-option ion-radio.hydrated::part(container) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
font-size: var(--text-size);
|
||||
}
|
||||
ion-item.core-select-option-border-bottom {
|
||||
border-bottom: 1px solid var(--stroke);
|
||||
}
|
||||
|
||||
ion-item.core-select-option-title {
|
||||
cursor: pointer;
|
||||
ion-radio::part(container) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Case with ion-input + ion-select inside.
|
||||
ion-item.item-input.item-multiple-inputs {
|
||||
.flex-row {
|
||||
width: 100%;
|
||||
ion-select {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.select-alert.ios {
|
||||
.alert-checkbox-icon {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.alert-radio-icon {
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
min-width: var(--size);
|
||||
border-radius: var(--border-radius);
|
||||
border-width: var(--outer-border-width);
|
||||
border-style: var(--border-style);
|
||||
border-color: var(--color);
|
||||
@include margin(10px, 8px, 10px, 8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.alert-radio-inner {
|
||||
top: auto;
|
||||
left: auto;
|
||||
position: static;
|
||||
border-radius: var(--inner-border-radius);
|
||||
width: calc(50% + var(--outer-border-width));
|
||||
height: calc(50% + var(--outer-border-width));
|
||||
transform: scale3d(0, 0, 0);
|
||||
transition: transform 280ms cubic-bezier(.4, 0, .2, 1);
|
||||
background: var(--contrast-background);
|
||||
border: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
button[aria-checked=true] .alert-radio-icon {
|
||||
border-color: var(--color-checked);
|
||||
background: var(--color-checked);
|
||||
|
||||
.alert-radio-inner {
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
ion-tab-bar.mainmenu-tabs {
|
||||
--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);
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
|
||||
swiper-container {
|
||||
--swiper-theme-color: var(--ion-color-primary, #3880ff);
|
||||
--swiper-pagination-bullet-inactive-color: var(--ion-color-step-200, #cccccc);
|
||||
--swiper-pagination-color: var(--swiper-theme-color);
|
||||
--swiper-pagination-progressbar-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.25);
|
||||
--swiper-scrollbar-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.1);
|
||||
--swiper-scrollbar-drag-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.5);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100vh;
|
||||
// CSS Grid/Flexbox bug size workaround
|
||||
// @see https://github.com/kenwheeler/slick/issues/982
|
||||
// @see https://github.com/nolimits4web/swiper/issues/3599
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
|
||||
swiper-slide {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
font-size: 18px;
|
||||
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
img {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// To make core-swipe-slides fill the remaining height.
|
||||
.core-swipe-slides-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
min-height: 100%;
|
||||
|
||||
core-swipe-slides {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
|
||||
swiper-container {
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -66,7 +66,42 @@
|
|||
}
|
||||
}
|
||||
|
||||
@mixin core-focus() {
|
||||
@mixin core-focus-over() {
|
||||
&:focus-visible {
|
||||
@include core-focus-over-internal();
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
@at-root:focus {
|
||||
@include core-focus-over-internal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin core-focus-outline() {
|
||||
&:focus-visible {
|
||||
@include core-focus-outline-internal();
|
||||
}
|
||||
@supports not selector(:focus-visible) {
|
||||
@at-root:focus {
|
||||
@include core-focus-outline-internal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin core-focus-background() {
|
||||
&:focus-visible {
|
||||
@include core-focus-background-internal();
|
||||
}
|
||||
@supports not selector(:focus-visible) {
|
||||
@at-root:focus {
|
||||
@include core-focus-background-internal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mixin core-focus-over-internal() {
|
||||
outline: none;
|
||||
position: relative;
|
||||
|
||||
|
@ -74,17 +109,26 @@
|
|||
@include position(0px, 0px, 0px, 0px);
|
||||
position: absolute;
|
||||
content: "";
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
@include core-focus-style();
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
@include core-focus-background-internal();
|
||||
}
|
||||
}
|
||||
|
||||
@mixin core-focus-style() {
|
||||
box-shadow: var(--a11y-shadow-focus-boxShadow);
|
||||
border-radius: var(--border-radius);
|
||||
// Thicker option:
|
||||
// outline: var(--a11y-shadow-focus-outline);
|
||||
@mixin core-focus-outline-internal() {
|
||||
// box-shadow: var(--a11y-shadow-focus-boxShadow);
|
||||
// border-radius: var(--border-radius);
|
||||
outline: var(--a11y-shadow-focus-outline);
|
||||
}
|
||||
|
||||
|
||||
@mixin core-focus-background-internal() {
|
||||
--background-focused: var(--background-focused, var(--a11y-background-focus-background));
|
||||
--background-focused-opacity: var(--a11y-background-focus-opacity);
|
||||
--background: var(--a11y-background-focus-background-rgb);
|
||||
background: var(--background);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@mixin core-transition($properties: all, $duration: 500ms, $timing-function: ease-in-out) {
|
||||
|
|
|
@ -72,77 +72,6 @@ html[dir=rtl] {
|
|||
.font-lg { font-size: 1.7rem; }
|
||||
.font-sm { font-size: 1.2rem; }
|
||||
|
||||
// Item Headings.
|
||||
// Some styles taken from ion-label
|
||||
.item > ion-label,
|
||||
.fake-ion-item > ion-label,
|
||||
ion-item .in-item {
|
||||
p {
|
||||
--color: var(--subdued-text-color);
|
||||
color: var(--color);
|
||||
@include margin(2px, 0);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.item-heading {
|
||||
@include margin(2px, 0);
|
||||
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
|
||||
text-overflow: inherit;
|
||||
overflow: inherit;
|
||||
--color: initial;
|
||||
color: var(--color);
|
||||
|
||||
&.item-heading-secondary {
|
||||
@include margin(2px, 0);
|
||||
|
||||
font-size: var(--text-size);
|
||||
font-weight: normal;
|
||||
line-height: normal;
|
||||
|
||||
--color: var(--subdued-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Correctly inherit ion-text-wrap onto labels.
|
||||
.item > ion-label,
|
||||
.fake-ion-item,
|
||||
.item.ion-text-wrap > ion-checkbox::part(label),
|
||||
ion-checkbox.ion-text-wrap::part(label) {
|
||||
core-format-text,
|
||||
core-format-text > *:not(pre) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.item.ion-text-wrap > ion-label,
|
||||
ion-item > .in-item,
|
||||
.fake-ion-item.ion-text-wrap,
|
||||
.item.ion-text-wrap > ion-checkbox::part(label),
|
||||
ion-checkbox.ion-text-wrap::part(label) {
|
||||
core-format-text,
|
||||
core-format-text > *:not(pre) {
|
||||
white-space: normal;
|
||||
overflow: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.item.ion-text-wrap > ion-label,
|
||||
.item.ion-text-wrap ion-checkbox::part(label),
|
||||
ion-checkbox.ion-text-wrap::part(label),
|
||||
.item.ion-text-wrap ion-radio::part(label),
|
||||
ion-radio.ion-text-wrap::part(label) {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
ion-item .core-input-errors-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@each $color-name, $unused in $colors {
|
||||
.text-#{$color-name},
|
||||
|
@ -566,47 +495,6 @@ body.core-iframe-fullscreen ion-router-outlet {
|
|||
}
|
||||
}
|
||||
|
||||
.item.item-file {
|
||||
ion-thumbnail {
|
||||
--size: 32px;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
|
||||
p.item-heading {
|
||||
font-size: var(--text-size);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
ion-label {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
ion-button {
|
||||
--a11y-sizing-minTargetSize: 40px;
|
||||
}
|
||||
|
||||
&.item-directory ion-icon {
|
||||
@include margin-horizontal(0px, 16px);
|
||||
}
|
||||
|
||||
[slot=end] {
|
||||
@include margin-horizontal(16px, null);
|
||||
}
|
||||
}
|
||||
|
||||
.item-dimmed {
|
||||
opacity: 0.7;
|
||||
--background: var(--light);
|
||||
ion-item {
|
||||
--background: var(--light);
|
||||
}
|
||||
}
|
||||
|
||||
// Extra text colors.
|
||||
.text-gray {
|
||||
color: var(--gray-500);
|
||||
|
@ -626,24 +514,39 @@ body.core-iframe-fullscreen ion-router-outlet {
|
|||
--background: var(--color-tint);
|
||||
--color: var(--color-shade);
|
||||
|
||||
ion-item {
|
||||
ion-item.item {
|
||||
--background: var(--color-tint);
|
||||
--color: var(--color-shade);
|
||||
--inner-border-width: 0px;
|
||||
--border-width: 0px;
|
||||
font-size: var(--text-size);
|
||||
|
||||
ion-label, ion-label > p {
|
||||
&.item-label > ion-label,
|
||||
&.item-label > ion-label > p {
|
||||
--color: var(--color-shade);
|
||||
}
|
||||
|
||||
> ion-icon[slot] {
|
||||
color: var(--color-shade);
|
||||
@include margin-horizontal(null, 16px);
|
||||
}
|
||||
}
|
||||
|
||||
ion-label {
|
||||
white-space: normal !important;
|
||||
}
|
||||
ion-item > ion-icon[slot] {
|
||||
color: var(--color-shade);
|
||||
@include margin-horizontal(null, 16px);
|
||||
}
|
||||
|
||||
ion-item.item.core-#{$color-name}-item {
|
||||
--color-base: var(--ion-color-#{$color-name});
|
||||
--color-shade: var(--ion-color-#{$color-name}-shade);
|
||||
|
||||
--border-width: 0 0 3px 0;
|
||||
--border-color: var(--color-base);
|
||||
--inner-border-width: 0px;
|
||||
|
||||
> ion-icon[slot] {
|
||||
color: var(--color-base);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -748,7 +651,7 @@ ion-toolbar h1 .core-bar-button-image img {
|
|||
// Radio.
|
||||
ion-radio,
|
||||
input[type=radio],
|
||||
.select-alert.ios .alert-radio-icon {
|
||||
.select-alert .alert-radio-icon {
|
||||
--border-radius: 50%;
|
||||
--border-width: 2px;
|
||||
--outer-border-width: 2px;
|
||||
|
@ -761,7 +664,6 @@ input[type=radio],
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.ios ion-radio,
|
||||
.ios input[type=radio],
|
||||
.select-alert.ios .alert-radio-icon {
|
||||
|
@ -803,43 +705,6 @@ input[type=radio],
|
|||
}
|
||||
}
|
||||
|
||||
.select-alert.ios {
|
||||
.alert-radio-icon {
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
min-width: var(--size);
|
||||
border-radius: var(--border-radius);
|
||||
border-width: var(--outer-border-width);
|
||||
border-style: var(--border-style);
|
||||
border-color: var(--color);
|
||||
@include margin(10px, 8px, 10px, 8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.alert-radio-inner {
|
||||
top: auto;
|
||||
left: auto;
|
||||
position: static;
|
||||
border-radius: var(--inner-border-radius);
|
||||
width: calc(50% + var(--outer-border-width));
|
||||
height: calc(50% + var(--outer-border-width));
|
||||
transform: scale3d(0, 0, 0);
|
||||
transition: transform 280ms cubic-bezier(.4, 0, .2, 1);
|
||||
background: var(--contrast-background);
|
||||
border: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
button[aria-checked=true] .alert-radio-icon {
|
||||
border-color: var(--color-checked);
|
||||
background: var(--color-checked);
|
||||
|
||||
.alert-radio-inner {
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Checkbox.
|
||||
ion-checkbox,
|
||||
|
@ -857,48 +722,10 @@ input[type=checkbox] {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.ios input[type=checkbox] {
|
||||
--outer-border-width: 1px;
|
||||
}
|
||||
|
||||
.select-alert.ios .alert-checkbox-icon {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
// Select.
|
||||
ion-select {
|
||||
&::part(text) {
|
||||
white-space: normal;
|
||||
}
|
||||
&::part(icon) {
|
||||
opacity: 1;
|
||||
}
|
||||
&::part(label) {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
ion-select-popover {
|
||||
ion-list ion-radio-group ion-item.select-interface-option ion-radio.hydrated::part(container) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
font-size: 14px;
|
||||
}
|
||||
ion-item.core-select-option-border-bottom {
|
||||
border-bottom: 1px solid var(--stroke);
|
||||
}
|
||||
|
||||
ion-item.core-select-option-title {
|
||||
cursor: pointer;
|
||||
ion-radio::part(container) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ion-badge {
|
||||
line-height: 1.1;
|
||||
padding: 2px 8px;
|
||||
|
@ -1048,65 +875,6 @@ ion-datetime.datetime-disabled {
|
|||
opacity: .65 !important;
|
||||
}
|
||||
|
||||
// Make links clickable when inside radio or checkbox items. Style part.
|
||||
@media (hover: hover) {
|
||||
ion-item.item-multiple-inputs:not(.item-rte):hover::part(native) {
|
||||
color: var(--color-hover);
|
||||
|
||||
&::after {
|
||||
background: var(--background-hover);
|
||||
opacity: var(--background-hover-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
ion-item.ion-color.item-multiple-inputs:hover::part(native) {
|
||||
color: #{current-color(contrast)};
|
||||
|
||||
&::after {
|
||||
background: #{current-color(contrast)};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// It fixes the click on links where ion-ripple-effect is present.
|
||||
// Make links clickable when inside radio or checkbox items. Pointer and cursor part.
|
||||
ion-item.item-multiple-inputs:not(.only-links):not(.item-rte),
|
||||
ion-item.ion-activatable:not(.only-links) {
|
||||
cursor: pointer;
|
||||
ion-label {
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
|
||||
ion-anchor, a,
|
||||
ion-button, button,
|
||||
ion-item.ion-focusable,
|
||||
audio, video, select, input, iframe {
|
||||
pointer-events: visible;
|
||||
}
|
||||
}
|
||||
|
||||
ion-checkbox, ion-datetime, ion-radio, ion-select{
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
ion-item.item-multiple-inputs.only-links {
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
// Case with ion-input + ion-select inside.
|
||||
ion-item.item-input.item-multiple-inputs {
|
||||
.flex-row {
|
||||
width: 100%;
|
||||
ion-select {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ion-item-divider.item,
|
||||
ion-item.item,
|
||||
td {
|
||||
|
@ -1122,68 +890,15 @@ td {
|
|||
}
|
||||
|
||||
// Change default outline.
|
||||
:focus-visible {
|
||||
@include core-focus-style();
|
||||
border-radius: inherit;
|
||||
outline: none;
|
||||
.ion-activatable,
|
||||
.clickable,
|
||||
.ion-focusable.ion-focused,
|
||||
.ion-selectable {
|
||||
@include core-focus-background();
|
||||
}
|
||||
|
||||
// Focus highlight for accessibility.
|
||||
.ion-focused:not(.item-multiple-inputs):not(:focus),
|
||||
ion-input.has-focus,
|
||||
ion-card:focus {
|
||||
@include core-focus();
|
||||
|
||||
:focus-visible,
|
||||
.clickable:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ion-focused.item-multiple-inputs {
|
||||
ion-toggle:focus-within,
|
||||
ion-select:focus-within,
|
||||
ion-checkbox:focus-within,
|
||||
ion-radio:focus-within {
|
||||
@include core-focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Treat cases where there's a focusable element inside an item, like a button.
|
||||
ion-item.item-input:not(.item-multiple-inputs):not(:focus),
|
||||
ion-item.item-has-focus:not(.item-multiple-inputs):not(:focus),
|
||||
ion-item.item-input ion-input.has-focus {
|
||||
position: relative;
|
||||
&::after {
|
||||
box-shadow: revert;
|
||||
opacity: revert;
|
||||
z-index: revert;
|
||||
}
|
||||
.item-highlight, .item-inner-highlight {
|
||||
position: unset;
|
||||
}
|
||||
}
|
||||
|
||||
textarea, button, select, input, a, .clickable {
|
||||
&:focus {
|
||||
@include core-focus-style();
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
ion-loading:focus-visible,
|
||||
ion-alert:focus-visible,
|
||||
ion-popover:focus-visible,
|
||||
ion-modal:focus-visible {
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
ion-input .native-input {
|
||||
&:focus, &:focus-visible {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
:not(.hydrated):not(.native-input):not(.native-textarea) { // Not an ionic component.
|
||||
@include core-focus-outline();
|
||||
}
|
||||
|
||||
ion-input,
|
||||
|
@ -1296,67 +1011,6 @@ ion-grid.core-no-grid > ion-row {
|
|||
}
|
||||
}
|
||||
|
||||
// To make core-swipe-slides fill the remaining height.
|
||||
.core-swipe-slides-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
min-height: 100%;
|
||||
|
||||
core-swipe-slides {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
|
||||
swiper-container {
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
swiper-container {
|
||||
--swiper-theme-color: var(--ion-color-primary, #3880ff);
|
||||
--swiper-pagination-bullet-inactive-color: var(--ion-color-step-200, #cccccc);
|
||||
--swiper-pagination-color: var(--swiper-theme-color);
|
||||
--swiper-pagination-progressbar-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.25);
|
||||
--swiper-scrollbar-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.1);
|
||||
--swiper-scrollbar-drag-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.5);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100vh;
|
||||
// CSS Grid/Flexbox bug size workaround
|
||||
// @see https://github.com/kenwheeler/slick/issues/982
|
||||
// @see https://github.com/nolimits4web/swiper/issues/3599
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
|
||||
swiper-slide {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
font-size: 18px;
|
||||
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
img {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.has-spacer,
|
||||
.core-flex-fill {
|
||||
display: flex;
|
||||
|
|