@emuanalytics/flow-rdf-ui 4.1.27 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/autocomplete/autocomplete.component.mjs +135 -28
- package/dist/components/calendar/calendar.component.mjs +534 -118
- package/dist/components/calendar/calendar.styles.mjs +142 -2
- package/dist/components/date-time-picker/date-time-picker.component.mjs +125 -9
- package/dist/components/query-builder/converters/flow-query-converters.mjs +21 -9
- package/dist/components/query-builder/converters/javascript-filter-converter.mjs +17 -1
- package/dist/components/query-builder/converters/map-layer-filter-converter.mjs +16 -1
- package/dist/components/query-builder/default-value-templates.mjs +68 -71
- package/dist/components/query-builder/helpers.mjs +18 -2
- package/dist/components/query-builder/query-builder.component.mjs +8 -3
- package/dist/custom-elements.json +3989 -3278
- package/dist/index.d.mts +288 -19
- package/dist/index.d.ts +288 -19
- package/dist/index.mjs +1 -1
- package/dist/internal/date-range.mjs +107 -0
- package/dist/internal/datetime.mjs +2 -13
- package/dist/package.json.mjs +1 -1
- package/dist/translations/en.mjs +5 -0
- package/dist/vscode.html-custom-data.json +348 -298
- package/package.json +3 -3
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { html, nothing } from 'lit';
|
|
2
2
|
import { query, state, property } from 'lit/decorators.js';
|
|
3
3
|
import { classMap } from 'lit/directives/class-map.js';
|
|
4
|
-
import {
|
|
4
|
+
import { live } from 'lit/directives/live.js';
|
|
5
|
+
import { Interval, DateTime, Info } from 'luxon';
|
|
6
|
+
import { parseDateLimits, endTimeOfRange, atTimeOfDay, parseDateRange, lastDayOfRange } from '../../internal/date-range.mjs';
|
|
5
7
|
import FlowRdfElement from '../../internal/flow-ui-rdf-element.mjs';
|
|
6
8
|
import { isStringFull, isDefined } from '../../internal/type-checks.mjs';
|
|
7
9
|
import { watch } from '../../internal/watch.mjs';
|
|
10
|
+
import { LocalizeController } from '../../utilities/localize.mjs';
|
|
8
11
|
import styles from './calendar.styles.mjs';
|
|
9
12
|
|
|
10
13
|
var __defProp = Object.defineProperty;
|
|
@@ -21,8 +24,13 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
21
24
|
class FlowUiCalendar extends FlowRdfElement {
|
|
22
25
|
constructor() {
|
|
23
26
|
super(...arguments);
|
|
27
|
+
this._startTime = { hour: 0, minute: 0, second: 0 };
|
|
28
|
+
this._endTime = { hour: 24, minute: 0, second: 0 };
|
|
29
|
+
this.localize = new LocalizeController(this);
|
|
24
30
|
this.displayMode = "date";
|
|
31
|
+
this.range = false;
|
|
25
32
|
this.size = "medium";
|
|
33
|
+
this.months = 1;
|
|
26
34
|
}
|
|
27
35
|
connectedCallback() {
|
|
28
36
|
super.connectedCallback();
|
|
@@ -30,23 +38,132 @@ class FlowUiCalendar extends FlowRdfElement {
|
|
|
30
38
|
}
|
|
31
39
|
disconnectedCallback() {
|
|
32
40
|
super.disconnectedCallback();
|
|
41
|
+
this.clearPendingRange();
|
|
42
|
+
}
|
|
43
|
+
/** The number of months shown. Anything other than `2` shows one month. */
|
|
44
|
+
get monthCount() {
|
|
45
|
+
return +this.months === 2 ? 2 : 1;
|
|
46
|
+
}
|
|
47
|
+
/** Whether the calendar selects a range: a date range, a datetime range or a time range. */
|
|
48
|
+
get isRangeMode() {
|
|
49
|
+
return this.range;
|
|
50
|
+
}
|
|
51
|
+
/** Whether the calendar selects a datetime range, with start and end times. */
|
|
52
|
+
get isDateTimeRangeMode() {
|
|
53
|
+
return this.isRangeMode && this.displayMode === "datetime";
|
|
54
|
+
}
|
|
55
|
+
/** Whether the calendar selects a time range: start and end times, with no days. */
|
|
56
|
+
get isTimeRangeMode() {
|
|
57
|
+
return this.isRangeMode && this.displayMode === "time";
|
|
58
|
+
}
|
|
59
|
+
/** The parsed `min`, `max` and `max-range`, in the calendar's timezone. There are no limits in time display mode. */
|
|
60
|
+
get limits() {
|
|
61
|
+
if (this.displayMode === "time") {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
return parseDateLimits(this.min, this.max, this.maxRange, { zone: this.timezone, locale: this.locale });
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether the user can't select `day` (the start of a day): it is outside `min` or `max`, or, while a range is
|
|
68
|
+
* pending, it would make the range longer than `max-range`.
|
|
69
|
+
*/
|
|
70
|
+
isDayDisabled(day, limits = this.limits) {
|
|
71
|
+
const { min, max, maxRange } = limits;
|
|
72
|
+
if (min && day < min.startOf("day") || max && day > max.startOf("day")) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
if (this.isRangeMode && this._pendingDay && maxRange) {
|
|
76
|
+
const [first, last] = day < this._pendingDay ? [day, this._pendingDay] : [this._pendingDay, day];
|
|
77
|
+
return first.plus(maxRange) < last.plus({ days: 1 }).startOf("day");
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
/** Clamp a date and time to `min` and `max`, so that the time inputs can't select a time outside the limits. */
|
|
82
|
+
clampToLimits(date) {
|
|
83
|
+
const { min, max } = this.limits;
|
|
84
|
+
if (min && date < min) {
|
|
85
|
+
return min;
|
|
86
|
+
}
|
|
87
|
+
return max && date > max ? max : date;
|
|
88
|
+
}
|
|
89
|
+
/** The first and last months the user can navigate to, as the start of each month, if `min` or `max` is set. */
|
|
90
|
+
get monthLimits() {
|
|
91
|
+
const { min, max } = this.limits;
|
|
92
|
+
const first = min?.startOf("month");
|
|
93
|
+
let last = max?.startOf("month").minus({ months: this.monthCount - 1 });
|
|
94
|
+
if (first && last && last < first) {
|
|
95
|
+
last = first;
|
|
96
|
+
}
|
|
97
|
+
return { first, last };
|
|
98
|
+
}
|
|
99
|
+
/** Clamp a month to the months the user can navigate to. */
|
|
100
|
+
clampMonth(month) {
|
|
101
|
+
const { first, last } = this.monthLimits;
|
|
102
|
+
if (first && month < first) {
|
|
103
|
+
return first;
|
|
104
|
+
}
|
|
105
|
+
return last && month > last ? last : month;
|
|
33
106
|
}
|
|
34
107
|
handleValueChange() {
|
|
108
|
+
if (this.isRangeMode) {
|
|
109
|
+
this.handleRangeValueChange();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this._selectedRange = void 0;
|
|
113
|
+
this.clearPendingRange();
|
|
35
114
|
this._selectedDate = this.makeDate(this.value)?.set({ millisecond: 0 });
|
|
36
115
|
if (this._selectedDate) {
|
|
37
116
|
if (this.displayMode === "date") {
|
|
38
117
|
this._selectedDate = this._selectedDate.startOf("day");
|
|
39
118
|
}
|
|
40
|
-
this.
|
|
119
|
+
this.showMonthOf(this._selectedDate);
|
|
41
120
|
} else {
|
|
42
|
-
this.
|
|
121
|
+
this.showMonthOf(this.makeDate());
|
|
43
122
|
}
|
|
44
123
|
}
|
|
124
|
+
handleRangeValueChange() {
|
|
125
|
+
const range = this.makeRange(this.value);
|
|
126
|
+
this._selectedDate = void 0;
|
|
127
|
+
if (range && this._selectedRange && +range.start === +this._selectedRange.start && +range.end === +this._selectedRange.end) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
this._selectedRange = range;
|
|
131
|
+
this.clearPendingRange();
|
|
132
|
+
this.showMonthOf(range ? range.start : this.makeDate());
|
|
133
|
+
this._startTime = range ? { hour: range.start.hour, minute: range.start.minute, second: range.start.second } : { hour: 0, minute: 0, second: 0 };
|
|
134
|
+
this._endTime = range ? endTimeOfRange(range) : { hour: 24, minute: 0, second: 0 };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Show the month of `date` as the first month. In the two-month view, a date in the second month is already shown,
|
|
138
|
+
* so the display doesn't move.
|
|
139
|
+
*/
|
|
140
|
+
showMonthOf(date) {
|
|
141
|
+
const month = date.startOf("month");
|
|
142
|
+
const isSecondMonth = this.monthCount === 2 && !!this._displayDate?.plus({ months: 1 }).hasSame(month, "month");
|
|
143
|
+
this._displayDate = isSecondMonth ? month.minus({ months: 1 }) : month;
|
|
144
|
+
}
|
|
45
145
|
handleTimezoneChange() {
|
|
146
|
+
this._selectedRange = void 0;
|
|
46
147
|
this.handleValueChange();
|
|
47
148
|
this.updateValue();
|
|
48
149
|
}
|
|
150
|
+
handleRangeChange() {
|
|
151
|
+
this._selectedRange = void 0;
|
|
152
|
+
this.handleValueChange();
|
|
153
|
+
}
|
|
154
|
+
handleDisplayModeChange() {
|
|
155
|
+
if (this.range) {
|
|
156
|
+
this.handleRangeChange();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
49
159
|
dateClicked(val) {
|
|
160
|
+
if (this.isDayDisabled(this.makeDate(val).startOf("day"))) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (this.isRangeMode) {
|
|
164
|
+
this.rangeDateClicked(val);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
50
167
|
const date = this.makeDate(val);
|
|
51
168
|
if (this._selectedDate) {
|
|
52
169
|
this._selectedDate = date.set({
|
|
@@ -57,17 +174,111 @@ class FlowUiCalendar extends FlowRdfElement {
|
|
|
57
174
|
} else {
|
|
58
175
|
this._selectedDate = date;
|
|
59
176
|
}
|
|
177
|
+
if (this.displayMode === "datetime") {
|
|
178
|
+
this._selectedDate = this.clampToLimits(this._selectedDate);
|
|
179
|
+
}
|
|
60
180
|
this.updateValue();
|
|
61
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Handle a day click in range mode. The first click sets the pending day. The second click completes the range,
|
|
184
|
+
* whichever order the two days were clicked in, so the user can click the last day first and then go back to the
|
|
185
|
+
* range start.
|
|
186
|
+
*/
|
|
187
|
+
rangeDateClicked(val) {
|
|
188
|
+
const day = this.makeDate(val).startOf("day");
|
|
189
|
+
if (!this._pendingDay) {
|
|
190
|
+
if (this.isSingleDayOnly(day)) {
|
|
191
|
+
this.completeRange(day, day);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this._pendingDay = day;
|
|
195
|
+
this._hoverDate = day;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const [first, last] = day < this._pendingDay ? [day, this._pendingDay] : [this._pendingDay, day];
|
|
199
|
+
this.completeRange(first, last);
|
|
200
|
+
}
|
|
201
|
+
/** Whether `max-range` only allows ranges of one day starting on `day`, so that a range is a single day. */
|
|
202
|
+
isSingleDayOnly(day) {
|
|
203
|
+
const { maxRange } = this.limits;
|
|
204
|
+
return !!maxRange && day.plus(maxRange) < day.plus({ days: 2 }).startOf("day");
|
|
205
|
+
}
|
|
206
|
+
/** Complete a range from `first` to `last` (both the start of a day) and update the value. */
|
|
207
|
+
completeRange(first, last) {
|
|
208
|
+
const range = this.isDateTimeRangeMode ? this.makeDateTimeRange(first, last) : Interval.fromDateTimes(first, last.plus({ days: 1 }).startOf("day"));
|
|
209
|
+
this.clearPendingRange();
|
|
210
|
+
if (range.isValid) {
|
|
211
|
+
this._selectedRange = range;
|
|
212
|
+
if (this.isDateTimeRangeMode) {
|
|
213
|
+
this._startTime = { hour: range.start.hour, minute: range.start.minute, second: range.start.second };
|
|
214
|
+
this._endTime = endTimeOfRange(range);
|
|
215
|
+
}
|
|
216
|
+
this.updateValue();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The datetime range from the start time on `first` to the end time on `last` (both the start of a day), clamped to
|
|
221
|
+
* `min` and `max`. If the times would make the range empty, for example an end time before the start time on a
|
|
222
|
+
* single day, the end time is reset to 24:00:00. If the range would be longer than `max-range`, the end is moved
|
|
223
|
+
* back.
|
|
224
|
+
*/
|
|
225
|
+
makeDateTimeRange(first, last) {
|
|
226
|
+
const start = this.clampToLimits(atTimeOfDay(first, this._startTime));
|
|
227
|
+
let end = this.clampToLimits(atTimeOfDay(last, this._endTime));
|
|
228
|
+
const { maxRange } = this.limits;
|
|
229
|
+
if (end <= start) {
|
|
230
|
+
end = this.clampToLimits(atTimeOfDay(last, { hour: 24, minute: 0, second: 0 }));
|
|
231
|
+
}
|
|
232
|
+
if (maxRange && start.plus(maxRange) < end) {
|
|
233
|
+
end = start.plus(maxRange);
|
|
234
|
+
}
|
|
235
|
+
return Interval.fromDateTimes(start, end);
|
|
236
|
+
}
|
|
237
|
+
dayHovered(val) {
|
|
238
|
+
const day = this.makeDate(val).startOf("day");
|
|
239
|
+
if (this._pendingDay && !this.isDayDisabled(day)) {
|
|
240
|
+
this._hoverDate = day;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Discard a pending range, so that the user can click a new range start. The value is unchanged. Returns `true` if a
|
|
245
|
+
* range was pending.
|
|
246
|
+
*
|
|
247
|
+
* @internal
|
|
248
|
+
*/
|
|
249
|
+
cancelPendingRange() {
|
|
250
|
+
const wasPending = !!this._pendingDay;
|
|
251
|
+
this.clearPendingRange();
|
|
252
|
+
return wasPending;
|
|
253
|
+
}
|
|
254
|
+
handleKeyDown(event) {
|
|
255
|
+
if (event.key === "Escape" && this.cancelPendingRange()) {
|
|
256
|
+
event.preventDefault();
|
|
257
|
+
event.stopPropagation();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
clearPendingRange() {
|
|
261
|
+
this._pendingDay = void 0;
|
|
262
|
+
this._hoverDate = void 0;
|
|
263
|
+
}
|
|
62
264
|
stepDisplayDate(duration) {
|
|
63
|
-
this._displayDate = this._displayDate.plus(duration);
|
|
265
|
+
this._displayDate = this.clampMonth(this._displayDate.plus(duration));
|
|
64
266
|
}
|
|
65
267
|
/**
|
|
66
|
-
* Generate a list of years for the year select dropdown
|
|
268
|
+
* Generate a list of years for the year select dropdown, newest first. With no limits, the years are from next year
|
|
269
|
+
* to six years ago. With `min` or `max`, the list starts or ends at their years, and always includes the year shown.
|
|
67
270
|
*/
|
|
68
|
-
|
|
271
|
+
yearOptions() {
|
|
272
|
+
const { min, max } = this.limits;
|
|
273
|
+
const thisYear = DateTime.now().year;
|
|
274
|
+
let first = min?.year ?? Math.min(thisYear, max?.year ?? thisYear) - 6;
|
|
275
|
+
let last = max?.year ?? Math.max(thisYear, min?.year ?? thisYear) + 1;
|
|
276
|
+
if (min || max) {
|
|
277
|
+
first = Math.min(first, this._displayDate.year);
|
|
278
|
+
last = Math.max(last, this._displayDate.year);
|
|
279
|
+
}
|
|
69
280
|
const years = [];
|
|
70
|
-
for (let y =
|
|
281
|
+
for (let y = last; y >= first; y--) {
|
|
71
282
|
years.push(y);
|
|
72
283
|
}
|
|
73
284
|
return years;
|
|
@@ -88,8 +299,33 @@ class FlowUiCalendar extends FlowRdfElement {
|
|
|
88
299
|
return void 0;
|
|
89
300
|
}
|
|
90
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Parse an ISO8601 interval string. In date mode, the interval is widened to whole days in the calendar's timezone:
|
|
304
|
+
* the start is rounded down to the start of its day and the end is rounded up to the start of the next day. An
|
|
305
|
+
* invalid or empty interval gives `undefined`.
|
|
306
|
+
*/
|
|
307
|
+
makeRange(value) {
|
|
308
|
+
const wholeDays = this.displayMode === "date";
|
|
309
|
+
return parseDateRange(value, { zone: this.timezone, locale: this.locale, wholeDays });
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* The days to highlight in range mode: the hover preview while a range is pending, otherwise the selected range.
|
|
313
|
+
*/
|
|
314
|
+
highlightedDays() {
|
|
315
|
+
if (this._pendingDay) {
|
|
316
|
+
const hover = this._hoverDate ?? this._pendingDay;
|
|
317
|
+
return hover < this._pendingDay ? { first: hover, last: this._pendingDay } : { first: this._pendingDay, last: hover };
|
|
318
|
+
}
|
|
319
|
+
if (this._selectedRange) {
|
|
320
|
+
return {
|
|
321
|
+
first: this._selectedRange.start.startOf("day"),
|
|
322
|
+
last: lastDayOfRange(this._selectedRange)
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return void 0;
|
|
326
|
+
}
|
|
91
327
|
updateValue(emitEvents = true) {
|
|
92
|
-
const iso = this._selectedDate?.toISO();
|
|
328
|
+
const iso = this.isRangeMode ? this._selectedRange?.toISO() : this._selectedDate?.toISO();
|
|
93
329
|
this.value = isDefined(iso) ? iso : "";
|
|
94
330
|
if (emitEvents) {
|
|
95
331
|
this.emit("flow-ui-input");
|
|
@@ -98,10 +334,12 @@ class FlowUiCalendar extends FlowRdfElement {
|
|
|
98
334
|
}
|
|
99
335
|
handleHeaderSelectChange(ev) {
|
|
100
336
|
ev.stopPropagation();
|
|
101
|
-
this._displayDate = this.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
337
|
+
this._displayDate = this.clampMonth(
|
|
338
|
+
this.makeDate().set({
|
|
339
|
+
year: +this._yearSelect.value,
|
|
340
|
+
month: +this._monthSelect.value
|
|
341
|
+
}).startOf("month")
|
|
342
|
+
);
|
|
105
343
|
}
|
|
106
344
|
handleTimeChange(ev) {
|
|
107
345
|
ev.stopPropagation();
|
|
@@ -111,45 +349,88 @@ class FlowUiCalendar extends FlowRdfElement {
|
|
|
111
349
|
const target = ev.target;
|
|
112
350
|
const unit = target.getAttribute("name");
|
|
113
351
|
this._selectedDate = this._selectedDate.set({ [unit]: +target.value });
|
|
352
|
+
if (this.displayMode === "datetime") {
|
|
353
|
+
this._selectedDate = this.clampToLimits(this._selectedDate);
|
|
354
|
+
}
|
|
114
355
|
this.updateValue();
|
|
115
356
|
}
|
|
116
|
-
|
|
357
|
+
/**
|
|
358
|
+
* Handle a change in the Start or End row of a datetime range or a time range. In a datetime range, the start time
|
|
359
|
+
* applies to the range start and the end time to the last day. The edited time can't pass the other one, so the range
|
|
360
|
+
* is never empty.
|
|
361
|
+
*/
|
|
362
|
+
handleRangeTimeChange(ev, row) {
|
|
363
|
+
ev.stopPropagation();
|
|
364
|
+
const target = ev.target;
|
|
365
|
+
const unit = target.getAttribute("name");
|
|
366
|
+
const time = { ...row === "start" ? this._startTime : this._endTime, [unit]: +target.value };
|
|
367
|
+
const endOfDay = { hour: 24, minute: 0, second: 0 };
|
|
368
|
+
if (row === "start") {
|
|
369
|
+
this._startTime = time;
|
|
370
|
+
} else {
|
|
371
|
+
this._endTime = time.hour >= 24 ? endOfDay : time;
|
|
372
|
+
}
|
|
373
|
+
let first;
|
|
374
|
+
let last;
|
|
375
|
+
if (this.isTimeRangeMode) {
|
|
376
|
+
first = last = (this._selectedRange?.start ?? this.makeDate()).startOf("day");
|
|
377
|
+
} else if (this._selectedRange && !this._pendingDay) {
|
|
378
|
+
first = this._selectedRange.start.startOf("day");
|
|
379
|
+
last = lastDayOfRange(this._selectedRange);
|
|
380
|
+
} else {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
let start = this.clampToLimits(atTimeOfDay(first, this._startTime));
|
|
384
|
+
let end = this.clampToLimits(atTimeOfDay(last, this._endTime));
|
|
385
|
+
if (end <= start) {
|
|
386
|
+
if (row === "start") {
|
|
387
|
+
start = end.minus({ seconds: 1 });
|
|
388
|
+
} else {
|
|
389
|
+
end = start.plus({ seconds: 1 });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const { maxRange } = this.limits;
|
|
393
|
+
if (maxRange && start.plus(maxRange) < end) {
|
|
394
|
+
if (row === "start") {
|
|
395
|
+
start = end.minus(maxRange);
|
|
396
|
+
} else {
|
|
397
|
+
end = start.plus(maxRange);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const range = Interval.fromDateTimes(start, end);
|
|
401
|
+
if (range.isValid) {
|
|
402
|
+
this._selectedRange = range;
|
|
403
|
+
this._startTime = { hour: start.hour, minute: start.minute, second: start.second };
|
|
404
|
+
this._endTime = endTimeOfRange(range);
|
|
405
|
+
this.updateValue();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* The header for the month at `index`. The first month has the month and year selects and the second month has a
|
|
410
|
+
* static label. The previous button is on the first month and the next button on the last. In the two-month view, a
|
|
411
|
+
* hidden button takes the place of each missing button, so the month label stays centered.
|
|
412
|
+
*/
|
|
413
|
+
calendarHeaderTemplate(month, index) {
|
|
117
414
|
const locale = isStringFull(this.locale) ? this.locale : void 0;
|
|
118
415
|
const monthsShort = Info.months("short", { locale });
|
|
416
|
+
const isFirst = index === 0;
|
|
417
|
+
const isLast = index === this.monthCount - 1;
|
|
418
|
+
const { first, last } = this.monthLimits;
|
|
119
419
|
return html`
|
|
120
420
|
<div class="calendar__header">
|
|
121
421
|
<flow-ui-icon-button
|
|
422
|
+
class=${classMap({ "calendar__header-button--hidden": !isFirst })}
|
|
423
|
+
?disabled=${!!first && this._displayDate <= first}
|
|
122
424
|
name="chevron-left"
|
|
123
425
|
library="system"
|
|
124
426
|
@click=${() => this.stepDisplayDate({ month: -1 })}
|
|
125
427
|
></flow-ui-icon-button>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
class="calendar__month-select calendar__header-select"
|
|
130
|
-
value=${this._displayDate.month}
|
|
131
|
-
@flow-ui-change=${this.handleHeaderSelectChange}
|
|
132
|
-
>
|
|
133
|
-
${monthsShort.map(
|
|
134
|
-
(month, index) => html`
|
|
135
|
-
<flow-ui-option value=${index + 1}>${month}</flow-ui-option>
|
|
136
|
-
`
|
|
137
|
-
)}
|
|
138
|
-
</flow-ui-select>
|
|
139
|
-
<flow-ui-select
|
|
140
|
-
inline
|
|
141
|
-
sync="width"
|
|
142
|
-
class="calendar__year-select calendar__header-select"
|
|
143
|
-
value=${this._displayDate.year}
|
|
144
|
-
@flow-ui-change=${this.handleHeaderSelectChange}
|
|
145
|
-
>
|
|
146
|
-
${this.yearOptionsForDate(DateTime.now()).map(
|
|
147
|
-
(year) => html`
|
|
148
|
-
<flow-ui-option value=${year}>${year}</flow-ui-option>
|
|
149
|
-
`
|
|
150
|
-
)}
|
|
151
|
-
</flow-ui-select>
|
|
428
|
+
${isFirst ? this.headerSelectsTemplate(monthsShort) : html`
|
|
429
|
+
<span class="calendar__header-month">${monthsShort[month.month - 1]} ${month.year}</span>
|
|
430
|
+
`}
|
|
152
431
|
<flow-ui-icon-button
|
|
432
|
+
class=${classMap({ "calendar__header-button--hidden": !isLast })}
|
|
433
|
+
?disabled=${!!last && this._displayDate >= last}
|
|
153
434
|
name="chevron-right"
|
|
154
435
|
library="system"
|
|
155
436
|
@click=${() => this.stepDisplayDate({ month: 1 })}
|
|
@@ -157,6 +438,42 @@ class FlowUiCalendar extends FlowRdfElement {
|
|
|
157
438
|
</div>
|
|
158
439
|
`;
|
|
159
440
|
}
|
|
441
|
+
headerSelectsTemplate(monthsShort) {
|
|
442
|
+
return html`
|
|
443
|
+
<flow-ui-select
|
|
444
|
+
inline
|
|
445
|
+
sync="width"
|
|
446
|
+
class="calendar__month-select calendar__header-select"
|
|
447
|
+
value=${this._displayDate.month}
|
|
448
|
+
@flow-ui-change=${this.handleHeaderSelectChange}
|
|
449
|
+
>
|
|
450
|
+
${monthsShort.map(
|
|
451
|
+
(month, index) => html`
|
|
452
|
+
<flow-ui-option value=${index + 1} ?disabled=${this.isMonthDisabled(index + 1)}>${month}</flow-ui-option>
|
|
453
|
+
`
|
|
454
|
+
)}
|
|
455
|
+
</flow-ui-select>
|
|
456
|
+
<flow-ui-select
|
|
457
|
+
inline
|
|
458
|
+
sync="width"
|
|
459
|
+
class="calendar__year-select calendar__header-select"
|
|
460
|
+
value=${this._displayDate.year}
|
|
461
|
+
@flow-ui-change=${this.handleHeaderSelectChange}
|
|
462
|
+
>
|
|
463
|
+
${this.yearOptions().map(
|
|
464
|
+
(year) => html`
|
|
465
|
+
<flow-ui-option value=${year}>${year}</flow-ui-option>
|
|
466
|
+
`
|
|
467
|
+
)}
|
|
468
|
+
</flow-ui-select>
|
|
469
|
+
`;
|
|
470
|
+
}
|
|
471
|
+
/** Whether a month of the year shown is outside the months the user can navigate to. */
|
|
472
|
+
isMonthDisabled(month) {
|
|
473
|
+
const { first, last } = this.monthLimits;
|
|
474
|
+
const date = this._displayDate.set({ month });
|
|
475
|
+
return !!first && date < first || !!last && date > last;
|
|
476
|
+
}
|
|
160
477
|
dayOfWeekGridTemplate() {
|
|
161
478
|
const locale = isStringFull(this.locale) ? this.locale : void 0;
|
|
162
479
|
const days = Info.weekdays("narrow", { locale });
|
|
@@ -168,102 +485,162 @@ class FlowUiCalendar extends FlowRdfElement {
|
|
|
168
485
|
)}
|
|
169
486
|
`;
|
|
170
487
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
488
|
+
/**
|
|
489
|
+
* The days shown in the grid for a month, including the filler days from the adjacent months that complete the
|
|
490
|
+
* first and last weeks. In the two-month view, the filler days between the two months are hidden, because the other
|
|
491
|
+
* month shows them.
|
|
492
|
+
*/
|
|
493
|
+
daysForMonth(month, hideLeadingFiller = false, hideTrailingFiller = false) {
|
|
494
|
+
const today = this.makeDate().startOf("day");
|
|
495
|
+
const highlighted = this.isRangeMode ? this.highlightedDays() : void 0;
|
|
496
|
+
const limits = this.limits;
|
|
497
|
+
const startOfMonth = month.startOf("month");
|
|
498
|
+
const endOfMonth = month.endOf("month");
|
|
175
499
|
const calendarStart = startOfMonth.startOf("week");
|
|
176
500
|
const calendarEnd = endOfMonth.endOf("week");
|
|
177
|
-
const daysInMonth = this._displayDate.daysInMonth;
|
|
178
501
|
const days = [];
|
|
179
|
-
for (let date = calendarStart; date <
|
|
180
|
-
days.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
isFiller: true,
|
|
184
|
-
today: date === today,
|
|
185
|
-
selected: !!this._selectedDate && date.startOf("day").equals(this._selectedDate.startOf("day"))
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
for (let day = 1; day <= daysInMonth; day++) {
|
|
189
|
-
const date = startOfMonth.plus({ days: day - 1 }).startOf("day");
|
|
190
|
-
days.push({
|
|
191
|
-
day: date.toFormat("d"),
|
|
192
|
-
date: date.toISO(),
|
|
193
|
-
isFiller: false,
|
|
194
|
-
today: date.startOf("day").equals(today.startOf("day")),
|
|
195
|
-
selected: !!this._selectedDate && date.startOf("day").equals(this._selectedDate.startOf("day"))
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
for (let date = endOfMonth.plus({ day: 1 }).startOf("day"); date < calendarEnd; date = date.plus({ day: 1 })) {
|
|
502
|
+
for (let date = calendarStart; date < calendarEnd; date = date.plus({ days: 1 }).startOf("day")) {
|
|
503
|
+
const column = days.length % 7;
|
|
504
|
+
const hidden = hideLeadingFiller && date < startOfMonth || hideTrailingFiller && date > endOfMonth;
|
|
505
|
+
const inRange = !hidden && !!highlighted && date >= highlighted.first && date <= highlighted.last;
|
|
199
506
|
days.push({
|
|
200
507
|
day: date.toFormat("d"),
|
|
201
508
|
date: date.toISO(),
|
|
202
|
-
isFiller:
|
|
203
|
-
|
|
204
|
-
|
|
509
|
+
isFiller: date < startOfMonth || date > endOfMonth,
|
|
510
|
+
hidden,
|
|
511
|
+
today: date.equals(today),
|
|
512
|
+
disabled: this.isDayDisabled(date, limits),
|
|
513
|
+
selected: !this.isRangeMode && !!this._selectedDate && date.equals(this._selectedDate.startOf("day")),
|
|
514
|
+
rangeStart: inRange && +date === +highlighted.first,
|
|
515
|
+
rangeLast: inRange && +date === +highlighted.last,
|
|
516
|
+
inRange,
|
|
517
|
+
// The range band also has rounded ends next to the hidden days
|
|
518
|
+
rowStart: column === 0 || hideLeadingFiller && +date === +startOfMonth,
|
|
519
|
+
rowEnd: column === 6 || hideTrailingFiller && +date === +endOfMonth.startOf("day")
|
|
205
520
|
});
|
|
206
521
|
}
|
|
522
|
+
return days;
|
|
523
|
+
}
|
|
524
|
+
dayGridTemplate(month, index) {
|
|
525
|
+
const days = this.daysForMonth(month, index > 0, index < this.monthCount - 1);
|
|
207
526
|
return html`
|
|
208
527
|
${days.map(
|
|
209
|
-
(day) => html`
|
|
210
|
-
|
|
211
|
-
|
|
528
|
+
(day) => day.hidden ? html`
|
|
529
|
+
<span class="calendar__day-placeholder"></span>
|
|
530
|
+
` : html`
|
|
531
|
+
<button
|
|
532
|
+
class=${classMap({
|
|
212
533
|
calendar__day: true,
|
|
213
534
|
"calendar__day--filler": day.isFiller,
|
|
214
535
|
"calendar__day--today": day.today,
|
|
215
|
-
"calendar__day--
|
|
536
|
+
"calendar__day--disabled": day.disabled,
|
|
537
|
+
"calendar__day--selected": day.selected,
|
|
538
|
+
"calendar__day--range-start": day.rangeStart,
|
|
539
|
+
"calendar__day--range-last": day.rangeLast,
|
|
540
|
+
"calendar__day--in-range": day.inRange,
|
|
541
|
+
"calendar__day--range-preview": day.inRange && !!this._pendingDay,
|
|
542
|
+
"calendar__day--row-start": day.rowStart,
|
|
543
|
+
"calendar__day--row-end": day.rowEnd
|
|
216
544
|
})}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
545
|
+
value=${day.date}
|
|
546
|
+
?disabled=${day.disabled}
|
|
547
|
+
@click=${() => this.dateClicked(day.date)}
|
|
548
|
+
@mouseenter=${() => this.dayHovered(day.date)}
|
|
549
|
+
@focus=${() => this.dayHovered(day.date)}
|
|
550
|
+
>
|
|
551
|
+
${day.day}
|
|
552
|
+
</button>
|
|
553
|
+
`
|
|
223
554
|
)}
|
|
224
555
|
`;
|
|
225
556
|
}
|
|
557
|
+
monthTemplate(index) {
|
|
558
|
+
const month = this._displayDate.plus({ months: index });
|
|
559
|
+
return html`
|
|
560
|
+
<div class="calendar__month">
|
|
561
|
+
${this.calendarHeaderTemplate(month, index)}
|
|
562
|
+
<div class="calendar__grid" @mouseleave=${this.handleGridMouseLeave}>
|
|
563
|
+
${this.dayOfWeekGridTemplate()} ${this.dayGridTemplate(month, index)}
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
`;
|
|
567
|
+
}
|
|
568
|
+
handleGridMouseLeave() {
|
|
569
|
+
if (this._pendingDay) {
|
|
570
|
+
this._hoverDate = void 0;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/** The hour, minute and second inputs for `time`. */
|
|
574
|
+
timeInputsTemplate(time, onChange, maxHour = 23) {
|
|
575
|
+
const size = this.size === "small" ? "small" : "medium";
|
|
576
|
+
return html`
|
|
577
|
+
<flow-ui-input
|
|
578
|
+
filled
|
|
579
|
+
type="number"
|
|
580
|
+
size=${size}
|
|
581
|
+
name="hour"
|
|
582
|
+
min="0"
|
|
583
|
+
max=${maxHour}
|
|
584
|
+
format="02"
|
|
585
|
+
class="calendar__time-input"
|
|
586
|
+
.value=${live(String(time.hour))}
|
|
587
|
+
@flow-ui-change=${onChange}
|
|
588
|
+
></flow-ui-input>
|
|
589
|
+
<span class="calendar__time-separator">:</span>
|
|
590
|
+
<flow-ui-input
|
|
591
|
+
filled
|
|
592
|
+
type="number"
|
|
593
|
+
size=${size}
|
|
594
|
+
name="minute"
|
|
595
|
+
min="0"
|
|
596
|
+
max="59"
|
|
597
|
+
format="02"
|
|
598
|
+
class="calendar__time-input"
|
|
599
|
+
.value=${live(String(time.minute))}
|
|
600
|
+
@flow-ui-change=${onChange}
|
|
601
|
+
></flow-ui-input>
|
|
602
|
+
<span class="calendar__time-separator">:</span>
|
|
603
|
+
<flow-ui-input
|
|
604
|
+
filled
|
|
605
|
+
type="number"
|
|
606
|
+
size=${size}
|
|
607
|
+
name="second"
|
|
608
|
+
min="0"
|
|
609
|
+
max="59"
|
|
610
|
+
format="02"
|
|
611
|
+
class="calendar__time-input"
|
|
612
|
+
.value=${live(String(time.second))}
|
|
613
|
+
@flow-ui-change=${onChange}
|
|
614
|
+
></flow-ui-input>
|
|
615
|
+
`;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* The time inputs: one row in single mode, or the Start and End rows of a datetime range or a time range. The Start
|
|
619
|
+
* and End rows are stacked, except in a datetime range in the two-month view.
|
|
620
|
+
*/
|
|
226
621
|
timeTemplate() {
|
|
622
|
+
if (this.isDateTimeRangeMode || this.isTimeRangeMode) {
|
|
623
|
+
const isSplit = this.isDateTimeRangeMode && this.monthCount === 2;
|
|
624
|
+
return html`
|
|
625
|
+
<div class=${classMap({ "calendar__time-rows": true, "calendar__time-rows--split": isSplit })}>
|
|
626
|
+
<div class="calendar__time-container calendar__time-container--range">
|
|
627
|
+
<span class="calendar__time-label">${this.localize.term("rangeStartTime")}</span>
|
|
628
|
+
${this.timeInputsTemplate(this._startTime, (ev) => this.handleRangeTimeChange(ev, "start"))}
|
|
629
|
+
</div>
|
|
630
|
+
<div class="calendar__time-container calendar__time-container--range">
|
|
631
|
+
<span class="calendar__time-label">${this.localize.term("rangeEndTime")}</span>
|
|
632
|
+
${this.timeInputsTemplate(this._endTime, (ev) => this.handleRangeTimeChange(ev, "end"), 24)}
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
`;
|
|
636
|
+
}
|
|
637
|
+
const date = this._selectedDate;
|
|
227
638
|
return html`
|
|
228
639
|
<div class="calendar__time-container">
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
name="hour"
|
|
234
|
-
min="0"
|
|
235
|
-
max="23"
|
|
236
|
-
format="02"
|
|
237
|
-
class="calendar__time-input"
|
|
238
|
-
value=${this._selectedDate ? this._selectedDate.hour : 0}
|
|
239
|
-
@flow-ui-change=${this.handleTimeChange}
|
|
240
|
-
></flow-ui-input>
|
|
241
|
-
<span class="calendar__time-separator">:</span>
|
|
242
|
-
<flow-ui-input
|
|
243
|
-
filled
|
|
244
|
-
type="number"
|
|
245
|
-
size=${this.size === "small" ? "small" : "medium"}
|
|
246
|
-
name="minute"
|
|
247
|
-
min="0"
|
|
248
|
-
max="59"
|
|
249
|
-
format="02"
|
|
250
|
-
class="calendar__time-input"
|
|
251
|
-
value=${this._selectedDate ? this._selectedDate.minute : 0}
|
|
252
|
-
@flow-ui-change=${this.handleTimeChange}
|
|
253
|
-
></flow-ui-input>
|
|
254
|
-
<span class="calendar__time-separator">:</span>
|
|
255
|
-
<flow-ui-input
|
|
256
|
-
filled
|
|
257
|
-
type="number"
|
|
258
|
-
size=${this.size === "small" ? "small" : "medium"}
|
|
259
|
-
name="second"
|
|
260
|
-
min="0"
|
|
261
|
-
max="59"
|
|
262
|
-
format="02"
|
|
263
|
-
class="calendar__time-input"
|
|
264
|
-
value=${this._selectedDate ? this._selectedDate.second : 0}
|
|
265
|
-
@flow-ui-change=${this.handleTimeChange}
|
|
266
|
-
></flow-ui-input>
|
|
640
|
+
${this.timeInputsTemplate(
|
|
641
|
+
{ hour: date?.hour ?? 0, minute: date?.minute ?? 0, second: date?.second ?? 0 },
|
|
642
|
+
this.handleTimeChange
|
|
643
|
+
)}
|
|
267
644
|
</div>
|
|
268
645
|
`;
|
|
269
646
|
}
|
|
@@ -273,16 +650,19 @@ class FlowUiCalendar extends FlowRdfElement {
|
|
|
273
650
|
return html`
|
|
274
651
|
<div
|
|
275
652
|
part="base"
|
|
653
|
+
@keydown=${this.handleKeyDown}
|
|
276
654
|
class=${classMap({
|
|
277
655
|
calendar: true,
|
|
278
656
|
"calendar--small": this.size === "small",
|
|
279
657
|
"calendar--medium": this.size === "medium",
|
|
280
|
-
"calendar--large": this.size === "large"
|
|
658
|
+
"calendar--large": this.size === "large",
|
|
659
|
+
"calendar--two-months": this.monthCount === 2 && showCalendar
|
|
281
660
|
})}
|
|
282
661
|
>
|
|
283
662
|
${showCalendar ? html`
|
|
284
|
-
|
|
285
|
-
|
|
663
|
+
<div class="calendar__months">
|
|
664
|
+
${Array.from({ length: this.monthCount }, (_, index) => this.monthTemplate(index))}
|
|
665
|
+
</div>
|
|
286
666
|
` : nothing}
|
|
287
667
|
${showTime ? this.timeTemplate() : nothing}
|
|
288
668
|
</div>
|
|
@@ -302,12 +682,30 @@ __decorateClass([
|
|
|
302
682
|
__decorateClass([
|
|
303
683
|
state()
|
|
304
684
|
], FlowUiCalendar.prototype, "_selectedDate", 2);
|
|
685
|
+
__decorateClass([
|
|
686
|
+
state()
|
|
687
|
+
], FlowUiCalendar.prototype, "_selectedRange", 2);
|
|
688
|
+
__decorateClass([
|
|
689
|
+
state()
|
|
690
|
+
], FlowUiCalendar.prototype, "_pendingDay", 2);
|
|
691
|
+
__decorateClass([
|
|
692
|
+
state()
|
|
693
|
+
], FlowUiCalendar.prototype, "_hoverDate", 2);
|
|
694
|
+
__decorateClass([
|
|
695
|
+
state()
|
|
696
|
+
], FlowUiCalendar.prototype, "_startTime", 2);
|
|
697
|
+
__decorateClass([
|
|
698
|
+
state()
|
|
699
|
+
], FlowUiCalendar.prototype, "_endTime", 2);
|
|
305
700
|
__decorateClass([
|
|
306
701
|
property({ reflect: true })
|
|
307
702
|
], FlowUiCalendar.prototype, "value", 2);
|
|
308
703
|
__decorateClass([
|
|
309
704
|
property({ attribute: "display-mode" })
|
|
310
705
|
], FlowUiCalendar.prototype, "displayMode", 2);
|
|
706
|
+
__decorateClass([
|
|
707
|
+
property({ type: Boolean, reflect: true })
|
|
708
|
+
], FlowUiCalendar.prototype, "range", 2);
|
|
311
709
|
__decorateClass([
|
|
312
710
|
property()
|
|
313
711
|
], FlowUiCalendar.prototype, "timezone", 2);
|
|
@@ -317,11 +715,29 @@ __decorateClass([
|
|
|
317
715
|
__decorateClass([
|
|
318
716
|
property()
|
|
319
717
|
], FlowUiCalendar.prototype, "size", 2);
|
|
718
|
+
__decorateClass([
|
|
719
|
+
property({ type: Number })
|
|
720
|
+
], FlowUiCalendar.prototype, "months", 2);
|
|
721
|
+
__decorateClass([
|
|
722
|
+
property()
|
|
723
|
+
], FlowUiCalendar.prototype, "min", 2);
|
|
724
|
+
__decorateClass([
|
|
725
|
+
property()
|
|
726
|
+
], FlowUiCalendar.prototype, "max", 2);
|
|
727
|
+
__decorateClass([
|
|
728
|
+
property({ attribute: "max-range" })
|
|
729
|
+
], FlowUiCalendar.prototype, "maxRange", 2);
|
|
320
730
|
__decorateClass([
|
|
321
731
|
watch("value", { waitUntilFirstUpdate: true })
|
|
322
732
|
], FlowUiCalendar.prototype, "handleValueChange", 1);
|
|
323
733
|
__decorateClass([
|
|
324
734
|
watch(["timezone"], { waitUntilFirstUpdate: true })
|
|
325
735
|
], FlowUiCalendar.prototype, "handleTimezoneChange", 1);
|
|
736
|
+
__decorateClass([
|
|
737
|
+
watch("range", { waitUntilFirstUpdate: true })
|
|
738
|
+
], FlowUiCalendar.prototype, "handleRangeChange", 1);
|
|
739
|
+
__decorateClass([
|
|
740
|
+
watch("displayMode", { waitUntilFirstUpdate: true })
|
|
741
|
+
], FlowUiCalendar.prototype, "handleDisplayModeChange", 1);
|
|
326
742
|
|
|
327
743
|
export { FlowUiCalendar as default };
|