MOBILE-3021 calendar: UX improvements

main
Pau Ferrer Ocaña 2019-08-22 12:49:38 +02:00
parent 0868a4646c
commit b24cbab400
11 changed files with 109 additions and 41 deletions

View File

@ -91,6 +91,7 @@
"addon.calendar.calendarreminders": "local_moodlemobileapp",
"addon.calendar.confirmeventdelete": "calendar",
"addon.calendar.confirmeventseriesdelete": "calendar",
"addon.calendar.currentmonth": "local_moodlemobileapp",
"addon.calendar.daynext": "calendar",
"addon.calendar.dayprev": "calendar",
"addon.calendar.defaultnotificationtime": "local_moodlemobileapp",

View File

@ -1,9 +1,9 @@
<!-- Add buttons to the nav bar. -->
<core-navbar-buttons end prepend>
<button [hidden]="!canNavigate || isCurrentMonth || !displayNavButtons" ion-button icon-only clear (click)="goToCurrentMonth()">
<core-icon name="fa-calendar-times-o"></core-icon>
</button>
<core-context-menu>
<core-context-menu-item *ngIf="canNavigate && !isCurrentMonth && displayNavButtons" [priority]="900" [content]="'addon.calendar.currentmonth' | translate" [iconAction]="'fa-calendar-times-o'" (action)="goToCurrentMonth()"></core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<core-loading [hideUntil]="loaded" class="core-loading-center">
@ -31,15 +31,16 @@
<!-- List of days. -->
<ion-row>
<ion-col text-center *ngFor="let day of weekDays" class="addon-calendar-weekday">
{{ day.shortname | translate }}
<span class="hidden-tablet" [title]="day.fullname | translate">{{ day.shortname | translate }}</span>
<span class="hidden-phone">{{ day.fullname | translate }}</span>
</ion-col>
</ion-row>
<!-- Weeks. -->
<ion-row *ngFor="let week of weeks" class="addon-calendar-week">
<ion-col *ngFor="let value of week.prepadding" class="dayblank addon-calendar-day"></ion-col> <!-- Empty slots (first week). -->
<ion-col text-center *ngFor="let day of week.days" (click)="dayClicked(day.mday)" [ngClass]='{"hasevents": day.hasevents, "today": day.istoday, "weekend": day.isweekend, "duration_finish": day.haslastdayofevent}' class="addon-calendar-day">
<p class="addon-calendar-day-number">{{ day.mday }}</p>
<ion-col text-center *ngFor="let day of week.days" (click)="dayClicked(day.mday)" [ngClass]='{"hasevents": day.hasevents, "today": day.istoday, "weekend": day.isweekend, "duration_finish": day.haslastdayofevent}' class="addon-calendar-day" [class.addon-calendar-event-past-day]="isPastMonth || day.ispast">
<p class="addon-calendar-day-number"><span>{{ day.mday }}</span></p>
<!-- In phone, display some dots to indicate the type of events. -->
<p class="hidden-tablet addon-calendar-dot-types"><span *ngFor="let type of day.calendareventtypes" class="calendar_event_type calendar_event_{{type}}"></span></p>
@ -47,12 +48,12 @@
<!-- In tablet, display list of events. -->
<div class="hidden-phone addon-calendar-day-events">
<ng-container *ngFor="let event of day.filteredEvents | slice:0:4; let index = index">
<p *ngIf="index < 3 || day.filteredEvents.length == 4" class="addon-calendar-event" (click)="eventClicked(event, $event)">
<p *ngIf="index < 3 || day.filteredEvents.length == 4" class="addon-calendar-event" (click)="eventClicked(event, $event)" [class.addon-calendar-event-past]="event.ispast">
<span class="calendar_event_type calendar_event_{{event.formattedType}}"></span>
<ion-icon *ngIf="event.offline && !event.deleted" name="time"></ion-icon>
<ion-icon *ngIf="event.deleted" name="trash"></ion-icon>
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" alt="" role="presentation" class="core-module-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">
<span class="addon-calendar-event-name">{{event.name}}</span>
</p>
</ng-container>

