@open-rlb/ng-bootstrap 3.3.17 → 3.3.18

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.
@@ -2513,17 +2513,43 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
2513
2513
  args: [{ selector: 'rlb-fab', changeDetection: ChangeDetectionStrategy.OnPush, imports: [ButtonComponent], template: "<button rlb-button [color]=\"color()\" [disabled]=\"disabled()\" [outline]=\"outline()\"\n class=\"fab-button d-flex align-items-center justify-content-center\" [class]=\"[sizeClass(), positionClass()]\">\n <ng-content></ng-content>\n</button>", styles: [".fab-button{border-radius:50%;box-shadow:0 4px 12px #00000040;z-index:1050;transition:transform .2s ease,box-shadow .2s ease;padding:0}.fab-button:hover{transform:scale(1.08);box-shadow:0 6px 18px #00000059}.fab-button:active{transform:scale(.95)}:host{width:fit-content;display:block}.fab-xs{width:32px;height:32px;font-size:1rem}.fab-sm{width:44px;height:44px;font-size:1.2rem}.fab-md{width:56px;height:56px;font-size:1.4rem}.fab-lg{width:68px;height:68px;font-size:1.7rem}.fab-bottom-right{bottom:24px;right:24px}.fab-bottom-left{bottom:24px;left:24px}.fab-top-right{top:24px;right:24px}.fab-top-left{top:24px;left:24px}\n"] }]
2514
2514
  }], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], outline: [{ type: i0.Input, args: [{ isSignal: true, alias: "outline", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }] } });
2515
2515
 
2516
+ const MS_PER_DAY = 86_400_000;
2517
+ /**
2518
+ * NOTE on date-tz: the getters (`.hour`, `.minute`, `.day`, `.dayOfWeek`, ...)
2519
+ * and `toString()` are timezone-aware (they add `timezoneOffset` to the raw
2520
+ * timestamp), but the mutators `set()`, `add()` and `stripSecMillis()` are
2521
+ * timezone-NAIVE — they decompose/recompose the raw timestamp as if it were
2522
+ * UTC. So `set(0, 'hour')` zeroes the UTC hour, not the local one.
2523
+ *
2524
+ * For calendar layout we need timezone-aware day math, so the helpers below
2525
+ * derive day boundaries and intra-day offsets from the tz-aware side of the
2526
+ * library instead of from `set()/add()`.
2527
+ */
2516
2528
  /** Returns the IANA timezone resolved from the browser/runtime. */
2517
2529
  function getBrowserTimezone() {
2518
2530
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
2519
2531
  }
2532
+ /** Epoch ms of local midnight (in `timezone`) of the day containing `d`. */
2533
+ function startOfDayTs(d, timezone) {
2534
+ const local = d.cloneToTimezone(timezone);
2535
+ const localMs = local.timestamp + local.timezoneOffset;
2536
+ return Math.floor(localMs / MS_PER_DAY) * MS_PER_DAY - local.timezoneOffset;
2537
+ }
2538
+ /** Minutes elapsed since local midnight (in `timezone`) for `d`. */
2539
+ function minutesSinceMidnight(d, timezone) {
2540
+ const local = d.cloneToTimezone(timezone);
2541
+ return local.hour * 60 + local.minute;
2542
+ }
2543
+ /** Local-midnight instance (in `timezone`) for the day at index `offsetDays` from `d`. */
2544
+ function dayAt(d, timezone, offsetDays = 0) {
2545
+ const baseMidnight = startOfDayTs(d, timezone);
2546
+ // Re-floor after the raw day shift so DST transitions can't drift the result.
2547
+ const shifted = new DateTz(baseMidnight + offsetDays * MS_PER_DAY, timezone);
2548
+ return new DateTz(startOfDayTs(shifted, timezone), timezone);
2549
+ }
2520
2550
  function isSameDay(a, b, timezone) {
2521
2551
  const tz = timezone ?? getBrowserTimezone();
2522
- const da = a.cloneToTimezone(tz);
2523
- const db = b.cloneToTimezone(tz);
2524
- return da.year === db.year &&
2525
- da.month === db.month &&
2526
- da.day === db.day;
2552
+ return startOfDayTs(a, tz) === startOfDayTs(b, tz);
2527
2553
  }
