Line data Source code
1 : import 'dart:async';
2 :
3 : import 'package:flutter/material.dart';
4 :
5 : import 'date_period.dart';
6 : import 'day_picker.dart' as day_picker;
7 : import 'day_type.dart';
8 : import 'range_picker.dart';
9 : import 'unselectable_period_error.dart';
10 : import 'utils.dart';
11 :
12 : /// Interface for selection logic of the different date pickers.
13 : ///
14 : /// T - is selection type.
15 : abstract class ISelectablePicker<T> {
16 : /// The earliest date the user is permitted to pick.
17 : ///
18 : /// Time is the midnight. Otherwise [isDisabled] may return undesired result.
19 : final DateTime firstDate;
20 :
21 : /// The latest date the user is permitted to pick.
22 : ///
23 : /// Time is the millisecond before next day midnight.
24 : /// Otherwise [isDisabled] may return undesired result.
25 : final DateTime lastDate;
26 :
27 : /// Function returns if day can be selected or not.
28 : final SelectableDayPredicate _selectableDayPredicate;
29 :
30 : /// StreamController for new selection (T).
31 : @protected
32 : StreamController<T> onUpdateController = StreamController<T>.broadcast();
33 :
34 : /// Stream with new selected (T) event.
35 : ///
36 : /// Throws [UnselectablePeriodException]
37 : /// if there is any custom disabled date in selected.
38 0 : Stream<T> get onUpdate => onUpdateController.stream;
39 :
40 : /// Constructor with required fields that used in non-abstract methods
41 : /// ([isDisabled]).
42 5 : ISelectablePicker(
43 : DateTime firstDate,
44 : DateTime lastDate, {
45 : SelectableDayPredicate? selectableDayPredicate,
46 5 : }) : firstDate = DatePickerUtils.startOfTheDay(firstDate),
47 5 : lastDate = DatePickerUtils.endOfTheDay(lastDate),
48 : _selectableDayPredicate =
49 : selectableDayPredicate ?? _defaultSelectableDayPredicate;
50 :
51 : /// If current selection exists and includes day/days that can't be selected
52 : /// according to the [_selectableDayPredicate]'
53 : bool get curSelectionIsCorrupted;
54 :
55 : /// Returns [DayType] for given [day].
56 : DayType getDayType(DateTime day);
57 :
58 : /// Call when user tap on the day cell.
59 : void onDayTapped(DateTime selectedDate);
60 :
61 : /// Returns if given day is disabled.
62 : ///
63 : /// Returns weather given day before the beginning of the [firstDate]
64 : /// or after the end of the [lastDate].
65 : ///
66 : /// If [_selectableDayPredicate] is set checks it as well.
67 4 : @protected
68 : bool isDisabled(DateTime day) {
69 : final bool customDisabled =
70 8 : _selectableDayPredicate != null ? !_selectableDayPredicate(day) : false;
71 :
72 16 : return day.isAfter(lastDate) || day.isBefore(firstDate) || customDisabled;
73 : }
74 :
75 : /// Closes [onUpdateController].
76 : /// After it [onUpdateController] can't get new events.
77 0 : void dispose() {
78 0 : onUpdateController.close();
79 : }
80 :
81 1 : static bool _defaultSelectableDayPredicate(_) => true;
82 : }
83 :
84 : /// Selection logic for WeekPicker.
85 : class WeekSelectable extends ISelectablePicker<DatePeriod> {
86 : /// Initialized in ctor body.
87 : late DateTime _firstDayOfSelectedWeek;
88 :
89 : /// Initialized in ctor body.
90 : late DateTime _lastDayOfSelectedWeek;
91 :
92 : // It is int from 0 to 6 where 0 points to Sunday and 6 points to Saturday.
93 : // According to MaterialLocalization.firstDayOfWeekIndex.
94 : final int _firstDayOfWeekIndex;
95 :
96 0 : @override
97 0 : bool get curSelectionIsCorrupted => _checkCurSelection();
98 :
99 : /// Creates selection logic for WeekPicker.
100 : ///
101 : /// Entire week will be selected if
102 : /// * it is between [firstDate] and [lastDate]
103 : /// * it doesn't include unselectable days according to the
104 : /// [selectableDayPredicate]
105 : ///
106 : /// If one or more days of the week are before [firstDate]
107 : /// first selection date will be the same as [firstDate].
108 : ///
109 : /// If one or more days of the week are after [lastDate]
110 : /// last selection date will be the same as [lastDate].
111 : ///
112 : /// If one or more days of week are not selectable according to the
113 : /// [selectableDayPredicate] nothing will be returned as selection
114 : /// but [UnselectablePeriodException] will be thrown.
115 1 : WeekSelectable(DateTime selectedDate, this._firstDayOfWeekIndex,
116 : DateTime firstDate, DateTime lastDate,
117 : {SelectableDayPredicate? selectableDayPredicate})
118 1 : : super(firstDate, lastDate,
119 : selectableDayPredicate: selectableDayPredicate) {
120 1 : DatePeriod selectedWeek = _getNewSelectedPeriod(selectedDate);
121 2 : _firstDayOfSelectedWeek = selectedWeek.start;
122 2 : _lastDayOfSelectedWeek = selectedWeek.end;
123 1 : _checkCurSelection();
124 : }
125 :
126 1 : @override
127 : DayType getDayType(DateTime date) {
128 : DayType result;
129 :
130 : DatePeriod selectedPeriod =
131 3 : DatePeriod(_firstDayOfSelectedWeek, _lastDayOfSelectedWeek);
132 : bool selectedPeriodIsBroken =
133 2 : _disabledDatesInPeriod(selectedPeriod).isNotEmpty;
134 :
135 1 : if (isDisabled(date)) {
136 : result = DayType.disabled;
137 1 : } else if (_isDaySelected(date) && !selectedPeriodIsBroken) {
138 : DateTime firstNotDisabledDayOfSelectedWeek =
139 3 : _firstDayOfSelectedWeek.isBefore(firstDate)
140 0 : ? firstDate
141 1 : : _firstDayOfSelectedWeek;
142 :
143 : DateTime lastNotDisabledDayOfSelectedWeek =
144 3 : _lastDayOfSelectedWeek.isAfter(lastDate)
145 0 : ? lastDate
146 1 : : _lastDayOfSelectedWeek;
147 :
148 1 : if (DatePickerUtils.sameDate(date, firstNotDisabledDayOfSelectedWeek) &&
149 1 : DatePickerUtils.sameDate(date, lastNotDisabledDayOfSelectedWeek)) {
150 : result = DayType.single;
151 2 : } else if (DatePickerUtils.sameDate(date, _firstDayOfSelectedWeek) ||
152 2 : DatePickerUtils.sameDate(date, firstDate)) {
153 : result = DayType.start;
154 2 : } else if (DatePickerUtils.sameDate(date, _lastDayOfSelectedWeek) ||
155 2 : DatePickerUtils.sameDate(date, lastDate)) {
156 : result = DayType.end;
157 : } else {
158 : result = DayType.middle;
159 : }
160 : } else {
161 : result = DayType.notSelected;
162 : }
163 :
164 : return result;
165 : }
166 :
167 0 : @override
168 : void onDayTapped(DateTime selectedDate) {
169 0 : DatePeriod newPeriod = _getNewSelectedPeriod(selectedDate);
170 0 : List<DateTime> customDisabledDays = _disabledDatesInPeriod(newPeriod);
171 :
172 0 : customDisabledDays.isEmpty
173 0 : ? onUpdateController.add(newPeriod)
174 0 : : onUpdateController.addError(
175 0 : UnselectablePeriodException(customDisabledDays, newPeriod));
176 : }
177 :
178 : // Returns new selected period according to tapped date.
179 : // Doesn't check custom disabled days.
180 : // You have to check it separately if it needs.
181 1 : DatePeriod _getNewSelectedPeriod(DateTime tappedDay) {
182 : DatePeriod newPeriod;
183 :
184 : DateTime firstDayOfTappedWeek =
185 2 : DatePickerUtils.getFirstDayOfWeek(tappedDay, _firstDayOfWeekIndex);
186 :
187 : DateTime lastDayOfTappedWeek =
188 2 : DatePickerUtils.getLastDayOfWeek(tappedDay, _firstDayOfWeekIndex);
189 :
190 : DateTime firstNotDisabledDayOfSelectedWeek =
191 2 : firstDayOfTappedWeek.isBefore(firstDate)
192 0 : ? firstDate
193 : : firstDayOfTappedWeek;
194 :
195 : DateTime lastNotDisabledDayOfSelectedWeek =
196 2 : lastDayOfTappedWeek.isAfter(lastDate) ? lastDate : lastDayOfTappedWeek;
197 :
198 1 : newPeriod = DatePeriod(
199 : firstNotDisabledDayOfSelectedWeek, lastNotDisabledDayOfSelectedWeek);
200 : return newPeriod;
201 : }
202 :
203 1 : bool _isDaySelected(DateTime date) {
204 : DateTime startOfTheStartDay =
205 2 : DatePickerUtils.startOfTheDay(_firstDayOfSelectedWeek);
206 : DateTime endOfTheLastDay =
207 2 : DatePickerUtils.endOfTheDay(_lastDayOfSelectedWeek);
208 1 : return !(date.isBefore(startOfTheStartDay) ||
209 1 : date.isAfter(endOfTheLastDay));
210 : }
211 :
212 1 : List<DateTime> _disabledDatesInPeriod(DatePeriod period) {
213 1 : List<DateTime> result = <DateTime>[];
214 :
215 1 : var date = period.start;
216 :
217 2 : while (!date.isAfter(period.end)) {
218 1 : if (isDisabled(date)) result.add(date);
219 :
220 2 : date = date.add(Duration(days: 1));
221 : }
222 :
223 : return result;
224 : }
225 :
226 : // Returns if current selection contains disabled dates.
227 : // Returns false if there is no any selection.
228 1 : bool _checkCurSelection() {
229 : bool noSelection =
230 2 : _firstDayOfSelectedWeek == null || _lastDayOfSelectedWeek == null;
231 :
232 : if (noSelection) return false;
233 :
234 : DatePeriod selectedPeriod =
235 3 : DatePeriod(_firstDayOfSelectedWeek, _lastDayOfSelectedWeek);
236 1 : List<DateTime> disabledDates = _disabledDatesInPeriod(selectedPeriod);
237 :
238 1 : bool selectedPeriodIsBroken = disabledDates.isNotEmpty;
239 : return selectedPeriodIsBroken;
240 : }
241 : }
242 :
243 : /// Selection logic for [day_picker.DayPicker].
244 : class DaySelectable extends ISelectablePicker<DateTime> {
245 : /// Currently selected date.
246 : DateTime selectedDate;
247 :
248 0 : @override
249 0 : bool get curSelectionIsCorrupted => _checkCurSelection();
250 :
251 : /// Creates selection logic for [day_picker.DayPicker].
252 : ///
253 : /// Every day can be selected if it is between [firstDate] and [lastDate]
254 : /// and not unselectable according to the [selectableDayPredicate].
255 : ///
256 : /// If day is not selectable according to the [selectableDayPredicate]
257 : /// nothing will be returned as selection
258 : /// but [UnselectablePeriodException] will be thrown.
259 1 : DaySelectable(this.selectedDate, DateTime firstDate, DateTime lastDate,
260 : {SelectableDayPredicate? selectableDayPredicate})
261 1 : : super(firstDate, lastDate,
262 : selectableDayPredicate: selectableDayPredicate);
263 :
264 1 : @override
265 : DayType getDayType(DateTime date) {
266 : DayType result;
267 :
268 1 : if (isDisabled(date)) {
269 : result = DayType.disabled;
270 1 : } else if (_isDaySelected(date)) {
271 : result = DayType.single;
272 : } else {
273 : result = DayType.notSelected;
274 : }
275 :
276 : return result;
277 : }
278 :
279 0 : @override
280 : void onDayTapped(DateTime selectedDate) {
281 0 : DateTime newSelected = DatePickerUtils.sameDate(firstDate, selectedDate)
282 : ? selectedDate
283 0 : : DateTime(selectedDate.year, selectedDate.month, selectedDate.day);
284 0 : onUpdateController.add(newSelected);
285 : }
286 :
287 1 : bool _isDaySelected(DateTime date) =>
288 2 : DatePickerUtils.sameDate(date, selectedDate);
289 :
290 : // Returns if current selection is disabled
291 : // according to the [_selectableDayPredicate].
292 : //
293 : // Returns false if there is no any selection.
294 0 : bool _checkCurSelection() {
295 0 : if (selectedDate == null) return false;
296 0 : bool selectedIsBroken = _selectableDayPredicate(selectedDate);
297 :
298 : return selectedIsBroken;
299 : }
300 : }
301 :
302 : /// Selection logic for [day_picker.DayPicker] where many single days can be
303 : /// selected.
304 : class DayMultiSelectable extends ISelectablePicker<List<DateTime>> {
305 : /// Currently selected dates.
306 : List<DateTime> selectedDates;
307 :
308 : /// Creates selection logic for [day_picker.DayPicker].
309 : ///
310 : /// Every day can be selected if it is between [firstDate] and [lastDate]
311 : /// and not unselectable according to the [selectableDayPredicate].
312 : ///
313 : /// If day is not selectable according to the [selectableDayPredicate]
314 : /// nothing will be returned as selection
315 : /// but [UnselectablePeriodException] will be thrown.
316 1 : DayMultiSelectable(this.selectedDates, DateTime firstDate, DateTime lastDate,
317 : {SelectableDayPredicate? selectableDayPredicate})
318 1 : : super(firstDate, lastDate,
319 : selectableDayPredicate: selectableDayPredicate);
320 :
321 0 : @override
322 0 : bool get curSelectionIsCorrupted => _checkCurSelection();
323 :
324 1 : @override
325 : DayType getDayType(DateTime date) {
326 : DayType result;
327 :
328 1 : if (isDisabled(date)) {
329 : result = DayType.disabled;
330 1 : } else if (_isDaySelected(date)) {
331 : result = DayType.single;
332 : } else {
333 : result = DayType.notSelected;
334 : }
335 :
336 : return result;
337 : }
338 :
339 0 : @override
340 : void onDayTapped(DateTime selectedDate) {
341 : bool alreadyExist =
342 0 : selectedDates.any((d) => DatePickerUtils.sameDate(d, selectedDate));
343 :
344 : if (alreadyExist) {
345 0 : List<DateTime> newSelectedDates = List.from(selectedDates)
346 0 : ..removeWhere((d) => DatePickerUtils.sameDate(d, selectedDate));
347 :
348 0 : onUpdateController.add(newSelectedDates);
349 : } else {
350 0 : DateTime newSelected = DatePickerUtils.sameDate(firstDate, selectedDate)
351 : ? selectedDate
352 0 : : DateTime(selectedDate.year, selectedDate.month, selectedDate.day);
353 :
354 0 : List<DateTime> newSelectedDates = List.from(selectedDates)
355 0 : ..add(newSelected);
356 :
357 0 : onUpdateController.add(newSelectedDates);
358 : }
359 : }
360 :
361 1 : bool _isDaySelected(DateTime date) =>
362 4 : selectedDates.any((d) => DatePickerUtils.sameDate(date, d));
363 :
364 : // Returns if current selection is disabled
365 : // according to the [_selectableDayPredicate].
366 : //
367 : // Returns false if there is no any selection.
368 0 : bool _checkCurSelection() {
369 0 : if (selectedDates == null || selectedDates.isEmpty) return false;
370 0 : bool selectedIsBroken = selectedDates.every(_selectableDayPredicate);
371 :
372 : return selectedIsBroken;
373 : }
374 : }
375 :
376 : /// Selection logic for [RangePicker].
377 : class RangeSelectable extends ISelectablePicker<DatePeriod> {
378 : /// Initially selected period.
379 : ///
380 : /// [selectedPeriod.start] time is midnight.
381 : /// [selectedPeriod.end] time is millisecond before next day midnight.
382 : DatePeriod selectedPeriod;
383 :
384 0 : @override
385 0 : bool get curSelectionIsCorrupted => _checkCurSelection();
386 :
387 : /// Creates selection logic for [RangePicker].
388 : ///
389 : /// Period can be selected if
390 : /// * it is between [firstDate] and [lastDate]
391 : /// * it doesn't include unselectable days according to the
392 : /// [selectableDayPredicate]
393 : ///
394 : ///
395 : /// If one or more days of the period are not selectable according to the
396 : /// [selectableDayPredicate] nothing will be returned as selection
397 : /// but [UnselectablePeriodException] will be thrown.
398 1 : RangeSelectable(
399 : DatePeriod selectedPeriod, DateTime firstDate, DateTime lastDate,
400 : {SelectableDayPredicate? selectableDayPredicate})
401 1 : : selectedPeriod = DatePeriod(
402 2 : DatePickerUtils.startOfTheDay(selectedPeriod.start),
403 2 : DatePickerUtils.endOfTheDay(selectedPeriod.end),
404 : ),
405 1 : super(firstDate, lastDate,
406 : selectableDayPredicate: selectableDayPredicate);
407 :
408 1 : @override
409 : DayType getDayType(DateTime date) {
410 : DayType result;
411 :
412 : bool selectedPeriodIsBroken =
413 3 : _disabledDatesInPeriod(selectedPeriod).isNotEmpty;
414 :
415 1 : if (isDisabled(date)) {
416 : result = DayType.disabled;
417 1 : } else if (_isDaySelected(date) && !selectedPeriodIsBroken) {
418 3 : if (DatePickerUtils.sameDate(date, selectedPeriod.start) &&
419 3 : DatePickerUtils.sameDate(date, selectedPeriod.end)) {
420 : result = DayType.single;
421 3 : } else if (DatePickerUtils.sameDate(date, selectedPeriod.start) ||
422 2 : DatePickerUtils.sameDate(date, firstDate)) {
423 : result = DayType.start;
424 3 : } else if (DatePickerUtils.sameDate(date, selectedPeriod.end) ||
425 2 : DatePickerUtils.sameDate(date, lastDate)) {
426 : result = DayType.end;
427 : } else {
428 : result = DayType.middle;
429 : }
430 : } else {
431 : result = DayType.notSelected;
432 : }
433 :
434 : return result;
435 : }
436 :
437 0 : @override
438 : void onDayTapped(DateTime selectedDate) {
439 0 : DatePeriod newPeriod = _getNewSelectedPeriod(selectedDate);
440 0 : List<DateTime> customDisabledDays = _disabledDatesInPeriod(newPeriod);
441 :
442 0 : customDisabledDays.isEmpty
443 0 : ? onUpdateController.add(newPeriod)
444 0 : : onUpdateController.addError(
445 0 : UnselectablePeriodException(customDisabledDays, newPeriod));
446 : }
447 :
448 : // Returns new selected period according to tapped date.
449 0 : DatePeriod _getNewSelectedPeriod(DateTime tappedDate) {
450 : // check if was selected only one date and we should generate period
451 : bool sameDate =
452 0 : DatePickerUtils.sameDate(selectedPeriod.start, selectedPeriod.end);
453 : DatePeriod newPeriod;
454 :
455 : // Was selected one-day-period.
456 : // With new user tap will be generated 2 dates as a period.
457 : if (sameDate) {
458 : // if user tap on the already selected single day
459 : bool selectedAlreadySelectedDay =
460 0 : DatePickerUtils.sameDate(tappedDate, selectedPeriod.end);
461 0 : bool isSelectedFirstDay = DatePickerUtils.sameDate(tappedDate, firstDate);
462 0 : bool isSelectedLastDay = DatePickerUtils.sameDate(tappedDate, lastDate);
463 :
464 : if (selectedAlreadySelectedDay) {
465 : if (isSelectedFirstDay && isSelectedLastDay) {
466 0 : newPeriod = DatePeriod(firstDate, lastDate);
467 : } else if (isSelectedFirstDay) {
468 : newPeriod =
469 0 : DatePeriod(firstDate, DatePickerUtils.endOfTheDay(firstDate));
470 : } else if (isSelectedLastDay) {
471 : newPeriod =
472 0 : DatePeriod(DatePickerUtils.startOfTheDay(lastDate), lastDate);
473 : } else {
474 0 : newPeriod = DatePeriod(DatePickerUtils.startOfTheDay(tappedDate),
475 0 : DatePickerUtils.endOfTheDay(tappedDate));
476 : }
477 : } else {
478 : DateTime startOfTheSelectedDay =
479 0 : DatePickerUtils.startOfTheDay(selectedPeriod.start);
480 :
481 0 : if (!tappedDate.isAfter(startOfTheSelectedDay)) {
482 0 : newPeriod = DatePickerUtils.sameDate(tappedDate, firstDate)
483 0 : ? DatePeriod(firstDate, selectedPeriod.end)
484 0 : : DatePeriod(DatePickerUtils.startOfTheDay(tappedDate),
485 0 : selectedPeriod.end);
486 : } else {
487 0 : newPeriod = DatePickerUtils.sameDate(tappedDate, lastDate)
488 0 : ? DatePeriod(selectedPeriod.start, lastDate)
489 0 : : DatePeriod(selectedPeriod.start,
490 0 : DatePickerUtils.endOfTheDay(tappedDate));
491 : }
492 : }
493 :
494 : // Was selected 2 dates as a period.
495 : // With new user tap new one-day-period will be generated.
496 : } else {
497 0 : bool sameAsFirst = DatePickerUtils.sameDate(tappedDate, firstDate);
498 0 : bool sameAsLast = DatePickerUtils.sameDate(tappedDate, lastDate);
499 :
500 : if (sameAsFirst && sameAsLast) {
501 0 : newPeriod = DatePeriod(firstDate, lastDate);
502 : } else if (sameAsFirst) {
503 : newPeriod =
504 0 : DatePeriod(firstDate, DatePickerUtils.endOfTheDay(firstDate));
505 : } else if (sameAsLast) {
506 : newPeriod =
507 0 : DatePeriod(DatePickerUtils.startOfTheDay(tappedDate), lastDate);
508 : } else {
509 0 : newPeriod = DatePeriod(DatePickerUtils.startOfTheDay(tappedDate),
510 0 : DatePickerUtils.endOfTheDay(tappedDate));
511 : }
512 : }
513 :
514 : return newPeriod;
515 : }
516 :
517 : // Returns if current selection contains disabled dates.
518 : // Returns false if there is no any selection.
519 0 : bool _checkCurSelection() {
520 0 : if (selectedPeriod == null) return false;
521 0 : List<DateTime> disabledDates = _disabledDatesInPeriod(selectedPeriod);
522 :
523 0 : bool selectedPeriodIsBroken = disabledDates.isNotEmpty;
524 : return selectedPeriodIsBroken;
525 : }
526 :
527 1 : List<DateTime> _disabledDatesInPeriod(DatePeriod period) {
528 1 : List<DateTime> result = <DateTime>[];
529 :
530 1 : var date = period.start;
531 :
532 2 : while (!date.isAfter(period.end)) {
533 1 : if (isDisabled(date)) result.add(date);
534 :
535 2 : date = date.add(Duration(days: 1));
536 : }
537 :
538 : return result;
539 : }
540 :
541 1 : bool _isDaySelected(DateTime date) {
542 2 : DateTime startOfTheStartDay = selectedPeriod.start;
543 2 : DateTime endOfTheLastDay = selectedPeriod.end;
544 :
545 1 : return !(date.isBefore(startOfTheStartDay) ||
546 1 : date.isAfter(endOfTheLastDay));
547 : }
548 : }
549 :
550 : /// Selection logic for [day_picker.MonthPicker.single].
551 : class MonthSelectable extends ISelectablePicker<DateTime> {
552 : /// Currently selected date.
553 : DateTime selectedDate;
554 :
555 0 : @override
556 0 : bool get curSelectionIsCorrupted => _checkCurSelection();
557 :
558 : /// Creates selection logic for [day_picker.MonthPicker] with single selection.
559 : ///
560 : /// Every date can be selected if it is between [firstDate] and [lastDate]
561 : /// and not unselectable according to the [selectableDayPredicate].
562 : ///
563 : /// If date is not selectable according to the [selectableDayPredicate]
564 : /// nothing will be returned as selection
565 : /// but [UnselectablePeriodException] will be thrown.
566 1 : MonthSelectable(this.selectedDate, DateTime firstDate, DateTime lastDate,
567 : {SelectableDayPredicate? selectableDayPredicate})
568 1 : : super(firstDate, lastDate,
569 : selectableDayPredicate: selectableDayPredicate);
570 :
571 1 : @override
572 : DayType getDayType(DateTime date) {
573 : DayType result;
574 :
575 1 : if (isDisabled(date)) {
576 : result = DayType.disabled;
577 1 : } else if (_isDaySelected(date)) {
578 : result = DayType.single;
579 : } else {
580 : result = DayType.notSelected;
581 : }
582 :
583 : return result;
584 : }
585 :
586 0 : @override
587 : void onDayTapped(DateTime selectedDate) {
588 0 : DateTime newSelected = DatePickerUtils.sameMonth(firstDate, selectedDate)
589 : ? selectedDate
590 0 : : DateTime(selectedDate.year, selectedDate.month, selectedDate.day);
591 0 : onUpdateController.add(newSelected);
592 : }
593 :
594 1 : bool _isDaySelected(DateTime date) =>
595 2 : DatePickerUtils.sameMonth(date, selectedDate);
596 :
597 : // Returns if current selection is disabled
598 : // according to the [_selectableDayPredicate].
599 : //
600 : // Returns false if there is no any selection.
601 0 : bool _checkCurSelection() {
602 0 : if (selectedDate == null) return false;
603 0 : bool selectedIsBroken = _selectableDayPredicate(selectedDate);
604 :
605 : return selectedIsBroken;
606 : }
607 :
608 : // We only need to know if month of passed day
609 : // before the month of the firstDate or after the month of the lastDate.
610 : //
611 : // Don't need to compare day and time.
612 1 : @protected
613 : @override
614 : bool isDisabled(DateTime month) {
615 : DateTime beginningOfTheFirstDateMonth =
616 5 : DateTime(firstDate.year, firstDate.month);
617 :
618 1 : DateTime endOfTheLastDateMonth = DateTime(
619 2 : lastDate.year,
620 2 : lastDate.month,
621 5 : DatePickerUtils.getDaysInMonth(lastDate.year, lastDate.month),
622 : 23,
623 : 59,
624 : 59,
625 : 999,
626 : );
627 :
628 1 : return month.isAfter(endOfTheLastDateMonth) ||
629 1 : month.isBefore(beginningOfTheFirstDateMonth);
630 : }
631 : }
632 :
633 : /// Selection logic for [day_picker.MonthPicker.multi]
634 : /// where many single months can be selected.
635 : class MonthMultiSelectable extends ISelectablePicker<List<DateTime>> {
636 : /// Currently selected dates.
637 : List<DateTime> selectedDates;
638 :
639 : /// Creates selection logic for [day_picker.MonthPicker] with multi selection.
640 : ///
641 : /// Every day can be selected if it is between [firstDate] and [lastDate]
642 : /// and not unselectable according to the [selectableDayPredicate].
643 : ///
644 : /// If day is not selectable according to the [selectableDayPredicate]
645 : /// nothing will be returned as selection
646 : /// but [UnselectablePeriodException] will be thrown.
647 0 : MonthMultiSelectable(
648 : this.selectedDates,
649 : DateTime firstDate,
650 : DateTime lastDate, {
651 : SelectableDayPredicate? selectableDayPredicate,
652 0 : }) : super(firstDate, lastDate,
653 : selectableDayPredicate: selectableDayPredicate);
654 :
655 0 : @override
656 0 : bool get curSelectionIsCorrupted => _checkCurSelection();
657 :
658 0 : @override
659 : DayType getDayType(DateTime date) {
660 : DayType result;
661 :
662 0 : if (isDisabled(date)) {
663 : result = DayType.disabled;
664 0 : } else if (_isDaySelected(date)) {
665 : result = DayType.single;
666 : } else {
667 : result = DayType.notSelected;
668 : }
669 :
670 : return result;
671 : }
672 :
673 0 : @override
674 : void onDayTapped(DateTime selectedDate) {
675 : bool alreadyExist =
676 0 : selectedDates.any((d) => DatePickerUtils.sameMonth(d, selectedDate));
677 :
678 : if (alreadyExist) {
679 0 : List<DateTime> newSelectedDates = List.from(selectedDates)
680 0 : ..removeWhere((d) => DatePickerUtils.sameMonth(d, selectedDate));
681 :
682 0 : onUpdateController.add(newSelectedDates);
683 : } else {
684 0 : DateTime newSelected = DatePickerUtils.sameMonth(firstDate, selectedDate)
685 0 : ? firstDate
686 0 : : DateTime(selectedDate.year, selectedDate.month);
687 :
688 0 : List<DateTime> newSelectedDates = List.from(selectedDates)
689 0 : ..add(newSelected);
690 :
691 0 : onUpdateController.add(newSelectedDates);
692 : }
693 : }
694 :
695 0 : bool _isDaySelected(DateTime date) =>
696 0 : selectedDates.any((d) => DatePickerUtils.sameMonth(date, d));
697 :
698 : // Returns if current selection is disabled
699 : // according to the [_selectableDayPredicate].
700 : //
701 : // Returns false if there is no any selection.
702 0 : bool _checkCurSelection() {
703 0 : if (selectedDates == null || selectedDates.isEmpty) return false;
704 0 : bool selectedIsBroken = selectedDates.every(_selectableDayPredicate);
705 :
706 : return selectedIsBroken;
707 : }
708 : }
|