add-to-calendar-button 2.12.8 → 2.12.10

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.
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Style: 3D
7
7
  *
8
- * Version: 2.12.8
8
+ * Version: 2.12.10
9
9
  * Creator: Jens Kuerschner (https://jekuer.com)
10
10
  * Project: https://github.com/add2cal/add-to-calendar-button
11
11
  * License: Elastic License 2.0 (ELv2) (https://github.com/add2cal/add-to-calendar-button/blob/main/LICENSE.txt)
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Style: Date
7
7
  *
8
- * Version: 2.12.8
8
+ * Version: 2.12.10
9
9
  * Creator: Jens Kuerschner (https://jekuer.com)
10
10
  * Project: https://github.com/add2cal/add-to-calendar-button
11
11
  * License: Elastic License 2.0 (ELv2) (https://github.com/add2cal/add-to-calendar-button/blob/main/LICENSE.txt)
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Style: Flat
7
7
  *
8
- * Version: 2.12.8
8
+ * Version: 2.12.10
9
9
  * Creator: Jens Kuerschner (https://jekuer.com)
10
10
  * Project: https://github.com/add2cal/add-to-calendar-button
11
11
  * License: Elastic License 2.0 (ELv2) (https://github.com/add2cal/add-to-calendar-button/blob/main/LICENSE.txt)
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Style: Neumorphism
7
7
  *
8
- * Version: 2.12.8
8
+ * Version: 2.12.10
9
9
  * Creator: Jens Kuerschner (https://jekuer.com)
10
10
  * Project: https://github.com/add2cal/add-to-calendar-button
11
11
  * License: Elastic License 2.0 (ELv2) (https://github.com/add2cal/add-to-calendar-button/blob/main/LICENSE.txt)
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Style: Round
7
7
  *
8
- * Version: 2.12.8
8
+ * Version: 2.12.10
9
9
  * Creator: Jens Kuerschner (https://jekuer.com)
10
10
  * Project: https://github.com/add2cal/add-to-calendar-button
11
11
  * License: Elastic License 2.0 (ELv2) (https://github.com/add2cal/add-to-calendar-button/blob/main/LICENSE.txt)
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Style: Simple
7
7
  *
8
- * Version: 2.12.8
8
+ * Version: 2.12.10
9
9
  * Creator: Jens Kuerschner (https://jekuer.com)
10
10
  * Project: https://github.com/add2cal/add-to-calendar-button
11
11
  * License: Elastic License 2.0 (ELv2) (https://github.com/add2cal/add-to-calendar-button/blob/main/LICENSE.txt)
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Style: Text
7
7
  *
8
- * Version: 2.12.8
8
+ * Version: 2.12.10
9
9
  * Creator: Jens Kuerschner (https://jekuer.com)
10
10
  * Project: https://github.com/add2cal/add-to-calendar-button
11
11
  * License: Elastic License 2.0 (ELv2) (https://github.com/add2cal/add-to-calendar-button/blob/main/LICENSE.txt)
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Style: Default
7
7
  *
8
- * Version: 2.12.8
8
+ * Version: 2.12.10
9
9
  * Creator: Jens Kuerschner (https://jekuer.com)
10
10
  * Project: https://github.com/add2cal/add-to-calendar-button
11
11
  * License: Elastic License 2.0 (ELv2) (https://github.com/add2cal/add-to-calendar-button/blob/main/LICENSE.txt)
@@ -223,14 +223,14 @@ function tzlib_get_timezones(jsonType = false) {
223
223
  * Add to Calendar Button
224
224
  * ++++++++++++++++++++++
225
225
  *
226
- * Version: 2.12.8
226
+ * Version: 2.12.10
227
227
  * Creator: Jens Kuerschner (https://jekuer.com)
228
228
  * Project: https://github.com/add2cal/add-to-calendar-button
229
229
  * License: Elastic License 2.0 (ELv2) (https://github.com/add2cal/add-to-calendar-button/blob/main/LICENSE.txt)
230
230
  * Note: DO NOT REMOVE THE COPYRIGHT NOTICE ABOVE!
231
231
  *
232
232
  */
233
- const atcbVersion = '2.12.8';
233
+ const atcbVersion = '2.12.10';
234
234
  const atcbCssTemplate = {};
235
235
  const atcbIsBrowser = () => {
236
236
  if (typeof window === 'undefined') {
@@ -595,9 +595,10 @@ function atcb_decorate_data_rrule(data) {
595
595
  function atcb_decorate_data_recurring_events(data) {
596
596
  const startDate = data.dates?.[0].startDate || data.startDate;
597
597
  const startTime = data.dates?.[0].startTime || data.startTime;
598
+ const tzid = data.dates?.[0].timeZone || data.timeZone || 'UTC';
599
+ const offset = startTime && startTime !== '' ? tzlib_get_offset(tzid, startDate, startTime) : '';
598
600
  const startDateTime = (function () {
599
601
  if (startTime && startTime !== '') {
600
- const offset = tzlib_get_offset(data.dates?.[0].timeZone || data.timeZone, startDate, startTime);
601
602
  return new Date(startDate + ' ' + startTime + ':00 GMT' + offset);
602
603
  }
603
604
  return new Date(startDate + 'T00:00:00Z');
@@ -607,31 +608,28 @@ function atcb_decorate_data_recurring_events(data) {
607
608
  if (!occurenceData || !occurenceData.nextOccurrence) {
608
609
  return data;
609
610
  }
610
- const nextOccurrence =
611
- String(occurenceData.nextOccurrence.getFullYear()) +
612
- '-' +
613
- String(occurenceData.nextOccurrence.getMonth() + 1).padStart(2, '0') +
614
- '-' +
615
- String(occurenceData.nextOccurrence.getDate()).padStart(2, '0') +
616
- (startTime && startTime !== '' ? 'T' + String(occurenceData.nextOccurrence.getHours()).padStart(2, '0') + ':' + String(occurenceData.nextOccurrence.getMinutes()).padStart(2, '0') : '');
617
- data.startDate = nextOccurrence.slice(0, 10);
611
+ function formatInTz(dateObj, timeZone, includeTime) {
612
+ const opts = includeTime ? { timeZone, hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' } : { timeZone, hour12: false, year: 'numeric', month: '2-digit', day: '2-digit' };
613
+ const parts = new Intl.DateTimeFormat('en-CA', opts).formatToParts(dateObj);
614
+ const get = (t) => parts.find((p) => p.type === t)?.value || '';
615
+ return {
616
+ date: `${get('year')}-${get('month')}-${get('day')}`,
617
+ time: includeTime ? `${get('hour')}:${get('minute')}` : '',
618
+ };
619
+ }
620
+ const nextLocal = formatInTz(occurenceData.nextOccurrence, tzid, !!(startTime && startTime !== ''));
621
+ data.startDate = nextLocal.date;
618
622
  if (startTime && startTime !== '') {
619
- data.startTime = nextOccurrence.slice(11, 16);
623
+ data.startTime = nextLocal.time;
620
624
  }
621
625
  const endDate = data.dates?.[0].endDate || data.endDate || startDate;
622
626
  const endTime = data.dates?.[0].endTime || data.endTime || '';
623
627
  const diff = new Date(endDate + (endTime && endTime !== '' ? 'T' + endTime : '')).getTime() - new Date(startDate + (startTime && startTime !== '' ? 'T' + startTime : '')).getTime();
624
628
  const newEndDateTime = new Date(occurenceData.nextOccurrence.getTime() + diff);
625
- const newEndDateTimeString =
626
- String(newEndDateTime.getFullYear()) +
627
- '-' +
628
- String(newEndDateTime.getMonth() + 1).padStart(2, '0') +
629
- '-' +
630
- String(newEndDateTime.getDate()).padStart(2, '0') +
631
- (endTime && endTime !== '' ? 'T' + String(newEndDateTime.getHours()).padStart(2, '0') + ':' + String(newEndDateTime.getMinutes()).padStart(2, '0') : '');
632
- data.endDate = newEndDateTimeString.slice(0, 10);
629
+ const nextEndLocal = formatInTz(newEndDateTime, tzid, !!(endTime && endTime !== ''));
630
+ data.endDate = nextEndLocal.date;
633
631
  if (endTime && endTime !== '') {
634
- data.endTime = newEndDateTimeString.slice(11, 16);
632
+ data.endTime = nextEndLocal.time;
635
633
  }
636
634
  if ((data.recurrence_count && data.recurrence_count !== '') || (data.recurrence_until && data.recurrence_until !== '')) {
637
635
  if (occurenceData.adjustedCount < 2) {
@@ -2916,7 +2914,7 @@ function atcb_determine_ical_filename(data, subEvent) {
2916
2914
  return filenamePart + filenameSuffix;
2917
2915
  }
2918
2916
  }
2919
- return 'event-to-save-in-my-calendar' + filenameSuffix;
2917
+ return 'event' + filenameSuffix;
2920
2918
  }
2921
2919
  function atcb_ical_copy_note(host, dataUrl, data, keyboardTrigger) {
2922
2920
  atcb_copy_to_clipboard(dataUrl);
@@ -3505,6 +3503,65 @@ function atcb_apply_transformation(value, transform) {
3505
3503
  return value;
3506
3504
  }
3507
3505
  }
3506
+ function atcb_parseByWeekdayTokens(rawByDay) {
3507
+ const tokens = rawByDay ? rawByDay.toString().split(',') : [];
3508
+ const mapWeekdayCode = (wd) => {
3509
+ switch (wd) {
3510
+ case 'SU':
3511
+ return 0;
3512
+ case 'MO':
3513
+ return 1;
3514
+ case 'TU':
3515
+ return 2;
3516
+ case 'WE':
3517
+ return 3;
3518
+ case 'TH':
3519
+ return 4;
3520
+ case 'FR':
3521
+ return 5;
3522
+ case 'SA':
3523
+ return 6;
3524
+ default:
3525
+ return undefined;
3526
+ }
3527
+ };
3528
+ const plainWeekdays = [];
3529
+ const ordinals = [];
3530
+ for (const tok of tokens) {
3531
+ const t = tok.trim().toUpperCase();
3532
+ if (t.length < 2) continue;
3533
+ const wd = t.slice(-2);
3534
+ const day = mapWeekdayCode(wd);
3535
+ if (day === undefined) continue;
3536
+ const prefix = t.slice(0, t.length - 2);
3537
+ if (prefix) {
3538
+ let sign = 1;
3539
+ let digits = prefix;
3540
+ if (digits[0] === '+') {
3541
+ digits = digits.slice(1);
3542
+ } else if (digits[0] === '-') {
3543
+ sign = -1;
3544
+ digits = digits.slice(1);
3545
+ }
3546
+ if (!digits || digits.length > 2) continue;
3547
+ let validDigits = true;
3548
+ for (let i = 0; i < digits.length; i++) {
3549
+ const code = digits.charCodeAt(i);
3550
+ if (code < 48 || code > 57) {
3551
+ validDigits = false;
3552
+ break;
3553
+ }
3554
+ }
3555
+ if (!validDigits) continue;
3556
+ const abs = parseInt(digits, 10);
3557
+ if (abs < 1 || abs > 53) continue;
3558
+ ordinals.push({ n: sign * abs, day });
3559
+ } else {
3560
+ plainWeekdays.push(day);
3561
+ }
3562
+ }
3563
+ return { plainWeekdays, ordinals };
3564
+ }
3508
3565
  function atcb_parseRRule(rruleStr, deep = true) {
3509
3566
  const parts = rruleStr
3510
3567
  .replace('RRULE:', '')
@@ -3516,21 +3573,21 @@ function atcb_parseRRule(rruleStr, deep = true) {
3516
3573
  }, {});
3517
3574
  if (!parts.FREQ) throw new Error('RRULE must have FREQ');
3518
3575
  parts.FREQ = parts.FREQ.toUpperCase();
3519
- parts.INTERVAL = parseInt(parts.INTERVAL.toString() || '1', 10);
3576
+ parts.INTERVAL = parts.INTERVAL ? parseInt(parts.INTERVAL.toString(), 10) : 1;
3520
3577
  parts.COUNT = parts.COUNT ? parseInt(parts.COUNT.toString(), 10) : null;
3521
3578
  if (parts.UNTIL) {
3522
3579
  const untilStr = parts.UNTIL.toString();
3523
3580
  parts.UNTIL = deep ? new Date(Date.UTC(parseInt(untilStr.slice(0, 4), 10), parseInt(untilStr.slice(4, 6), 10) - 1, parseInt(untilStr.slice(6, 8), 10), parseInt(untilStr.slice(9, 11) || '0', 10), parseInt(untilStr.slice(11, 13) || '0', 10))) : untilStr;
3524
3581
  }
3525
3582
  if (parts.BYWEEKDAY || parts.BYDAY) {
3526
- const dayMap = { SU: 0, MO: 1, TU: 2, WE: 3, TH: 4, FR: 5, SA: 6 };
3527
- parts.BYWEEKDAY = deep
3528
- ? (parts.BYWEEKDAY || parts.BYDAY)
3529
- ?.toString()
3530
- .split(',')
3531
- .map((day) => dayMap[day.trim().toUpperCase()])
3532
- .filter((n) => n !== undefined)
3533
- : parts.BYWEEKDAY || parts.BYDAY;
3583
+ const rawByDay = (parts.BYWEEKDAY || parts.BYDAY)?.toString();
3584
+ if (deep) {
3585
+ const { plainWeekdays, ordinals } = atcb_parseByWeekdayTokens(rawByDay);
3586
+ parts.BYWEEKDAY = plainWeekdays.length ? plainWeekdays : null;
3587
+ parts.BYDAY_ORDINALS = ordinals.length ? ordinals : null;
3588
+ } else {
3589
+ parts.BYWEEKDAY = parts.BYWEEKDAY || parts.BYDAY;
3590
+ }
3534
3591
  }
3535
3592
  parts.BYMONTH =
3536
3593
  deep && parts.BYMONTH
@@ -3606,16 +3663,73 @@ function matchesBYRules(date, rrule) {
3606
3663
  if (rrule.BYYEARDAY && !rrule.BYYEARDAY.includes(getDayOfYear(date))) return false;
3607
3664
  if (rrule.BYMONTHDAY && !rrule.BYMONTHDAY.includes(date.getUTCDate())) return false;
3608
3665
  if (rrule.BYWEEKNO && !rrule.BYWEEKNO.includes(getWeekNumber(date))) return false;
3609
- if (rrule.BYWEEKDAY && !rrule.BYWEEKDAY.includes(date.getUTCDay())) return false;
3666
+ const hasPlainWeekday = !!(rrule.BYWEEKDAY && rrule.BYWEEKDAY.length);
3667
+ const plainWeekdayOk = hasPlainWeekday ? rrule.BYWEEKDAY.includes(date.getUTCDay()) : null;
3668
+ let ordinalOk = null;
3669
+ if (rrule.BYDAY_ORDINALS && Array.isArray(rrule.BYDAY_ORDINALS) && rrule.BYDAY_ORDINALS.length > 0) {
3670
+ const dow = date.getUTCDay();
3671
+ const year = date.getUTCFullYear();
3672
+ const month = date.getUTCMonth();
3673
+ const dayOfYear = getDayOfYear(date);
3674
+ const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
3675
+ const daysInYear = getDayOfYear(new Date(Date.UTC(year, 11, 31)));
3676
+ const isNthWeekdayOfMonth = (n, weekday) => {
3677
+ if (n === 0) return false;
3678
+ if (n > 0) {
3679
+ const firstOfMonth = new Date(Date.UTC(year, month, 1));
3680
+ const firstDow = firstOfMonth.getUTCDay();
3681
+ const offset = (weekday - firstDow + 7) % 7;
3682
+ const targetDay = 1 + offset + (n - 1) * 7;
3683
+ return targetDay >= 1 && targetDay <= daysInMonth && date.getUTCDate() === targetDay;
3684
+ } else {
3685
+ const lastOfMonth = new Date(Date.UTC(year, month + 1, 0));
3686
+ const lastDow = lastOfMonth.getUTCDay();
3687
+ const backOffset = (lastDow - weekday + 7) % 7;
3688
+ const targetDay = lastOfMonth.getUTCDate() - backOffset + (n + 1) * 7;
3689
+ return targetDay >= 1 && targetDay <= daysInMonth && date.getUTCDate() === targetDay;
3690
+ }
3691
+ };
3692
+ const isNthWeekdayOfYear = (n, weekday) => {
3693
+ if (n === 0) return false;
3694
+ if (n > 0) {
3695
+ const jan1 = new Date(Date.UTC(year, 0, 1));
3696
+ const jan1Dow = jan1.getUTCDay();
3697
+ const offset = (weekday - jan1Dow + 7) % 7;
3698
+ const targetDoy = 1 + offset + (n - 1) * 7;
3699
+ return targetDoy >= 1 && targetDoy <= daysInYear && dayOfYear === targetDoy;
3700
+ } else {
3701
+ const dec31 = new Date(Date.UTC(year, 11, 31));
3702
+ const dec31Dow = dec31.getUTCDay();
3703
+ const backOffset = (dec31Dow - weekday + 7) % 7;
3704
+ const targetDoy = daysInYear - backOffset + (n + 1) * 7;
3705
+ return targetDoy >= 1 && targetDoy <= daysInYear && dayOfYear === targetDoy;
3706
+ }
3707
+ };
3708
+ const anyOrdinalMatch = rrule.BYDAY_ORDINALS.some(({ n, day }) => {
3709
+ if (day !== dow) return false;
3710
+ if (rrule.FREQ === 'MONTHLY') return isNthWeekdayOfMonth(n, day);
3711
+ if (rrule.FREQ === 'YEARLY') {
3712
+ if (rrule.BYMONTH && rrule.BYMONTH.length > 0) return isNthWeekdayOfMonth(n, day);
3713
+ if (!rrule.BYWEEKNO) return isNthWeekdayOfYear(n, day);
3714
+ return false;
3715
+ }
3716
+ return false;
3717
+ });
3718
+ ordinalOk = anyOrdinalMatch;
3719
+ }
3720
+ if (plainWeekdayOk === false && ordinalOk === false) return false;
3721
+ if (plainWeekdayOk === false && ordinalOk === null) return false;
3722
+ if (ordinalOk === false && plainWeekdayOk === null) return false;
3610
3723
  if (rrule.BYHOUR && !rrule.BYHOUR.includes(date.getUTCHours())) return false;
3611
3724
  return true;
3612
3725
  }
3613
3726
  function matchesImplicitRules(date, rrule, startDate) {
3614
3727
  if (!rrule.BYHOUR && date.getUTCHours() !== startDate.getUTCHours()) return false;
3615
- if (rrule.FREQ === 'WEEKLY' && !rrule.BYWEEKDAY && date.getUTCDay() !== startDate.getUTCDay()) return false;
3616
- if (rrule.FREQ === 'MONTHLY' && !rrule.BYMONTHDAY && !rrule.BYWEEKDAY && date.getUTCDate() !== startDate.getUTCDate()) return false;
3728
+ const hasByWeekdayAny = !!(rrule.BYWEEKDAY && rrule.BYWEEKDAY.length) || !!(rrule.BYDAY_ORDINALS && rrule.BYDAY_ORDINALS.length);
3729
+ if (rrule.FREQ === 'WEEKLY' && !hasByWeekdayAny && date.getUTCDay() !== startDate.getUTCDay()) return false;
3730
+ if (rrule.FREQ === 'MONTHLY' && !rrule.BYMONTHDAY && !hasByWeekdayAny && date.getUTCDate() !== startDate.getUTCDate()) return false;
3617
3731
  if (rrule.FREQ === 'YEARLY' && !rrule.BYMONTH && date.getUTCMonth() !== startDate.getUTCMonth()) return false;
3618
- if (rrule.FREQ === 'YEARLY' && !rrule.BYMONTHDAY && !rrule.BYWEEKDAY && !rrule.BYYEARDAY && !rrule.BYWEEKNO && date.getUTCDate() !== startDate.getUTCDate()) return false;
3732
+ if (rrule.FREQ === 'YEARLY' && !rrule.BYMONTHDAY && !hasByWeekdayAny && !rrule.BYYEARDAY && !rrule.BYWEEKNO && date.getUTCDate() !== startDate.getUTCDate()) return false;
3619
3733
  return true;
3620
3734
  }
3621
3735
  function atcb_getNextOccurrence(rruleStr, startDateTime, allday) {
@@ -3634,16 +3748,17 @@ function atcb_getNextOccurrence(rruleStr, startDateTime, allday) {
3634
3748
  let maxIterations = 10000;
3635
3749
  while (true) {
3636
3750
  if (rrule.UNTIL && currentDate > rrule.UNTIL) break;
3637
- if (matchesFreq(currentDate, rrule, startDateTime) && matchesRRule(currentDate, rrule, startDateTime)) {
3751
+ const isMatch = matchesFreq(currentDate, rrule, startDateTime) && matchesRRule(currentDate, rrule, startDateTime);
3752
+ if (isMatch) {
3638
3753
  occurrences.push(currentDate);
3639
3754
  count++;
3640
3755
  if (rrule.COUNT && count >= rrule.COUNT) break;
3641
- if (!rrule.COUNT && !rrule.UNTIL && occurrences.length > 0 && (allday ? currentDate >= now : currentDate > now)) break;
3756
+ if (!rrule.COUNT && !rrule.UNTIL && (allday ? currentDate >= now : currentDate > now)) break;
3642
3757
  }
3643
- currentDate = new Date(currentDate.getTime() + stepMs);
3644
3758
  if (--maxIterations <= 0) {
3645
3759
  break;
3646
3760
  }
3761
+ currentDate = new Date(currentDate.getTime() + stepMs);
3647
3762
  }
3648
3763
  let nextDate = null;
3649
3764
  let countDate = 0;
@@ -223,14 +223,14 @@ function tzlib_get_timezones(jsonType = false) {
223
223
  * Add to Calendar Button
224
224
  * ++++++++++++++++++++++
225
225
  *
226
- * Version: 2.12.8
226
+ * Version: 2.12.10
227
227
  * Creator: Jens Kuerschner (https://jekuer.com)
228
228
  * Project: https://github.com/add2cal/add-to-calendar-button
229
229
  * License: Elastic License 2.0 (ELv2) (https://github.com/add2cal/add-to-calendar-button/blob/main/LICENSE.txt)
230
230
  * Note: DO NOT REMOVE THE COPYRIGHT NOTICE ABOVE!
231
231
  *
232
232
  */
233
- const atcbVersion = '2.12.8';
233
+ const atcbVersion = '2.12.10';
234
234
  const atcbCssTemplate = {
235
235
  if (typeof window === 'undefined') {
236
236
  return false;
@@ -594,9 +594,10 @@ function atcb_decorate_data_rrule(data) {
594
594
  function atcb_decorate_data_recurring_events(data) {
595
595
  const startDate = data.dates?.[0].startDate || data.startDate;
596
596
  const startTime = data.dates?.[0].startTime || data.startTime;
597
+ const tzid = data.dates?.[0].timeZone || data.timeZone || 'UTC';
598
+ const offset = startTime && startTime !== '' ? tzlib_get_offset(tzid, startDate, startTime) : '';
597
599
  const startDateTime = (function () {
598
600
  if (startTime && startTime !== '') {
599
- const offset = tzlib_get_offset(data.dates?.[0].timeZone || data.timeZone, startDate, startTime);
600
601
  return new Date(startDate + ' ' + startTime + ':00 GMT' + offset);
601
602
  }
602
603
  return new Date(startDate + 'T00:00:00Z');
@@ -606,31 +607,28 @@ function atcb_decorate_data_recurring_events(data) {
606
607
  if (!occurenceData || !occurenceData.nextOccurrence) {
607
608
  return data;
608
609
  }
609
- const nextOccurrence =
610
- String(occurenceData.nextOccurrence.getFullYear()) +
611
- '-' +
612
- String(occurenceData.nextOccurrence.getMonth() + 1).padStart(2, '0') +
613
- '-' +
614
- String(occurenceData.nextOccurrence.getDate()).padStart(2, '0') +
615
- (startTime && startTime !== '' ? 'T' + String(occurenceData.nextOccurrence.getHours()).padStart(2, '0') + ':' + String(occurenceData.nextOccurrence.getMinutes()).padStart(2, '0') : '');
616
- data.startDate = nextOccurrence.slice(0, 10);
610
+ function formatInTz(dateObj, timeZone, includeTime) {
611
+ const opts = includeTime ? { timeZone, hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' } : { timeZone, hour12: false, year: 'numeric', month: '2-digit', day: '2-digit' };
612
+ const parts = new Intl.DateTimeFormat('en-CA', opts).formatToParts(dateObj);
613
+ const get = (t) => parts.find((p) => p.type === t)?.value || '';
614
+ return {
615
+ date: `${get('year')}-${get('month')}-${get('day')}`,
616
+ time: includeTime ? `${get('hour')}:${get('minute')}` : '',
617
+ };
618
+ }
619
+ const nextLocal = formatInTz(occurenceData.nextOccurrence, tzid, !!(startTime && startTime !== ''));
620
+ data.startDate = nextLocal.date;
617
621
  if (startTime && startTime !== '') {
618
- data.startTime = nextOccurrence.slice(11, 16);
622
+ data.startTime = nextLocal.time;
619
623
  }
620
624
  const endDate = data.dates?.[0].endDate || data.endDate || startDate;
621
625
  const endTime = data.dates?.[0].endTime || data.endTime || '';
622
626
  const diff = new Date(endDate + (endTime && endTime !== '' ? 'T' + endTime : '')).getTime() - new Date(startDate + (startTime && startTime !== '' ? 'T' + startTime : '')).getTime();
623
627
  const newEndDateTime = new Date(occurenceData.nextOccurrence.getTime() + diff);
624
- const newEndDateTimeString =
625
- String(newEndDateTime.getFullYear()) +
626
- '-' +
627
- String(newEndDateTime.getMonth() + 1).padStart(2, '0') +
628
- '-' +
629
- String(newEndDateTime.getDate()).padStart(2, '0') +
630
- (endTime && endTime !== '' ? 'T' + String(newEndDateTime.getHours()).padStart(2, '0') + ':' + String(newEndDateTime.getMinutes()).padStart(2, '0') : '');
631
- data.endDate = newEndDateTimeString.slice(0, 10);
628
+ const nextEndLocal = formatInTz(newEndDateTime, tzid, !!(endTime && endTime !== ''));
629
+ data.endDate = nextEndLocal.date;
632
630
  if (endTime && endTime !== '') {
633
- data.endTime = newEndDateTimeString.slice(11, 16);
631
+ data.endTime = nextEndLocal.time;
634
632
  }
635
633
  if ((data.recurrence_count && data.recurrence_count !== '') || (data.recurrence_until && data.recurrence_until !== '')) {
636
634
  if (occurenceData.adjustedCount < 2) {
@@ -2915,7 +2913,7 @@ function atcb_determine_ical_filename(data, subEvent) {
2915
2913
  return filenamePart + filenameSuffix;
2916
2914
  }
2917
2915
  }
2918
- return 'event-to-save-in-my-calendar' + filenameSuffix;
2916
+ return 'event' + filenameSuffix;
2919
2917
  }
2920
2918
  function atcb_ical_copy_note(host, dataUrl, data, keyboardTrigger) {
2921
2919
  atcb_copy_to_clipboard(dataUrl);
@@ -3504,6 +3502,65 @@ function atcb_apply_transformation(value, transform) {
3504
3502
  return value;
3505
3503
  }
3506
3504
  }
3505
+ function atcb_parseByWeekdayTokens(rawByDay) {
3506
+ const tokens = rawByDay ? rawByDay.toString().split(',') : [];
3507
+ const mapWeekdayCode = (wd) => {
3508
+ switch (wd) {
3509
+ case 'SU':
3510
+ return 0;
3511
+ case 'MO':
3512
+ return 1;
3513
+ case 'TU':
3514
+ return 2;
3515
+ case 'WE':
3516
+ return 3;
3517
+ case 'TH':
3518
+ return 4;
3519
+ case 'FR':
3520
+ return 5;
3521
+ case 'SA':
3522
+ return 6;
3523
+ default:
3524
+ return undefined;
3525
+ }
3526
+ };
3527
+ const plainWeekdays = [];
3528
+ const ordinals = [];
3529
+ for (const tok of tokens) {
3530
+ const t = tok.trim().toUpperCase();
3531
+ if (t.length < 2) continue;
3532
+ const wd = t.slice(-2);
3533
+ const day = mapWeekdayCode(wd);
3534
+ if (day === undefined) continue;
3535
+ const prefix = t.slice(0, t.length - 2);
3536
+ if (prefix) {
3537
+ let sign = 1;
3538
+ let digits = prefix;
3539
+ if (digits[0] === '+') {
3540
+ digits = digits.slice(1);
3541
+ } else if (digits[0] === '-') {
3542
+ sign = -1;
3543
+ digits = digits.slice(1);
3544
+ }
3545
+ if (!digits || digits.length > 2) continue;
3546
+ let validDigits = true;
3547
+ for (let i = 0; i < digits.length; i++) {
3548
+ const code = digits.charCodeAt(i);
3549
+ if (code < 48 || code > 57) {
3550
+ validDigits = false;
3551
+ break;
3552
+ }
3553
+ }
3554
+ if (!validDigits) continue;
3555
+ const abs = parseInt(digits, 10);
3556
+ if (abs < 1 || abs > 53) continue;
3557
+ ordinals.push({ n: sign * abs, day });
3558
+ } else {
3559
+ plainWeekdays.push(day);
3560
+ }
3561
+ }
3562
+ return { plainWeekdays, ordinals };
3563
+ }
3507
3564
  function atcb_parseRRule(rruleStr, deep = true) {
3508
3565
  const parts = rruleStr
3509
3566
  .replace('RRULE:', '')
@@ -3515,21 +3572,21 @@ function atcb_parseRRule(rruleStr, deep = true) {
3515
3572
  }, {});
3516
3573
  if (!parts.FREQ) throw new Error('RRULE must have FREQ');
3517
3574
  parts.FREQ = parts.FREQ.toUpperCase();
3518
- parts.INTERVAL = parseInt(parts.INTERVAL.toString() || '1', 10);
3575
+ parts.INTERVAL = parts.INTERVAL ? parseInt(parts.INTERVAL.toString(), 10) : 1;
3519
3576
  parts.COUNT = parts.COUNT ? parseInt(parts.COUNT.toString(), 10) : null;
3520
3577
  if (parts.UNTIL) {
3521
3578
  const untilStr = parts.UNTIL.toString();
3522
3579
  parts.UNTIL = deep ? new Date(Date.UTC(parseInt(untilStr.slice(0, 4), 10), parseInt(untilStr.slice(4, 6), 10) - 1, parseInt(untilStr.slice(6, 8), 10), parseInt(untilStr.slice(9, 11) || '0', 10), parseInt(untilStr.slice(11, 13) || '0', 10))) : untilStr;
3523
3580
  }
3524
3581
  if (parts.BYWEEKDAY || parts.BYDAY) {
3525
- const dayMap = { SU: 0, MO: 1, TU: 2, WE: 3, TH: 4, FR: 5, SA: 6 };
3526
- parts.BYWEEKDAY = deep
3527
- ? (parts.BYWEEKDAY || parts.BYDAY)
3528
- ?.toString()
3529
- .split(',')
3530
- .map((day) => dayMap[day.trim().toUpperCase()])
3531
- .filter((n) => n !== undefined)
3532
- : parts.BYWEEKDAY || parts.BYDAY;
3582
+ const rawByDay = (parts.BYWEEKDAY || parts.BYDAY)?.toString();
3583
+ if (deep) {
3584
+ const { plainWeekdays, ordinals } = atcb_parseByWeekdayTokens(rawByDay);
3585
+ parts.BYWEEKDAY = plainWeekdays.length ? plainWeekdays : null;
3586
+ parts.BYDAY_ORDINALS = ordinals.length ? ordinals : null;
3587
+ } else {
3588
+ parts.BYWEEKDAY = parts.BYWEEKDAY || parts.BYDAY;
3589
+ }
3533
3590
  }
3534
3591
  parts.BYMONTH =
3535
3592
  deep && parts.BYMONTH
@@ -3605,16 +3662,73 @@ function matchesBYRules(date, rrule) {
3605
3662
  if (rrule.BYYEARDAY && !rrule.BYYEARDAY.includes(getDayOfYear(date))) return false;
3606
3663
  if (rrule.BYMONTHDAY && !rrule.BYMONTHDAY.includes(date.getUTCDate())) return false;
3607
3664
  if (rrule.BYWEEKNO && !rrule.BYWEEKNO.includes(getWeekNumber(date))) return false;
3608
- if (rrule.BYWEEKDAY && !rrule.BYWEEKDAY.includes(date.getUTCDay())) return false;
3665
+ const hasPlainWeekday = !!(rrule.BYWEEKDAY && rrule.BYWEEKDAY.length);
3666
+ const plainWeekdayOk = hasPlainWeekday ? rrule.BYWEEKDAY.includes(date.getUTCDay()) : null;
3667
+ let ordinalOk = null;
3668
+ if (rrule.BYDAY_ORDINALS && Array.isArray(rrule.BYDAY_ORDINALS) && rrule.BYDAY_ORDINALS.length > 0) {
3669
+ const dow = date.getUTCDay();
3670
+ const year = date.getUTCFullYear();
3671
+ const month = date.getUTCMonth();
3672
+ const dayOfYear = getDayOfYear(date);
3673
+ const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
3674
+ const daysInYear = getDayOfYear(new Date(Date.UTC(year, 11, 31)));
3675
+ const isNthWeekdayOfMonth = (n, weekday) => {
3676
+ if (n === 0) return false;
3677
+ if (n > 0) {
3678
+ const firstOfMonth = new Date(Date.UTC(year, month, 1));
3679
+ const firstDow = firstOfMonth.getUTCDay();
3680
+ const offset = (weekday - firstDow + 7) % 7;
3681
+ const targetDay = 1 + offset + (n - 1) * 7;
3682
+ return targetDay >= 1 && targetDay <= daysInMonth && date.getUTCDate() === targetDay;
3683
+ } else {
3684
+ const lastOfMonth = new Date(Date.UTC(year, month + 1, 0));
3685
+ const lastDow = lastOfMonth.getUTCDay();
3686
+ const backOffset = (lastDow - weekday + 7) % 7;
3687
+ const targetDay = lastOfMonth.getUTCDate() - backOffset + (n + 1) * 7;
3688
+ return targetDay >= 1 && targetDay <= daysInMonth && date.getUTCDate() === targetDay;
3689
+ }
3690
+ };
3691
+ const isNthWeekdayOfYear = (n, weekday) => {
3692
+ if (n === 0) return false;
3693
+ if (n > 0) {
3694
+ const jan1 = new Date(Date.UTC(year, 0, 1));
3695
+ const jan1Dow = jan1.getUTCDay();
3696
+ const offset = (weekday - jan1Dow + 7) % 7;
3697
+ const targetDoy = 1 + offset + (n - 1) * 7;
3698
+ return targetDoy >= 1 && targetDoy <= daysInYear && dayOfYear === targetDoy;
3699
+ } else {
3700
+ const dec31 = new Date(Date.UTC(year, 11, 31));
3701
+ const dec31Dow = dec31.getUTCDay();
3702
+ const backOffset = (dec31Dow - weekday + 7) % 7;
3703
+ const targetDoy = daysInYear - backOffset + (n + 1) * 7;
3704
+ return targetDoy >= 1 && targetDoy <= daysInYear && dayOfYear === targetDoy;
3705
+ }
3706
+ };
3707
+ const anyOrdinalMatch = rrule.BYDAY_ORDINALS.some(({ n, day }) => {
3708
+ if (day !== dow) return false;
3709
+ if (rrule.FREQ === 'MONTHLY') return isNthWeekdayOfMonth(n, day);
3710
+ if (rrule.FREQ === 'YEARLY') {
3711
+ if (rrule.BYMONTH && rrule.BYMONTH.length > 0) return isNthWeekdayOfMonth(n, day);
3712
+ if (!rrule.BYWEEKNO) return isNthWeekdayOfYear(n, day);
3713
+ return false;
3714
+ }
3715
+ return false;
3716
+ });
3717
+ ordinalOk = anyOrdinalMatch;
3718
+ }
3719
+ if (plainWeekdayOk === false && ordinalOk === false) return false;
3720
+ if (plainWeekdayOk === false && ordinalOk === null) return false;
3721
+ if (ordinalOk === false && plainWeekdayOk === null) return false;
3609
3722
  if (rrule.BYHOUR && !rrule.BYHOUR.includes(date.getUTCHours())) return false;
3610
3723
  return true;
3611
3724
  }
3612
3725
  function matchesImplicitRules(date, rrule, startDate) {
3613
3726
  if (!rrule.BYHOUR && date.getUTCHours() !== startDate.getUTCHours()) return false;
3614
- if (rrule.FREQ === 'WEEKLY' && !rrule.BYWEEKDAY && date.getUTCDay() !== startDate.getUTCDay()) return false;
3615
- if (rrule.FREQ === 'MONTHLY' && !rrule.BYMONTHDAY && !rrule.BYWEEKDAY && date.getUTCDate() !== startDate.getUTCDate()) return false;
3727
+ const hasByWeekdayAny = !!(rrule.BYWEEKDAY && rrule.BYWEEKDAY.length) || !!(rrule.BYDAY_ORDINALS && rrule.BYDAY_ORDINALS.length);
3728
+ if (rrule.FREQ === 'WEEKLY' && !hasByWeekdayAny && date.getUTCDay() !== startDate.getUTCDay()) return false;
3729
+ if (rrule.FREQ === 'MONTHLY' && !rrule.BYMONTHDAY && !hasByWeekdayAny && date.getUTCDate() !== startDate.getUTCDate()) return false;
3616
3730
  if (rrule.FREQ === 'YEARLY' && !rrule.BYMONTH && date.getUTCMonth() !== startDate.getUTCMonth()) return false;
3617
- if (rrule.FREQ === 'YEARLY' && !rrule.BYMONTHDAY && !rrule.BYWEEKDAY && !rrule.BYYEARDAY && !rrule.BYWEEKNO && date.getUTCDate() !== startDate.getUTCDate()) return false;
3731
+ if (rrule.FREQ === 'YEARLY' && !rrule.BYMONTHDAY && !hasByWeekdayAny && !rrule.BYYEARDAY && !rrule.BYWEEKNO && date.getUTCDate() !== startDate.getUTCDate()) return false;
3618
3732
  return true;
3619
3733
  }
3620
3734
  function atcb_getNextOccurrence(rruleStr, startDateTime, allday) {
@@ -3633,16 +3747,17 @@ function atcb_getNextOccurrence(rruleStr, startDateTime, allday) {
3633
3747
  let maxIterations = 10000;
3634
3748
  while (true) {
3635
3749
  if (rrule.UNTIL && currentDate > rrule.UNTIL) break;
3636
- if (matchesFreq(currentDate, rrule, startDateTime) && matchesRRule(currentDate, rrule, startDateTime)) {
3750
+ const isMatch = matchesFreq(currentDate, rrule, startDateTime) && matchesRRule(currentDate, rrule, startDateTime);
3751
+ if (isMatch) {
3637
3752
  occurrences.push(currentDate);
3638
3753
  count++;
3639
3754
  if (rrule.COUNT && count >= rrule.COUNT) break;
3640
- if (!rrule.COUNT && !rrule.UNTIL && occurrences.length > 0 && (allday ? currentDate >= now : currentDate > now)) break;
3755
+ if (!rrule.COUNT && !rrule.UNTIL && (allday ? currentDate >= now : currentDate > now)) break;
3641
3756
  }
3642
- currentDate = new Date(currentDate.getTime() + stepMs);
3643
3757
  if (--maxIterations <= 0) {
3644
3758
  break;
3645
3759
  }
3760
+ currentDate = new Date(currentDate.getTime() + stepMs);
3646
3761
  }
3647
3762
  let nextDate = null;
3648
3763
  let countDate = 0;