View File

@ -32,17 +32,34 @@ ion-app.app-root addon-calendar-calendar {
@include border-end(0, null, null);
@include padding(null, 8px, null, null);
}
.addon-calendar-day-number {
height: 24px;
line-height: 24px;
width: max-content;
min-width: 24px;
text-align: center;
font-weight: 500;
display: inline-block;
margin: 3px;
&.addon-calendar-event-past-day > .addon-calendar-dot-types,
&.addon-calendar-event-past-day > .addon-calendar-day-events {
opacity: 0.5;
}
&.today .addon-calendar-day-number {
.addon-calendar-day-number {
margin: 0;
span {
line-height: 24px;
font-weight: 500;
display: inline-block;
margin: 3px;
width: max-content;
width: 24px;
height: 24px;
text-align: center;
}
}
@include media-breakpoint-up(md) {
.addon-calendar-day-number {
text-align: left;
}
}
&.today .addon-calendar-day-number span {
background-color: $calendar-today-bgcolor;
color: $calendar-today-color;
@ -58,6 +75,10 @@ ion-app.app-root addon-calendar-calendar {
overflow: hidden;
white-space: nowrap;
&.addon-calendar-event-past {
opacity: 0.5;
}
.addon-calendar-event-name {
font-weight: 500;
}
@ -81,7 +102,6 @@ ion-app.app-root addon-calendar-calendar {
}
.addon-calendar-weekday {
color: $gray-dark;
border-bottom: 1px solid $list-md-border-color;
}
@ -130,6 +150,6 @@ ion-app.app-root addon-calendar-calendar {
width: 16px;
height: 16px;
display: inline-block;
vertical-align: middle;
vertical-align: bottom;
}
}

View File

@ -48,6 +48,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest
loaded = false;
timeFormat: string;
isCurrentMonth: boolean;
isPastMonth: boolean;
protected year: number;
protected month: number;
@ -57,6 +58,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest
protected offlineEvents: {[monthId: string]: {[day: number]: any[]}} = {}; // Offline events classified in month & day.
protected offlineEditedEventsIds = []; // IDs of events edited in offline.
protected deletedEvents = []; // Events deleted in offline.
protected currentTime: number;
// Observers.
protected undeleteEventObserver: any;
@ -200,6 +202,28 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest
this.weekDays = this.calendarProvider.getWeekDays(result.daynames[0].dayno);
this.weeks = result.weeks;
this.calculateIsCurrentMonth();
if (this.isCurrentMonth) {
let isPast = true;
this.weeks.forEach((week) => {
week.days.some((day) => {
day.ispast = isPast && !day.istoday;
isPast = day.ispast;
if (day.istoday) {
day.events.forEach((event) => {
event.ispast = this.isEventPast(event);
});
return true;
}
return day.istoday;
});
});
}
// Merge the online events with offline data.
this.mergeEvents();
@ -288,7 +312,6 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest
this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
this.decreaseMonth();
}).finally(() => {
this.calculateIsCurrentMonth();
this.loaded = true;
});
}
@ -305,7 +328,6 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest
this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
this.increaseMonth();
}).finally(() => {
this.calculateIsCurrentMonth();
this.loaded = true;
});
}
@ -336,7 +358,10 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest
calculateIsCurrentMonth(): void {
const now = new Date();
this.currentTime = this.timeUtils.timestamp();
this.isCurrentMonth = this.year == now.getFullYear() && this.month == now.getMonth() + 1;
this.isPastMonth = this.year < now.getFullYear() || (this.year == now.getFullYear() && this.month < now.getMonth() + 1);
}
/**
@ -466,6 +491,15 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest
});
}
/**
* Returns if the event is in the past or not.
* @param {any} event Event object.
* @return {boolean} True if it's in the past.
*/
isEventPast(event: any): boolean {
return (event.timestart + event.timeduration) < this.currentTime;
}
/**
* Component destroyed.
*/

