|
6 | 6 | * found in the LICENSE file at https://angular.io/license
|
7 | 7 | */
|
8 | 8 |
|
| 9 | +import {FactoryProvider, Injectable, OnDestroy, Optional, SkipSelf} from '@angular/core'; |
9 | 10 | import {DateAdapter} from '@angular/material/core';
|
10 |
| -import {FactoryProvider, Optional, SkipSelf, Injectable} from '@angular/core'; |
11 |
| -import {Subject, Observable} from 'rxjs'; |
| 11 | +import {Observable, Subject} from 'rxjs'; |
12 | 12 |
|
| 13 | +/** A class representing a range of dates. */ |
13 | 14 | export class DateRange<D> {
|
14 |
| - // DateRange should be a class with a private member. |
15 |
| - // Otherwise any object with a `start` and `end` property might be considered a |
16 |
| - // `DateRange` |
17 |
| - private _disableStructuralEquivalency: never; |
| 15 | + /** |
| 16 | + * Ensures that objects with a `start` and `end` property can't be assigned to a variable that |
| 17 | + * expects a `DateRange` |
| 18 | + */ |
| 19 | + private _disableStructuralEquivalency: never; |
18 | 20 |
|
19 |
| - start: D | null; |
20 |
| - end: D | null; |
| 21 | + /** The start date of the range. */ |
| 22 | + readonly start: D | null; |
21 | 23 |
|
22 |
| - constructor(range?: { start?: D | null, end?: D | null } | null) { |
23 |
| - this.start = range && range.start || null; |
24 |
| - this.end = range && range.end || null; |
25 |
| - } |
| 24 | + /** The end date of the range. */ |
| 25 | + readonly end: D | null; |
| 26 | + |
| 27 | + constructor(range?: {start?: D | null, end?: D | null} | null) { |
| 28 | + this.start = range && range.start || null; |
| 29 | + this.end = range && range.end || null; |
| 30 | + } |
26 | 31 | }
|
27 | 32 |
|
28 |
| -type ExtractDateTypeFromSelection<T> = T extends DateRange<infer D> ? D : T; |
| 33 | +type ExtractDateTypeFromSelection<T> = T extends DateRange<infer D> ? D : NonNullable<T>; |
29 | 34 |
|
30 |
| -export abstract class MatDateSelectionModel<S, D = ExtractDateTypeFromSelection<S>> { |
31 |
| - protected _valueChangesSubject = new Subject<void>(); |
| 35 | +/** A selection model containing a date selection. */ |
| 36 | +export abstract class MatDateSelectionModel<S, D = ExtractDateTypeFromSelection<S>> |
| 37 | + implements OnDestroy { |
| 38 | + /** Subject used to emit value change events. */ |
| 39 | + private _valueChangesSubject = new Subject<void>(); |
| 40 | + |
| 41 | + /** Observable of value change events. */ |
32 | 42 | valueChanges: Observable<void> = this._valueChangesSubject.asObservable();
|
33 | 43 |
|
34 |
| - constructor(protected readonly adapter: DateAdapter<D>, protected _selection: S | |
35 |
| - null = null) {} |
| 44 | + /** The current selection. */ |
| 45 | + get selection (): S { return this._selection; } |
| 46 | + set selection(s: S) { |
| 47 | + this._selection = s; |
| 48 | + this._valueChangesSubject.next(); |
| 49 | + } |
| 50 | + |
| 51 | + protected constructor(protected readonly adapter: DateAdapter<D>, private _selection: S) {} |
36 | 52 |
|
37 |
| - abstract get selection(): S | null; |
38 |
| - abstract set selection(selection: S | null); |
39 |
| - abstract add(date: D): void; |
| 53 | + ngOnDestroy() { |
| 54 | + this._valueChangesSubject.complete(); |
| 55 | + } |
| 56 | + |
| 57 | + /** Adds a date to the current selection. */ |
| 58 | + abstract add(date: D | null): void; |
| 59 | + |
| 60 | + /** Checks whether the current selection is complete. */ |
40 | 61 | abstract isComplete(): boolean;
|
| 62 | + |
| 63 | + /** |
| 64 | + * Checks whether the current selection is the same as the selection in the given selection model. |
| 65 | + */ |
| 66 | + abstract isSame(other: MatDateSelectionModel<D>): boolean; |
| 67 | + |
| 68 | + /** Checks whether the current selection is valid. */ |
41 | 69 | abstract isValid(): boolean;
|
42 |
| - abstract isSame(other: MatDateSelectionModel<any, any>):boolean; |
| 70 | + |
| 71 | + /** Checks whether the current selection overlaps with the given range. */ |
43 | 72 | abstract overlaps(range: DateRange<D>): boolean;
|
44 | 73 | }
|
45 | 74 |
|
46 |
| -/** |
47 |
| - * Concrete implementation of a MatDateSelectionModel that holds a single date. |
48 |
| - */ |
| 75 | +/** A selection model that contains a single date. */ |
49 | 76 | @Injectable()
|
50 |
| -export class MatSingleDateSelectionModel<D> extends MatDateSelectionModel<D, D> { |
51 |
| - |
| 77 | +export class MatSingleDateSelectionModel<D> extends MatDateSelectionModel<D | null, D> { |
52 | 78 | constructor(adapter: DateAdapter<D>, date?: D | null) {
|
53 |
| - super(adapter, date); |
54 |
| - } |
55 |
| - |
56 |
| - get selection(): D | null { |
57 |
| - return this._selection; |
58 |
| - } |
59 |
| - |
60 |
| - set selection(selection: D | null) { |
61 |
| - this._selection = selection; |
62 |
| - } |
63 |
| - |
64 |
| - add(date: D) { |
65 |
| - this._selection = date; |
66 |
| - this._valueChangesSubject.next(); |
| 79 | + super(adapter, date || null); |
67 | 80 | }
|
68 | 81 |
|
69 |
| - isComplete(): boolean { |
70 |
| - return this._selection != null; |
| 82 | + /** |
| 83 | + * Adds a date to the current selection. In the case of a single date selection, the added date |
| 84 | + * simply overwrites the previous selection |
| 85 | + */ |
| 86 | + add(date: D | null) { |
| 87 | + this.selection = date; |
71 | 88 | }
|
72 | 89 |
|
73 |
| - isValid(): boolean { |
74 |
| - return this._selection != null && this.adapter.isDateInstance(this._selection) && |
75 |
| - this.adapter.isValid(this._selection); |
76 |
| - } |
| 90 | + /** |
| 91 | + * Checks whether the current selection is complete. In the case of a single date selection, this |
| 92 | + * is true if the current selection is not null. |
| 93 | + */ |
| 94 | + isComplete() { return this.selection != null; } |
77 | 95 |
|
78 |
| - isSame(other: MatDateSelectionModel<D>): boolean { |
| 96 | + /** |
| 97 | + * Checks whether the current selection is the same as the selection in the given selection model. |
| 98 | + */ |
| 99 | + isSame(other: MatDateSelectionModel<any>): boolean { |
79 | 100 | return other instanceof MatSingleDateSelectionModel &&
|
80 |
| - this._selection === other.selection; |
| 101 | + this.adapter.sameDate(other.selection, this.selection); |
81 | 102 | }
|
82 | 103 |
|
83 | 104 | /**
|
84 |
| - * Determines if the single date is within a given date range. Retuns false if either dates of |
85 |
| - * the range is null or if the selection is undefined. |
| 105 | + * Checks whether the current selection is valid. In the case of a single date selection, this |
| 106 | + * means that the current selection is not null and is a valid date. |
86 | 107 | */
|
| 108 | + isValid(): boolean { |
| 109 | + return this.selection != null && this.adapter.isDateInstance(this.selection) && |
| 110 | + this.adapter.isValid(this.selection); |
| 111 | + } |
| 112 | + |
| 113 | + /** Checks whether the current selection overlaps with the given range. */ |
87 | 114 | overlaps(range: DateRange<D>): boolean {
|
88 |
| - return !!(this._selection && range.start && range.end && |
89 |
| - this.adapter.compareDate(range.start, this._selection) <= 0 && |
90 |
| - this.adapter.compareDate(this._selection, range.end) <= 0); |
| 115 | + return !!(this.selection && range.start && range.end && |
| 116 | + this.adapter.compareDate(range.start, this.selection) <= 0 && |
| 117 | + this.adapter.compareDate(this.selection, range.end) <= 0); |
91 | 118 | }
|
92 | 119 | }
|
93 | 120 |
|
94 |
| -/** |
95 |
| - * Concrete implementation of a MatDateSelectionModel that holds a date range, represented by |
96 |
| - * a start date and an end date. |
97 |
| - */ |
| 121 | +/** A selection model that contains a date range. */ |
98 | 122 | @Injectable()
|
99 |
| -export class MatRangeDateSelectionModel<D> extends |
100 |
| - MatDateSelectionModel<DateRange<D>, D> { |
101 |
| - protected _selection: DateRange<D>; |
102 |
| - |
103 |
| - constructor(adapter: DateAdapter<D>, range?: DateRange<D> | null) { |
104 |
| - super(adapter, range || new DateRange<D>()); |
105 |
| - } |
106 |
| - |
107 |
| - get selection(): DateRange<D> | null { |
108 |
| - return new DateRange(this._selection); |
109 |
| - } |
110 |
| - |
111 |
| - set selection(selection: DateRange<D> | null) { |
112 |
| - this._selection = new DateRange(selection); |
| 123 | +export class MatRangeDateSelectionModel<D> extends MatDateSelectionModel<DateRange<D>, D> { |
| 124 | + constructor(adapter: DateAdapter<D>, range?: {start?: D | null, end?: D | null} | null) { |
| 125 | + super(adapter, new DateRange(range)); |
113 | 126 | }
|
114 | 127 |
|
115 | 128 | /**
|
116 |
| - * Adds an additional date to the range. If no date is set thus far, it will set it to the |
117 |
| - * beginning. If the beginning is set, it will set it to the end. |
118 |
| - * If add is called on a complete selection, it will empty the selection and set it as the start. |
| 129 | + * Adds a date to the current selection. In the case of a date range selection, the added date |
| 130 | + * fills in the next `null` value in the range. If both the start and the end already have a date, |
| 131 | + * the selection is reset so that the given date is the new `start` and the `end` is null. |
119 | 132 | */
|
120 | 133 | add(date: D | null): void {
|
121 |
| - if (this._selection.start == null) { |
122 |
| - this._selection.start = date; |
123 |
| - } else if (this._selection.end == null) { |
124 |
| - this._selection.end = date; |
| 134 | + let {start, end} = this.selection; |
| 135 | + |
| 136 | + if (start == null) { |
| 137 | + start = date; |
| 138 | + } else if (end == null) { |
| 139 | + end = date; |
125 | 140 | } else {
|
126 |
| - this._selection.start = date; |
127 |
| - this._selection.end = null; |
| 141 | + start = date; |
| 142 | + end = null; |
128 | 143 | }
|
129 | 144 |
|
130 |
| - this._valueChangesSubject.next(); |
131 |
| - } |
132 |
| - |
133 |
| - setRange(start: D | null, end: D | null) { |
134 |
| - this._selection.start = start; |
135 |
| - this._selection.end = end; |
| 145 | + this.selection = new DateRange<D>({start, end}); |
136 | 146 | }
|
137 | 147 |
|
| 148 | + /** |
| 149 | + * Checks whether the current selection is complete. In the case of a date range selection, this |
| 150 | + * is true if the current selection has a non-null `start` and `end`. |
| 151 | + */ |
138 | 152 | isComplete(): boolean {
|
139 |
| - return this._selection && this._selection.start != null && this._selection.end != null; |
140 |
| - } |
141 |
| - |
142 |
| - isValid(): boolean { |
143 |
| - return this._selection.start != null && this._selection.end != null && |
144 |
| - this.adapter.isValid(this._selection.start!) && |
145 |
| - this.adapter.isValid(this._selection.end!); |
| 153 | + return this.selection.start != null && this.selection.end != null; |
146 | 154 | }
|
147 | 155 |
|
148 |
| - isSame(other: MatDateSelectionModel<D>): boolean { |
| 156 | + /** |
| 157 | + * Checks whether the current selection is the same as the selection in the given selection model. |
| 158 | + */ |
| 159 | + isSame(other: MatDateSelectionModel<any>): boolean { |
149 | 160 | if (other instanceof MatRangeDateSelectionModel) {
|
150 |
| - return this._selection.start === other._selection.start && |
151 |
| - this._selection.end === other._selection.end; |
| 161 | + return this.adapter.sameDate(this.selection.start, other.selection.start) && |
| 162 | + this.adapter.sameDate(this.selection.end, other.selection.end); |
152 | 163 | }
|
153 | 164 | return false;
|
154 | 165 | }
|
155 | 166 |
|
| 167 | + /** |
| 168 | + * Checks whether the current selection is valid. In the case of a date range selection, this |
| 169 | + * means that the current selection has a `start` and `end` that are both non-null and valid |
| 170 | + * dates. |
| 171 | + */ |
| 172 | + isValid(): boolean { |
| 173 | + return this.selection.start != null && this.selection.end != null && |
| 174 | + this.adapter.isValid(this.selection.start!) && this.adapter.isValid(this.selection.end!); |
| 175 | + } |
| 176 | + |
156 | 177 | /**
|
157 | 178 | * Returns true if the given range and the selection overlap in any way. False if otherwise, that
|
158 | 179 | * includes incomplete selections or ranges.
|
159 | 180 | */
|
160 | 181 | overlaps(range: DateRange<D>): boolean {
|
161 |
| - const selectionStart = this._selection.start; |
162 |
| - const selectionEnd = this._selection.end; |
163 |
| - const rangeStart = range.start; |
164 |
| - const rangeEnd = range.end; |
165 |
| - if (!(selectionStart && selectionEnd && rangeStart && rangeEnd)) { |
| 182 | + if (!(this.selection.start && this.selection.end && range.start && range.end)) { |
166 | 183 | return false;
|
167 | 184 | }
|
168 | 185 |
|
169 | 186 | return (
|
170 |
| - this._isBetween(rangeStart, selectionStart, selectionEnd) || |
171 |
| - this._isBetween(rangeEnd, selectionStart, selectionEnd) || |
172 |
| - ( |
173 |
| - this.adapter.compareDate(rangeStart, selectionStart) <= 0 && |
174 |
| - this.adapter.compareDate(selectionEnd, rangeEnd) <= 0 |
175 |
| - ) |
| 187 | + this._isBetween(range.start, this.selection.start, this.selection.end) || |
| 188 | + this._isBetween(range.end, this.selection.start, this.selection.end) || |
| 189 | + ( |
| 190 | + this.adapter.compareDate(range.start, this.selection.start) <= 0 && |
| 191 | + this.adapter.compareDate(this.selection.end, range.end) <= 0 |
| 192 | + ) |
176 | 193 | );
|
177 | 194 | }
|
178 | 195 |
|
179 | 196 | private _isBetween(value: D, from: D, to: D): boolean {
|
180 | 197 | return this.adapter.compareDate(from, value) <= 0 && this.adapter.compareDate(value, to) <= 0;
|
181 | 198 | }
|
182 | 199 | }
|
| 200 | + |
| 201 | +export function MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY<D>(parent: |
| 202 | + MatSingleDateSelectionModel<D>, |
| 203 | + adapter: DateAdapter<D>) { |
| 204 | + return parent || new MatSingleDateSelectionModel(adapter); |
| 205 | +} |
| 206 | + |
| 207 | +export const MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER: FactoryProvider = { |
| 208 | + provide: MatDateSelectionModel, |
| 209 | + deps: [[new Optional(), new SkipSelf(), MatDateSelectionModel], DateAdapter], |
| 210 | + useFactory: MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY, |
| 211 | +}; |
0 commit comments