MOBILE-3753 calendar: Fix calendar navigation

main
Pau Ferrer Ocaña 2021-05-11 11:54:30 +02:00
parent 064a60ca49
commit ef0ed6c7fb
7 changed files with 179 additions and 113 deletions

View File

@ -18,7 +18,7 @@
</ion-button> </ion-button>
</ion-col> </ion-col>
<ion-col class="ion-text-center addon-calendar-period"> <ion-col class="ion-text-center addon-calendar-period">
<h3>{{ periodName }}</h3> <h3 id="addon-calendar-monthname">{{ periodName }}</h3>
</ion-col> </ion-col>
<ion-col class="ion-text-end" *ngIf="canNavigate"> <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]="'core.next' | translate">
@ -29,58 +29,88 @@
</ion-grid> </ion-grid>
<!-- Calendar view. --> <!-- Calendar view. -->
<ion-grid class="addon-calendar-months"> <ion-grid class="addon-calendar-months" role="table" aria-describedby="addon-calendar-monthname">
<!-- List of days. --> <div role="rowgroup">
<ion-row> <!-- List of days. -->
<ion-col class="ion-text-center addon-calendar-weekday" *ngFor="let day of weekDays"> <ion-row role="row">
<span class="ion-hide-md-up" [title]="day.fullname | translate">{{ day.shortname | translate }}</span> <ion-col class="ion-text-center addon-calendar-weekday" *ngFor="let day of weekDays" role="columnheader">
<span class="ion-hide-md-down">{{ day.fullname | translate }}</span> <span class="sr-only">{{ day.fullname | translate }}</span>
</ion-col> <span class="ion-hide-md-up" aria-hidden="true">{{ day.shortname | translate }}</span>
</ion-row> <span class="ion-hide-md-down" aria-hidden="true">{{ day.fullname | translate }}</span>
</ion-col>
</ion-row>
</div>
<div role="rowgroup">
<!-- Weeks. --> <!-- Weeks. -->
<ion-row *ngFor="let week of weeks" class="addon-calendar-week"> <ion-row *ngFor="let week of weeks" class="addon-calendar-week" role="row">
<!-- Empty slots (first week). --> <!-- Empty slots (first week). -->
<ion-col *ngFor="let value of week.prepadding" class="dayblank addon-calendar-day"></ion-col> <ion-col *ngFor="let value of week.prepadding" class="dayblank addon-calendar-day" role="cell"></ion-col>
<ion-col class="addon-calendar-day ion-text-center" *ngFor="let day of week.days" (click)="dayClicked(day.mday)" <ion-col
[ngClass]='{"hasevents": day.hasevents, "today": isCurrentMonth && day.istoday, *ngFor="let day of week.days"
"weekend": day.isweekend, "duration_finish": day.haslastdayofevent}' class="addon-calendar-day ion-text-center"
[class.addon-calendar-event-past-day]="isPastMonth || day.ispast"> [ngClass]='{
<p class="addon-calendar-day-number"><span>{{ day.mday }}</span></p> "hasevents": day.hasevents,
"today": isCurrentMonth && day.istoday,
<!-- In phone, display some dots to indicate the type of events. --> "weekend": day.isweekend,
<p class="ion-hide-md-up addon-calendar-dot-types"><span *ngFor="let type of day.calendareventtypes" "duration_finish": day.haslastdayofevent
class="calendar_event_type calendar_event_{{type}}"></span></p> }'
[class.addon-calendar-event-past-day]="isPastMonth || day.ispast"
<!-- In tablet, display list of events. --> role="button cell"
<div class="ion-hide-md-down addon-calendar-day-events"> tabindex="0"
<ng-container *ngFor="let event of day.filteredEvents | slice:0:4; let index = index"> (click)="dayClicked(day.mday)"
<div role="button" *ngIf="index < 3 || day.filteredEvents.length == 4" class="addon-calendar-event" (keyup)="dayAction.keyUp($event, day.mday)"
(click)="eventClicked(event, $event)" [class.addon-calendar-event-past]="event.ispast"> (keydown)="dayAction.keyDown($event)"
<span class="calendar_event_type calendar_event_{{event.formattedType}}"></span> >
<ion-icon *ngIf="event.offline && !event.deleted" name="fas-clock" <p class="addon-calendar-day-number">
[attr.aria-label]="'core.notsent' | translate"></ion-icon> <span aria-hidden="true">{{ day.mday }}</span>
<ion-icon *ngIf="event.deleted" name="fas-trash" [attr.aria-label]="'core.deletedoffline' | translate"> <span class="sr-only">{{ day.periodName | translate }}</span>
</ion-icon>
<span class="addon-calendar-event-time">{{ event.timestart * 1000 | coreFormatDate: timeFormat }}</span>
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" alt="" role="presentation"
class="core-module-icon">
<!-- Add the icon title so accessibility tools read it. -->
<span class="sr-only">
{{ 'addon.calendar.type' + event.formattedType | translate }}
<span class="sr-only" *ngIf="event.moduleIcon && event.iconTitle">{{ event.iconTitle }}</span>
</span>
<span class="addon-calendar-event-name">{{event.name}}</span>
</div>
</ng-container>
<p *ngIf="day.filteredEvents.length > 4" class="addon-calendar-day-more">
<b>{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}</b>
</p> </p>
</div>
</ion-col> <!-- In phone, display some dots to indicate the type of events. -->
<!-- Empty slots (last week). --> <p class="ion-hide-md-up addon-calendar-dot-types"><span *ngFor="let type of day.calendareventtypes"
<ion-col *ngFor="let value of week.postpadding" class="dayblank addon-calendar-day"></ion-col> class="calendar_event_type calendar_event_{{type}}"></span></p>
</ion-row>
<!-- In tablet, display list of events. -->
<div class="ion-hide-md-down addon-calendar-day-events">
<ng-container *ngFor="let event of day.filteredEvents | slice:0:4; let index = index">
<div
*ngIf="index < 3 || day.filteredEvents.length == 4"
class="addon-calendar-event"
[class.addon-calendar-event-past]="event.ispast"
role="button"
tabindex="0"
(click)="eventClicked(event, $event)"
(keyup)="eventAction.keyUp($event, event)"
(keydown)="eventAction.keyDown($event)"
>
<span class="calendar_event_type calendar_event_{{event.formattedType}}"></span>
<ion-icon *ngIf="event.offline && !event.deleted" name="fas-clock"
[attr.aria-label]="'core.notsent' | translate"></ion-icon>
<ion-icon *ngIf="event.deleted" name="fas-trash"
[attr.aria-label]="'core.deletedoffline' | translate"></ion-icon>
<span class="addon-calendar-event-time">
{{ event.timestart * 1000 | coreFormatDate: timeFormat }}
</span>
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" alt="" role="presentation"
class="core-module-icon">
<!-- Add the icon title so accessibility tools read it. -->
<span class="sr-only">
{{ 'addon.calendar.type' + event.formattedType | translate }}
<span class="sr-only" *ngIf="event.moduleIcon && event.iconTitle">{{ event.iconTitle }}</span>
</span>
<span class="addon-calendar-event-name">{{event.name}}</span>
</div>
</ng-container>
<p *ngIf="day.filteredEvents.length > 4" class="addon-calendar-day-more">
<b>{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}</b>
</p>
</div>
</ion-col>
<!-- Empty slots (last week). -->
<ion-col *ngFor="let value of week.postpadding" class="dayblank addon-calendar-day" role="cell"></ion-col>
</ion-row>
</div>
</ion-grid> </ion-grid>
</core-loading> </core-loading>

