לוח שנה / תאריכון / calendar , כולם אומרים דבר אחד, להתעסק עם תאריכים ולהבין מה אפשר להוציא מהאובייקט Date.
נתחיל קודם בקוד ואחרי זה נראה מה בדיוק קורה שם.
לוח השנה יהיה אין סופי ולא משנה לאיזה כיוון נדפדף, הוא יהיה נגיש כך שבעזרת מקלדת ניתן לתפעל אותו והוא יהיה בסיסי כך שלא יהיה "רעש" בקוד של דברים מיותרים.
אתם מוזמים להוסיף ולשפר אותו 😎
<div class="calendar {{calendarSize}}">
<div class="controls-wrapper">
<h3 aria-live="polite" class="current-date-title">
<span class="current-month">{{date|date:calendarSize==="small"?"MMM":"MMMM"}}</span>
<span class="current-year">({{year}})</span>
</h3>
<div class="controller">
<button class="today-btn" (click)="todayFn()">Today</button>
<button (click)="moveOneMonthRev()" class="oneMonthRev" title="Prev"><img src="assets/icons/Arrow_Right.svg" alt="prev"></button>
<button (click)="moveOneMonthFwd()" class="oneMonthFwd" title="Next"><img src="assets/icons/Arrow_Right.svg" alt="next"></button>
</div>
</div>
<table class="main-table">
<thead>
<tr>
<th *ngFor="let dayName of daysInWeekNames" class="main-top-th" scope="col"><span>{{dayName}}</span></th>
</tr>
</thead>
<tbody role="radiogroup">
<tr *ngFor="let week of calendarArr; trackBy:trackByFn">
<td *ngFor="let day of week;let last=last;let idx=index;" class="main-td">
<div class="day-wrapper" [attr.role]="day.dayNumber!==0 ? 'radio' : null" [ngClass]="{'weekend':last,'weekend-partial':(idx===week.length-2),'selected-cell':day.dayNumber+'/'+(month+1)+'/'+year===colorSelectedCell}" [attr.tabindex]="day.dayNumber!==0 ? 0 : null"
[attr.title]="day.dayNumber!==0 ? day.dayNumber+'/'+(month+1)+'/'+year : null" (click)="onDateSelectedFn($event,day.dayNumber,month,year)" (keyup)="onDateSelectedFn($event,day.dayNumber,month,year)" [attr.aria-checked]="day.dayNumber+'/'+(month+1)+'/'+year===colorSelectedCell">
<span [ngClass]="{'current-day':currentDate.day===day.dayNumber && currentDate.month===month && currentDate.year===year}" *ngIf="day.dayNumber!==0" class="day-in-calendar">{{day.dayNumber}}</span>
<div class="day-in-calendar-data" *ngIf="day.events.length>0">
<span *ngFor="let ev of day.events" class="day-event {{ev.eventClass}}">{{ev.eventText}}</span>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>.calendar {
height: 100%;
}
.calendar .main-table {
table-layout: fixed;
border-collapse: collapse;
border-spacing: 0;
width: 100%;
height: 100%;
}
.calendar .main-table .main-top-th,
.calendar .main-table .main-td {
border: 1px solid rgb(170, 201, 230);
text-align: center;
}
.calendar .main-table .main-top-th {
padding: 5px;
}
.calendar.small .main-table .main-top-th {
padding: 0;
}
.calendar .main-table .current-day {
color: blueviolet;
font-weight: bold;
}
.calendar .main-table .main-td .day-wrapper {
position: relative;
height: 100%;
cursor: pointer;
}
.calendar.large .main-table .main-td .day-wrapper {
min-height: 100px;
min-width: 100px;
}
.calendar.large .main-table .main-td .day-wrapper .day-in-calendar {
position: absolute;
left: 0;
top: 0;
font-size: 24px;
padding: 10px;
}
.calendar.medium .main-table .main-td .day-wrapper .day-in-calendar {
position: absolute;
left: 0;
top: 0;
font-size: 18px;
padding: 2px;
}
.calendar.large .main-table .main-td .day-wrapper .day-in-calendar-data {
max-height: 100px;
min-width: 100px;
overflow: auto;
padding-left: 45px;
box-sizing: border-box;
}
.calendar.medium .main-table .main-td .day-wrapper .day-in-calendar-data {
max-height: 40px;
min-width: 40px;
overflow: auto;
padding-left: 24px;
box-sizing: border-box;
}
.calendar.large .main-table .main-td .day-wrapper .day-in-calendar-data .day-event {
display: block;
background-color: rgba(100, 180, 255, 0.5);
text-align: left;
margin: 4px;
padding: 4px;
}
.calendar.medium .main-table .main-td .day-wrapper .day-in-calendar-data .day-event {
display: block;
background-color: rgba(100, 180, 255, 0.5);
text-align: left;
margin: 2px;
padding: 2px;
overflow: hidden;
white-space: nowrap;
font-size: 16px;
}
.calendar.large .main-table .main-td .day-wrapper .day-in-calendar-data .day-event.event-start {
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
}
.calendar.large .main-table .main-td .day-wrapper .day-in-calendar-data .day-event.event-end {
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
}
.calendar.medium .main-table .main-td .day-wrapper .day-in-calendar-data .day-event.event-start {
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
}
.calendar.medium .main-table .main-td .day-wrapper .day-in-calendar-data .day-event.event-end {
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
.calendar.small .main-table .main-td .day-wrapper .day-in-calendar-data {
display: none;
}
.calendar.medium .main-table .main-td .day-wrapper {
min-height: 40px;
min-width: 40px;
}
.calendar.small .main-table .main-td .day-wrapper {
min-height: 20px;
min-width: 20px;
}
.calendar.large .current-month {
font-size: 36px;
}
.calendar.medium .current-month {
font-size: 24px;
}
.calendar.small .current-month {
font-size: 18px;
}
.calendar .current-year {
vertical-align: top;
}
.calendar.large .current-year {
font-size: 24px;
}
.calendar.medium .current-year {
font-size: 18px;
}
.calendar.small .current-year {
font-size: 16px;
}
.calendar .current-date-title {
text-align: left;
margin: 0;
}
.calendar .oneMonthRev {
left: 20px;
border: 0;
background-color: transparent;
cursor: pointer;
transform: rotate(180deg);
-webkit-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-ms-transform: rotate(180deg);
-o-transform: rotate(180deg);
padding: 0;
margin-left: 20px;
}
.calendar .oneMonthFwd {
right: 20px;
border: 0;
background-color: transparent;
cursor: pointer;
margin-left: 20px;
padding: 0;
}
.calendar .oneMonthFwd:hover,
.calendar .oneMonthRev:hover,
.calendar .oneMonthFwd:focus,
.calendar .oneMonthRev:focus,
.calendar .today-btn:hover,
.calendar .today-btn:focus {
background-color: rgba(255, 255, 255, 0.5);
border-radius: 8px;
-webkit-border-radius: 8px;
-moz-border-radius: 8px;
-ms-border-radius: 8px;
-o-border-radius: 8px;
}
.calendar .oneMonthFwd,
.calendar .oneMonthRev {
vertical-align: middle;
height: 100%;
}
.calendar .oneMonthFwd img,
.calendar .oneMonthRev img {
height: 100%;
}
.calendar .controls-wrapper {
position: relative;
background-color: rgb(124, 158, 202);
padding: 5px;
}
.calendar.large .controls-wrapper {
min-height: 40px;
}
.calendar.medium .controls-wrapper {
min-height: 30px;
}
.calendar.small .controls-wrapper {
min-height: 22px;
}
.calendar .controls-wrapper .controller {
position: absolute;
top: 3px;
right: 3px;
}
.calendar.large .controls-wrapper .controller {
height: 44px;
}
.calendar.medium .controls-wrapper .controller {
height: 34px;
}
.calendar.small .controls-wrapper .controller {
height: 24px;
}
.calendar .today-btn {
background-color: transparent;
border: 0;
vertical-align: middle;
font-weight: bold;
cursor: pointer;
}
.calendar.large .today-btn {
height: 40px;
}
.calendar.medium .today-btn {
height: 34px;
}
.calendar.small .today-btn {
height: 28px;
}
.calendar .weekend {
background-color: rgb(170, 201, 230);
}
.calendar .weekend-partial {
background-image: linear-gradient(to right, rgba(170, 201, 230, 0), rgb(170, 201, 230));
}
.calendar .selected-cell {
background-color: rgb(255, 240, 191);
background-image: none;
}
@media screen and (max-width:1023px) and (min-width:769px) {
.calendar.large .main-table .main-td .day-wrapper .day-in-calendar {
font-size: 18px;
padding: 5px;
}
.calendar.large .main-table .main-td .day-wrapper .day-in-calendar-data {
padding-left: 30px;
}
}
@media screen and (max-width:768px) {
.calendar.large .main-table .main-td .day-wrapper .day-in-calendar {
font-size: 16px;
padding: 2px;
}
.calendar.large .main-table .main-td .day-wrapper .day-in-calendar-data {
padding-left: 0px;
padding-top: 20px;
}
}import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
import { IDayDate, IEventsData, ISingleDayData } from './calendar.interface';
@Component({
selector: 'app-calendar',
templateUrl: './calendar.component.html',
styleUrls: ['./calendar.component.css']
})
export class CalendarComponent implements OnInit, OnChanges {
@Input() calendarSize: "large" | "medium" | "small" = "large";
@Input() eventsData: IEventsData[] = [];
@Output() selectedDateEmiter: EventEmitter<Date> = new EventEmitter<Date>();
@Input() setSelectedDate: string = '';
daysInWeekNames: string[] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
calendarArr: Array<ISingleDayData[]> = [];
date: Date = new Date();
year: number = this.date.getFullYear();
month: number = this.date.getMonth();
firstDay: number = (new Date(this.year, this.month, 1)).getDay();
lastDay: number = (new Date(this.year, this.month + 1, 0)).getDate();
currentDay: number = this.date.getDate();
monthCorrection: number = 0;
currentDate: IDayDate = { day: this.currentDay, month: this.month, year: this.year };
selectedDate: Date;
dayDataArr: ISingleDayData[] = [];
emptyCells: number = 0;
colorSelectedCell: string = '';
constructor() {
}
ngOnInit() {
this.generateDateFn();
}
ngOnChanges(changes: SimpleChanges) {
let dateArr = (this.setSelectedDate).split('/');
if (dateArr.length === 3) {
this.setSelectedDateFn(parseInt(dateArr[0]), parseInt(dateArr[1]) - 1, parseInt(dateArr[2]));
}
if (changes.eventsData !== undefined) {
this.eventsData = changes.eventsData.currentValue;
this.generateDateFn();
}
}
generateDateFn() {
this.date = new Date(this.currentDate.year, this.currentDate.month + this.monthCorrection)
this.month = this.date.getMonth();
this.year = this.date.getFullYear();
this.firstDay = (new Date(this.year, this.month, 1)).getDay();
this.lastDay = (new Date(this.year, this.month + 1, 0)).getDate();
this.calcDaysFn();
}
calcDaysFn() {
let sumOfDaysInMonth = [];
for (this.emptyCells = 0; this.emptyCells < this.firstDay; this.emptyCells++) {
let emptyDay: ISingleDayData = { dayNumber: 0, events: [] };
sumOfDaysInMonth.push(emptyDay);
}
for (let arrCnt = 0; arrCnt < this.lastDay; arrCnt++) {
let itemDay: ISingleDayData = { dayNumber: arrCnt + 1, events: [] }
sumOfDaysInMonth.push(itemDay);
}
let weekArr: ISingleDayData[] = [];
this.calendarArr = [];
while (sumOfDaysInMonth.length > 7) {
weekArr = sumOfDaysInMonth.splice(0, 7);
this.calendarArr.push(weekArr);
}
if (sumOfDaysInMonth.length < 7) {
let arrLength = 7 - (sumOfDaysInMonth.length)
for (let addEmpty = 0; addEmpty < arrLength; addEmpty++) {
let emptyDay: ISingleDayData = { dayNumber: 0, events: [] };
sumOfDaysInMonth.push(emptyDay);
}
}
this.calendarArr.push(sumOfDaysInMonth);
this.eventsFn();
}
trackByFn(index, item) {
if (!item) return null;
if (item.id) {
return item.id;
}
if (item.itemValue) {
return item.itemValue;
}
return index;
}
moveOneMonthFwd() {
this.monthCorrection++;
this.generateDateFn();
}
moveOneMonthRev() {
this.monthCorrection--;
this.generateDateFn();
}
todayFn() {
this.monthCorrection = 0;
this.generateDateFn();
}
onDateSelectedFn(e, day: number, month: number, year: number) {
if (day !== 0 && (e.which === 32 || e.which === 13 || e.type === 'click')) {
this.setSelectedDateFn(day, month, year);
}
}
setSelectedDateFn(day: number, month: number, year: number) {
this.colorSelectedCell = day + '/' + (month + 1) + '/' + year;
this.selectedDate = new Date(year, month, day);
this.selectedDateEmiter.emit(this.selectedDate);
}
eventsFn() {
for (let ev = 0; ev < this.eventsData.length; ev++) {
let _event = this.eventsData[ev];
let startDay = _event.startDate.getDate();
let lengthEvent = Math.ceil((_event.endDate.getTime() - _event.startDate.getTime()) / (1000 * 60 * 60 * 24));
for (let le = 0; le < lengthEvent + 1; le++) {
let weekloc = Math.ceil((startDay + le + this.emptyCells) / 7);
let dayloc = ((startDay + le + this.emptyCells) % 7);
if (dayloc === 0) {
dayloc = 7;
}
let eventClass: string = '';
if (le === 0) {
eventClass = 'event-start';
}
if (le === lengthEvent) {
eventClass += ' event-end';
}
let selectedDay: ISingleDayData = this.calendarArr[weekloc - 1][dayloc - 1];
selectedDay.events.push({ eventText: this.eventsData[ev].eventText, eventIcon: this.eventsData[ev].eventIcon, eventClass: eventClass });
if (selectedDay.dayNumber === this.lastDay) {
break;
}
}
}
}
}
export interface IDayDate{
day:number,
month:number,
year:number
}
export interface IEventsData{
startDate:Date,
endDate:Date,
eventText:string,
eventIcon:string
}
export interface ISingleDayData{
dayNumber:number,
events:ISingleEvent[]
}
export interface ISingleEvent{
eventText:string,
eventIcon:string,
eventClass?:string
}נעבור על הדברים העקריים בקוד (אני ממליץ לעצור את הקוד בזמן פעולה ולראות מה כל פונקציה עושה).
נתחיל עם ה-HTML
שורה 1
המעטפת של הרכיב, מקבלת גם משתנה שהוא CLASS CSS נוסף ועל פי זה נקבע גודל הרכיב בדף, שימו לב שהרכיב רספונסיבי אבל על פי הגודל שבמשתנה יקבע לו גודל מקסימאלי.
המטרה היא בשלב מאוחר יותר לתת את האפשרות לשים אותו על הדף או לשדך אותו לרכיב מותאם טופס וכך ליצר datepicker.
שורה 2 – 14
מעטפת של הכותרת/ פס עליון של הלוח שנה, שם יהיו החצים קדימה ואחורה בחודשים, כפתור קפיצה להיום, מידע שאנחנו רוצים לתת למשתמש, כמו החודש והשנה.
שורה 4
יש שימוש ב-PIPE של אנגולר אשר משנה את תצוגת החודש בהתאם לגודל הלוח שנה שנקבע.
שורה 15 – 34
זה הלוח שנה שלנו, הוא מבוסס על TABLE, מאוד נוח לנגישות ובכלל לוח שנה הוא מידע טבלאי אז למה לא לשים אותו ברכיב שמתאים לו?
הכותרת בלוח השנה נלקחת ממערך של מלל כך שאם רוצים לשנות את המלל זה די פשוט
שורה 24
זו שורה די ארוכה אבל זה בעיקר בשביל נגישות כך שאין צורך להיבהל ממנה.
יש שם מאפיינים כמו tabindex כדי שהתא יקבל פוקוס בגלישת מקלדת, role בשביל הקורא מסך, aria שגם נועד לקורא המסך, וארועים עבור לחיצה רגילה בעכבר ועבור לחיצה במקלדת.
שורה 27 – 28
במידה ויש מידע על ארועים באותו היום הם יתווספו לתא עם שינוי בעיצוב בהתאם למקום שהם נמצאים בוא, תחילת/ אמצע/סוף הארוע.
CSS
מבחינת העיצוב אין משהו מיוחד, הוא רק נראה ארוך בגלל שיש טיפול במספר מצבים של גודל לוח השנה.
יש לנו גדול, בינוני וקטן, המצבים האלו גורמים להכפלת שורות כדי לתת טיפול מתאים.
בסוף הקוד, יש 2 נקודות שבירה עבור לוח שנה גדול.
TS
שורה 10
משנה שקובע את גודל הלוח שנה, במידה שלא נקבע מבחוץ גודל מסויים אז ברירת המחדל שלו, זה גדול.
שורה 11
מערך שארועים שיאוכלסו בלוח שנה על פי התאריכים המתאימים. כל ארוע צריך להיות עם תאריך התחלה וסיום
שורה 12
כאשר המשתמש בחר תאריך מסויים, הרכיב יוציא את התאריך החוצה לרכיב שעוטף אותו.
שורה 13
במידה שרוצים לסמן תאריך מבחוץ.
שורה 16 – 21
משיכת התאריך המלא, מתוך התאריך נוציא את השנה, החודש, המיקום של היום הראשון והיום האחרון בלוח השנה והמיקום של היום הנוכחי.
שורה 22
אחראי על התנועה של לוח השנה בחודשים של השנה.
במידה שהוא שווה ל-0 אז מדובר על החודש הנוכחי, במידה שהוא מעל או מתחת לאפס אז זה אומר שבודקים את החודש הנוכחי בתוספת של חודשים על פי המספר במשתנה.
אם אנחנו נמצאים בחודש ה-8 והמשתנה שווה ל-2 אז נעבור לחודש 10 (8+2) אם המשתנה שווה למינוס 2 אז נעבור לחודש 6 (8 + -2).
הדבר הנוסף שנשאר לנו הוא להוסיף תאים ריקים לפני ואחרי החודש, בגלל שהחודש לא תמיד מתחיל ביום ראשון ונגמר ביום שבת, זה אומר שיש לנו תאים ריקים שהם מיצגים את החלקים מהחודש הקודם.
אנחנו מטפלים בזה בפונקציה בשורה 55
שורה 83
אנחנו מחזירים זיהוי (KEY) לכל רכיב במערך שעובר ngFor כדי למנוע הרצה מחדש של כל המערך ללא צורך.
שורה 115
מטפלת בארועים שנמצאים בתאים שבחודש בתוספת של CLASS מתאים עבור תחילת הארוע וסוף הארוע.





