@fullcalendar/timeline 7.0.0-beta.3 → 7.0.0-beta.5
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/LICENSE.md +2 -2
- package/{index.cjs → cjs/index.cjs} +14 -7
- package/cjs/internal.cjs +1224 -0
- package/esm/index.d.ts +25 -0
- package/{index.js → esm/index.js} +13 -6
- package/{internal.d.ts → esm/internal.d.ts} +33 -48
- package/{internal.js → esm/internal.js} +455 -495
- package/{index.global.js → global.js} +464 -500
- package/global.min.js +6 -0
- package/package.json +24 -21
- package/index.d.ts +0 -8
- package/index.global.min.js +0 -6
- package/internal.cjs +0 -1261
package/cjs/internal.cjs
ADDED
|
@@ -0,0 +1,1224 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var core = require('@fullcalendar/core');
|
|
6
|
+
var internal = require('@fullcalendar/core/internal');
|
|
7
|
+
var classNames = require('@fullcalendar/core/internal-classnames');
|
|
8
|
+
var preact = require('@fullcalendar/core/preact');
|
|
9
|
+
var internal$1 = require('@fullcalendar/scrollgrid/internal');
|
|
10
|
+
|
|
11
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
12
|
+
|
|
13
|
+
var classNames__default = /*#__PURE__*/_interopDefaultLegacy(classNames);
|
|
14
|
+
|
|
15
|
+
const MIN_AUTO_LABELS = 18; // more than `12` months but less that `24` hours
|
|
16
|
+
const MAX_AUTO_SLOTS_PER_LABEL = 6; // allows 6 10-min slots in an hour
|
|
17
|
+
const MAX_AUTO_CELLS = 200; // allows 4-days to have a :30 slot duration
|
|
18
|
+
internal.config.MAX_TIMELINE_SLOTS = 1000;
|
|
19
|
+
// potential nice values for slot-duration and interval-duration
|
|
20
|
+
const STOCK_SUB_DURATIONS = [
|
|
21
|
+
{ years: 1 },
|
|
22
|
+
{ months: 1 },
|
|
23
|
+
{ days: 1 },
|
|
24
|
+
{ hours: 1 },
|
|
25
|
+
{ minutes: 30 },
|
|
26
|
+
{ minutes: 15 },
|
|
27
|
+
{ minutes: 10 },
|
|
28
|
+
{ minutes: 5 },
|
|
29
|
+
{ minutes: 1 },
|
|
30
|
+
{ seconds: 30 },
|
|
31
|
+
{ seconds: 15 },
|
|
32
|
+
{ seconds: 10 },
|
|
33
|
+
{ seconds: 5 },
|
|
34
|
+
{ seconds: 1 },
|
|
35
|
+
{ milliseconds: 500 },
|
|
36
|
+
{ milliseconds: 100 },
|
|
37
|
+
{ milliseconds: 10 },
|
|
38
|
+
{ milliseconds: 1 },
|
|
39
|
+
];
|
|
40
|
+
function buildTimelineDateProfile(dateProfile, dateEnv, allOptions, dateProfileGenerator) {
|
|
41
|
+
let tDateProfile = {
|
|
42
|
+
labelInterval: allOptions.slotHeaderInterval,
|
|
43
|
+
slotDuration: allOptions.slotDuration,
|
|
44
|
+
};
|
|
45
|
+
validateLabelAndSlot(tDateProfile, dateProfile, dateEnv); // validate after computed grid duration
|
|
46
|
+
ensureLabelInterval(tDateProfile, dateProfile, dateEnv);
|
|
47
|
+
ensureSlotDuration(tDateProfile, dateProfile, dateEnv);
|
|
48
|
+
let input = allOptions.slotHeaderFormat;
|
|
49
|
+
let rawFormats = Array.isArray(input) ? input :
|
|
50
|
+
(input != null) ? [input] :
|
|
51
|
+
computeHeaderFormats(tDateProfile, dateProfile, dateEnv, allOptions);
|
|
52
|
+
tDateProfile.headerFormats = rawFormats.map((rawFormat) => internal.createFormatter(rawFormat));
|
|
53
|
+
tDateProfile.isTimeScale = Boolean(tDateProfile.slotDuration.milliseconds);
|
|
54
|
+
let largeUnit = null;
|
|
55
|
+
if (!tDateProfile.isTimeScale) {
|
|
56
|
+
const slotUnit = internal.greatestDurationDenominator(tDateProfile.slotDuration).unit;
|
|
57
|
+
if (/year|month|week/.test(slotUnit)) {
|
|
58
|
+
largeUnit = slotUnit;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
tDateProfile.largeUnit = largeUnit;
|
|
62
|
+
/*
|
|
63
|
+
console.log('label interval =', timelineView.labelInterval.humanize())
|
|
64
|
+
console.log('slot duration =', timelineView.slotDuration.humanize())
|
|
65
|
+
console.log('header formats =', timelineView.headerFormats)
|
|
66
|
+
console.log('isTimeScale', timelineView.isTimeScale)
|
|
67
|
+
console.log('largeUnit', timelineView.largeUnit)
|
|
68
|
+
*/
|
|
69
|
+
let rawSnapDuration = allOptions.snapDuration;
|
|
70
|
+
let snapDuration;
|
|
71
|
+
let snapsPerSlot;
|
|
72
|
+
if (rawSnapDuration) {
|
|
73
|
+
snapDuration = internal.createDuration(rawSnapDuration);
|
|
74
|
+
snapsPerSlot = internal.wholeDivideDurations(tDateProfile.slotDuration, snapDuration);
|
|
75
|
+
// ^ TODO: warning if not whole?
|
|
76
|
+
}
|
|
77
|
+
if (snapsPerSlot == null) {
|
|
78
|
+
snapDuration = tDateProfile.slotDuration;
|
|
79
|
+
snapsPerSlot = 1;
|
|
80
|
+
}
|
|
81
|
+
tDateProfile.snapDuration = snapDuration;
|
|
82
|
+
tDateProfile.snapsPerSlot = snapsPerSlot;
|
|
83
|
+
// more...
|
|
84
|
+
let timeWindowMs = internal.asRoughMs(dateProfile.slotMaxTime) - internal.asRoughMs(dateProfile.slotMinTime);
|
|
85
|
+
// TODO: why not use normalizeRange!?
|
|
86
|
+
let normalizedStart = normalizeDate(dateProfile.renderRange.start, tDateProfile, dateEnv);
|
|
87
|
+
let normalizedEnd = normalizeDate(dateProfile.renderRange.end, tDateProfile, dateEnv);
|
|
88
|
+
// apply slotMinTime/slotMaxTime
|
|
89
|
+
// TODO: View should be responsible.
|
|
90
|
+
if (tDateProfile.isTimeScale) {
|
|
91
|
+
normalizedStart = dateEnv.add(normalizedStart, dateProfile.slotMinTime);
|
|
92
|
+
normalizedEnd = dateEnv.add(internal.addDays(normalizedEnd, -1), dateProfile.slotMaxTime);
|
|
93
|
+
}
|
|
94
|
+
tDateProfile.timeWindowMs = timeWindowMs;
|
|
95
|
+
tDateProfile.normalizedRange = { start: normalizedStart, end: normalizedEnd };
|
|
96
|
+
let slotDates = [];
|
|
97
|
+
let slotDatesMajor = [];
|
|
98
|
+
let date = normalizedStart;
|
|
99
|
+
let majorUnit = internal.computeMajorUnit(dateProfile, dateEnv);
|
|
100
|
+
while (date < normalizedEnd) {
|
|
101
|
+
if (isValidDate(date, tDateProfile, dateProfile, dateProfileGenerator)) {
|
|
102
|
+
slotDates.push(date);
|
|
103
|
+
slotDatesMajor.push(internal.isMajorUnit(date, majorUnit, dateEnv));
|
|
104
|
+
}
|
|
105
|
+
date = dateEnv.add(date, tDateProfile.slotDuration);
|
|
106
|
+
}
|
|
107
|
+
tDateProfile.slotDates = slotDates;
|
|
108
|
+
tDateProfile.slotDatesMajor = slotDatesMajor;
|
|
109
|
+
// more...
|
|
110
|
+
let snapIndex = -1;
|
|
111
|
+
let snapDiff = 0; // index of the diff :(
|
|
112
|
+
const snapDiffToIndex = [];
|
|
113
|
+
const snapIndexToDiff = [];
|
|
114
|
+
date = normalizedStart;
|
|
115
|
+
while (date < normalizedEnd) {
|
|
116
|
+
if (isValidDate(date, tDateProfile, dateProfile, dateProfileGenerator)) {
|
|
117
|
+
snapIndex += 1;
|
|
118
|
+
snapDiffToIndex.push(snapIndex);
|
|
119
|
+
snapIndexToDiff.push(snapDiff);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
snapDiffToIndex.push(snapIndex + 0.5);
|
|
123
|
+
}
|
|
124
|
+
date = dateEnv.add(date, tDateProfile.snapDuration);
|
|
125
|
+
snapDiff += 1;
|
|
126
|
+
}
|
|
127
|
+
tDateProfile.snapDiffToIndex = snapDiffToIndex;
|
|
128
|
+
tDateProfile.snapIndexToDiff = snapIndexToDiff;
|
|
129
|
+
tDateProfile.snapCnt = snapIndex + 1; // is always one behind
|
|
130
|
+
tDateProfile.slotCnt = tDateProfile.snapCnt / tDateProfile.snapsPerSlot;
|
|
131
|
+
// more...
|
|
132
|
+
tDateProfile.cellRows = buildCellRows(tDateProfile, dateEnv, majorUnit);
|
|
133
|
+
tDateProfile.slotsPerLabel = internal.wholeDivideDurations(tDateProfile.labelInterval, tDateProfile.slotDuration);
|
|
134
|
+
return tDateProfile;
|
|
135
|
+
}
|
|
136
|
+
/*
|
|
137
|
+
snaps to appropriate unit
|
|
138
|
+
*/
|
|
139
|
+
function normalizeDate(date, tDateProfile, dateEnv) {
|
|
140
|
+
let normalDate = date;
|
|
141
|
+
if (!tDateProfile.isTimeScale) {
|
|
142
|
+
normalDate = internal.startOfDay(normalDate);
|
|
143
|
+
if (tDateProfile.largeUnit) {
|
|
144
|
+
normalDate = dateEnv.startOf(normalDate, tDateProfile.largeUnit);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return normalDate;
|
|
148
|
+
}
|
|
149
|
+
/*
|
|
150
|
+
snaps to appropriate unit
|
|
151
|
+
*/
|
|
152
|
+
function normalizeRange(range, tDateProfile, dateEnv) {
|
|
153
|
+
if (!tDateProfile.isTimeScale) {
|
|
154
|
+
range = internal.computeVisibleDayRange(range);
|
|
155
|
+
if (tDateProfile.largeUnit) {
|
|
156
|
+
let dayRange = range; // preserve original result
|
|
157
|
+
range = {
|
|
158
|
+
start: dateEnv.startOf(range.start, tDateProfile.largeUnit),
|
|
159
|
+
end: dateEnv.startOf(range.end, tDateProfile.largeUnit),
|
|
160
|
+
};
|
|
161
|
+
// if date is partially through the interval, or is in the same interval as the start,
|
|
162
|
+
// make the exclusive end be the *next* interval
|
|
163
|
+
if (range.end.valueOf() !== dayRange.end.valueOf() || range.end <= range.start) {
|
|
164
|
+
range = {
|
|
165
|
+
start: range.start,
|
|
166
|
+
end: dateEnv.add(range.end, tDateProfile.slotDuration),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return range;
|
|
172
|
+
}
|
|
173
|
+
function isValidDate(date, tDateProfile, dateProfile, dateProfileGenerator) {
|
|
174
|
+
if (dateProfileGenerator.isHiddenDay(date)) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
if (tDateProfile.isTimeScale) {
|
|
178
|
+
// determine if the time is within slotMinTime/slotMaxTime, which may have wacky values
|
|
179
|
+
let day = internal.startOfDay(date);
|
|
180
|
+
let timeMs = date.valueOf() - day.valueOf();
|
|
181
|
+
let ms = timeMs - internal.asRoughMs(dateProfile.slotMinTime); // milliseconds since slotMinTime
|
|
182
|
+
ms = ((ms % 86400000) + 86400000) % 86400000; // make negative values wrap to 24hr clock
|
|
183
|
+
return ms < tDateProfile.timeWindowMs; // before the slotMaxTime?
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
function validateLabelAndSlot(tDateProfile, dateProfile, dateEnv) {
|
|
188
|
+
const { currentRange } = dateProfile;
|
|
189
|
+
// make sure labelInterval doesn't exceed the max number of cells
|
|
190
|
+
if (tDateProfile.labelInterval) {
|
|
191
|
+
const labelCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, tDateProfile.labelInterval);
|
|
192
|
+
if (labelCnt > internal.config.MAX_TIMELINE_SLOTS) {
|
|
193
|
+
console.warn('slotHeaderInterval results in too many cells');
|
|
194
|
+
tDateProfile.labelInterval = null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// make sure slotDuration doesn't exceed the maximum number of cells
|
|
198
|
+
if (tDateProfile.slotDuration) {
|
|
199
|
+
const slotCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, tDateProfile.slotDuration);
|
|
200
|
+
if (slotCnt > internal.config.MAX_TIMELINE_SLOTS) {
|
|
201
|
+
console.warn('slotDuration results in too many cells');
|
|
202
|
+
tDateProfile.slotDuration = null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// make sure labelInterval is a multiple of slotDuration
|
|
206
|
+
if (tDateProfile.labelInterval && tDateProfile.slotDuration) {
|
|
207
|
+
const slotsPerLabel = internal.wholeDivideDurations(tDateProfile.labelInterval, tDateProfile.slotDuration);
|
|
208
|
+
if (slotsPerLabel === null || slotsPerLabel < 1) {
|
|
209
|
+
console.warn('slotHeaderInterval must be a multiple of slotDuration');
|
|
210
|
+
tDateProfile.slotDuration = null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function ensureLabelInterval(tDateProfile, dateProfile, dateEnv) {
|
|
215
|
+
const { currentRange } = dateProfile;
|
|
216
|
+
let { labelInterval } = tDateProfile;
|
|
217
|
+
if (!labelInterval) {
|
|
218
|
+
// compute based off the slot duration
|
|
219
|
+
// find the largest label interval with an acceptable slots-per-label
|
|
220
|
+
let input;
|
|
221
|
+
if (tDateProfile.slotDuration) {
|
|
222
|
+
for (input of STOCK_SUB_DURATIONS) {
|
|
223
|
+
const tryLabelInterval = internal.createDuration(input);
|
|
224
|
+
const slotsPerLabel = internal.wholeDivideDurations(tryLabelInterval, tDateProfile.slotDuration);
|
|
225
|
+
if (slotsPerLabel !== null && slotsPerLabel <= MAX_AUTO_SLOTS_PER_LABEL) {
|
|
226
|
+
labelInterval = tryLabelInterval;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// use the slot duration as a last resort
|
|
231
|
+
if (!labelInterval) {
|
|
232
|
+
labelInterval = tDateProfile.slotDuration;
|
|
233
|
+
}
|
|
234
|
+
// compute based off the view's duration
|
|
235
|
+
// find the largest label interval that yields the minimum number of labels
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
for (input of STOCK_SUB_DURATIONS) {
|
|
239
|
+
labelInterval = internal.createDuration(input);
|
|
240
|
+
const labelCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, labelInterval);
|
|
241
|
+
if (labelCnt >= MIN_AUTO_LABELS) {
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
tDateProfile.labelInterval = labelInterval;
|
|
247
|
+
}
|
|
248
|
+
return labelInterval;
|
|
249
|
+
}
|
|
250
|
+
function ensureSlotDuration(tDateProfile, dateProfile, dateEnv) {
|
|
251
|
+
const { currentRange } = dateProfile;
|
|
252
|
+
let { slotDuration } = tDateProfile;
|
|
253
|
+
if (!slotDuration) {
|
|
254
|
+
const labelInterval = ensureLabelInterval(tDateProfile, dateProfile, dateEnv); // will compute if necessary
|
|
255
|
+
// compute based off the label interval
|
|
256
|
+
// find the largest slot duration that is different from labelInterval, but still acceptable
|
|
257
|
+
for (let input of STOCK_SUB_DURATIONS) {
|
|
258
|
+
const trySlotDuration = internal.createDuration(input);
|
|
259
|
+
const slotsPerLabel = internal.wholeDivideDurations(labelInterval, trySlotDuration);
|
|
260
|
+
if (slotsPerLabel !== null && slotsPerLabel > 1 && slotsPerLabel <= MAX_AUTO_SLOTS_PER_LABEL) {
|
|
261
|
+
slotDuration = trySlotDuration;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// only allow the value if it won't exceed the view's # of slots limit
|
|
266
|
+
if (slotDuration) {
|
|
267
|
+
const slotCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, slotDuration);
|
|
268
|
+
if (slotCnt > MAX_AUTO_CELLS) {
|
|
269
|
+
slotDuration = null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// use the label interval as a last resort
|
|
273
|
+
if (!slotDuration) {
|
|
274
|
+
slotDuration = labelInterval;
|
|
275
|
+
}
|
|
276
|
+
tDateProfile.slotDuration = slotDuration;
|
|
277
|
+
}
|
|
278
|
+
return slotDuration;
|
|
279
|
+
}
|
|
280
|
+
function computeHeaderFormats(tDateProfile, dateProfile, dateEnv, allOptions) {
|
|
281
|
+
let format1;
|
|
282
|
+
let format2;
|
|
283
|
+
const { labelInterval } = tDateProfile;
|
|
284
|
+
const { currentRange } = dateProfile;
|
|
285
|
+
let unit = internal.greatestDurationDenominator(labelInterval).unit;
|
|
286
|
+
const weekNumbersVisible = allOptions.weekNumbers;
|
|
287
|
+
let format0 = (format1 = (format2 = null));
|
|
288
|
+
// NOTE: weekNumber computation function wont work
|
|
289
|
+
if ((unit === 'week') && !weekNumbersVisible) {
|
|
290
|
+
unit = 'day';
|
|
291
|
+
}
|
|
292
|
+
switch (unit) {
|
|
293
|
+
case 'year':
|
|
294
|
+
format0 = { year: 'numeric' }; // '2015'
|
|
295
|
+
break;
|
|
296
|
+
case 'month':
|
|
297
|
+
if (dateEnv.diffWholeYears(currentRange.start, currentRange.end) > 1) {
|
|
298
|
+
format0 = { year: 'numeric' }; // '2015'
|
|
299
|
+
}
|
|
300
|
+
format1 = { month: 'short' }; // 'Jan'
|
|
301
|
+
break;
|
|
302
|
+
case 'week':
|
|
303
|
+
if (dateEnv.diffWholeYears(currentRange.start, currentRange.end) > 1) {
|
|
304
|
+
format0 = { year: 'numeric' }; // '2015'
|
|
305
|
+
}
|
|
306
|
+
format1 = { week: 'narrow' }; // 'Wk4'
|
|
307
|
+
break;
|
|
308
|
+
case 'day':
|
|
309
|
+
if (dateEnv.diffWholeYears(currentRange.start, currentRange.end) > 1) {
|
|
310
|
+
format0 = { year: 'numeric', month: 'long' }; // 'January 2014'
|
|
311
|
+
}
|
|
312
|
+
else if (dateEnv.diffWholeMonths(currentRange.start, currentRange.end) > 1) {
|
|
313
|
+
format0 = { month: 'long' }; // 'January'
|
|
314
|
+
}
|
|
315
|
+
if (weekNumbersVisible) {
|
|
316
|
+
format1 = { week: 'short' }; // 'Wk 4'
|
|
317
|
+
}
|
|
318
|
+
format2 = { weekday: 'narrow', day: 'numeric' }; // 'Su 9'
|
|
319
|
+
break;
|
|
320
|
+
case 'hour':
|
|
321
|
+
if (weekNumbersVisible) {
|
|
322
|
+
format0 = { week: 'short' }; // 'Wk 4'
|
|
323
|
+
}
|
|
324
|
+
if (internal.diffWholeDays(currentRange.start, currentRange.end) > 1) {
|
|
325
|
+
format1 = { weekday: 'short', day: 'numeric', month: 'numeric', omitCommas: true }; // Sat 4/7
|
|
326
|
+
}
|
|
327
|
+
format2 = {
|
|
328
|
+
hour: 'numeric',
|
|
329
|
+
minute: '2-digit',
|
|
330
|
+
omitZeroMinute: true,
|
|
331
|
+
meridiem: 'short',
|
|
332
|
+
};
|
|
333
|
+
break;
|
|
334
|
+
case 'minute':
|
|
335
|
+
// sufficiently large number of different minute cells?
|
|
336
|
+
if ((internal.asRoughMinutes(labelInterval) / 60) >= MAX_AUTO_SLOTS_PER_LABEL) {
|
|
337
|
+
format0 = {
|
|
338
|
+
hour: 'numeric',
|
|
339
|
+
meridiem: 'short',
|
|
340
|
+
};
|
|
341
|
+
format1 = (params) => (':' + internal.padStart(params.date.minute, 2) // ':30'
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
format0 = {
|
|
346
|
+
hour: 'numeric',
|
|
347
|
+
minute: 'numeric',
|
|
348
|
+
meridiem: 'short',
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
case 'second':
|
|
353
|
+
// sufficiently large number of different second cells?
|
|
354
|
+
if ((internal.asRoughSeconds(labelInterval) / 60) >= MAX_AUTO_SLOTS_PER_LABEL) {
|
|
355
|
+
format0 = { hour: 'numeric', minute: '2-digit', meridiem: 'lowercase' }; // '8:30 PM'
|
|
356
|
+
format1 = (params) => (':' + internal.padStart(params.date.second, 2) // ':30'
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
format0 = { hour: 'numeric', minute: '2-digit', second: '2-digit', meridiem: 'lowercase' }; // '8:30:45 PM'
|
|
361
|
+
}
|
|
362
|
+
break;
|
|
363
|
+
case 'millisecond':
|
|
364
|
+
format0 = { hour: 'numeric', minute: '2-digit', second: '2-digit', meridiem: 'lowercase' }; // '8:30:45 PM'
|
|
365
|
+
format1 = (params) => ('.' + internal.padStart(params.millisecond, 3));
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
return [].concat(format0 || [], format1 || [], format2 || []);
|
|
369
|
+
}
|
|
370
|
+
function buildCellRows(tDateProfile, dateEnv, majorUnit) {
|
|
371
|
+
let slotDates = tDateProfile.slotDates;
|
|
372
|
+
let formats = tDateProfile.headerFormats;
|
|
373
|
+
let cellRows = formats.map(() => []); // indexed by row,col
|
|
374
|
+
let slotAsDays = internal.asCleanDays(tDateProfile.slotDuration);
|
|
375
|
+
let guessedSlotUnit = slotAsDays === 7 ? 'week' :
|
|
376
|
+
slotAsDays === 1 ? 'day' :
|
|
377
|
+
null;
|
|
378
|
+
// specifically for navclicks
|
|
379
|
+
let rowUnitsFromFormats = formats.map((format) => (format.getSmallestUnit ? format.getSmallestUnit() : null));
|
|
380
|
+
// builds cellRows and slotCells
|
|
381
|
+
for (let i = 0; i < slotDates.length; i += 1) {
|
|
382
|
+
let date = slotDates[i];
|
|
383
|
+
for (let row = 0; row < formats.length; row += 1) {
|
|
384
|
+
let format = formats[row];
|
|
385
|
+
let rowCells = cellRows[row];
|
|
386
|
+
let leadingCell = rowCells[rowCells.length - 1];
|
|
387
|
+
let isLastRow = row === formats.length - 1;
|
|
388
|
+
let isSuperRow = formats.length > 1 && !isLastRow; // more than one row and not the last
|
|
389
|
+
let isMajor = internal.isMajorUnit(date, majorUnit, dateEnv);
|
|
390
|
+
let newCell = null;
|
|
391
|
+
let rowUnit = rowUnitsFromFormats[row] || (isLastRow ? guessedSlotUnit : null);
|
|
392
|
+
if (isSuperRow) {
|
|
393
|
+
let [text] = dateEnv.format(date, format);
|
|
394
|
+
if (!leadingCell || (leadingCell.text !== text)) {
|
|
395
|
+
newCell = buildCellObject(date, isMajor, text, rowUnit);
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
leadingCell.colspan += 1;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else if (!leadingCell ||
|
|
402
|
+
internal.isInt(dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, date, tDateProfile.labelInterval))) {
|
|
403
|
+
let [text] = dateEnv.format(date, format);
|
|
404
|
+
newCell = buildCellObject(date, isMajor, text, rowUnit);
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
leadingCell.colspan += 1;
|
|
408
|
+
}
|
|
409
|
+
if (newCell) {
|
|
410
|
+
rowCells.push(newCell);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return cellRows;
|
|
415
|
+
}
|
|
416
|
+
function buildCellObject(date, isMajor, text, rowUnit) {
|
|
417
|
+
return { date, isMajor, text, rowUnit, colspan: 1 }; // colspan mutated later
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
class TimelineSlatCell extends internal.BaseComponent {
|
|
421
|
+
constructor() {
|
|
422
|
+
super(...arguments);
|
|
423
|
+
// memo
|
|
424
|
+
this.getDateMeta = internal.memoize(internal.getDateMeta);
|
|
425
|
+
}
|
|
426
|
+
render() {
|
|
427
|
+
let { props, context } = this;
|
|
428
|
+
let { dateEnv, options } = context;
|
|
429
|
+
let { date, tDateProfile, isMajor } = props;
|
|
430
|
+
let dateMeta = this.getDateMeta(props.date, dateEnv, props.dateProfile, props.todayRange, props.nowDate);
|
|
431
|
+
let isMinor = tDateProfile.isTimeScale &&
|
|
432
|
+
!internal.isInt(dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, props.date, tDateProfile.labelInterval));
|
|
433
|
+
let renderProps = Object.assign(Object.assign({}, dateMeta), { isMajor,
|
|
434
|
+
isMinor, view: context.viewApi });
|
|
435
|
+
return (preact.createElement(internal.ContentContainer, { tag: "div", className: core.joinClassNames(classNames__default["default"].tight, classNames__default["default"].alignStart, // shrinks width of InnerContent
|
|
436
|
+
props.borderStart ? classNames__default["default"].borderOnlyS : classNames__default["default"].borderNone, classNames__default["default"].internalTimelineSlot), attrs: Object.assign({ 'data-date': dateEnv.formatIso(date, {
|
|
437
|
+
omitTimeZoneOffset: true,
|
|
438
|
+
omitTime: !tDateProfile.isTimeScale,
|
|
439
|
+
}) }, (dateMeta.isToday ? { 'aria-current': 'date' } : {})), style: {
|
|
440
|
+
width: props.width,
|
|
441
|
+
}, renderProps: renderProps, generatorName: undefined, classNameGenerator: options.slotLaneClass, didMount: options.slotLaneDidMount, willUnmount: options.slotLaneWillUnmount }));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
class TimelineSlats extends internal.BaseComponent {
|
|
446
|
+
render() {
|
|
447
|
+
let { props } = this;
|
|
448
|
+
let { tDateProfile, slotWidth } = props;
|
|
449
|
+
let { slotDates, slotDatesMajor } = tDateProfile;
|
|
450
|
+
return (preact.createElement("div", { "aria-hidden": true, className: core.joinClassNames(classNames__default["default"].flexRow, classNames__default["default"].fill), style: { height: props.height } }, slotDates.map((slotDate, i) => {
|
|
451
|
+
let key = slotDate.toISOString();
|
|
452
|
+
return (preact.createElement(TimelineSlatCell, { key: key, date: slotDate, dateProfile: props.dateProfile, tDateProfile: tDateProfile, nowDate: props.nowDate, todayRange: props.todayRange, isMajor: slotDatesMajor[i], borderStart: Boolean(i),
|
|
453
|
+
// dimensions
|
|
454
|
+
width: slotWidth }));
|
|
455
|
+
})));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
class TimelineHeaderCell extends internal.BaseComponent {
|
|
460
|
+
constructor() {
|
|
461
|
+
super(...arguments);
|
|
462
|
+
// memo
|
|
463
|
+
this.getDateMeta = internal.memoize(internal.getDateMeta);
|
|
464
|
+
// ref
|
|
465
|
+
this.innerWrapperElRef = preact.createRef();
|
|
466
|
+
}
|
|
467
|
+
render() {
|
|
468
|
+
let { props, state, context } = this;
|
|
469
|
+
let { dateEnv, options } = context;
|
|
470
|
+
let { cell, dateProfile, tDateProfile } = props;
|
|
471
|
+
// the cell.rowUnit is f'd
|
|
472
|
+
// giving 'month' for a 3-day view
|
|
473
|
+
// workaround: to infer day, do NOT time
|
|
474
|
+
let dateMeta = this.getDateMeta(cell.date, dateEnv, dateProfile, props.todayRange, props.nowDate);
|
|
475
|
+
let hasNavLink = options.navLinks && !dateMeta.isDisabled && (cell.rowUnit && cell.rowUnit !== 'time');
|
|
476
|
+
let isTime = tDateProfile.isTimeScale && !props.rowLevel; // HACK: faulty way of determining this
|
|
477
|
+
let renderProps = Object.assign(Object.assign({}, dateMeta), { level: props.rowLevel, isMajor: cell.isMajor, isMinor: false, isNarrow: false, isTime,
|
|
478
|
+
hasNavLink, text: cell.text, isFirst: props.isFirst, view: context.viewApi });
|
|
479
|
+
const { slotHeaderAlign } = options;
|
|
480
|
+
const align = this.align =
|
|
481
|
+
typeof slotHeaderAlign === 'function'
|
|
482
|
+
? slotHeaderAlign({ level: props.rowLevel, isTime })
|
|
483
|
+
: slotHeaderAlign;
|
|
484
|
+
const isSticky = this.isSticky =
|
|
485
|
+
props.rowLevel && options.slotHeaderSticky !== false;
|
|
486
|
+
let edgeCoord;
|
|
487
|
+
if (isSticky) {
|
|
488
|
+
if (align === 'center') {
|
|
489
|
+
if (state.innerWidth != null) {
|
|
490
|
+
edgeCoord = `calc(50% - ${state.innerWidth / 2}px)`;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
edgeCoord = (typeof options.slotHeaderSticky === 'number' ||
|
|
495
|
+
typeof options.slotHeaderSticky === 'string') ? options.slotHeaderSticky : 0;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return (preact.createElement(internal.ContentContainer, { tag: "div", className: internal.joinArrayishClassNames(classNames__default["default"].tight, classNames__default["default"].flexCol, props.isFirst ? classNames__default["default"].borderNone : classNames__default["default"].borderOnlyS, align === 'center' ? classNames__default["default"].alignCenter :
|
|
499
|
+
align === 'end' ? classNames__default["default"].alignEnd :
|
|
500
|
+
classNames__default["default"].alignStart, classNames__default["default"].internalTimelineSlot), attrs: Object.assign({ 'data-date': dateEnv.formatIso(cell.date, {
|
|
501
|
+
omitTime: !tDateProfile.isTimeScale,
|
|
502
|
+
omitTimeZoneOffset: true,
|
|
503
|
+
}) }, (dateMeta.isToday ? { 'aria-current': 'date' } : {})), style: {
|
|
504
|
+
width: props.slotWidth != null
|
|
505
|
+
? props.slotWidth * cell.colspan
|
|
506
|
+
: undefined,
|
|
507
|
+
}, renderProps: renderProps, generatorName: "slotHeaderContent", customGenerator: options.slotHeaderContent, defaultGenerator: renderInnerContent, classNameGenerator: options.slotHeaderClass, didMount: options.slotHeaderDidMount, willUnmount: options.slotHeaderWillUnmount }, (InnerContent) => (preact.createElement("div", { ref: this.innerWrapperElRef, className: core.joinClassNames(classNames__default["default"].flexCol, classNames__default["default"].rigid, isSticky && classNames__default["default"].sticky), style: {
|
|
508
|
+
left: edgeCoord,
|
|
509
|
+
right: edgeCoord,
|
|
510
|
+
} },
|
|
511
|
+
preact.createElement(InnerContent, { tag: 'div', attrs: hasNavLink
|
|
512
|
+
// not tabbable because parent is aria-hidden
|
|
513
|
+
? internal.buildNavLinkAttrs(context, cell.date, cell.rowUnit, undefined, /* isTabbable = */ false)
|
|
514
|
+
: {} // don't bother with aria-hidden because parent already hidden
|
|
515
|
+
, className: internal.generateClassName(options.slotHeaderInnerClass, renderProps) })))));
|
|
516
|
+
}
|
|
517
|
+
componentDidMount() {
|
|
518
|
+
const { props } = this;
|
|
519
|
+
const innerWrapperEl = this.innerWrapperElRef.current; // TODO: make dynamic with useEffect
|
|
520
|
+
this.disconnectSize = internal.watchSize(innerWrapperEl, (width, height) => {
|
|
521
|
+
internal.setRef(props.innerWidthRef, width);
|
|
522
|
+
internal.setRef(props.innerHeightRef, height);
|
|
523
|
+
if (this.align === 'center' && this.isSticky) {
|
|
524
|
+
this.setState({ innerWidth: width });
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
componentWillUnmount() {
|
|
529
|
+
const { props } = this;
|
|
530
|
+
this.disconnectSize();
|
|
531
|
+
internal.setRef(props.innerWidthRef, null);
|
|
532
|
+
internal.setRef(props.innerHeightRef, null);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Utils
|
|
536
|
+
// -------------------------------------------------------------------------------------------------
|
|
537
|
+
function renderInnerContent(renderProps) {
|
|
538
|
+
return renderProps.text;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
class TimelineHeaderRow extends internal.BaseComponent {
|
|
542
|
+
constructor() {
|
|
543
|
+
super(...arguments);
|
|
544
|
+
// refs
|
|
545
|
+
this.innerWidthRefMap = new internal.RefMap(() => {
|
|
546
|
+
internal.afterSize(this.handleInnerWidths);
|
|
547
|
+
});
|
|
548
|
+
this.innerHeightRefMap = new internal.RefMap(() => {
|
|
549
|
+
internal.afterSize(this.handleInnerHeights);
|
|
550
|
+
});
|
|
551
|
+
this.handleInnerWidths = () => {
|
|
552
|
+
const innerWidthMap = this.innerWidthRefMap.current;
|
|
553
|
+
let max = 0;
|
|
554
|
+
for (const innerWidth of innerWidthMap.values()) {
|
|
555
|
+
max = Math.max(max, innerWidth);
|
|
556
|
+
}
|
|
557
|
+
// TODO: ensure not equal?
|
|
558
|
+
internal.setRef(this.props.innerWidthRef, max);
|
|
559
|
+
};
|
|
560
|
+
this.handleInnerHeights = () => {
|
|
561
|
+
const innerHeightMap = this.innerHeightRefMap.current;
|
|
562
|
+
let max = 0;
|
|
563
|
+
for (const innerHeight of innerHeightMap.values()) {
|
|
564
|
+
max = Math.max(max, innerHeight);
|
|
565
|
+
}
|
|
566
|
+
// TODO: ensure not equal?
|
|
567
|
+
internal.setRef(this.props.innerHeighRef, max);
|
|
568
|
+
this.setState({ innerHeight: max });
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
render() {
|
|
572
|
+
const { props, innerWidthRefMap, innerHeightRefMap, state, context } = this;
|
|
573
|
+
const { options } = context;
|
|
574
|
+
return (preact.createElement("div", { className: internal.joinArrayishClassNames(options.slotHeaderRowClass, classNames__default["default"].flexRow, classNames__default["default"].grow, props.rowLevel // not the last row?
|
|
575
|
+
? classNames__default["default"].borderOnlyB
|
|
576
|
+
: classNames__default["default"].borderNone), style: {
|
|
577
|
+
// we assign height because we allow cells to have distorted heights for visual effect
|
|
578
|
+
// but we still want to keep the overall extrenal mass
|
|
579
|
+
height: state.innerHeight,
|
|
580
|
+
} }, props.cells.map((cell, cellI) => {
|
|
581
|
+
// TODO: make this part of the cell obj?
|
|
582
|
+
// TODO: rowUnit seems wrong sometimes. says 'month' when it should be day
|
|
583
|
+
// TODO: rowUnit is relevant to whole row. put it on a row object, not the cells
|
|
584
|
+
// TODO: use rowUnit to key the Row itself?
|
|
585
|
+
const key = cell.rowUnit + ':' + cell.date.toISOString();
|
|
586
|
+
return (preact.createElement(TimelineHeaderCell, { key: key, cell: cell, rowLevel: props.rowLevel, dateProfile: props.dateProfile, tDateProfile: props.tDateProfile, todayRange: props.todayRange, nowDate: props.nowDate, isFirst: cellI === 0,
|
|
587
|
+
// refs
|
|
588
|
+
innerWidthRef: innerWidthRefMap.createRef(key), innerHeightRef: innerHeightRefMap.createRef(key),
|
|
589
|
+
// dimensions
|
|
590
|
+
slotWidth: props.slotWidth }));
|
|
591
|
+
})));
|
|
592
|
+
}
|
|
593
|
+
componentWillUnmount() {
|
|
594
|
+
internal.setRef(this.props.innerWidthRef, null);
|
|
595
|
+
internal.setRef(this.props.innerHeighRef, null);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Timeline-specific
|
|
600
|
+
// -------------------------------------------------------------------------------------------------
|
|
601
|
+
const MIN_SLOT_WIDTH = 30; // for real
|
|
602
|
+
/*
|
|
603
|
+
TODO: DRY with computeSlatHeight?
|
|
604
|
+
*/
|
|
605
|
+
function computeSlotWidth(slatCnt, slatsPerLabel, slatMinWidth, labelInnerWidth, viewportWidth) {
|
|
606
|
+
if (labelInnerWidth == null || viewportWidth == null) {
|
|
607
|
+
return [undefined, undefined, false];
|
|
608
|
+
}
|
|
609
|
+
slatMinWidth = Math.max(slatMinWidth || 0, (labelInnerWidth + 1) / slatsPerLabel, MIN_SLOT_WIDTH);
|
|
610
|
+
const slatTryWidth = viewportWidth / slatCnt;
|
|
611
|
+
let slotLiquid;
|
|
612
|
+
let slatWidth;
|
|
613
|
+
if (slatTryWidth >= slatMinWidth) {
|
|
614
|
+
slotLiquid = true;
|
|
615
|
+
slatWidth = slatTryWidth;
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
slotLiquid = false;
|
|
619
|
+
slatWidth = Math.max(slatMinWidth, slatTryWidth);
|
|
620
|
+
}
|
|
621
|
+
return [slatWidth * slatCnt, slatWidth, slotLiquid];
|
|
622
|
+
}
|
|
623
|
+
function timeToCoord(// pixels
|
|
624
|
+
time, dateEnv, dateProfile, tDateProfile, slowWidth) {
|
|
625
|
+
let date = dateEnv.add(dateProfile.activeRange.start, time);
|
|
626
|
+
if (!tDateProfile.isTimeScale) {
|
|
627
|
+
date = internal.startOfDay(date);
|
|
628
|
+
}
|
|
629
|
+
return dateToCoord(date, dateEnv, tDateProfile, slowWidth);
|
|
630
|
+
}
|
|
631
|
+
function dateToCoord(// pixels
|
|
632
|
+
date, dateEnv, tDateProfile, slotWidth) {
|
|
633
|
+
let snapCoverage = computeDateSnapCoverage$1(date, tDateProfile, dateEnv);
|
|
634
|
+
let slotCoverage = snapCoverage / tDateProfile.snapsPerSlot;
|
|
635
|
+
return slotCoverage * slotWidth;
|
|
636
|
+
}
|
|
637
|
+
/*
|
|
638
|
+
returned value is between 0 and the number of snaps
|
|
639
|
+
*/
|
|
640
|
+
function computeDateSnapCoverage$1(date, tDateProfile, dateEnv) {
|
|
641
|
+
let snapDiff = dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, date, tDateProfile.snapDuration);
|
|
642
|
+
if (snapDiff < 0) {
|
|
643
|
+
return 0;
|
|
644
|
+
}
|
|
645
|
+
if (snapDiff >= tDateProfile.snapDiffToIndex.length) {
|
|
646
|
+
return tDateProfile.snapCnt;
|
|
647
|
+
}
|
|
648
|
+
let snapDiffInt = Math.floor(snapDiff);
|
|
649
|
+
let snapCoverage = tDateProfile.snapDiffToIndex[snapDiffInt];
|
|
650
|
+
if (internal.isInt(snapCoverage)) { // not an in-between value
|
|
651
|
+
snapCoverage += snapDiff - snapDiffInt; // add the remainder
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
// a fractional value, meaning the date is not visible
|
|
655
|
+
// always round up in this case. works for start AND end dates in a range.
|
|
656
|
+
snapCoverage = Math.ceil(snapCoverage);
|
|
657
|
+
}
|
|
658
|
+
return snapCoverage;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
class TimelineNowIndicatorLine extends internal.BaseComponent {
|
|
662
|
+
render() {
|
|
663
|
+
const { props, context } = this;
|
|
664
|
+
const xStyle = props.slotWidth == null
|
|
665
|
+
? {}
|
|
666
|
+
: {
|
|
667
|
+
insetInlineStart: dateToCoord(props.nowDate, context.dateEnv, props.tDateProfile, props.slotWidth)
|
|
668
|
+
};
|
|
669
|
+
return (preact.createElement("div", { className: classNames__default["default"].fill, style: {
|
|
670
|
+
zIndex: 2,
|
|
671
|
+
pointerEvents: 'none', // TODO: className
|
|
672
|
+
} },
|
|
673
|
+
preact.createElement(internal.NowIndicatorLineContainer, { className: core.joinClassNames(classNames__default["default"].fillY, classNames__default["default"].noMarginY, classNames__default["default"].borderlessY), style: xStyle, date: props.nowDate }),
|
|
674
|
+
preact.createElement("div", { className: core.joinClassNames(classNames__default["default"].flexCol, // better for negative margins
|
|
675
|
+
classNames__default["default"].fillY), style: xStyle },
|
|
676
|
+
preact.createElement("div", {
|
|
677
|
+
// stickiness on NowIndicatorDot misbehaves b/c of negative marginss
|
|
678
|
+
className: classNames__default["default"].stickyT },
|
|
679
|
+
preact.createElement(internal.NowIndicatorDot, null)))));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/*
|
|
684
|
+
TODO: DRY with other NowIndicator components
|
|
685
|
+
*/
|
|
686
|
+
class TimelineNowIndicatorArrow extends internal.BaseComponent {
|
|
687
|
+
render() {
|
|
688
|
+
const { props, context } = this;
|
|
689
|
+
const xStyle = props.slotWidth == null
|
|
690
|
+
? {}
|
|
691
|
+
: {
|
|
692
|
+
insetInlineStart: dateToCoord(props.nowDate, context.dateEnv, props.tDateProfile, props.slotWidth)
|
|
693
|
+
};
|
|
694
|
+
return (preact.createElement("div", {
|
|
695
|
+
// crop any overflow that the arrow/line might cause
|
|
696
|
+
// TODO: just do this on the entire canvas within the scroller
|
|
697
|
+
className: core.joinClassNames(classNames__default["default"].fill, classNames__default["default"].crop), style: {
|
|
698
|
+
zIndex: 2,
|
|
699
|
+
pointerEvents: 'none', // TODO: className
|
|
700
|
+
} },
|
|
701
|
+
preact.createElement(internal.NowIndicatorHeaderContainer, { className: classNames__default["default"].abs, style: xStyle, date: props.nowDate })));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function getTimelineSlotEl(parentEl, index) {
|
|
706
|
+
return parentEl.querySelectorAll(`.${classNames__default["default"].internalTimelineSlot}`)[index];
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/*
|
|
710
|
+
TODO: rename this file!
|
|
711
|
+
*/
|
|
712
|
+
// returned value is between 0 and the number of snaps
|
|
713
|
+
function computeDateSnapCoverage(date, tDateProfile, dateEnv) {
|
|
714
|
+
let snapDiff = dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, date, tDateProfile.snapDuration);
|
|
715
|
+
if (snapDiff < 0) {
|
|
716
|
+
return 0;
|
|
717
|
+
}
|
|
718
|
+
if (snapDiff >= tDateProfile.snapDiffToIndex.length) {
|
|
719
|
+
return tDateProfile.snapCnt;
|
|
720
|
+
}
|
|
721
|
+
let snapDiffInt = Math.floor(snapDiff);
|
|
722
|
+
let snapCoverage = tDateProfile.snapDiffToIndex[snapDiffInt];
|
|
723
|
+
if (internal.isInt(snapCoverage)) { // not an in-between value
|
|
724
|
+
snapCoverage += snapDiff - snapDiffInt; // add the remainder
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
// a fractional value, meaning the date is not visible
|
|
728
|
+
// always round up in this case. works for start AND end dates in a range.
|
|
729
|
+
snapCoverage = Math.ceil(snapCoverage);
|
|
730
|
+
}
|
|
731
|
+
return snapCoverage;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
class TimelineLaneSlicer extends internal.Slicer {
|
|
735
|
+
sliceRange(origRange, dateProfile, dateProfileGenerator, tDateProfile, dateEnv) {
|
|
736
|
+
let normalRange = normalizeRange(origRange, tDateProfile, dateEnv);
|
|
737
|
+
let segs = [];
|
|
738
|
+
// protect against when the span is entirely in an invalid date region
|
|
739
|
+
if (computeDateSnapCoverage(normalRange.start, tDateProfile, dateEnv)
|
|
740
|
+
< computeDateSnapCoverage(normalRange.end, tDateProfile, dateEnv)) {
|
|
741
|
+
// intersect the footprint's range with the grid's range
|
|
742
|
+
let slicedRange = internal.intersectRanges(normalRange, tDateProfile.normalizedRange);
|
|
743
|
+
if (slicedRange) {
|
|
744
|
+
segs.push({
|
|
745
|
+
startDate: slicedRange.start,
|
|
746
|
+
endDate: slicedRange.end,
|
|
747
|
+
isStart: slicedRange.start.valueOf() === normalRange.start.valueOf()
|
|
748
|
+
&& isValidDate(slicedRange.start, tDateProfile, dateProfile, dateProfileGenerator),
|
|
749
|
+
isEnd: slicedRange.end.valueOf() === normalRange.end.valueOf()
|
|
750
|
+
&& isValidDate(internal.addMs(slicedRange.end, -1), tDateProfile, dateProfile, dateProfileGenerator),
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return segs;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const DEFAULT_TIME_FORMAT = internal.createFormatter({
|
|
759
|
+
hour: 'numeric',
|
|
760
|
+
minute: '2-digit',
|
|
761
|
+
omitZeroMinute: true,
|
|
762
|
+
meridiem: 'narrow',
|
|
763
|
+
});
|
|
764
|
+
class TimelineEvent extends internal.BaseComponent {
|
|
765
|
+
render() {
|
|
766
|
+
let { props } = this;
|
|
767
|
+
return (preact.createElement(internal.StandardEvent, Object.assign({}, props, { display: 'row', defaultTimeFormat: DEFAULT_TIME_FORMAT, defaultDisplayEventTime: !props.isTimeScale })));
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
class TimelineLaneMoreLink extends internal.BaseComponent {
|
|
772
|
+
render() {
|
|
773
|
+
let { props } = this;
|
|
774
|
+
let { hiddenSegs, resourceId } = props;
|
|
775
|
+
let dateSpanProps = resourceId ? { resourceId } : {};
|
|
776
|
+
return (preact.createElement(internal.MoreLinkContainer, { display: 'row', allDayDate: null, segs: hiddenSegs, hiddenSegs: hiddenSegs, dateProfile: props.dateProfile, todayRange: props.todayRange, dateSpanProps: dateSpanProps, isNarrow: false, isMicro: false, popoverContent: () => (preact.createElement(preact.Fragment, null, hiddenSegs.map((seg) => {
|
|
777
|
+
let { eventRange } = seg;
|
|
778
|
+
let { instanceId } = eventRange.instance;
|
|
779
|
+
let isDragging = Boolean(props.eventDrag && props.eventDrag.affectedInstances[instanceId]);
|
|
780
|
+
let isResizing = Boolean(props.eventResize && props.eventResize.affectedInstances[instanceId]);
|
|
781
|
+
let isInvisible = isDragging || isResizing;
|
|
782
|
+
return (preact.createElement("div", { key: instanceId, style: { visibility: isInvisible ? 'hidden' : undefined } },
|
|
783
|
+
preact.createElement(TimelineEvent, Object.assign({ isTimeScale: props.isTimeScale, eventRange: eventRange, isStart: seg.isStart, isEnd: seg.isEnd, isDragging: isDragging, isResizing: isResizing, isMirror: false, isSelected: instanceId === props.eventSelection }, internal.getEventRangeMeta(eventRange, props.todayRange, props.nowDate)))));
|
|
784
|
+
}))) }));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function computeManySegHorizontals(segs, segMinWidth, dateEnv, tDateProfile, slotWidth) {
|
|
789
|
+
const res = {};
|
|
790
|
+
for (const seg of segs) {
|
|
791
|
+
res[internal.getEventKey(seg)] = computeSegHorizontals(seg, segMinWidth, dateEnv, tDateProfile, slotWidth);
|
|
792
|
+
}
|
|
793
|
+
return res;
|
|
794
|
+
}
|
|
795
|
+
function computeSegHorizontals(seg, segMinWidth, dateEnv, tDateProfile, slotWidth) {
|
|
796
|
+
const startCoord = dateToCoord(seg.startDate, dateEnv, tDateProfile, slotWidth);
|
|
797
|
+
const endCoord = dateToCoord(seg.endDate, dateEnv, tDateProfile, slotWidth);
|
|
798
|
+
let size = endCoord - startCoord;
|
|
799
|
+
if (segMinWidth) {
|
|
800
|
+
size = Math.max(size, segMinWidth);
|
|
801
|
+
}
|
|
802
|
+
return { start: startCoord, size };
|
|
803
|
+
}
|
|
804
|
+
function computeFgSegPlacements(// mostly horizontals
|
|
805
|
+
segs, segHorizontals, // TODO: use Map
|
|
806
|
+
segHeights, // keyed by instanceId
|
|
807
|
+
hiddenGroupHeights, strictOrder, maxDepth) {
|
|
808
|
+
const segRanges = [];
|
|
809
|
+
// isn't it true that there will either be ALL hcoords or NONE? can optimize
|
|
810
|
+
for (const seg of segs) {
|
|
811
|
+
const hcoords = segHorizontals[internal.getEventKey(seg)];
|
|
812
|
+
if (hcoords) {
|
|
813
|
+
segRanges.push(Object.assign(Object.assign({}, seg), { start: hcoords.start, end: hcoords.start + hcoords.size }));
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const hierarchy = new internal.SegHierarchy(segRanges, (seg) => segHeights.get(internal.getEventKey(seg)), strictOrder, undefined, // maxCoord
|
|
817
|
+
maxDepth);
|
|
818
|
+
const segTops = new Map();
|
|
819
|
+
hierarchy.traverseSegs((seg, segTop) => {
|
|
820
|
+
segTops.set(internal.getEventKey(seg), segTop);
|
|
821
|
+
});
|
|
822
|
+
const { hiddenSegs } = hierarchy;
|
|
823
|
+
let totalHeight = 0;
|
|
824
|
+
for (const segRange of segRanges) {
|
|
825
|
+
const segKey = internal.getEventKey(segRange);
|
|
826
|
+
const segHeight = segHeights.get(segKey);
|
|
827
|
+
const segTop = segTops.get(segKey);
|
|
828
|
+
if (segHeight != null) {
|
|
829
|
+
if (segTop != null) {
|
|
830
|
+
totalHeight = Math.max(totalHeight, segTop + segHeight);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
const hiddenGroups = internal.groupIntersectingSegs(hiddenSegs);
|
|
835
|
+
const hiddenGroupTops = new Map();
|
|
836
|
+
// HACK for hiddenGroup findInsertion() call
|
|
837
|
+
hierarchy.strictOrder = true;
|
|
838
|
+
for (const hiddenGroup of hiddenGroups) {
|
|
839
|
+
const { levelCoord: top } = hierarchy.findInsertion(hiddenGroup, 0);
|
|
840
|
+
const hiddenGroupHeight = hiddenGroupHeights.get(hiddenGroup.key) || 0;
|
|
841
|
+
hiddenGroupTops.set(hiddenGroup.key, top);
|
|
842
|
+
totalHeight = Math.max(totalHeight, top + hiddenGroupHeight);
|
|
843
|
+
}
|
|
844
|
+
return [
|
|
845
|
+
segTops,
|
|
846
|
+
hiddenGroups,
|
|
847
|
+
hiddenGroupTops,
|
|
848
|
+
totalHeight,
|
|
849
|
+
];
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/*
|
|
853
|
+
TODO: make DRY with other Event Harnesses
|
|
854
|
+
*/
|
|
855
|
+
class TimelineEventHarness extends preact.Component {
|
|
856
|
+
constructor() {
|
|
857
|
+
super(...arguments);
|
|
858
|
+
// ref
|
|
859
|
+
this.rootElRef = preact.createRef();
|
|
860
|
+
}
|
|
861
|
+
render() {
|
|
862
|
+
const { props } = this;
|
|
863
|
+
return (preact.createElement("div", { className: classNames__default["default"].abs, style: props.style, ref: this.rootElRef }, props.children));
|
|
864
|
+
}
|
|
865
|
+
componentDidMount() {
|
|
866
|
+
const rootEl = this.rootElRef.current; // TODO: make dynamic with useEffect
|
|
867
|
+
this.disconnectHeight = internal.watchHeight(rootEl, (height) => {
|
|
868
|
+
internal.setRef(this.props.heightRef, height);
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
componentWillUnmount() {
|
|
872
|
+
this.disconnectHeight();
|
|
873
|
+
internal.setRef(this.props.heightRef, null);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
class TimelineFg extends internal.BaseComponent {
|
|
878
|
+
constructor() {
|
|
879
|
+
super(...arguments);
|
|
880
|
+
// memo
|
|
881
|
+
this.sortEventSegs = internal.memoize(internal.sortEventSegs);
|
|
882
|
+
// refs
|
|
883
|
+
this.segHeightRefMap = new internal.RefMap(() => {
|
|
884
|
+
internal.afterSize(this.handleSegHeights);
|
|
885
|
+
});
|
|
886
|
+
this.moreLinkHeightRefMap = new internal.RefMap(() => {
|
|
887
|
+
internal.afterSize(this.handleMoreLinkHeights);
|
|
888
|
+
});
|
|
889
|
+
this.handleMoreLinkHeights = () => {
|
|
890
|
+
this.setState({ moreLinkHeightRev: this.moreLinkHeightRefMap.rev }); // will trigger rerender
|
|
891
|
+
};
|
|
892
|
+
this.handleSegHeights = () => {
|
|
893
|
+
this.setState({ segHeightRev: this.segHeightRefMap.rev }); // will trigger rerender
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
/*
|
|
897
|
+
TODO: lots of memoization needed here!
|
|
898
|
+
*/
|
|
899
|
+
render() {
|
|
900
|
+
let { props, context, segHeightRefMap, moreLinkHeightRefMap } = this;
|
|
901
|
+
let { options } = context;
|
|
902
|
+
let { tDateProfile } = props;
|
|
903
|
+
let mirrorSegs = (props.eventDrag ? props.eventDrag.segs : null) ||
|
|
904
|
+
(props.eventResize ? props.eventResize.segs : null) ||
|
|
905
|
+
[];
|
|
906
|
+
let fgSegs = this.sortEventSegs(props.fgEventSegs, options.eventOrder);
|
|
907
|
+
let fgSegHorizontals = props.slotWidth != null
|
|
908
|
+
? computeManySegHorizontals(fgSegs, options.eventMinWidth, context.dateEnv, tDateProfile, props.slotWidth)
|
|
909
|
+
: {};
|
|
910
|
+
let [fgSegTops, hiddenGroups, hiddenGroupTops, totalHeight] = computeFgSegPlacements(fgSegs, fgSegHorizontals, segHeightRefMap.current, moreLinkHeightRefMap.current, options.eventOrderStrict, options.eventMaxStack);
|
|
911
|
+
this.totalHeight = totalHeight;
|
|
912
|
+
return (preact.createElement("div", { className: core.joinClassNames(classNames__default["default"].rel, classNames__default["default"].noShrink), style: {
|
|
913
|
+
height: totalHeight,
|
|
914
|
+
} },
|
|
915
|
+
this.renderFgSegs(fgSegs, fgSegHorizontals, fgSegTops, hiddenGroups, hiddenGroupTops,
|
|
916
|
+
/* isMirror = */ false),
|
|
917
|
+
this.renderFgSegs(mirrorSegs, props.slotWidth // TODO: memoize
|
|
918
|
+
? computeManySegHorizontals(mirrorSegs, options.eventMinWidth, context.dateEnv, tDateProfile, props.slotWidth)
|
|
919
|
+
: {}, fgSegTops,
|
|
920
|
+
/* hiddenGroups = */ [],
|
|
921
|
+
/* hiddenGroupTops = */ new Map(),
|
|
922
|
+
/* isMirror = */ true)));
|
|
923
|
+
}
|
|
924
|
+
renderFgSegs(segs, segHorizontals, segTops, hiddenGroups, hiddenGroupTops, isMirror) {
|
|
925
|
+
let { props, segHeightRefMap, moreLinkHeightRefMap } = this;
|
|
926
|
+
return (preact.createElement(preact.Fragment, null,
|
|
927
|
+
segs.map((seg) => {
|
|
928
|
+
const { eventRange } = seg;
|
|
929
|
+
const { instanceId } = eventRange.instance;
|
|
930
|
+
const segTop = segTops.get(instanceId);
|
|
931
|
+
const segHorizontalMaybe = segHorizontals[instanceId];
|
|
932
|
+
const segHorizontal = segHorizontalMaybe || {};
|
|
933
|
+
const isDragging = Boolean(props.eventDrag && props.eventDrag.affectedInstances[instanceId]);
|
|
934
|
+
const isResizing = Boolean(props.eventResize && props.eventResize.affectedInstances[instanceId]);
|
|
935
|
+
const isInvisible = !isMirror && (isDragging || isResizing || !segHorizontalMaybe || segTop == null);
|
|
936
|
+
return (preact.createElement(TimelineEventHarness, { key: instanceId, style: {
|
|
937
|
+
visibility: isInvisible ? 'hidden' : undefined,
|
|
938
|
+
zIndex: 1,
|
|
939
|
+
top: segTop || 0,
|
|
940
|
+
insetInlineStart: segHorizontal.start,
|
|
941
|
+
width: segHorizontal.size,
|
|
942
|
+
}, heightRef: isMirror ? undefined : segHeightRefMap.createRef(instanceId) },
|
|
943
|
+
preact.createElement(TimelineEvent, Object.assign({ isTimeScale: props.tDateProfile.isTimeScale, eventRange: eventRange, isStart: seg.isStart, isEnd: seg.isEnd, isDragging: isDragging, isResizing: isResizing, isMirror: isMirror, isSelected: instanceId === props.eventSelection }, internal.getEventRangeMeta(eventRange, props.todayRange, props.nowDate)))));
|
|
944
|
+
}),
|
|
945
|
+
hiddenGroups.map((hiddenGroup) => (preact.createElement(TimelineEventHarness, { key: hiddenGroup.key, style: {
|
|
946
|
+
top: hiddenGroupTops.get(hiddenGroup.key) || 0,
|
|
947
|
+
insetInlineStart: hiddenGroup.start,
|
|
948
|
+
width: hiddenGroup.end - hiddenGroup.start,
|
|
949
|
+
}, heightRef: moreLinkHeightRefMap.createRef(hiddenGroup.key) },
|
|
950
|
+
preact.createElement(TimelineLaneMoreLink, { hiddenSegs: hiddenGroup.segs, dateProfile: props.dateProfile, nowDate: props.nowDate, todayRange: props.todayRange, isTimeScale: props.tDateProfile.isTimeScale, eventDrag: props.eventDrag, eventResize: props.eventResize, eventSelection: props.eventSelection, resourceId: props.resourceId }))))));
|
|
951
|
+
}
|
|
952
|
+
/*
|
|
953
|
+
componentDidMount(): void {
|
|
954
|
+
// might want to do firedTotalHeight, but won't be ready on first render
|
|
955
|
+
}
|
|
956
|
+
*/
|
|
957
|
+
componentDidUpdate() {
|
|
958
|
+
if (this.totalHeight !== this.firedTotalHeight) {
|
|
959
|
+
this.firedTotalHeight = this.totalHeight;
|
|
960
|
+
internal.setRef(this.props.heightRef, this.totalHeight);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
componentWillUnmount() {
|
|
964
|
+
internal.setRef(this.props.heightRef, null);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
class TimelineBg extends internal.BaseComponent {
|
|
969
|
+
render() {
|
|
970
|
+
let { props } = this;
|
|
971
|
+
let highlightSeg = [].concat(props.eventResizeSegs || [], props.dateSelectionSegs);
|
|
972
|
+
return (preact.createElement(preact.Fragment, null,
|
|
973
|
+
this.renderSegs(props.businessHourSegs || [], 'non-business'),
|
|
974
|
+
this.renderSegs(props.bgEventSegs || [], 'bg-event'),
|
|
975
|
+
this.renderSegs(highlightSeg, 'highlight')));
|
|
976
|
+
}
|
|
977
|
+
renderSegs(segs, fillType) {
|
|
978
|
+
let { tDateProfile, todayRange, nowDate, slotWidth } = this.props;
|
|
979
|
+
let { dateEnv, options } = this.context;
|
|
980
|
+
return (preact.createElement(preact.Fragment, null, segs.map((seg) => {
|
|
981
|
+
let hStyle = {};
|
|
982
|
+
if (slotWidth != null) {
|
|
983
|
+
let segHorizontal = computeSegHorizontals(seg, undefined, dateEnv, tDateProfile, slotWidth);
|
|
984
|
+
hStyle = { insetInlineStart: segHorizontal.start, width: segHorizontal.size };
|
|
985
|
+
}
|
|
986
|
+
return (preact.createElement("div", { key: internal.buildEventRangeKey(seg.eventRange), className: classNames__default["default"].fillY, style: hStyle }, fillType === 'bg-event' ? (preact.createElement(internal.BgEvent, Object.assign({ eventRange: seg.eventRange, isStart: seg.isStart, isEnd: seg.isEnd, isVertical: false }, internal.getEventRangeMeta(seg.eventRange, todayRange, nowDate)))) : (internal.renderFill(fillType, options))));
|
|
987
|
+
})));
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
class TimelineView extends internal.DateComponent {
|
|
992
|
+
constructor() {
|
|
993
|
+
super(...arguments);
|
|
994
|
+
// memoized
|
|
995
|
+
this.buildTimelineDateProfile = internal.memoize(buildTimelineDateProfile);
|
|
996
|
+
this.computeSlotWidth = internal.memoize(computeSlotWidth);
|
|
997
|
+
// refs
|
|
998
|
+
this.headerScrollerRef = preact.createRef();
|
|
999
|
+
this.bodyScrollerRef = preact.createRef();
|
|
1000
|
+
this.footerScrollerRef = preact.createRef();
|
|
1001
|
+
this.headerRowInnerWidthMap = new internal.RefMap(() => {
|
|
1002
|
+
internal.afterSize(this.handleSlotInnerWidths);
|
|
1003
|
+
});
|
|
1004
|
+
this.scrollTime = null;
|
|
1005
|
+
this.slicer = new TimelineLaneSlicer();
|
|
1006
|
+
// Sizing
|
|
1007
|
+
// -----------------------------------------------------------------------------------------------
|
|
1008
|
+
this.handleSlotInnerWidths = () => {
|
|
1009
|
+
const headerSlotInnerWidth = this.headerRowInnerWidthMap.current.get(this.tDateProfile.cellRows.length - 1);
|
|
1010
|
+
if (headerSlotInnerWidth != null && headerSlotInnerWidth !== this.state.slotInnerWidth) {
|
|
1011
|
+
this.setState({ slotInnerWidth: headerSlotInnerWidth });
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
this.handleTotalWidth = (totalWidth) => {
|
|
1015
|
+
this.setState({
|
|
1016
|
+
totalWidth,
|
|
1017
|
+
});
|
|
1018
|
+
};
|
|
1019
|
+
this.handleClientWidth = (clientWidth) => {
|
|
1020
|
+
this.setState({
|
|
1021
|
+
clientWidth,
|
|
1022
|
+
});
|
|
1023
|
+
};
|
|
1024
|
+
this.handleTimeScrollRequest = (scrollTime) => {
|
|
1025
|
+
this.scrollTime = scrollTime;
|
|
1026
|
+
this.applyTimeScroll();
|
|
1027
|
+
};
|
|
1028
|
+
this.handleTimeScrollEnd = (isUser) => {
|
|
1029
|
+
if (isUser) {
|
|
1030
|
+
this.scrollTime = null;
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
// Hit System
|
|
1034
|
+
// -----------------------------------------------------------------------------------------------
|
|
1035
|
+
this.handeBodyEl = (el) => {
|
|
1036
|
+
this.bodyEl = el;
|
|
1037
|
+
if (el) {
|
|
1038
|
+
this.context.registerInteractiveComponent(this, { el });
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
this.context.unregisterInteractiveComponent(this);
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
render() {
|
|
1046
|
+
const { props, state, context } = this;
|
|
1047
|
+
const { options } = context;
|
|
1048
|
+
const { totalWidth, clientWidth } = state;
|
|
1049
|
+
const endScrollbarWidth = (totalWidth != null && clientWidth != null)
|
|
1050
|
+
? totalWidth - clientWidth
|
|
1051
|
+
: undefined;
|
|
1052
|
+
/* date */
|
|
1053
|
+
const tDateProfile = this.tDateProfile = this.buildTimelineDateProfile(props.dateProfile, context.dateEnv, options, context.dateProfileGenerator);
|
|
1054
|
+
const { cellRows } = tDateProfile;
|
|
1055
|
+
const timerUnit = internal.greatestDurationDenominator(tDateProfile.slotDuration).unit;
|
|
1056
|
+
/* table settings */
|
|
1057
|
+
const verticalScrolling = !props.forPrint && !internal.getIsHeightAuto(options);
|
|
1058
|
+
const stickyHeaderDates = !props.forPrint && internal.getStickyHeaderDates(options);
|
|
1059
|
+
const stickyFooterScrollbar = !props.forPrint && internal.getStickyFooterScrollbar(options);
|
|
1060
|
+
/* table positions */
|
|
1061
|
+
const [canvasWidth, slotWidth] = this.computeSlotWidth(tDateProfile.slotCnt, tDateProfile.slotsPerLabel, options.slotMinWidth, state.slotInnerWidth, // is ACTUALLY the label width. rename?
|
|
1062
|
+
clientWidth);
|
|
1063
|
+
this.slotWidth = slotWidth;
|
|
1064
|
+
/* sliced */
|
|
1065
|
+
let slicedProps = this.slicer.sliceProps(props, props.dateProfile, tDateProfile.isTimeScale ? null : options.nextDayThreshold, context, // wish we didn't have to pass in the rest of the args...
|
|
1066
|
+
props.dateProfile, context.dateProfileGenerator, tDateProfile, context.dateEnv);
|
|
1067
|
+
return (preact.createElement(internal.NowTimer, { unit: timerUnit }, (nowDate, todayRange) => {
|
|
1068
|
+
const enableNowIndicator = // TODO: DRY
|
|
1069
|
+
options.nowIndicator &&
|
|
1070
|
+
slotWidth != null &&
|
|
1071
|
+
internal.rangeContainsMarker(props.dateProfile.currentRange, nowDate);
|
|
1072
|
+
return (preact.createElement(internal.ViewContainer, { viewSpec: context.viewSpec, className: internal.joinArrayishClassNames(
|
|
1073
|
+
// HACK for Safari print-mode, where noScrollbars won't take effect for
|
|
1074
|
+
// the below Scrollers if they have liquid flex height
|
|
1075
|
+
!props.forPrint && classNames__default["default"].flexCol, props.className, options.tableClass, classNames__default["default"].isolate), borderlessX: props.borderlessX, borderlessTop: props.borderlessTop, borderlessBottom: props.borderlessBottom, noEdgeEffects: props.noEdgeEffects },
|
|
1076
|
+
preact.createElement("div", { className: core.joinClassNames(internal.generateClassName(options.tableHeaderClass, {
|
|
1077
|
+
isSticky: stickyHeaderDates,
|
|
1078
|
+
}), props.borderlessX && classNames__default["default"].borderlessX, classNames__default["default"].flexCol, stickyHeaderDates && classNames__default["default"].tableHeaderSticky), style: {
|
|
1079
|
+
zIndex: 1,
|
|
1080
|
+
} },
|
|
1081
|
+
preact.createElement(internal.Scroller, { horizontal: true, hideScrollbars: true, className: classNames__default["default"].flexRow, ref: this.headerScrollerRef },
|
|
1082
|
+
preact.createElement("div", {
|
|
1083
|
+
// TODO: DRY
|
|
1084
|
+
className: core.joinClassNames(classNames__default["default"].rel, // origin for now-indicator
|
|
1085
|
+
canvasWidth == null && classNames__default["default"].liquid), style: { width: canvasWidth } },
|
|
1086
|
+
cellRows.map((cells, rowIndex) => {
|
|
1087
|
+
const rowLevel = cellRows.length - rowIndex - 1;
|
|
1088
|
+
return (preact.createElement(TimelineHeaderRow, { key: rowIndex, dateProfile: props.dateProfile, tDateProfile: tDateProfile, nowDate: nowDate, todayRange: todayRange, rowLevel: rowLevel, cells: cells, slotWidth: slotWidth, innerWidthRef: this.headerRowInnerWidthMap.createRef(rowIndex) }));
|
|
1089
|
+
}),
|
|
1090
|
+
enableNowIndicator && (preact.createElement(TimelineNowIndicatorArrow, { tDateProfile: tDateProfile, nowDate: nowDate, slotWidth: slotWidth }))),
|
|
1091
|
+
Boolean(endScrollbarWidth) && (preact.createElement("div", { className: internal.joinArrayishClassNames(internal.generateClassName(options.fillerClass, { isHeader: true }), classNames__default["default"].borderOnlyS), style: { minWidth: endScrollbarWidth } }))),
|
|
1092
|
+
preact.createElement("div", { className: internal.generateClassName(options.slotHeaderDividerClass, {
|
|
1093
|
+
isHeader: true,
|
|
1094
|
+
options: { dayMinWidth: options.dayMinWidth },
|
|
1095
|
+
}) })),
|
|
1096
|
+
preact.createElement(internal.Scroller, { vertical: verticalScrolling, horizontal: true, hideScrollbars: stickyFooterScrollbar ||
|
|
1097
|
+
props.forPrint // prevents blank space in print-view on Safari
|
|
1098
|
+
, className: internal.joinArrayishClassNames(options.tableBodyClass, props.borderlessX && classNames__default["default"].borderlessX, stickyHeaderDates && classNames__default["default"].borderlessTop, (stickyHeaderDates || props.noEdgeEffects) && classNames__default["default"].noEdgeEffects, classNames__default["default"].flexCol, verticalScrolling && classNames__default["default"].liquid), style: {
|
|
1099
|
+
zIndex: 0,
|
|
1100
|
+
}, ref: this.bodyScrollerRef, clientWidthRef: this.handleClientWidth },
|
|
1101
|
+
preact.createElement("div", { "aria-label": options.eventsHint, className: core.joinClassNames(classNames__default["default"].rel, // for canvas origin?
|
|
1102
|
+
classNames__default["default"].grow), style: { width: canvasWidth }, ref: this.handeBodyEl },
|
|
1103
|
+
preact.createElement(TimelineSlats, { dateProfile: props.dateProfile, tDateProfile: tDateProfile, nowDate: nowDate, todayRange: todayRange,
|
|
1104
|
+
// dimensions
|
|
1105
|
+
slotWidth: slotWidth }),
|
|
1106
|
+
preact.createElement(TimelineBg, { tDateProfile: tDateProfile, nowDate: nowDate, todayRange: todayRange,
|
|
1107
|
+
// content
|
|
1108
|
+
bgEventSegs: slicedProps.bgEventSegs, businessHourSegs: slicedProps.businessHourSegs, dateSelectionSegs: slicedProps.dateSelectionSegs, eventResizeSegs: slicedProps.eventResize ? slicedProps.eventResize.segs : null,
|
|
1109
|
+
// dimensions
|
|
1110
|
+
slotWidth: slotWidth }),
|
|
1111
|
+
preact.createElement("div", { className: internal.joinArrayishClassNames(options.timelineTopClass) }),
|
|
1112
|
+
preact.createElement(TimelineFg, { dateProfile: props.dateProfile, tDateProfile: tDateProfile, nowDate: nowDate, todayRange: todayRange,
|
|
1113
|
+
// content
|
|
1114
|
+
fgEventSegs: slicedProps.fgEventSegs, eventDrag: slicedProps.eventDrag, eventResize: slicedProps.eventResize, eventSelection: slicedProps.eventSelection,
|
|
1115
|
+
// dimensions
|
|
1116
|
+
slotWidth: slotWidth }),
|
|
1117
|
+
preact.createElement("div", { className: internal.joinArrayishClassNames(options.timelineBottomClass) }),
|
|
1118
|
+
enableNowIndicator && (preact.createElement(TimelineNowIndicatorLine, { tDateProfile: tDateProfile, nowDate: nowDate, slotWidth: slotWidth })))),
|
|
1119
|
+
Boolean(stickyFooterScrollbar) && (preact.createElement(internal.FooterScrollbar, { isSticky: true, canvasWidth: canvasWidth, scrollerRef: this.footerScrollerRef })),
|
|
1120
|
+
preact.createElement(internal.Ruler, { widthRef: this.handleTotalWidth })));
|
|
1121
|
+
}));
|
|
1122
|
+
}
|
|
1123
|
+
// Lifecycle
|
|
1124
|
+
// -----------------------------------------------------------------------------------------------
|
|
1125
|
+
componentDidMount() {
|
|
1126
|
+
this.syncedScroller = new internal$1.ScrollerSyncer(true); // horizontal=true
|
|
1127
|
+
this.updateSyncedScroller();
|
|
1128
|
+
this.resetScroll();
|
|
1129
|
+
this.context.emitter.on('_timeScrollRequest', this.handleTimeScrollRequest);
|
|
1130
|
+
this.syncedScroller.addScrollEndListener(this.handleTimeScrollEnd);
|
|
1131
|
+
}
|
|
1132
|
+
componentDidUpdate(prevProps) {
|
|
1133
|
+
this.updateSyncedScroller();
|
|
1134
|
+
if (prevProps.dateProfile !== this.props.dateProfile && this.context.options.scrollTimeReset) {
|
|
1135
|
+
this.resetScroll();
|
|
1136
|
+
}
|
|
1137
|
+
else {
|
|
1138
|
+
// TODO: inefficient to update so often
|
|
1139
|
+
this.applyTimeScroll();
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
componentWillUnmount() {
|
|
1143
|
+
this.syncedScroller.destroy();
|
|
1144
|
+
this.context.emitter.off('_timeScrollRequest', this.handleTimeScrollRequest);
|
|
1145
|
+
this.syncedScroller.removeScrollEndListener(this.handleTimeScrollEnd);
|
|
1146
|
+
}
|
|
1147
|
+
// Scrolling
|
|
1148
|
+
// -----------------------------------------------------------------------------------------------
|
|
1149
|
+
updateSyncedScroller() {
|
|
1150
|
+
this.syncedScroller.handleChildren([
|
|
1151
|
+
this.headerScrollerRef.current,
|
|
1152
|
+
this.bodyScrollerRef.current,
|
|
1153
|
+
this.footerScrollerRef.current
|
|
1154
|
+
]);
|
|
1155
|
+
}
|
|
1156
|
+
resetScroll() {
|
|
1157
|
+
this.handleTimeScrollRequest(this.context.options.scrollTime);
|
|
1158
|
+
}
|
|
1159
|
+
applyTimeScroll() {
|
|
1160
|
+
const { props, context, tDateProfile, scrollTime, slotWidth } = this;
|
|
1161
|
+
if (scrollTime != null && slotWidth != null) {
|
|
1162
|
+
let x = timeToCoord(scrollTime, context.dateEnv, props.dateProfile, tDateProfile, slotWidth);
|
|
1163
|
+
if (x) {
|
|
1164
|
+
x += 1; // overcome border. TODO: DRY this up
|
|
1165
|
+
}
|
|
1166
|
+
this.syncedScroller.scrollTo({ x });
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
queryHit(isRtl, positionLeft, positionTop, elWidth, elHeight) {
|
|
1170
|
+
const { props, context, tDateProfile, slotWidth } = this;
|
|
1171
|
+
const { dateEnv } = context;
|
|
1172
|
+
if (slotWidth) {
|
|
1173
|
+
const x = isRtl ? elWidth - positionLeft : positionLeft;
|
|
1174
|
+
const slatIndex = Math.floor(x / slotWidth);
|
|
1175
|
+
const slatX = slatIndex * slotWidth;
|
|
1176
|
+
const partial = (x - slatX) / slotWidth; // floating point number between 0 and 1
|
|
1177
|
+
const localSnapIndex = Math.floor(partial * tDateProfile.snapsPerSlot); // the snap # relative to start of slat
|
|
1178
|
+
let startDate = dateEnv.add(tDateProfile.slotDates[slatIndex], internal.multiplyDuration(tDateProfile.snapDuration, localSnapIndex));
|
|
1179
|
+
let endDate = dateEnv.add(startDate, tDateProfile.snapDuration);
|
|
1180
|
+
// TODO: generalize this coord stuff to TimeGrid?
|
|
1181
|
+
let snapWidth = slotWidth / tDateProfile.snapsPerSlot;
|
|
1182
|
+
let startCoord = slatIndex * slotWidth + (snapWidth * localSnapIndex);
|
|
1183
|
+
let endCoord = startCoord + snapWidth;
|
|
1184
|
+
let left, right;
|
|
1185
|
+
if (isRtl) {
|
|
1186
|
+
left = elWidth - endCoord;
|
|
1187
|
+
right = elWidth - startCoord;
|
|
1188
|
+
}
|
|
1189
|
+
else {
|
|
1190
|
+
left = startCoord;
|
|
1191
|
+
right = endCoord;
|
|
1192
|
+
}
|
|
1193
|
+
return {
|
|
1194
|
+
dateProfile: props.dateProfile,
|
|
1195
|
+
dateSpan: {
|
|
1196
|
+
range: { start: startDate, end: endDate },
|
|
1197
|
+
allDay: !tDateProfile.isTimeScale,
|
|
1198
|
+
},
|
|
1199
|
+
rect: {
|
|
1200
|
+
left,
|
|
1201
|
+
right,
|
|
1202
|
+
top: 0,
|
|
1203
|
+
bottom: elHeight,
|
|
1204
|
+
},
|
|
1205
|
+
getDayEl: () => getTimelineSlotEl(this.bodyEl, slatIndex),
|
|
1206
|
+
layer: 0,
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
exports.TimelineBg = TimelineBg;
|
|
1214
|
+
exports.TimelineFg = TimelineFg;
|
|
1215
|
+
exports.TimelineHeaderRow = TimelineHeaderRow;
|
|
1216
|
+
exports.TimelineLaneSlicer = TimelineLaneSlicer;
|
|
1217
|
+
exports.TimelineNowIndicatorArrow = TimelineNowIndicatorArrow;
|
|
1218
|
+
exports.TimelineNowIndicatorLine = TimelineNowIndicatorLine;
|
|
1219
|
+
exports.TimelineSlats = TimelineSlats;
|
|
1220
|
+
exports.TimelineView = TimelineView;
|
|
1221
|
+
exports.buildTimelineDateProfile = buildTimelineDateProfile;
|
|
1222
|
+
exports.computeSlotWidth = computeSlotWidth;
|
|
1223
|
+
exports.getTimelineSlotEl = getTimelineSlotEl;
|
|
1224
|
+
exports.timeToCoord = timeToCoord;
|