@event-calendar/core 5.6.1 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -136,6 +136,7 @@ Inspired by [FullCalendar](https://fullcalendar.io/), it implements similar opti
136
136
  - [slotWidth](#slotwidth)
137
137
  - [snapDuration](#snapduration)
138
138
  - [theme](#theme)
139
+ - [timeZone](#timezone)
139
140
  - [titleFormat](#titleformat)
140
141
  - [unselect](#unselect)
141
142
  - [unselectAuto](#unselectauto)
@@ -260,8 +261,8 @@ This bundle contains a version of the calendar that includes all plugins and is
260
261
 
261
262
  The first step is to include the following lines of code in the `<head>` section of your page:
262
263
  ```html
263
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@event-calendar/build@5.6.1/dist/event-calendar.min.css">
264
- <script src="https://cdn.jsdelivr.net/npm/@event-calendar/build@5.6.1/dist/event-calendar.min.js"></script>
264
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@event-calendar/build@5.7.0/dist/event-calendar.min.css">
265
+ <script src="https://cdn.jsdelivr.net/npm/@event-calendar/build@5.7.0/dist/event-calendar.min.js"></script>
265
266
  ```
266
267
 
267
268
  <details>
@@ -1652,6 +1653,15 @@ Start date of the range the calendar needs events for
1652
1653
  End date of the range the calendar needs events for
1653
1654
  </td>
1654
1655
  </tr>
1656
+ <tr>
1657
+ <td>
1658
+
1659
+ `timeZone`
1660
+ </td>
1661
+ <td>
1662
+ The value of the calendar's [timeZone](#timezone) option. Sent only when `timeZone` is not `'local'`
1663
+ </td>
1664
+ </tr>
1655
1665
  </table>
1656
1666
  </td>
1657
1667
  </tr>
@@ -1721,6 +1731,13 @@ function(fetchInfo, successCallback, failureCallback) { }
1721
1731
  </td>
1722
1732
  <td>ISO8601 string representation of the end date</td>
1723
1733
  </tr>
1734
+ <tr>
1735
+ <td>
1736
+
1737
+ `timeZone`
1738
+ </td>
1739
+ <td>The value of the calendar's [timeZone](#timezone) option</td>
1740
+ </tr>
1724
1741
  </table>
1725
1742
 
1726
1743
  The `successCallback` function must be called by the custom function with an array of [parsable](#parsing-event-from-a-plain-object) [Event](#event-object) objects.
@@ -2640,6 +2657,39 @@ function (theme) {
2640
2657
  </tr>
2641
2658
  </table>
2642
2659
 
2660
+ ### timeZone
2661
+ - Type `string`
2662
+ - Default `'local'`
2663
+
2664
+ The time zone the calendar uses to display dates and times.
2665
+
2666
+ The following values are accepted:
2667
+ - `'local'` — uses the browser's local time zone
2668
+ - `'UTC'` — uses UTC (zero offset)
2669
+ - A UTC offset string in the form `'±HH:MM'`, e.g. `'+05:30'` or `'-06:00'`
2670
+
2671
+ Event dates that contain an explicit timezone offset in their ISO string (e.g. `'2028-06-01T10:00:00+02:00'`) will be shifted to the calendar's timezone. Event dates without timezone info (e.g. `'2028-06-01T10:00:00'`) are treated as floating — they display their wall-clock time as-is and will be interpreted in the calendar's timezone from that point forward.
2672
+
2673
+ When the `timeZone` option changes at runtime, all already-loaded events and the current `date` option are automatically shifted to the new timezone. Events from [eventSources](#eventsources) are re-fetched automatically.
2674
+
2675
+ ```js
2676
+ let ec = new EventCalendar(document.getElementById('ec'), {
2677
+ timeZone: '+02:00',
2678
+ events: [
2679
+ {
2680
+ start: '2028-06-01T10:00:00', // floating — displayed as 10:00
2681
+ end: '2028-06-01T12:00:00',
2682
+ title: 'Meeting'
2683
+ },
2684
+ {
2685
+ start: '2028-06-01T10:00:00+00:00', // UTC — displayed as 12:00 in +02:00
2686
+ end: '2028-06-01T12:00:00+00:00',
2687
+ title: 'Call'
2688
+ }
2689
+ ]
2690
+ });
2691
+ ```
2692
+
2643
2693
  ### titleFormat
2644
2694
  - Type `object` or `function`
2645
2695
  - Default `{year: 'numeric', month: 'short', day: 'numeric'}`
package/dist/index.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * EventCalendar v5.6.1
2
+ * EventCalendar v5.7.0
3
3
  * https://github.com/vkurko/calendar
4
4
  */
5
5
  .ec {
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * EventCalendar v5.6.1
2
+ * EventCalendar v5.7.0
3
3
  * https://github.com/vkurko/calendar
4
4
  */
5
5
  import { getAbortSignal, getContext, mount, onMount, setContext, tick, unmount, untrack } from "svelte";
@@ -90,6 +90,9 @@ function length(array) {
90
90
  function empty(array) {
91
91
  return !length(array);
92
92
  }
93
+ function tzOffset(date = /* @__PURE__ */ new Date()) {
94
+ return -date.getTimezoneOffset();
95
+ }
93
96
  function isArray(value) {
94
97
  return Array.isArray(value);
95
98
  }
@@ -121,9 +124,8 @@ function undefinedOr(fn) {
121
124
  //#endregion
122
125
  //#region packages/core/src/lib/date.js
123
126
  var DAY_IN_SECONDS = 86400;
124
- function createDate(input = void 0) {
125
- if (input !== void 0) return isDate(input) ? _fromLocalDate(input) : _fromISOString(input);
126
- return _fromLocalDate(/* @__PURE__ */ new Date());
127
+ function createDate(input = /* @__PURE__ */ new Date(), offset = void 0) {
128
+ return isDate(input) ? _fromLocalDate(input, offset) : _fromISOString(input, offset);
127
129
  }
128
130
  function createDuration(input) {
129
131
  if (typeof input === "number") input = { seconds: input };
@@ -146,7 +148,9 @@ function createDuration(input) {
146
148
  };
147
149
  }
148
150
  function cloneDate(date) {
149
- return new Date(date.getTime());
151
+ let result = new Date(date.getTime());
152
+ setOffset(result, getOffset(date));
153
+ return result;
150
154
  }
151
155
  function addDuration(date, duration, x = 1) {
152
156
  date.setUTCFullYear(date.getUTCFullYear() + x * duration.years);
@@ -227,9 +231,6 @@ function prevDate(date, duration, hiddenDays) {
227
231
  _skipHiddenDays(date, hiddenDays, subtractDay);
228
232
  return date;
229
233
  }
230
- function _skipHiddenDays(date, hiddenDays, dateFn) {
231
- if (hiddenDays.length && hiddenDays.length < 7) while (hiddenDays.includes(date.getUTCDay())) dateFn(date);
232
- }
233
234
  /**
234
235
  * For a given date, get its week number
235
236
  * - ISO @see https://stackoverflow.com/questions/6117814/get-week-of-year-in-javascript-like-in-php
@@ -249,15 +250,49 @@ function createWeekNumberContent(week, weekNumberContent, date) {
249
250
  }) : weekNumberContent;
250
251
  return "W" + String(week).padStart(2, "0");
251
252
  }
253
+ function parseOffset(str, match = {}) {
254
+ let parts = str.match(/([+-])(\d{2}):(\d{2})$/);
255
+ if (parts) {
256
+ assign(match, parts);
257
+ return +(parts[1] + "1") * (+parts[2] * 60 + +parts[3]);
258
+ }
259
+ }
260
+ /**
261
+ * Apply timezone offset difference in minutes to a date
262
+ */
263
+ function applyOffsetDiff(date, offsetDiff) {
264
+ if (offsetDiff) date.setUTCMinutes(date.getUTCMinutes() + offsetDiff);
265
+ return date;
266
+ }
267
+ var offsetSymbol = Symbol("ec");
268
+ function setOffset(date, offset) {
269
+ date[offsetSymbol] = offset;
270
+ return date;
271
+ }
272
+ function getOffset(date) {
273
+ return date[offsetSymbol];
274
+ }
252
275
  /**
253
276
  * Private functions
254
277
  */
255
- function _fromLocalDate(date) {
256
- return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()));
278
+ function _fromLocalDate(date, offset = void 0) {
279
+ let result = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()));
280
+ applyOffsetDiff(result, offset ? offset - tzOffset(result) : 0);
281
+ setOffset(result, offset ?? tzOffset(result));
282
+ return result;
257
283
  }
258
- function _fromISOString(str) {
259
- const parts = str.match(/\d+/g);
260
- return new Date(Date.UTC(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]), Number(parts[3] || 0), Number(parts[4] || 0), Number(parts[5] || 0)));
284
+ function _fromISOString(str, offset = void 0) {
285
+ let match = {};
286
+ let inputOffset = parseOffset(str, match);
287
+ if (inputOffset !== void 0) str = str.substring(0, match.index);
288
+ let parts = str.match(/\d+/g);
289
+ let result = new Date(Date.UTC(+parts[0], +parts[1] - 1, +parts[2], +parts[3] || 0, +parts[4] || 0, +parts[5] || 0));
290
+ if (offset !== void 0 && inputOffset !== void 0) applyOffsetDiff(result, offset - inputOffset);
291
+ setOffset(result, offset ?? inputOffset);
292
+ return result;
293
+ }
294
+ function _skipHiddenDays(date, hiddenDays, dateFn) {
295
+ if (hiddenDays.length && hiddenDays.length < 7) while (hiddenDays.includes(date.getUTCDay())) dateFn(date);
261
296
  }
262
297
  //#endregion
263
298
  //#region packages/core/src/lib/payload.js
@@ -338,14 +373,14 @@ function toViewWithLocalDates(view) {
338
373
  //#endregion
339
374
  //#region packages/core/src/lib/events.js
340
375
  var eventId = 1;
341
- function createEvents(input) {
376
+ function createEvents(input, offset = void 0) {
342
377
  return input.map((event) => {
343
378
  let result = {
344
379
  id: "id" in event ? String(event.id) : `{generated-${eventId++}}`,
345
380
  resourceIds: toArrayProp(event, "resourceId").map(String),
346
381
  allDay: event.allDay ?? (noTimePart(event.start) && noTimePart(event.end)),
347
- start: createDate(event.start),
348
- end: createDate(event.end),
382
+ start: createDate(event.start, offset),
383
+ end: createDate(event.end, offset),
349
384
  title: event.title ?? "",
350
385
  editable: event.editable,
351
386
  startEditable: event.startEditable,
@@ -886,6 +921,7 @@ function createOptions(plugins) {
886
921
  ],
887
922
  weekNumber: "ec-week-number"
888
923
  },
924
+ timeZone: "local",
889
925
  titleFormat: {
890
926
  year: "numeric",
891
927
  month: "short",
@@ -941,7 +977,7 @@ function optionsState(plugins, userOptions) {
941
977
  let component = extractOption(opts, "component");
942
978
  delete opts.view;
943
979
  for (let key of keys(opts)) if (hasOwn(options, key)) {
944
- if (!setters[key]) setters[key] = [];
980
+ setters[key] ??= [];
945
981
  setters[key].push(specialOptions.includes(key) ? (value) => opts[key] = isFunction(value) ? value(defOpts[key]) : value : (value) => opts[key] = value);
946
982
  } else delete opts[key];
947
983
  viewOptions[view] = opts;
@@ -1014,27 +1050,27 @@ function switchView(mainState) {
1014
1050
  }
1015
1051
  function loadEvents(mainState, loadingInvoker) {
1016
1052
  return () => {
1017
- let { activeRange, fetchedRange: { events: fetchedRange }, viewDates, options: { events, eventSources, lazyFetching } } = mainState;
1053
+ let { activeRange, fetchedRange: { events: fetchedRange }, offset, viewDates, options: { events, eventSources, lazyFetching, timeZone } } = mainState;
1018
1054
  untrack(() => {
1019
- load(eventSources.map((source) => isFunction(source.events) ? source.events : source), events, createEvents, (result) => mainState.events = arrayProxy(result), activeRange, fetchedRange, viewDates, true, lazyFetching, loadingInvoker);
1055
+ load(eventSources.map((source) => isFunction(source.events) ? source.events : source), events, (input) => createEvents(input, offset), (result) => mainState.events = arrayProxy(result), timeZone, activeRange, fetchedRange, viewDates, true, lazyFetching, loadingInvoker);
1020
1056
  });
1021
1057
  };
1022
1058
  }
1023
1059
  function loadResources(mainState, loadingInvoker) {
1024
1060
  return () => {
1025
- let { activeRange, fetchedRange: { resources: fetchedRange }, viewDates, options: { lazyFetching, refetchResourcesOnNavigate, resources } } = mainState;
1061
+ let { activeRange, fetchedRange: { resources: fetchedRange }, viewDates, options: { lazyFetching, refetchResourcesOnNavigate, resources, timeZone } } = mainState;
1026
1062
  untrack(() => {
1027
- load(isArray(resources) ? [] : [resources], resources, createResources, (result) => mainState.resources = arrayProxy(result), activeRange, fetchedRange, viewDates, refetchResourcesOnNavigate, lazyFetching, loadingInvoker);
1063
+ load(isArray(resources) ? [] : [resources], resources, createResources, (result) => mainState.resources = arrayProxy(result), timeZone, activeRange, fetchedRange, viewDates, refetchResourcesOnNavigate, lazyFetching, loadingInvoker);
1028
1064
  });
1029
1065
  };
1030
1066
  }
1031
- function load(sources, defaultResult, parseResult, applyResult, activeRange, fetchedRange, viewDates, refetchOnNavigate, lazyFetching, loading) {
1067
+ function load(sources, defaultResult, parseResult, applyResult, timeZone, activeRange, fetchedRange, viewDates, refetchOnNavigate, lazyFetching, loading) {
1032
1068
  if (empty(viewDates)) return;
1033
1069
  if (empty(sources)) {
1034
1070
  applyResult(defaultResult);
1035
1071
  return;
1036
1072
  }
1037
- if ((refetchOnNavigate || !fetchedRange.start) && (!lazyFetching || !fetchedRange.start || fetchedRange.start > activeRange.start || fetchedRange.end < activeRange.end)) {
1073
+ if ((refetchOnNavigate || !fetchedRange.start) && (!lazyFetching || !fetchedRange.start || fetchedRange.start > activeRange.start || fetchedRange.end < activeRange.end || fetchedRange.timeZone !== timeZone)) {
1038
1074
  let result = [];
1039
1075
  let failure = (e) => loading.stop();
1040
1076
  let success = (data) => {
@@ -1051,7 +1087,8 @@ function load(sources, defaultResult, parseResult, applyResult, activeRange, fet
1051
1087
  start: toLocalDate(activeRange.start),
1052
1088
  end: toLocalDate(activeRange.end),
1053
1089
  startStr,
1054
- endStr
1090
+ endStr,
1091
+ timeZone
1055
1092
  } : {}, success, failure);
1056
1093
  if (result !== void 0) Promise.resolve(result).then(success, failure);
1057
1094
  } else {
@@ -1059,6 +1096,7 @@ function load(sources, defaultResult, parseResult, applyResult, activeRange, fet
1059
1096
  if (refetchOnNavigate) {
1060
1097
  params.start = startStr;
1061
1098
  params.end = endStr;
1099
+ if (timeZone !== "local") params.timeZone = timeZone;
1062
1100
  }
1063
1101
  params = new URLSearchParams(params);
1064
1102
  let url = source.url, headers = {}, body;
@@ -1076,7 +1114,10 @@ function load(sources, defaultResult, parseResult, applyResult, activeRange, fet
1076
1114
  }).then((response) => response.json()).then(success).catch(failure);
1077
1115
  }
1078
1116
  }
1079
- assign(fetchedRange, activeRange);
1117
+ assign(fetchedRange, {
1118
+ ...activeRange,
1119
+ timeZone
1120
+ });
1080
1121
  }
1081
1122
  }
1082
1123
  function createLoadingInvoker(mainState) {
@@ -1092,8 +1133,9 @@ function createLoadingInvoker(mainState) {
1092
1133
  }
1093
1134
  function setNowAndToday(mainState) {
1094
1135
  return () => {
1136
+ let { offset } = mainState;
1095
1137
  let interval = setInterval(() => {
1096
- let now = createDate();
1138
+ let now = createDate(void 0, offset);
1097
1139
  let today = setMidnight(cloneDate(now));
1098
1140
  mainState.now = now;
1099
1141
  if (!datesEqual(mainState.today, today)) mainState.today = today;
@@ -1101,6 +1143,25 @@ function setNowAndToday(mainState) {
1101
1143
  return () => clearInterval(interval);
1102
1144
  };
1103
1145
  }
1146
+ function handleTimeZoneChange(mainState) {
1147
+ return () => {
1148
+ let { offset, options } = mainState;
1149
+ untrack(() => {
1150
+ for (let event of mainState.events) if (!event.allDay) for (let prop of ["start", "end"]) {
1151
+ let dateOffset = getOffset(event[prop]);
1152
+ if (dateOffset !== void 0) applyOffsetDiff(event[prop], offset - dateOffset);
1153
+ setOffset(event[prop], offset);
1154
+ }
1155
+ let dateOffset = getOffset(options.date);
1156
+ if (dateOffset !== void 0) {
1157
+ let diff = createDate(void 0, offset).getUTCDay() - createDate(void 0, dateOffset).getUTCDay();
1158
+ let date = addDay(cloneDate(options.date), diff);
1159
+ mainState.setOption("date", date);
1160
+ }
1161
+ setOffset(options.date, offset);
1162
+ });
1163
+ };
1164
+ }
1104
1165
  function runDatesSet(mainState) {
1105
1166
  return () => {
1106
1167
  let { activeRange, options: { datesSet } } = mainState;
@@ -1194,6 +1255,16 @@ function filteredEvents(mainState) {
1194
1255
  return result;
1195
1256
  };
1196
1257
  }
1258
+ function offset(mainState) {
1259
+ return () => {
1260
+ let { options: { timeZone } } = mainState;
1261
+ let offset;
1262
+ untrack(() => {
1263
+ offset = timeZone === "local" ? tzOffset() : timeZone === "UTC" ? 0 : parseOffset(timeZone) ?? tzOffset();
1264
+ });
1265
+ return offset;
1266
+ };
1267
+ }
1197
1268
  function viewDates(mainState) {
1198
1269
  return () => {
1199
1270
  let { options, activeRange } = mainState;
@@ -1246,6 +1317,13 @@ var State = class {
1246
1317
  set auxComponents(value) {
1247
1318
  $.set(this.#auxComponents, value, true);
1248
1319
  }
1320
+ #offset;
1321
+ get offset() {
1322
+ return $.get(this.#offset);
1323
+ }
1324
+ set offset(value) {
1325
+ $.set(this.#offset, value);
1326
+ }
1249
1327
  #currentRange;
1250
1328
  get currentRange() {
1251
1329
  return $.get(this.#currentRange);
@@ -1406,6 +1484,7 @@ var State = class {
1406
1484
  constructor(plugins, options) {
1407
1485
  [this.options, this.setOption, this.setViewOptions] = optionsState(plugins, options);
1408
1486
  this.#auxComponents = $.state($.proxy([]));
1487
+ this.#offset = $.derived(offset(this));
1409
1488
  this.#currentRange = $.derived(currentRange(this));
1410
1489
  this.#activeRange = $.derived(activeRange(this));
1411
1490
  this.#fetchedRange = $.state($.proxy({
@@ -1415,7 +1494,7 @@ var State = class {
1415
1494
  this.#events = $.state(arrayProxy(this.options.events));
1416
1495
  this.#filteredEvents = $.derived(filteredEvents(this));
1417
1496
  this.#mainEl = $.state();
1418
- this.#now = $.state($.proxy(createDate()));
1497
+ this.#now = $.state($.proxy(createDate(void 0, this.offset)));
1419
1498
  this.#resources = $.state(arrayProxy(isArray(this.options.resources) ? this.options.resources : []));
1420
1499
  this.#today = $.state($.proxy(setMidnight(cloneDate(this.now))));
1421
1500
  this.#intlEventTime = $.derived(intlRange(this, "eventTimeFormat"));
@@ -1438,6 +1517,7 @@ var State = class {
1438
1517
  #initEffects() {
1439
1518
  let loading = createLoadingInvoker(this);
1440
1519
  $.user_pre_effect(switchView(this));
1520
+ $.user_pre_effect(handleTimeZoneChange(this));
1441
1521
  $.user_pre_effect(setNowAndToday(this));
1442
1522
  $.user_effect(loadEvents(this, loading));
1443
1523
  $.user_effect(loadResources(this, loading));
@@ -1654,7 +1734,7 @@ function Calendar($$anchor, $$props) {
1654
1734
  let plugins = $.prop($$props, "plugins", 19, () => []), options = $.prop($$props, "options", 19, () => ({}));
1655
1735
  let mainState = new State(plugins(), options());
1656
1736
  setContext("state", mainState);
1657
- let auxComponents = $.derived(() => mainState.auxComponents), features = $.derived(() => mainState.features), events = $.derived(() => mainState.events), interaction = $.derived(() => mainState.interaction), iClass = $.derived(() => mainState.iClass), view = $.derived(() => mainState.view), View = $.derived(() => mainState.viewComponent), date = $.derived(() => mainState.options.date), dateIncrement = $.derived(() => mainState.options.dateIncrement), duration = $.derived(() => mainState.options.duration), height = $.derived(() => mainState.options.height), hiddenDays = $.derived(() => mainState.options.hiddenDays), customScrollbars = $.derived(() => mainState.options.customScrollbars), theme = $.derived(() => mainState.options.theme);
1737
+ let auxComponents = $.derived(() => mainState.auxComponents), features = $.derived(() => mainState.features), events = $.derived(() => mainState.events), interaction = $.derived(() => mainState.interaction), iClass = $.derived(() => mainState.iClass), offset = $.derived(() => mainState.offset), view = $.derived(() => mainState.view), View = $.derived(() => mainState.viewComponent), date = $.derived(() => mainState.options.date), dateIncrement = $.derived(() => mainState.options.dateIncrement), duration = $.derived(() => mainState.options.duration), height = $.derived(() => mainState.options.height), hiddenDays = $.derived(() => mainState.options.hiddenDays), customScrollbars = $.derived(() => mainState.options.customScrollbars), theme = $.derived(() => mainState.options.theme);
1658
1738
  let prevOptions = { ...options() };
1659
1739
  $.user_pre_effect(() => {
1660
1740
  for (let [name, value] of diff(options(), prevOptions)) untrack(() => {
@@ -1687,7 +1767,7 @@ function Calendar($$anchor, $$props) {
1687
1767
  return null;
1688
1768
  }
1689
1769
  function addEvent(event) {
1690
- event = createEvents([event])[0];
1770
+ event = createEvents([event], $.get(offset))[0];
1691
1771
  $.get(events).push(event);
1692
1772
  return toEventWithLocalDates(event);
1693
1773
  }
@@ -1695,7 +1775,7 @@ function Calendar($$anchor, $$props) {
1695
1775
  let id = String(event.id);
1696
1776
  let idx = $.get(events).findIndex((event) => event.id === id);
1697
1777
  if (idx >= 0) {
1698
- event = createEvents([event])[0];
1778
+ event = createEvents([event], $.get(offset))[0];
1699
1779
  $.get(events)[idx] = event;
1700
1780
  return toEventWithLocalDates(event);
1701
1781
  }
@@ -3877,11 +3957,11 @@ function slotLabelPeriodicity(mainState) {
3877
3957
  }
3878
3958
  function slots(mainState, viewState) {
3879
3959
  return () => {
3880
- let { options: { slotDuration } } = mainState;
3960
+ let { offset, options: { slotDuration } } = mainState;
3881
3961
  let { intlSlotLabel, slotLabelPeriodicity, slotTimeLimits } = viewState;
3882
3962
  let slots;
3883
3963
  untrack(() => {
3884
- slots = createSlots(setMidnight(createDate()), slotDuration, slotLabelPeriodicity, slotTimeLimits, intlSlotLabel);
3964
+ slots = createSlots(setMidnight(createDate(void 0, offset)), slotDuration, slotLabelPeriodicity, slotTimeLimits, intlSlotLabel);
3885
3965
  });
3886
3966
  return slots;
3887
3967
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event-calendar/core",
3
- "version": "5.6.1",
3
+ "version": "5.7.0",
4
4
  "title": "Event Calendar Core package",
5
5
  "description": "Full-sized drag & drop event calendar with resource & timeline views",
6
6
  "keywords": [
@@ -32,6 +32,6 @@
32
32
  "#components": "./src/lib/components/index.js"
33
33
  },
34
34
  "dependencies": {
35
- "svelte": "^5.55.2"
35
+ "svelte": "^5.55.4"
36
36
  }
37
37
  }
@@ -16,7 +16,7 @@
16
16
  setContext('state', mainState);
17
17
 
18
18
  let {
19
- auxComponents, features, events, interaction, iClass, view, viewComponent: View,
19
+ auxComponents, features, events, interaction, iClass, offset, view, viewComponent: View,
20
20
  options: {date, dateIncrement, duration, height, hiddenDays, customScrollbars, theme}
21
21
  } = $derived(mainState);
22
22
 
@@ -67,7 +67,7 @@
67
67
  }
68
68
 
69
69
  export function addEvent(event) {
70
- event = createEvents([event])[0];
70
+ event = createEvents([event], offset)[0];
71
71
  events.push(event);
72
72
  return toEventWithLocalDates(event);
73
73
  }
@@ -76,7 +76,7 @@
76
76
  let id = String(event.id);
77
77
  let idx = events.findIndex(event => event.id === id);
78
78
  if (idx >= 0) {
79
- event = createEvents([event])[0];
79
+ event = createEvents([event], offset)[0];
80
80
  events[idx] = event;
81
81
  return toEventWithLocalDates(event);
82
82
  }
package/src/lib/date.js CHANGED
@@ -1,13 +1,9 @@
1
- import {isDate, isFunction} from './utils.js';
1
+ import {assign, isDate, isFunction, tzOffset} from './utils.js';
2
2
 
3
3
  export const DAY_IN_SECONDS = 86400;
4
4
 
5
- export function createDate(input = undefined) {
6
- if (input !== undefined) {
7
- return isDate(input) ? _fromLocalDate(input) : _fromISOString(input);
8
- }
9
-
10
- return _fromLocalDate(new Date());
5
+ export function createDate(input = new Date(), offset = undefined) {
6
+ return isDate(input) ? _fromLocalDate(input, offset) : _fromISOString(input, offset);
11
7
  }
12
8
 
13
9
  export function createDuration(input) {
@@ -38,7 +34,10 @@ export function createDuration(input) {
38
34
  }
39
35
 
40
36
  export function cloneDate(date) {
41
- return new Date(date.getTime());
37
+ let result = new Date(date.getTime());
38
+ setOffset(result, getOffset(date));
39
+
40
+ return result;
42
41
  }
43
42
 
44
43
  export function addDuration(date, duration, x = 1) {
@@ -152,14 +151,6 @@ export function prevDate(date, duration, hiddenDays) {
152
151
  return date;
153
152
  }
154
153
 
155
- function _skipHiddenDays(date, hiddenDays, dateFn) {
156
- if (hiddenDays.length && hiddenDays.length < 7) {
157
- while (hiddenDays.includes(date.getUTCDay())) {
158
- dateFn(date);
159
- }
160
- }
161
- }
162
-
163
154
  /**
164
155
  * For a given date, get its week number
165
156
  * - ISO @see https://stackoverflow.com/questions/6117814/get-week-of-year-in-javascript-like-in-php
@@ -192,12 +183,42 @@ export function createWeekNumberContent(week, weekNumberContent, date) {
192
183
  return 'W' + String(week).padStart(2, '0');
193
184
  }
194
185
 
186
+ export function parseOffset(str, match = {}) {
187
+ let parts = str.match(/([+-])(\d{2}):(\d{2})$/);
188
+ if (parts) {
189
+ assign(match, parts);
190
+ return +(parts[1] + '1') * (+parts[2] * 60 + +parts[3]);
191
+ }
192
+ return undefined;
193
+ }
194
+
195
+ /**
196
+ * Apply timezone offset difference in minutes to a date
197
+ */
198
+ export function applyOffsetDiff(date, offsetDiff) {
199
+ if (offsetDiff) {
200
+ date.setUTCMinutes(date.getUTCMinutes() + offsetDiff);
201
+ }
202
+
203
+ return date;
204
+ }
205
+
206
+ let offsetSymbol = Symbol('ec');
207
+ export function setOffset(date, offset) {
208
+ date[offsetSymbol] = offset;
209
+ return date;
210
+ }
211
+
212
+ export function getOffset(date) {
213
+ return date[offsetSymbol];
214
+ }
215
+
195
216
  /**
196
217
  * Private functions
197
218
  */
198
219
 
199
- function _fromLocalDate(date) {
200
- return new Date(Date.UTC(
220
+ function _fromLocalDate(date, offset = undefined) {
221
+ let result = new Date(Date.UTC(
201
222
  date.getFullYear(),
202
223
  date.getMonth(),
203
224
  date.getDate(),
@@ -205,16 +226,39 @@ function _fromLocalDate(date) {
205
226
  date.getMinutes(),
206
227
  date.getSeconds()
207
228
  ));
229
+ applyOffsetDiff(result, offset ? offset - tzOffset(result) : 0);
230
+ setOffset(result, offset ?? tzOffset(result));
231
+
232
+ return result;
208
233
  }
209
234
 
210
- function _fromISOString(str) {
211
- const parts = str.match(/\d+/g);
212
- return new Date(Date.UTC(
213
- Number(parts[0]),
214
- Number(parts[1]) - 1,
215
- Number(parts[2]),
216
- Number(parts[3] || 0),
217
- Number(parts[4] || 0),
218
- Number(parts[5] || 0)
235
+ function _fromISOString(str, offset = undefined) {
236
+ let match = {};
237
+ let inputOffset = parseOffset(str, match);
238
+ if (inputOffset !== undefined) {
239
+ str = str.substring(0, match.index);
240
+ }
241
+ let parts = str.match(/\d+/g);
242
+ let result = new Date(Date.UTC(
243
+ +parts[0],
244
+ +parts[1] - 1,
245
+ +parts[2],
246
+ +parts[3] || 0,
247
+ +parts[4] || 0,
248
+ +parts[5] || 0
219
249
  ));
250
+ if (offset !== undefined && inputOffset !== undefined) {
251
+ applyOffsetDiff(result, offset - inputOffset);
252
+ }
253
+ setOffset(result, offset ?? inputOffset);
254
+
255
+ return result;
256
+ }
257
+
258
+ function _skipHiddenDays(date, hiddenDays, dateFn) {
259
+ if (hiddenDays.length && hiddenDays.length < 7) {
260
+ while (hiddenDays.includes(date.getUTCDay())) {
261
+ dateFn(date);
262
+ }
263
+ }
220
264
  }
package/src/lib/events.js CHANGED
@@ -6,14 +6,14 @@ import {assign, isArray, isFunction} from './utils.js';
6
6
  import {toViewWithLocalDates} from './view.js';
7
7
 
8
8
  let eventId = 1;
9
- export function createEvents(input) {
9
+ export function createEvents(input, offset = undefined) {
10
10
  return input.map(event => {
11
11
  let result = {
12
12
  id: 'id' in event ? String(event.id) : `{generated-${eventId++}}`,
13
13
  resourceIds: toArrayProp(event, 'resourceId').map(String),
14
14
  allDay: event.allDay ?? (noTimePart(event.start) && noTimePart(event.end)),
15
- start: createDate(event.start),
16
- end: createDate(event.end),
15
+ start: createDate(event.start, offset),
16
+ end: createDate(event.end, offset),
17
17
  title: event.title ?? '',
18
18
  editable: event.editable,
19
19
  startEditable: event.startEditable,
package/src/lib/utils.js CHANGED
@@ -42,6 +42,10 @@ export function empty(array) {
42
42
  return !length(array);
43
43
  }
44
44
 
45
+ export function tzOffset(date = new Date()) {
46
+ return -date.getTimezoneOffset();
47
+ }
48
+
45
49
  export function isArray(value) {
46
50
  return Array.isArray(value);
47
51
  }
@@ -134,13 +134,19 @@ export function slotLabelPeriodicity(mainState) {
134
134
  export function slots(mainState, viewState) {
135
135
  return () => {
136
136
  // Dependencies
137
- let {options: {slotDuration}} = mainState;
137
+ let {offset, options: {slotDuration}} = mainState;
138
138
  let {intlSlotLabel, slotLabelPeriodicity, slotTimeLimits} = viewState;
139
139
 
140
140
  let slots;
141
141
 
142
142
  untrack(() => {
143
- slots = createSlots(setMidnight(createDate()), slotDuration, slotLabelPeriodicity, slotTimeLimits, intlSlotLabel);
143
+ slots = createSlots(
144
+ setMidnight(createDate(undefined, offset)),
145
+ slotDuration,
146
+ slotLabelPeriodicity,
147
+ slotTimeLimits,
148
+ intlSlotLabel
149
+ );
144
150
  });
145
151
 
146
152
  return slots;
@@ -1,7 +1,7 @@
1
1
  import {tick, untrack} from 'svelte';
2
2
  import {
3
3
  addDay, addDuration, cloneDate, createView, isFunction, prevClosestDay, setMidnight, subtractDay,
4
- toEventWithLocalDates, toViewWithLocalDates
4
+ toEventWithLocalDates, toViewWithLocalDates, parseOffset, tzOffset, applyOffsetDiff
5
5
  } from '#lib';
6
6
 
7
7
  export function currentRange(mainState) {
@@ -85,6 +85,23 @@ export function filteredEvents(mainState) {
85
85
  };
86
86
  }
87
87
 
88
+ export function offset(mainState) {
89
+ return () => {
90
+ // Dependencies
91
+ let {options: {timeZone}} = mainState;
92
+
93
+ let offset;
94
+
95
+ untrack(() => {
96
+ offset = timeZone === 'local' ? tzOffset() : (
97
+ timeZone === 'UTC' ? 0 : (parseOffset(timeZone) ?? tzOffset())
98
+ );
99
+ });
100
+
101
+ return offset;
102
+ };
103
+ }
104
+
88
105
  export function viewDates(mainState) {
89
106
  return () => {
90
107
  // Dependencies
@@ -1,7 +1,8 @@
1
1
  import {getAbortSignal, tick, untrack} from 'svelte';
2
2
  import {
3
- assign, cloneDate, createDate, createEvents, createResources, datesEqual, empty, isArray, isFunction, setMidnight,
4
- toISOString, toLocalDate, toViewWithLocalDates
3
+ addDay,
4
+ applyOffsetDiff, assign, cloneDate, createDate, createEvents, createResources, datesEqual, empty, getOffset, isArray,
5
+ isFunction, setMidnight, setOffset, toISOString, toLocalDate, toViewWithLocalDates
5
6
  } from '#lib';
6
7
  import {arrayProxy} from './proxy.svelte.js';
7
8
 
@@ -22,15 +23,16 @@ export function switchView(mainState) {
22
23
  export function loadEvents(mainState, loadingInvoker) {
23
24
  return () => {
24
25
  // Dependencies
25
- let {activeRange, fetchedRange: {events: fetchedRange}, viewDates,
26
- options: {events, eventSources, lazyFetching}} = mainState;
26
+ let {activeRange, fetchedRange: {events: fetchedRange}, offset, viewDates,
27
+ options: {events, eventSources, lazyFetching, timeZone}} = mainState;
27
28
 
28
29
  untrack(() => {
29
30
  load(
30
31
  eventSources.map(source => isFunction(source.events) ? source.events : source),
31
32
  events,
32
- createEvents,
33
+ input => createEvents(input, offset),
33
34
  result => mainState.events = arrayProxy(result),
35
+ timeZone,
34
36
  activeRange,
35
37
  fetchedRange,
36
38
  viewDates,
@@ -46,7 +48,7 @@ export function loadResources(mainState, loadingInvoker) {
46
48
  return () => {
47
49
  // Dependencies
48
50
  let {activeRange, fetchedRange: {resources: fetchedRange}, viewDates,
49
- options: {lazyFetching, refetchResourcesOnNavigate, resources}} = mainState;
51
+ options: {lazyFetching, refetchResourcesOnNavigate, resources, timeZone}} = mainState;
50
52
 
51
53
  untrack(() => {
52
54
  load(
@@ -54,6 +56,7 @@ export function loadResources(mainState, loadingInvoker) {
54
56
  resources,
55
57
  createResources,
56
58
  result => mainState.resources = arrayProxy(result),
59
+ timeZone,
57
60
  activeRange,
58
61
  fetchedRange,
59
62
  viewDates,
@@ -65,7 +68,19 @@ export function loadResources(mainState, loadingInvoker) {
65
68
  };
66
69
  }
67
70
 
68
- function load(sources, defaultResult, parseResult, applyResult, activeRange, fetchedRange, viewDates, refetchOnNavigate, lazyFetching, loading) {
71
+ function load(
72
+ sources,
73
+ defaultResult,
74
+ parseResult,
75
+ applyResult,
76
+ timeZone,
77
+ activeRange,
78
+ fetchedRange,
79
+ viewDates,
80
+ refetchOnNavigate,
81
+ lazyFetching,
82
+ loading
83
+ ) {
69
84
  if (empty(viewDates)) {
70
85
  return;
71
86
  }
@@ -80,7 +95,8 @@ function load(sources, defaultResult, parseResult, applyResult, activeRange, fet
80
95
  !lazyFetching ||
81
96
  !fetchedRange.start ||
82
97
  fetchedRange.start > activeRange.start ||
83
- fetchedRange.end < activeRange.end
98
+ fetchedRange.end < activeRange.end ||
99
+ fetchedRange.timeZone !== timeZone
84
100
  )
85
101
  ) {
86
102
  let result = [];
@@ -103,7 +119,8 @@ function load(sources, defaultResult, parseResult, applyResult, activeRange, fet
103
119
  start: toLocalDate(activeRange.start),
104
120
  end: toLocalDate(activeRange.end),
105
121
  startStr,
106
- endStr
122
+ endStr,
123
+ timeZone
107
124
  } : {}, success, failure);
108
125
  if (result !== undefined) {
109
126
  Promise.resolve(result).then(success, failure);
@@ -115,6 +132,9 @@ function load(sources, defaultResult, parseResult, applyResult, activeRange, fet
115
132
  if (refetchOnNavigate) {
116
133
  params.start = startStr;
117
134
  params.end = endStr;
135
+ if (timeZone !== 'local') {
136
+ params.timeZone = timeZone;
137
+ }
118
138
  }
119
139
  params = new URLSearchParams(params);
120
140
  // Prepare fetch
@@ -135,7 +155,7 @@ function load(sources, defaultResult, parseResult, applyResult, activeRange, fet
135
155
  }
136
156
  }
137
157
  // Save current range for future requests
138
- assign(fetchedRange, activeRange);
158
+ assign(fetchedRange, {...activeRange, timeZone});
139
159
  }
140
160
  }
141
161
 
@@ -155,9 +175,12 @@ export function createLoadingInvoker(mainState) {
155
175
 
156
176
  export function setNowAndToday(mainState) {
157
177
  return () => {
178
+ // Dependencies
179
+ let {offset} = mainState;
180
+
158
181
  // Now and today
159
182
  let interval = setInterval(() => {
160
- let now = createDate();
183
+ let now = createDate(undefined, offset);
161
184
  let today = setMidnight(cloneDate(now));
162
185
  mainState.now = now;
163
186
  if (!datesEqual(mainState.today, today)) {
@@ -169,6 +192,38 @@ export function setNowAndToday(mainState) {
169
192
  }
170
193
  }
171
194
 
195
+ export function handleTimeZoneChange(mainState) {
196
+ return () => {
197
+ // Dependencies
198
+ let {offset, options} = mainState;
199
+
200
+ untrack(() => {
201
+ // Update events
202
+ for (let event of mainState.events) {
203
+ if (!event.allDay) {
204
+ for (let prop of ['start', 'end']) {
205
+ let dateOffset = getOffset(event[prop]);
206
+ // Dates parsed from strings with no timezone info have dateOffset === undefined;
207
+ // they are treated as floating and only get branded with the new offset, not shifted
208
+ if (dateOffset !== undefined) {
209
+ applyOffsetDiff(event[prop], offset - dateOffset);
210
+ }
211
+ setOffset(event[prop], offset);
212
+ }
213
+ }
214
+ }
215
+ // Update date option
216
+ let dateOffset = getOffset(options.date);
217
+ if (dateOffset !== undefined) {
218
+ let diff = createDate(undefined, offset).getUTCDay() - createDate(undefined, dateOffset).getUTCDay();
219
+ let date = addDay(cloneDate(options.date), diff);
220
+ mainState.setOption('date', date);
221
+ }
222
+ setOffset(options.date, offset);
223
+ });
224
+ }
225
+ }
226
+
172
227
  export function runDatesSet(mainState) {
173
228
  return () => {
174
229
  // Dependencies
@@ -94,6 +94,7 @@ function createOptions(plugins) {
94
94
  weekdays: ['ec-sun', 'ec-mon', 'ec-tue', 'ec-wed', 'ec-thu', 'ec-fri', 'ec-sat'],
95
95
  weekNumber: 'ec-week-number'
96
96
  },
97
+ timeZone: 'local',
97
98
  titleFormat: {
98
99
  year: 'numeric',
99
100
  month: 'short',
@@ -173,9 +174,7 @@ export function optionsState(plugins, userOptions) {
173
174
  // Set up option setters and delete unknown options
174
175
  for (let key of keys(opts)) {
175
176
  if (hasOwn(options, key)) {
176
- if (!setters[key]) {
177
- setters[key] = [];
178
- }
177
+ setters[key] ??= [];
179
178
  setters[key].push(
180
179
  specialOptions.includes(key)
181
180
  ? value => opts[key] = isFunction(value) ? value(defOpts[key]) : value
@@ -2,10 +2,13 @@ import {SvelteMap} from 'svelte/reactivity';
2
2
  import {cloneDate, createDate, identity, intl, intlRange, isArray, setMidnight} from '#lib';
3
3
  import {optionsState} from './options.js';
4
4
  import {
5
- createLoadingInvoker, loadEvents, loadResources, runDatesSet, runEventAllUpdated, runViewDidMount, setNowAndToday,
5
+ createLoadingInvoker, handleTimeZoneChange, loadEvents, loadResources, runDatesSet, runEventAllUpdated,
6
+ runViewDidMount, setNowAndToday,
6
7
  switchView
7
8
  } from './effects.js';
8
- import {activeRange, currentRange, filteredEvents, view, viewDates, viewTitle} from './derived.js';
9
+ import {
10
+ activeRange, currentRange, filteredEvents, offset, view, viewDates, viewTitle
11
+ } from './derived.js';
9
12
  import {arrayProxy} from './proxy.svelte.js';
10
13
 
11
14
  export default class State {
@@ -20,13 +23,14 @@ export default class State {
20
23
 
21
24
  // Create other states
22
25
  this.auxComponents = $state([]);
26
+ this.offset = $derived.by(offset(this));
23
27
  this.currentRange = $derived.by(currentRange(this));
24
28
  this.activeRange = $derived.by(activeRange(this));
25
29
  this.fetchedRange = $state({events: {}, resources: {}});
26
30
  this.events = $state.raw(arrayProxy(this.options.events));
27
31
  this.filteredEvents = $derived.by(filteredEvents(this));
28
32
  this.mainEl = $state();
29
- this.now = $state(createDate());
33
+ this.now = $state(createDate(undefined, this.offset));
30
34
  this.resources = $state.raw(arrayProxy(isArray(this.options.resources) ? this.options.resources : []));
31
35
  this.today = $state(setMidnight(cloneDate(this.now)));
32
36
  this.intlEventTime = $derived.by(intlRange(this, 'eventTimeFormat'));
@@ -56,6 +60,7 @@ export default class State {
56
60
  #initEffects() {
57
61
  let loading = createLoadingInvoker(this);
58
62
  $effect.pre(switchView(this));
63
+ $effect.pre(handleTimeZoneChange(this));
59
64
  $effect.pre(setNowAndToday(this));
60
65
  $effect(loadEvents(this, loading));
61
66
  $effect(loadResources(this, loading));