View File

@ -6,6 +6,7 @@
"calendarreminders": "Calendar reminders",
"confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?",
"confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?",
"currentmonth": "Current Month",
"daynext": "Next day",
"dayprev": "Previous day",
"defaultnotificationtime": "Default notification time",

View File

@ -2,13 +2,12 @@
<ion-navbar core-back-button>
<ion-title>{{ 'addon.calendar.calendarevents' | translate }}</ion-title>
<ion-buttons end>
<button *ngIf="!isCurrentDay" ion-button icon-only clear (click)="goToCurrentDay()">
<core-icon name="fa-calendar-times-o"></core-icon>
</button>
<button *ngIf="courses && courses.length" ion-button icon-only (click)="openCourseFilter($event)" [attr.aria-label]="'core.courses.filter' | translate">
<ion-icon name="funnel"></ion-icon>
<ion-icon name="funnel" *ngIf="courseId"></ion-icon>
<ion-icon name="ios-funnel-outline" *ngIf="!courseId"></ion-icon>
</button>
<core-context-menu>
<core-context-menu-item *ngIf="!isCurrentDay" [priority]="900" [content]="'addon.calendar.today' | translate" [iconAction]="'fa-calendar-times-o'" (action)="goToCurrentDay()"></core-context-menu-item>
<core-context-menu-item [hidden]="!loaded || !hasOffline || !isOnline" [priority]="400" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
</core-context-menu>
</ion-buttons>
@ -49,7 +48,7 @@
<ion-list *ngIf="filteredEvents && filteredEvents.length" no-margin>
<ng-container *ngFor="let event of filteredEvents">
<a ion-item text-wrap [title]="event.name" (click)="gotoEvent(event.id)">
<ion-item text-wrap [title]="event.name" (click)="gotoEvent(event.id)" [class.item-dimmed]="event.ispast">
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" item-start class="core-module-icon">
<core-icon *ngIf="event.icon && !event.moduleIcon" [name]="event.icon" item-start></core-icon>
<h2><core-format-text [text]="event.name"></core-format-text></h2>
@ -62,7 +61,7 @@
<ion-icon name="trash"></ion-icon>
<span text-wrap>{{ 'core.deletedoffline' | translate }}</span>
</ion-note>
</a>
</ion-item>
</ng-container>
</ion-list>

View File

