Line data Source code
1 : import 'package:alh_calendar/enums/day_of_week.dart';
2 : import 'package:alh_calendar/models/calendar_day_builder_model.dart';
3 : import 'package:alh_calendar/models/calendar_month.dart';
4 : import 'package:alh_calendar/utils/calendar_table_helper.dart';
5 : import 'package:alh_calendar/utils/date_helper.dart';
6 : import 'package:alh_calendar/widgets/calendar_body.dart';
7 : import 'package:alh_calendar/widgets/calendar_header/calendar_header.dart';
8 : import 'package:flutter/material.dart';
9 :
10 : /// Signature for a function that builds a widget for a given day.
11 : typedef DayBuilder = Widget Function(
12 : CalendarDayBuilderModel calendarDayBuilderModel,
13 : );
14 :
15 : /// Signature for a function that builds a widget for a title.
16 : typedef HeaderTitleBuilder = Widget Function(
17 : DateTime date,
18 : );
19 :
20 : /// Signature for a function that builds a widget for the day of the week
21 : /// calendarCell.
22 : typedef DayOfWeekBuilder = Widget Function(
23 : String dayOfWeek,
24 : bool isWeekEnd,
25 : );
26 :
27 : class AlhCalendar extends StatefulWidget {
28 : /// Custom builder for DayCalendarCells.
29 : ///
30 : /// Uses a [CalendarDayBuilderModel]
31 : final DayBuilder dayBuilder;
32 :
33 : /// Custom builder for the headerTitle.
34 : final HeaderTitleBuilder headerBuilder;
35 :
36 : /// custom widget that is left of header
37 : final Widget headerLeading;
38 :
39 : /// custom widget that is right of header
40 : final Widget headerTrailing;
41 :
42 : /// Custom builder for DayOfWeekCalendarCells.
43 : final DayOfWeekBuilder dayOfWeekBuilder;
44 :
45 : /// Defines initial Time, if null than DateTime.now()
46 : final DateTime? initialDateTime;
47 :
48 : /// Defines the minimum month.
49 : ///
50 : /// If minimum month is reached, user cannot go to previous month.
51 : final DateTime? minimumMonthDate;
52 :
53 : /// Defines the maximum month.
54 : ///
55 : /// If maximum month is reached, user cannot go to next month.
56 : final DateTime? maximumMonthDate;
57 :
58 : /// Sets the minimum day.
59 : ///
60 : /// Every day before chosen day is flagged as outside of Range.
61 : /// All days in between minimum day and maximum day are called in range. The [minimumMonthDate]
62 : /// and [minimumDayDate] have nothing in commom, [minimumMonthDate] sets the lowest month
63 : /// the user can navigate to, while [minimumDayDate] sets the first day of the Range.
64 : final DateTime? minimumDayDate;
65 :
66 : /// Sets the maximum day.
67 : ///
68 : /// All days after maximum day are flagged as outside of Range.
69 : /// All days in between minimum day and maximum day are called in range.
70 : /// The [maximumMonthDate] and [maximumDayDate] have nothing in commom, [maximumMonthDate] sets the last month
71 : /// the user can navigate to, while [maximumDayDate] sets the last day of the Range.
72 : final DateTime? maximumDayDate;
73 :
74 : /// needs a Map of <DayOfWeek, String> to fill DayOfWeekCalendarCells
75 : ///
76 : /// example:
77 : /// final dayOfWeekMap = {
78 : /// DayOfWeek.monday: 'Mon',
79 : /// DayOfWeek.thursday: 'Thu',
80 : /// DayOfWeek.wednesday: 'Wen',
81 : /// DayOfWeek.tuesday: 'Tue',
82 : /// DayOfWeek.friday: 'Fri',
83 : /// DayOfWeek.saturday: 'Sat',
84 : /// DayOfWeek.sunday: 'Sun',
85 : /// };
86 : final Map<DayOfWeek, String> daysOfWeek;
87 :
88 : /// Callback once minimum month is reached
89 : final VoidCallback? onReachedMinimumDate;
90 :
91 : /// Callback once maximum month is reached
92 : final VoidCallback? onReachedMaximumDate;
93 :
94 : /// Callback once month is changed
95 : final ValueChanged<DateTime>? onMonthChanged;
96 :
97 : /// Callback once day is changed
98 : final ValueChanged<DateTime>? onDayChanged;
99 :
100 : /// Flag if horizontal scrolling in calendar is enabled.
101 : ///
102 : /// Default value: true
103 : final bool? enableHorizontalSwipe;
104 :
105 : /// Sets the padding between headerTitle and both headerIcons
106 : ///
107 : /// Default value: EdgeInsets.all(3.0)
108 : final EdgeInsets iconPadding;
109 :
110 : /// Enables changing the month when a day outside of current month is tapped.
111 : ///
112 : /// Default value: true
113 : final bool enableJumpToOtherMonth;
114 :
115 : /// Forces Table to have 6 Rows, even when month would fit in 5.
116 : ///
117 : /// Helpful if any widget is placed in a column under AlhCalender that should not move up and down when month is changed.
118 : /// Default value: true
119 : final bool enableSixWeeksForEveryMonth;
120 :
121 : /// Makes sixth Row invisible if there are only days from next month shown in the last row.
122 : ///
123 : /// Default value: true
124 : final bool disableSixthRow;
125 :
126 : /// Should show days that are not in current month.
127 : ///
128 : /// If true days outside of current month are shown. If false the days are shown,
129 : /// but still flagged with isInCurrentMonth = false, so
130 : /// it can be used depending on the situation.
131 : ///
132 : /// Default value: true
133 : final bool showDaysOutsideCurrentMonth;
134 :
135 : /// If true days out of range can't be tapped
136 : ///
137 : /// Default value: true
138 : final bool disableTapOnOutOfRange;
139 :
140 1 : const AlhCalendar({
141 : required this.dayBuilder,
142 : required this.headerBuilder,
143 : required this.headerLeading,
144 : required this.headerTrailing,
145 : required this.dayOfWeekBuilder,
146 : required this.daysOfWeek,
147 : this.initialDateTime,
148 : this.maximumMonthDate,
149 : this.minimumMonthDate,
150 : this.minimumDayDate,
151 : this.maximumDayDate,
152 : this.onReachedMinimumDate,
153 : this.onReachedMaximumDate,
154 : this.onMonthChanged,
155 : this.onDayChanged,
156 : this.iconPadding = const EdgeInsets.all(3.0),
157 : this.enableHorizontalSwipe = true,
158 : this.enableSixWeeksForEveryMonth = true,
159 : this.showDaysOutsideCurrentMonth = true,
160 : this.disableTapOnOutOfRange = true,
161 : this.disableSixthRow = false,
162 : this.enableJumpToOtherMonth = true,
163 : super.key,
164 3 : }) : assert(daysOfWeek.length == 7);
165 :
166 1 : @override
167 1 : State<AlhCalendar> createState() => _AlhCalendarState();
168 : }
169 :
170 : class _AlhCalendarState extends State<AlhCalendar> {
171 : late CalendarMonth calendarMonth;
172 : late DateTime selectedDateTime;
173 : late DateTime currentDateTime;
174 :
175 1 : @override
176 : void initState() {
177 1 : super.initState();
178 3 : this.currentDateTime = this.widget.initialDateTime ?? DateTime.now();
179 2 : this.selectedDateTime = this.currentDateTime;
180 2 : this.calendarMonth = CalendarTableHelper.buildCurrentCalendarMonth(
181 5 : date: DateTime(this.currentDateTime.year, this.currentDateTime.month),
182 2 : forceSixWeekMonth: this.widget.enableSixWeeksForEveryMonth,
183 : );
184 : }
185 :
186 : /// Handles press on headerTrailing.
187 : ///
188 : /// Callback after isMaximumMonthDateReached is true and returns without doing anything
189 : /// If maximum month is not reached yet, [currentDateTime] and [calendarMonth] gets
190 : /// updated with next month. Also Callback after month is changed.
191 1 : void _goToNextMonth() {
192 2 : if (this.widget.maximumMonthDate != null &&
193 1 : DateHelper.isMaximumMonthDateReached(
194 2 : maximumDateTime: this.widget.maximumMonthDate!,
195 1 : currentDateTime: this.currentDateTime)) {
196 3 : this.widget.onReachedMaximumDate?.call();
197 : return;
198 : }
199 :
200 2 : setState(() {
201 1 : this.currentDateTime =
202 6 : DateTime(this.currentDateTime.year, this.currentDateTime.month + 1);
203 2 : this.calendarMonth = CalendarTableHelper.buildCurrentCalendarMonth(
204 1 : date: this.currentDateTime,
205 2 : forceSixWeekMonth: this.widget.enableSixWeeksForEveryMonth);
206 :
207 4 : this.widget.onMonthChanged?.call(this.currentDateTime);
208 : });
209 : }
210 :
211 : /// Handles tap on headerLeading.
212 : ///
213 : /// Callback if isMinimumMonthDateReached is reached and returns without doing
214 : /// anything. IF isMinimumMonthDateReached is false, updates [currentDateTime] and
215 : /// [calendarMonth] with previous month. Also Callback after changing the month.
216 1 : void _goToPreviousMonth() {
217 2 : if (this.widget.minimumMonthDate != null &&
218 1 : DateHelper.isMinimumMonthDateReached(
219 2 : minimumMonthDate: this.widget.minimumMonthDate!,
220 1 : currentDateTime: this.currentDateTime)) {
221 3 : this.widget.onReachedMinimumDate?.call();
222 : return;
223 : }
224 :
225 2 : setState(() {
226 1 : this.currentDateTime =
227 6 : DateTime(this.currentDateTime.year, this.currentDateTime.month - 1);
228 2 : this.calendarMonth = CalendarTableHelper.buildCurrentCalendarMonth(
229 1 : date: this.currentDateTime,
230 2 : forceSixWeekMonth: this.widget.enableSixWeeksForEveryMonth,
231 : );
232 2 : this.widget.onMonthChanged?.call(this.currentDateTime);
233 : });
234 : }
235 :
236 : /// Handles tap on a day.
237 : ///
238 : /// If enableJumpToOtherMonth is true and selected day is outside of
239 : /// the current month, then the calendar will jump to the month where the
240 : /// selected day is in.
241 : /// Either way [selectedDateTime] gets updated with selectedDate. Also
242 : /// Callback after day is changed.
243 1 : void _selectCalendarDay(DateTime date) {
244 2 : if (date != this.selectedDateTime) {
245 2 : if (this.widget.enableJumpToOtherMonth) {
246 5 : if (date.month == 12 && this.currentDateTime.month == 1) {
247 1 : this._goToPreviousMonth();
248 0 : } else if (date.month == 1 && this.currentDateTime.month == 12) {
249 0 : this._goToPreviousMonth();
250 0 : } else if (date.month > this.currentDateTime.month) {
251 0 : this._goToNextMonth();
252 0 : } else if (date.month < this.currentDateTime.month) {
253 0 : this._goToPreviousMonth();
254 : }
255 : }
256 :
257 2 : setState(() {
258 1 : this.selectedDateTime = date;
259 2 : this.widget.onDayChanged?.call(this.selectedDateTime);
260 : });
261 : }
262 : }
263 :
264 : /// Calculates the swiping direction and calls method depending on
265 : /// the outcome of calculation.
266 1 : void _onHorizontalDragEnd(DragEndDetails details) {
267 4 : if (details.velocity.pixelsPerSecond.dx < 0) {
268 1 : this._goToNextMonth();
269 4 : } else if (details.velocity.pixelsPerSecond.dx > 0) {
270 1 : this._goToPreviousMonth();
271 : }
272 : }
273 :
274 1 : @override
275 : Widget build(BuildContext context) {
276 1 : return GestureDetector(
277 : behavior: HitTestBehavior.opaque,
278 3 : onHorizontalDragEnd: this.widget.enableHorizontalSwipe == true
279 1 : ? this._onHorizontalDragEnd
280 : : null,
281 1 : child: Column(
282 : crossAxisAlignment: CrossAxisAlignment.start,
283 1 : children: [
284 1 : CalenderHeader(
285 1 : onPressedNext: this._goToNextMonth,
286 1 : onPressedPrevious: this._goToPreviousMonth,
287 4 : header: this.widget.headerBuilder(this.currentDateTime),
288 2 : padding: this.widget.iconPadding,
289 2 : iconLeft: this.widget.headerLeading,
290 2 : iconRight: this.widget.headerTrailing,
291 : ),
292 1 : CalendarBody(
293 1 : onSelectDay: this._selectCalendarDay,
294 1 : calendarMonth: this.calendarMonth,
295 2 : dayBuilder: this.widget.dayBuilder,
296 2 : dayOfWeekBuilder: this.widget.dayOfWeekBuilder,
297 2 : daysOfWeek: this.widget.daysOfWeek,
298 1 : selectedDateTime: this.selectedDateTime,
299 2 : minimumDayDate: this.widget.minimumDayDate,
300 2 : maximumDayDate: this.widget.maximumDayDate,
301 2 : disableClickOnOutOfRange: this.widget.disableTapOnOutOfRange,
302 2 : disableSixthRow: this.widget.disableSixthRow,
303 : ),
304 : ],
305 : ),
306 : );
307 : }
308 : }
|