View File

@ -71,6 +71,7 @@
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
color: var(--text-color); color: var(--text-color);
min-height: auto;
&.addon-calendar-event-past { &.addon-calendar-event-past {
opacity: 0.5; opacity: 0.5;
@ -102,6 +103,7 @@
.addon-calendar-weekday { .addon-calendar-weekday {
border-bottom: 1px solid var(--addon-calendar-border-color); border-bottom: 1px solid var(--addon-calendar-border-color);
font-weight: bold;
} }
.addon-calendar-day-events { .addon-calendar-day-events {

View File

@ -41,6 +41,7 @@ import { AddonCalendarOffline } from '../../services/calendar-offline';
import { CoreCategoryData, CoreCourses } from '@features/courses/services/courses'; import { CoreCategoryData, CoreCourses } from '@features/courses/services/courses';
import { CoreApp } from '@services/app'; import { CoreApp } from '@services/app';
import { CoreLocalNotifications } from '@services/local-notifications'; import { CoreLocalNotifications } from '@services/local-notifications';
import { CoreAriaRoleButton } from '@classes/aria-role-button';
/** /**
* Component that displays a calendar. * Component that displays a calendar.
@ -67,6 +68,8 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
timeFormat?: string; timeFormat?: string;
isCurrentMonth = false; isCurrentMonth = false;
isPastMonth = false; isPastMonth = false;
dayAction: AddonCalendarDayButton;
eventAction: AddonCalendarEventButton;
protected year?: number; protected year?: number;
protected month?: number; protected month?: number;
@ -88,6 +91,9 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
differs: KeyValueDiffers, differs: KeyValueDiffers,
) { ) {
this.dayAction = new AddonCalendarDayButton(this);
this.eventAction = new AddonCalendarEventButton(this);
this.currentSiteId = CoreSites.getCurrentSiteId(); this.currentSiteId = CoreSites.getCurrentSiteId();
if (CoreLocalNotifications.isAvailable()) { if (CoreLocalNotifications.isAvailable()) {
@ -233,6 +239,10 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
this.weeks.forEach((week) => { this.weeks.forEach((week) => {
week.days.forEach((day) => { week.days.forEach((day) => {
day.periodName = CoreTimeUtils.userDate(
new Date(this.year!, this.month! - 1, day.mday).getTime(),
'core.strftimedaydate',
);
day.eventsFormated = day.eventsFormated || []; day.eventsFormated = day.eventsFormated || [];
day.filteredEvents = day.filteredEvents || []; day.filteredEvents = day.filteredEvents || [];
day.events.forEach((event) => { day.events.forEach((event) => {
@ -372,7 +382,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
* @param calendarEvent Calendar event.. * @param calendarEvent Calendar event..
* @param event Mouse event. * @param event Mouse event.
*/ */
eventClicked(calendarEvent: AddonCalendarEventToDisplay, event: MouseEvent): void { eventClicked(calendarEvent: AddonCalendarEventToDisplay, event: Event): void {
this.onEventClicked.emit(calendarEvent.id); this.onEventClicked.emit(calendarEvent.id);
event.stopPropagation(); event.stopPropagation();
} }
@ -525,3 +535,31 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
} }
} }
/**
* Helper class to manage day button.
*/
class AddonCalendarDayButton extends CoreAriaRoleButton<AddonCalendarCalendarComponent> {
/**
* @inheritdoc
*/
click(event: Event, day: number): void {
this.componentInstance.dayClicked(day);
}
}
/**
* Helper class to manage event button.
*/
class AddonCalendarEventButton extends CoreAriaRoleButton<AddonCalendarCalendarComponent> {
/**
* @inheritdoc
*/
click(event: Event, calendarEvent: AddonCalendarEventToDisplay): void {
this.componentInstance.eventClicked(calendarEvent, event);
}
}