@ -51,6 +51,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
protected deletedEvents = []; // Events deleted in offline.
protected timeFormat: string;
protected currentMoment: moment.Moment;
protected currentTime: number;
// Observers.
protected newEventObserver: any;
@ -74,6 +75,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
isOnline = false;
syncIcon: string;
isCurrentDay: boolean;
isPastDay: boolean;
constructor(localNotificationsProvider: CoreLocalNotificationsProvider,
navParams: NavParams,
@ -308,9 +310,12 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
// Filter events by course.
this.filterEvents();
this.calculateIsCurrentDay();
// Re-calculate the formatted time so it uses the device date.
const dayTime = this.currentMoment.unix() * 1000;
this.events.forEach((event) => {
event.ispast = this.isPastDay || (this.isCurrentDay && this.isEventPast(event));
promises.push(this.calendarProvider.formatEventTime(event, this.timeFormat, true, dayTime).then((time) => {
event.formattedtime = time;
}));
@ -555,7 +560,11 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
calculateIsCurrentDay(): void {
const now = new Date();
this.currentTime = this.timeUtils.timestamp();
this.isCurrentDay = this.year == now.getFullYear() && this.month == now.getMonth() + 1 && this.day == now.getDate();
this.isPastDay = this.year < now.getFullYear() || (this.year == now.getFullYear() && this.month < now.getMonth()) ||
(this.year == now.getFullYear() && this.month == now.getMonth() + 1 && this.day < now.getDate());
}
/**
@ -600,7 +609,6 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
this.decreaseDay();
}).finally(() => {
this.calculateIsCurrentDay();
this.loaded = true;
});
}
@ -617,7 +625,6 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
this.increaseDay();
}).finally(() => {
this.calculateIsCurrentDay();
this.loaded = true;
});
}
@ -665,6 +672,15 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
return false;
}
/**
* Returns if the event is in the past or not.
* @param {any} event Event object.
* @return {boolean} True if it's in the past.
*/
isEventPast(event: any): boolean {
return (event.timestart + event.timeduration) < this.currentTime;
}
/**
* Page destroyed.
*/

View File

@ -2,16 +2,13 @@
<ion-navbar core-back-button>
<ion-title>{{ (showCalendar ? 'addon.calendar.calendarevents' : 'addon.calendar.upcomingevents') | translate }}</ion-title>
<ion-buttons end>
<button *ngIf="showCalendar" ion-button icon-only (click)="toggleDisplay()" [attr.aria-label]="'addon.calendar.upcomingevents' | translate">
<ion-icon name="list"></ion-icon>
</button>
<button *ngIf="!showCalendar" ion-button icon-only (click)="toggleDisplay()" [attr.aria-label]="'addon.calendar.monthlyview' | translate">
<core-icon name="fa-calendar-o"></core-icon>
</button>
<button *ngIf="courses && courses.length" ion-button icon-only (click)="openCourseFilter($event)" [attr.aria-label]="'core.courses.filter' | translate">
<ion-icon name="funnel"></ion-icon>
<ion-icon name="funnel" *ngIf="courseId"></ion-icon>
<ion-icon name="ios-funnel-outline" *ngIf="!courseId"></ion-icon>
</button>
<core-context-menu>
<core-context-menu-item *ngIf="showCalendar" [priority]="800" [content]="'addon.calendar.upcomingevents' | translate" [iconAction]="'list'" (action)="toggleDisplay()"></core-context-menu-item>
<core-context-menu-item *ngIf="!showCalendar" [priority]="800" [content]="'addon.calendar.monthlyview' | translate" [iconAction]="'fa-calendar-o'" (action)="toggleDisplay()"></core-context-menu-item>
<core-context-menu-item [hidden]="!notificationsEnabled" [priority]="600" [content]="'core.settings.settings' | translate" (action)="openSettings()" [iconAction]="'cog'"></core-context-menu-item>
<core-context-menu-item [hidden]="!loaded || !hasOffline || !isOnline" [priority]="400" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
</core-context-menu>

View File

@ -1,3 +0,0 @@
page-addon-calendar-index .toolbar-ios ion-title {
@include padding-horizontal(null, 120px);
}

View File

@ -3,7 +3,8 @@
<ion-title>{{ 'addon.calendar.calendarevents' | translate }}</ion-title>
<ion-buttons end>
<button *ngIf="courses && courses.length" ion-button icon-only (click)="openCourseFilter($event)" [attr.aria-label]="'core.courses.filter' | translate">
<ion-icon name="funnel"></ion-icon>
<ion-icon name="funnel" *ngIf="courseId"></ion-icon>
<ion-icon name="ios-funnel-outline" *ngIf="!courseId"></ion-icon>
</button>
<core-context-menu>
<core-context-menu-item [hidden]="!notificationsEnabled" [priority]="600" [content]="'core.settings.settings' | translate" (action)="openSettings()" [iconAction]="'cog'"></core-context-menu-item>

View File

@ -90,6 +90,7 @@
"addon.calendar.calendarreminders": "Calendar reminders",
"addon.calendar.confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?",
"addon.calendar.confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?",
"addon.calendar.currentmonth": "Current Month",
"addon.calendar.daynext": "Next day",
"addon.calendar.dayprev": "Previous day",
"addon.calendar.defaultnotificationtime": "Default notification time",