i_selectable_picker.dart 23 KB

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