View File

@ -16,9 +16,7 @@
<!-- Event name. --> <!-- Event name. -->
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label position="stacked"> <ion-label position="stacked">
<h2 [core-mark-required]="true"> <h2 [core-mark-required]="true">{{ 'addon.calendar.eventname' | translate }}</h2>
{{ 'addon.calendar.eventname' | translate }}
</h2>
</ion-label> </ion-label>
<ion-input type="text" name="name" [placeholder]="'addon.calendar.eventname' | translate" formControlName="name"> <ion-input type="text" name="name" [placeholder]="'addon.calendar.eventname' | translate" formControlName="name">
</ion-input> </ion-input>
@ -27,11 +25,7 @@
<!-- Date. --> <!-- Date. -->
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label position="stacked"> <ion-label position="stacked"><h2 [core-mark-required]="true">{{ 'core.date' | translate }}</h2></ion-label>
<h2 [core-mark-required]="true">
{{ 'core.date' | translate }}
</h2>
</ion-label>
<ion-datetime formControlName="timestart" [placeholder]="'core.date' | translate" [displayFormat]="dateFormat" <ion-datetime formControlName="timestart" [placeholder]="'core.date' | translate" [displayFormat]="dateFormat"
[max]="maxDate" [min]="minDate"> [max]="maxDate" [min]="minDate">
</ion-datetime> </ion-datetime>
@ -40,13 +34,15 @@
<!-- Type. --> <!-- Type. -->
<ion-item class="ion-text-wrap addon-calendar-eventtype-container"> <ion-item class="ion-text-wrap addon-calendar-eventtype-container">
<ion-label id="addon-calendar-eventtype-label"> <ion-label>
<h2 [core-mark-required]="true"> <h2 [core-mark-required]="true">{{ 'addon.calendar.eventkind' | translate }}</h2>
{{ 'addon.calendar.eventkind' | translate }}
</h2>
</ion-label> </ion-label>
<ion-select formControlName="eventtype" aria-labelledby="addon-calendar-eventtype-label" interface="action-sheet" <p *ngIf="eventTypes.length == 1" slot="end">{{eventTypes[0].name | translate }}</p>
[disabled]="eventTypes.length == 1"> <ion-select
*ngIf="eventTypes.length > 1"
formControlName="eventtype"
interface="action-sheet"
>
<ion-select-option *ngFor="let type of eventTypes" [value]="type.value"> <ion-select-option *ngFor="let type of eventTypes" [value]="type.value">
{{ type.name | translate }} {{ type.name | translate }}
</ion-select-option> </ion-select-option>
@ -55,12 +51,8 @@
<!-- Category. --> <!-- Category. -->
<ion-item class="ion-text-wrap" *ngIf="typeControl.value == 'category'"> <ion-item class="ion-text-wrap" *ngIf="typeControl.value == 'category'">
<ion-label id="addon-calendar-category-label"> <ion-label><h2 [core-mark-required]="true">{{ 'core.category' | translate }}</h2></ion-label>
<h2 [core-mark-required]="true"> <ion-select formControlName="categoryid" interface="action-sheet"
{{ 'core.category' | translate }}
</h2>
</ion-label>
<ion-select formControlName="categoryid" aria-labelledby="addon-calendar-category-label" interface="action-sheet"
[placeholder]="'core.noselection' | translate"> [placeholder]="'core.noselection' | translate">
<ion-select-option *ngFor="let category of categories" [value]="category.id"> <ion-select-option *ngFor="let category of categories" [value]="category.id">
{{ category.name }} {{ category.name }}
@ -70,12 +62,8 @@
<!-- Course. --> <!-- Course. -->
<ion-item class="ion-text-wrap" *ngIf="typeControl.value == 'course'"> <ion-item class="ion-text-wrap" *ngIf="typeControl.value == 'course'">
<ion-label id="addon-calendar-course-label"> <ion-label><h2 [core-mark-required]="true">{{ 'core.course' | translate }}</h2></ion-label>
<h2 [core-mark-required]="true"> <ion-select formControlName="courseid" interface="action-sheet"
{{ 'core.course' | translate }}
</h2>
</ion-label>
<ion-select formControlName="courseid" aria-labelledby="addon-calendar-course-label" interface="action-sheet"
[placeholder]="'core.noselection' | translate"> [placeholder]="'core.noselection' | translate">
<ion-select-option *ngFor="let course of courses" [value]="course.id">{{ course.fullname }}</ion-select-option> <ion-select-option *ngFor="let course of courses" [value]="course.id">{{ course.fullname }}</ion-select-option>
</ion-select> </ion-select>
@ -85,12 +73,8 @@
<ng-container *ngIf="typeControl.value == 'group'"> <ng-container *ngIf="typeControl.value == 'group'">
<!-- Select the course. --> <!-- Select the course. -->
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label id="addon-calendar-groupcourse-label"> <ion-label><h2 [core-mark-required]="true">{{ 'core.course' | translate }}</h2></ion-label>
<h2 [core-mark-required]="true"> <ion-select formControlName="groupcourseid"
{{ 'core.course' | translate }}
</h2>
</ion-label>
<ion-select formControlName="groupcourseid" aria-labelledby="addon-calendar-groupcourse-label"
interface="action-sheet" [placeholder]="'core.noselection' | translate" interface="action-sheet" [placeholder]="'core.noselection' | translate"
(ionChange)="groupCourseSelected($event)"> (ionChange)="groupCourseSelected($event)">
<ion-select-option *ngFor="let course of courses" [value]="course.id"> <ion-select-option *ngFor="let course of courses" [value]="course.id">
@ -104,12 +88,8 @@
</ion-item> </ion-item>
<!-- Select the group. --> <!-- Select the group. -->
<ion-item class="ion-text-wrap" *ngIf="!loadingGroups && groups.length > 0"> <ion-item class="ion-text-wrap" *ngIf="!loadingGroups && groups.length > 0">
<ion-label id="addon-calendar-group-label"> <ion-label><h2 [core-mark-required]="true">{{ 'core.group' | translate }}</h2></ion-label>
<h2 [core-mark-required]="true"> <ion-select formControlName="groupid" interface="action-sheet"
{{ 'core.group' | translate }}
</h2>
</ion-label>
<ion-select formControlName="groupid" aria-labelledby="addon-calendar-group-label" interface="action-sheet"
[placeholder]="'core.noselection' | translate"> [placeholder]="'core.noselection' | translate">
<ion-select-option *ngFor="let group of groups" [value]="group.id">{{ group.name }}</ion-select-option> <ion-select-option *ngFor="let group of groups" [value]="group.id">{{ group.name }}</ion-select-option>
</ion-select> </ion-select>
@ -121,23 +101,20 @@
</ng-container> </ng-container>
<!-- Advanced options. --> <!-- Advanced options. -->
<ion-item-divider class="ion-text-wrap core-expandable" (click)="toggleAdvanced()" <ion-item button class="ion-text-wrap core-expandable divider" (click)="toggleAdvanced()">
[attr.aria-label]="(advanced ? 'core.showless' : 'core.showmore') | translate" role="button">
<ion-icon *ngIf="!advanced" name="fas-caret-right" slot="start" aria-hidden="true"></ion-icon> <ion-icon *ngIf="!advanced" name="fas-caret-right" slot="start" aria-hidden="true"></ion-icon>
<ion-icon *ngIf="advanced" name="fas-caret-down" slot="start" aria-hidden="true"></ion-icon> <ion-icon *ngIf="advanced" name="fas-caret-down" slot="start" aria-hidden="true"></ion-icon>
<ion-label> <ion-label>
<h2 *ngIf="!advanced">{{ 'core.showmore' | translate }}</h2> <h2 *ngIf="!advanced">{{ 'core.showmore' | translate }}</h2>
<h2 *ngIf="advanced">{{ 'core.showless' | translate }}</h2> <h2 *ngIf="advanced">{{ 'core.showless' | translate }}</h2>
</ion-label> </ion-label>
</ion-item-divider> </ion-item>
<div [hidden]="!advanced"> <div [hidden]="!advanced">
<!-- Description. --> <!-- Description. -->
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label position="stacked"> <ion-label position="stacked"><h2>{{ 'core.description' | translate }}</h2></ion-label>
<h2>{{ 'core.description' | translate }}</h2> <core-rich-text-editor [control]="descriptionControl" [attr.aria-label]="'core.description' | translate"
</ion-label>
<core-rich-text-editor [control]="descriptionControl"
[placeholder]="'core.description' | translate" name="description" [component]="component" [placeholder]="'core.description' | translate" name="description" [component]="component"
[componentId]="eventId" [autoSave]="false"></core-rich-text-editor> [componentId]="eventId" [autoSave]="false"></core-rich-text-editor>
</ion-item> </ion-item>
@ -154,9 +131,7 @@
<ion-radio-group formControlName="duration"> <ion-radio-group formControlName="duration">
<ion-item class="addon-calendar-radio-title"> <ion-item class="addon-calendar-radio-title">
<ion-label> <ion-label>
<h2> <h2>{{ 'addon.calendar.eventduration' | translate }}</h2>
{{ 'addon.calendar.eventduration' | translate }}
</h2>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item> <ion-item>
@ -200,9 +175,7 @@
<ion-radio-group formControlName="repeateditall"> <ion-radio-group formControlName="repeateditall">
<ion-item class="addon-calendar-radio-title"> <ion-item class="addon-calendar-radio-title">
<ion-label> <ion-label>
<h2> <h2>{{ 'addon.calendar.repeatedevents' | translate }}</h2>
{{ 'addon.calendar.repeatedevents' | translate }}
</h2>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item> <ion-item>