2528
2554
  function addDays(date, days) {
2529
2555
  return new DateTz(date.add(days, 'day'));
@@ -2532,17 +2558,9 @@ function startOfMonth(date) {
2532
2558
  const d = new DateTz(date);
2533
2559
  return d.set(1, 'day');
2534
2560
  }
2535
- // export function endOfMonth(date: IDateTz): DateTz {
2536
- // const d = new DateTz(date.timestamp, 'UTC');
2537
- // return d.add(1, 'month').set(1, 'day').add(-1, 'day');
2538
- // }
2539
2561
  function isToday(date, timezone) {
2540
2562
  const tz = timezone ?? getBrowserTimezone();
2541
- const d = date.cloneToTimezone(tz);
2542
- const today = getToday(tz);
2543
- return d.year === today.year &&
2544
- d.month === today.month &&
2545
- d.day === today.day;
2563
+ return startOfDayTs(date, tz) === startOfDayTs(getToday(tz), tz);
2546
2564
  }
2547
2565
  function getToday(timezone) {
2548
2566
  return DateTz.now(timezone ?? getBrowserTimezone());
@@ -2733,13 +2751,12 @@ class CalendarWeekGridComponent {
2733
2751
  const rawMinutesFromStart = (newTopPx / this.layout().rowHeight) * 60;
2734
2752
  const snappedMinutes = Math.round(rawMinutesFromStart / this.SNAP_MINUTES) * this.SNAP_MINUTES;
2735
2753
  const validMinutes = Math.max(0, snappedMinutes);
2736
- const newStart = new DateTz$1(newDay)
2737
- .set(0, 'hour')
2738
- .set(0, 'minute')
2739
- .add(validMinutes, 'minute')
2740
- .stripSecMillis();
2754
+ // Local midnight of the target day + dropped offset (tz-aware).
2755
+ const tz = this.timezone();
2756
+ const newStartTs = startOfDayTs(newDay, tz) + validMinutes * 60 * 1000;
2757
+ const newStart = new DateTz$1(newStartTs, tz).stripSecMillis();
2741
2758
  const durationMs = movedEvent.end.timestamp - movedEvent.start.timestamp;
2742
- const newEnd = new DateTz$1(newStart.timestamp + durationMs, newStart.timezone).stripSecMillis();
2759
+ const newEnd = new DateTz$1(newStart.timestamp + durationMs, tz).stripSecMillis();
2743
2760
  if (newStart.timestamp !== movedEvent.start.timestamp) {
2744
2761
  const updatedEvent = {
2745
2762
  ...movedEvent,
@@ -2750,13 +2767,10 @@ class CalendarWeekGridComponent {
2750
2767
  }
2751
2768
  }
2752
2769
  getEventsForDay(day) {
2753
- const dayTs = new DateTz$1(day).set(0, 'hour').set(0, 'minute').stripSecMillis().timestamp;
2754
- return this.processedEvents().get(dayTs) || [];
2770
+ return this.processedEvents().get(startOfDayTs(day, this.timezone())) || [];
2755
2771
  }
2756
2772
  calculateEventTop(event) {
2757
- const startOfDay = new DateTz$1(event.start).set(0, 'hour').set(0, 'minute').stripSecMillis();
2758
- const diffMs = event.start.timestamp - startOfDay.timestamp;
2759
- const diffMinutes = diffMs / (1000 * 60);
2773
+ const diffMinutes = minutesSinceMidnight(event.start, this.timezone());
2760
2774
  return (diffMinutes / 60) * this.layout().rowHeight;
2761
2775
  }
2762
2776
  calculateEventHeight(event) {
@@ -2803,16 +2817,13 @@ class CalendarWeekGridComponent {
2803
2817
  return `var(--bs-${interval.color || 'success'})`;
2804
2818
  }
2805
2819
  buildWeekGrid(currentDate) {
2806
- const dayOfWeek = currentDate.dayOfWeek; // 0 = Sunday, 1 = Monday ...
2807
- // Week init (monday)
2808
- const start = new DateTz$1(currentDate)
2809
- .add(-(dayOfWeek - 1), 'day') // clone or create new for start date
2810
- .set(0, 'hour')
2811
- .set(0, 'minute')
2812
- .stripSecMillis();
2820
+ const tz = this.timezone();
2821
+ const dayOfWeek = currentDate.cloneToTimezone(tz).dayOfWeek; // 0 = Sunday, 1 = Monday ...
2822
+ // Offset (in days) from currentDate back to the week's Monday.
2823
+ const mondayOffset = 1 - dayOfWeek; // Sun(0) -> +1, Mon(1) -> 0, Tue(2) -> -1 ...
2813
2824
  const newDays = [];
2814
2825
  for (let i = 0; i < 7; i++) {
2815
- newDays.push(new DateTz$1(start).add(i, 'day'));
2826
+ newDays.push(dayAt(currentDate, tz, mondayOffset + i));
2816
2827
  }
2817
2828
  this.days.set(newDays);
2818
2829
  this.startNowTimer();
@@ -2833,37 +2844,32 @@ class CalendarWeekGridComponent {
2833
2844
  }
2834
2845
  processAllEvents(events) {
2835
2846
  const eventsByDay = new Map();
2847
+ const tz = this.timezone();
2836
2848
  for (const event of events) {
2837
- const originalStart = new DateTz$1(event.start).stripSecMillis();
2838
- const originalEnd = new DateTz$1(event.end).stripSecMillis();
2839
- let currentChunkStart = new DateTz$1(originalStart);
2840
- while (currentChunkStart.timestamp < originalEnd.timestamp) {
2841
- const startOfNextDay = new DateTz$1(currentChunkStart)
2842
- .set(0, 'hour')
2843
- .set(0, 'minute')
2844
- .add(1, 'day')
2845
- .stripSecMillis();
2846
- const chunkEndTimestamp = Math.min(originalEnd.timestamp, startOfNextDay.timestamp);
2847
- const chunkEnd = new DateTz$1(chunkEndTimestamp);
2848
- const currentDayStart = new DateTz$1(currentChunkStart)
2849
- .set(0, 'hour')
2850
- .set(0, 'minute')
2851
- .stripSecMillis();
2852
- const dayTimestamp = currentDayStart.timestamp;
2849
+ const originalStartTs = new DateTz$1(event.start).stripSecMillis().timestamp;
2850
+ const originalEndTs = new DateTz$1(event.end).stripSecMillis().timestamp;
2851
+ let chunkStartTs = originalStartTs;
2852
+ while (chunkStartTs < originalEndTs) {
2853
+ const chunkInst = new DateTz$1(chunkStartTs, tz);
2854
+ // tz-aware day boundaries (set()/add() would be UTC-naive here).
2855
+ const dayStartTs = startOfDayTs(chunkInst, tz);
2856
+ const startOfNextDayTs = dayAt(chunkInst, tz, 1).timestamp;
2857
+ const chunkEndTs = Math.min(originalEndTs, startOfNextDayTs);
2853
2858
  const visualEvent = {
2854
2859
  ...event,
2855
- start: currentChunkStart,
2856
- end: chunkEnd,
2857
- isContinuedBefore: currentChunkStart.timestamp > originalStart.timestamp,
2858
- isContinuedAfter: chunkEnd.timestamp < originalEnd.timestamp,
2860
+ // Chunks live in the calendar timezone, so labels and positions agree.
2861
+ start: new DateTz$1(chunkStartTs, tz),
2862
+ end: new DateTz$1(chunkEndTs, tz),
2863
+ isContinuedBefore: chunkStartTs > originalStartTs,
2864
+ isContinuedAfter: chunkEndTs < originalEndTs,
2859
2865
  left: 0,
2860
2866
  width: 0
2861
2867
  };
2862
- if (!eventsByDay.has(dayTimestamp)) {
2863
- eventsByDay.set(dayTimestamp, []);
2868
+ if (!eventsByDay.has(dayStartTs)) {
2869
+ eventsByDay.set(dayStartTs, []);
2864
2870
  }
2865
- eventsByDay.get(dayTimestamp).push(visualEvent);
2866
- currentChunkStart = startOfNextDay;
2871
+ eventsByDay.get(dayStartTs).push(visualEvent);
2872
+ chunkStartTs = startOfNextDayTs;
2867
2873
  }
2868
2874
  }
2869
2875
  const newProcessedEvents = new Map();
@@ -3079,21 +3085,18 @@ class CalendarMonthGridComponent {
3079
3085
  }
3080
3086
  // --- Core Logic ---
3081
3087
  buildMonthGrid() {
3082
- // 1. Calculate Grid Boundaries
3083
- const current = new DateTz(this.currentDate());
3084
- // Find the first day of the month
3085
- const startOfMonth = new DateTz(current).set(1, 'day').set(0, 'hour').set(0, 'minute').stripSecMillis();
3086
- // Calculate offset to find the start of the grid (e.g. Monday)
3087
- const dayOfWeek = startOfMonth.dayOfWeek; // 0 = Sun, 1 = Mon...
3088
- // Adjust logic based on your locale start of week (assuming Monday start here)
3088
+ const tz = this.timezone();
3089
+ // 1. Calculate Grid Boundaries (tz-aware; set()/add() would be UTC-naive)
3090
+ const current = this.currentDate().cloneToTimezone(tz);
3091
+ // First day of the month at local midnight
3092
+ const firstOfMonth = dayAt(current, tz, -(current.day - 1));
3093
+ // Calculate offset to find the start of the grid (Monday-first)
3094
+ const dayOfWeek = firstOfMonth.dayOfWeek; // 0 = Sun, 1 = Mon...
3089
3095
  const offset = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
3090
- const gridStart = new DateTz(startOfMonth).add(-offset, 'day');
3091
3096
  // 2. Generate Days
3092
3097
  const days = [];
3093
- let iter = new DateTz(gridStart);
3094
3098
  for (let i = 0; i < DAYS_IN_GRID; i++) {
3095
- days.push(iter);
3096
- iter = new DateTz(iter).add(1, 'day');
3099
+ days.push(dayAt(firstOfMonth, tz, i - offset));
3097
3100
  }
3098
3101
  // Header is just the first week
3099
3102
  this.weekDaysHeader.set(days.slice(0, 7));
@@ -3148,6 +3151,7 @@ class CalendarMonthGridComponent {
3148
3151
  * Places events into rows so that long events maintain their vertical position across days.
3149
3152
  */
3150
3153
  calculateEventSlots(days, events) {
3154
+ const tz = this.timezone();
3151
3155
  const dayMap = new Map();
3152
3156
  days.forEach(d => dayMap.set(d.timestamp, []));
3153
3157
  // Sort: Longest events first, then by start time
@@ -3161,25 +3165,18 @@ class CalendarMonthGridComponent {
3161
3165
  const gridStartTs = days[0].timestamp;
3162
3166
  const lastDay = days[days.length - 1];
3163
3167
  // End of grid is the start of the next day after the last cell
3164
- const gridEndTs = new DateTz(lastDay).add(1, 'day').timestamp;
3168
+ const gridEndTs = dayAt(lastDay, tz, 1).timestamp;
3165
3169
  for (const event of sortedEvents) {
3166
- // Normalize start to beginning of day
3167
- const evtStart = new DateTz(event.start).set(0, 'hour').set(0, 'minute').stripSecMillis();
3168
- const evtEndRaw = new DateTz(event.end).stripSecMillis();
3169
- // Normalize end.
3170
- // If ends at 00:00, it effectively ends at the previous day visually.
3171
- // Else, round up to the end of that day.
3172
- let evtEnd;
3173
- const isMidnight = evtEndRaw.hour === 0 && evtEndRaw.minute === 0;
3174
- if (evtEndRaw.timestamp > evtStart.timestamp && isMidnight) {
3175
- evtEnd = evtEndRaw; // Keeps it clean for comparison, effectively 00:00 of next day
3176
- }
3177
- else {
3178
- // Round to start of next day to cover full day
3179
- evtEnd = new DateTz(evtEndRaw).set(0, 'hour').set(0, 'minute').add(1, 'day').stripSecMillis();
3180
- }
3170
+ // tz-aware day-floors (set()/add() would be UTC-naive).
3171
+ const evtStartTs = startOfDayTs(event.start, tz);
3172
+ // Normalize end. If it ends exactly at local 00:00, it visually ends on the
3173
+ // previous day; otherwise round up to the start of the next day.
3174
+ const endsAtMidnight = minutesSinceMidnight(event.end, tz) === 0;
3175
+ const evtEndTs = (event.end.timestamp > evtStartTs && endsAtMidnight)
3176
+ ? startOfDayTs(event.end, tz)
3177
+ : dayAt(event.end, tz, 1).timestamp;
3181
3178
  // Skip if completely outside grid
3182
- if (evtEnd.timestamp <= gridStartTs || evtStart.timestamp >= gridEndTs) {
3179
+ if (evtEndTs <= gridStartTs || evtStartTs >= gridEndTs) {
3183
3180
  continue;
3184
3181
  }
3185
3182
  // Find which grid indices this event occupies
@@ -3187,7 +3184,7 @@ class CalendarMonthGridComponent {
3187
3184
  for (let i = 0; i < days.length; i++) {
3188
3185
  const dTs = days[i].timestamp;
3189
3186
  // Check intersection: [DayStart, DayEnd) overlaps [EventStart, EventEnd)
3190
- if (dTs >= evtStart.timestamp && dTs < evtEnd.timestamp) {
3187
+ if (dTs >= evtStartTs && dTs < evtEndTs) {
3191
3188
  daysIndices.push(i);
3192
3189
  }
3193
3190
  }
@@ -3226,10 +3223,10 @@ class CalendarMonthGridComponent {
3226
3223
  const isLastRenderDay = (i === daysIndices.length - 1);
3227
3224
  // `dayInTz` is the grid cell, already in the calendar timezone.
3228
3225
  const dayInTz = days[idx];
3229
- const currentDayEnd = new DateTz(dayInTz).add(1, 'day');
3226
+ const currentDayEnd = dayAt(dayInTz, tz, 1);
3230
3227
  // Visual flags for rounding corners
3231
- const isContinuedBefore = !isFirstRenderDay || (evtStart.timestamp < days[daysIndices[0]].timestamp);
3232
- const isContinuedAfter = !isLastRenderDay || (evtEnd.timestamp > currentDayEnd.timestamp);
3228
+ const isContinuedBefore = !isFirstRenderDay || (evtStartTs < days[daysIndices[0]].timestamp);
3229
+ const isContinuedAfter = !isLastRenderDay || (evtEndTs > currentDayEnd.timestamp);
3233
3230
  const eventView = {
3234
3231
  ...event,
3235
3232
  // Override start/end for the specific cell (crucial for DnD to know the cell context)
@@ -3327,13 +3324,12 @@ class CalendarDayGridComponent {
3327
3324
  const rawMinutesFromStart = (newTopPx / this.layout().rowHeight) * 60;
3328
3325
  const snappedMinutes = Math.round(rawMinutesFromStart / this.SNAP_MINUTES) * this.SNAP_MINUTES;
3329
3326
  const validMinutes = Math.max(0, snappedMinutes);
3330
- const newStart = new DateTz$1(newDay)
3331
- .set(0, 'hour')
3332
- .set(0, 'minute')
3333
- .add(validMinutes, 'minute')
3334
- .stripSecMillis();
3327
+ // Local midnight of the target day + dropped offset (tz-aware).
3328
+ const tz = this.timezone();
3329
+ const newStartTs = startOfDayTs(newDay, tz) + validMinutes * 60 * 1000;
3330
+ const newStart = new DateTz$1(newStartTs, tz).stripSecMillis();
3335
3331
  const durationMs = movedEvent.end.timestamp - movedEvent.start.timestamp;
3336
- const newEnd = new DateTz$1(newStart.timestamp + durationMs, newStart.timezone).stripSecMillis();
3332
+ const newEnd = new DateTz$1(newStart.timestamp + durationMs, tz).stripSecMillis();
3337
3333
  if (newStart.timestamp !== movedEvent.start.timestamp) {
3338
3334
  const updatedEvent = {
3339
3335
  ...movedEvent,
@@ -3344,16 +3340,9 @@ class CalendarDayGridComponent {
3344
3340
  }
3345
3341
  }
3346
3342
  calculateEventTop(event) {
3347
- const startOfDay = new DateTz$1(this.day()).set(0, 'hour').set(0, 'minute').stripSecMillis();
3348
- // Use event.start directly, but ensure we are calculating relative to THIS day
3349
- // If event starts on previous day, we should clamp?
3350
- // Logic from week grid handles "chunks", here we assume processAllEvents has given us the chunk for this day.
3351
- let eventStart = new DateTz$1(event.start);
3352
- if (eventStart.timestamp < startOfDay.timestamp) {
3353
- eventStart = startOfDay;
3354
- }
3355
- const diffMs = eventStart.timestamp - startOfDay.timestamp;
3356
- const diffMinutes = diffMs / (1000 * 60);
3343
+ // processAllEvents already clamped the event to this day, so its start is
3344
+ // within [00:00, 24:00) of the day. Minutes-since-local-midnight is tz-aware.
3345
+ const diffMinutes = minutesSinceMidnight(event.start, this.timezone());
3357
3346
  return (diffMinutes / 60) * this.layout().rowHeight;
3358
3347
  }
3359
3348
  calculateEventHeight(event) {
@@ -3400,7 +3389,8 @@ class CalendarDayGridComponent {
3400
3389
  return `var(--bs-${interval.color || 'success'})`;
3401
3390
  }
3402
3391
  buildDayGrid(currentDate) {
3403
- this.day.set(currentDate);
3392
+ // Anchor the day to local midnight in the calendar timezone.
3393
+ this.day.set(dayAt(currentDate, this.timezone(), 0));
3404
3394
  this.startNowTimer();
3405
3395
  }
3406
3396
  buildTimeSlots() {
@@ -3420,29 +3410,23 @@ class CalendarDayGridComponent {
3420
3410
  processAllEvents(events, day) {
3421
3411
  if (!day)
3422
3412
  return;
3423
- // Filter events for this day
3424
- const dayStart = new DateTz$1(day).set(0, 'hour').set(0, 'minute').stripSecMillis();
3425
- const dayEnd = new DateTz$1(day).set(0, 'hour').set(0, 'minute').add(1, 'day').stripSecMillis();
3413
+ const tz = this.timezone();
3414
+ // tz-aware day boundaries (set()/add() would be UTC-naive here).
3415
+ const dayStartTs = startOfDayTs(day, tz);
3416
+ const dayEndTs = dayAt(day, tz, 1).timestamp;
3426
3417
  const dayEvents = [];
3427
3418
  for (const event of events) {
3428
3419
  // Check overlap with the day
3429
- if (event.end.timestamp > dayStart.timestamp && event.start.timestamp < dayEnd.timestamp) {
3430
- let visualStart = event.start;
3431
- let visualEnd = event.end;
3432
- let isContinuedBefore = false;
3433
- let isContinuedAfter = false;
3434
- if (visualStart.timestamp < dayStart.timestamp) {
3435
- visualStart = dayStart;
3436
- isContinuedBefore = true;
3437
- }
3438
- if (visualEnd.timestamp > dayEnd.timestamp) {
3439
- visualEnd = dayEnd;
3440
- isContinuedAfter = true;
3441
- }
3420
+ if (event.end.timestamp > dayStartTs && event.start.timestamp < dayEndTs) {
3421
+ const isContinuedBefore = event.start.timestamp < dayStartTs;
3422
+ const isContinuedAfter = event.end.timestamp > dayEndTs;
3423
+ const visualStartTs = Math.max(event.start.timestamp, dayStartTs);
3424
+ const visualEndTs = Math.min(event.end.timestamp, dayEndTs);
3442
3425
  dayEvents.push({
3443
3426
  ...event,
3444
- start: visualStart,
3445
- end: visualEnd,
3427
+ // Chunks live in the calendar timezone, so labels and positions agree.
3428
+ start: new DateTz$1(visualStartTs, tz),
3429
+ end: new DateTz$1(visualEndTs, tz),
3446
3430
  isContinuedBefore,
3447
3431
  isContinuedAfter,
3448
3432
  left: 0,