View File

@ -2026,6 +2026,7 @@ export type AddonCalendarWeekDay = AddonCalendarDay & {
ispast?: boolean; // Calculated in the app. Whether the day is in the past. ispast?: boolean; // Calculated in the app. Whether the day is in the past.
filteredEvents?: AddonCalendarEventToDisplay[]; // Calculated in the app. Filtered events. filteredEvents?: AddonCalendarEventToDisplay[]; // Calculated in the app. Filtered events.
eventsFormated?: AddonCalendarEventToDisplay[]; // Events. eventsFormated?: AddonCalendarEventToDisplay[]; // Events.
periodName?: string;
}; };
/** /**

View File

@ -36,13 +36,14 @@ export abstract class CoreAriaRoleButton<T = unknown> {
* A11y key functionality that translates space and enter keys to click action. * A11y key functionality that translates space and enter keys to click action.
* *
* @param event Event. * @param event Event.
* @param args Additional args.
*/ */
keyUp(event: KeyboardEvent): void { keyUp(event: KeyboardEvent, ...args: unknown[]): void {
if ((event.key == ' ' || event.key == 'Enter') && this.isAllowed()) { if ((event.key == ' ' || event.key == 'Enter') && this.isAllowed()) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.click(event); this.click(event, ...args);
} }
} }
@ -50,9 +51,10 @@ export abstract class CoreAriaRoleButton<T = unknown> {
* A11y click functionality. * A11y click functionality.
* *
* @param event Event. * @param event Event.
* @param args Additional args.
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
click(event?: Event): void { click(event?: Event, ...args: unknown[]): void {
// Nothing defined here. // Nothing defined here.
} }

View File

@ -89,8 +89,8 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
protected resetObserver?: CoreEventObserver; protected resetObserver?: CoreEventObserver;
protected initHeightInterval?: number; protected initHeightInterval?: number;
protected isCurrentView = true; protected isCurrentView = true;
protected toolbarButtonWidth = 40; protected toolbarButtonWidth = 44;
protected toolbarArrowWidth = 28; protected toolbarArrowWidth = 44;
protected pageInstance: string; protected pageInstance: string;
protected autoSaveInterval?: number; protected autoSaveInterval?: number;
protected hideMessageTimeout?: number; protected hideMessageTimeout?: number;
@ -100,6 +100,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
protected resizeFunction?: () => Promise<number>; protected resizeFunction?: () => Promise<number>;
protected selectionChangeFunction?: () => void; protected selectionChangeFunction?: () => void;
protected languageChangedSubscription?: Subscription; protected languageChangedSubscription?: Subscription;
protected resizeObserver?: IntersectionObserver;
rteEnabled = false; rteEnabled = false;
isPhone = false; isPhone = false;
@ -127,6 +128,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
initialSlide: 0, initialSlide: 0,
slidesPerView: 6, slidesPerView: 6,
centerInsufficientSlides: true, centerInsufficientSlides: true,
watchSlidesVisibility: true,
}; };
constructor( constructor(
@ -136,6 +138,14 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
this.contentChanged = new EventEmitter<string>(); this.contentChanged = new EventEmitter<string>();
this.element = elementRef.nativeElement as HTMLDivElement; this.element = elementRef.nativeElement as HTMLDivElement;
this.pageInstance = 'app_' + Date.now(); // Generate a "unique" ID based on timestamp. this.pageInstance = 'app_' + Date.now(); // Generate a "unique" ID based on timestamp.
if ('IntersectionObserver' in window) {
this.resizeObserver = new IntersectionObserver((observerEntry: IntersectionObserverEntry[]) => {
if (observerEntry[0].boundingClientRect.width > 0) {
this.updateToolbarButtons();
}
});
}
} }
/** /**
@ -231,8 +241,12 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
}); });
this.resizeFunction = this.maximizeEditorSize.bind(this); this.resizeFunction = this.maximizeEditorSize.bind(this);
this.selectionChangeFunction = this.updateToolbarStyles.bind(this);
window.addEventListener('resize', this.resizeFunction!); window.addEventListener('resize', this.resizeFunction!);
// Start observing the target node for configured mutations
this.resizeObserver?.observe(this.element);
this.selectionChangeFunction = this.updateToolbarStyles.bind(this);
document.addEventListener('selectionchange', this.selectionChangeFunction!); document.addEventListener('selectionchange', this.selectionChangeFunction!);
this.keyboardObserver = CoreEvents.on(CoreEvents.KEYBOARD_CHANGE, (kbHeight: number) => { this.keyboardObserver = CoreEvents.on(CoreEvents.KEYBOARD_CHANGE, (kbHeight: number) => {
@ -273,7 +287,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
setTimeout(async () => { setTimeout(async () => {
// Editor is ready, adjust Height if needed. // Editor is ready, adjust Height if needed.
let height; let height: number;
if (CoreApp.isAndroid()) { if (CoreApp.isAndroid()) {
// In Android we ignore the keyboard height because it is not part of the web view. // In Android we ignore the keyboard height because it is not part of the web view.
@ -760,6 +774,11 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
* Show the toolbar. * Show the toolbar.
*/ */
showToolbar(event: Event): void { showToolbar(event: Event): void {
if (!('IntersectionObserver' in window)) {
// Fallback if IntersectionObserver is not supported.
this.updateToolbarButtons();
}
this.element.classList.add('ion-touched'); this.element.classList.add('ion-touched');
this.element.classList.remove('ion-untouched'); this.element.classList.remove('ion-untouched');
this.element.classList.add('has-focus'); this.element.classList.add('has-focus');
@ -776,7 +795,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
* @param event Event. * @param event Event.
*/ */
stopBubble(event: Event): void { stopBubble(event: Event): void {
if (event.type != 'mouseup' && event.type != 'keyup') { if (event.type != 'touchend' &&event.type != 'mouseup' && event.type != 'keyup') {
event.preventDefault(); event.preventDefault();
} }
event.stopPropagation(); event.stopPropagation();
@ -840,7 +859,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
* Update the number of toolbar buttons displayed. * Update the number of toolbar buttons displayed.
*/ */
async updateToolbarButtons(): Promise<void> { async updateToolbarButtons(): Promise<void> {
if (!this.isCurrentView || !this.toolbar || !this.toolbarSlides) { 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. // Don't calculate if component isn't in current view, the calculations are wrong.
return; return;
} }
@ -856,7 +875,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
return; return;
} }
if (width > length * this.toolbarButtonWidth) { if (length > 0 && width > length * this.toolbarButtonWidth) {
this.slidesOpts = { ...this.slidesOpts, slidesPerView: length }; this.slidesOpts = { ...this.slidesOpts, slidesPerView: length };
this.toolbarArrows = false; this.toolbarArrows = false;
} else { } else {
@ -1096,6 +1115,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
clearInterval(this.initHeightInterval); clearInterval(this.initHeightInterval);
clearInterval(this.autoSaveInterval); clearInterval(this.autoSaveInterval);
clearTimeout(this.hideMessageTimeout); clearTimeout(this.hideMessageTimeout);
this.resizeObserver?.disconnect();
this.resetObserver?.off(); this.resetObserver?.off();
this.keyboardObserver?.off(); this.keyboardObserver?.off();
} }