@event-calendar/core 5.2.3 → 5.3.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.
@@ -6,26 +6,29 @@ export function createChunks(event, days, monthView, withId = true) {
6
6
  let lastEnd;
7
7
  let gridColumn;
8
8
  let gridRow;
9
+ let resource;
9
10
  let left;
10
11
  let width = 0;
11
- for (let {gridColumn: column, gridRow: row, resource, dayStart, dayEnd, start, end, disabled} of days) {
12
+ for (let {gridColumn: column, gridRow: row, resource: dayResource, dayStart, dayEnd, start, end, disabled} of days) {
12
13
  if (!disabled) {
13
14
  if (monthView) {
14
- if (eventIntersects(event, dayStart, dayEnd, resource)) {
15
+ if (eventIntersects(event, dayStart, dayEnd, dayResource)) {
15
16
  if (!dates.length) {
16
17
  firstStart = dayStart;
17
18
  gridColumn = column;
18
19
  gridRow = row;
20
+ resource = dayResource;
19
21
  }
20
22
  dates.push(dayStart);
21
23
  lastEnd = end;
22
24
  }
23
25
  } else {
24
- if (eventIntersects(event, start, end, resource)) {
26
+ if (eventIntersects(event, start, end, dayResource)) {
25
27
  if (!dates.length) {
26
28
  firstStart = start;
27
29
  gridColumn = column;
28
30
  gridRow = row;
31
+ resource = dayResource;
29
32
  left = max(event.start - start, 0) / 1000;
30
33
  }
31
34
  dates.push(dayStart);
@@ -38,7 +41,7 @@ export function createChunks(event, days, monthView, withId = true) {
38
41
  if (dates.length) {
39
42
  let chunk = createEventChunk(event, firstStart, lastEnd);
40
43
  // Chunk layout
41
- assign(chunk, {gridColumn, gridRow, dates, left, width});
44
+ assign(chunk, {gridColumn, gridRow, resource, dates, left, width});
42
45
  if (withId) {
43
46
  assignChunkId(chunk);
44
47
  }
@@ -1,6 +1,6 @@
1
1
  <script>
2
2
  import {getContext, setContext, tick} from 'svelte';
3
- import {contentFrom, resizeObserver, runReposition, toSeconds} from '#lib';
3
+ import {contentFrom, empty, length, resizeObserver, runReposition, toSeconds} from '#lib';
4
4
  import {createAllDayContent} from './lib.js';
5
5
  import ViewState from './state.svelte.js';
6
6
  import {ColHead, DayHeader} from '#components';
@@ -29,9 +29,10 @@
29
29
 
30
30
  // Handle scrollTime
31
31
  $effect(() => {
32
- viewDates;
33
32
  scrollTime;
34
- tick().then(scrollToTime);
33
+ if (!empty(viewDates)) {
34
+ tick().then(scrollToTime);
35
+ }
35
36
  });
36
37
  function scrollToTime() {
37
38
  mainEl.scrollTop = (
@@ -47,97 +48,99 @@
47
48
  $effect(reposition);
48
49
  </script>
49
50
 
50
- <section
51
- bind:this={mainState.mainEl}
52
- class="{theme.main}"
53
- style:--ec-grid-cols="{grid.length * grid[0].length}"
54
- style:--ec-col-group-span="{grid[0].length}"
55
- style:--ec-col-width="{columnWidth ?? 'minmax(0, 1fr)'}"
56
- style:--ec-slot-label-periodicity="{slotLabelPeriodicity}"
57
- style:--ec-slot-height="{slotHeight}px"
58
- style:--ec-header-height="{headerHeight}px"
59
- style:--ec-sidebar-width="{sidebarWidth}px"
60
- {@attach resizeObserver(reposition)}
61
- >
62
- <header bind:offsetHeight={headerHeight} class="{theme.header}">
63
- <aside class="{theme.sidebar}" bind:offsetWidth={viewState.sidebarWidth}></aside>
64
- <div class="{theme.grid}" role="row">
65
- {#if header}
66
- {@render header()}
67
- {:else}
68
- {#each grid[0] as {dayStart: date, disabled, highlight}, i}
69
- <ColHead {date} colIndex={1 + i} {disabled} {highlight}>
70
- <DayHeader {date}/>
71
- </ColHead>
72
- {/each}
73
- {/if}
74
- </div>
51
+ {#if !empty(grid) && !empty(grid[0])}
52
+ <section
53
+ bind:this={mainState.mainEl}
54
+ class="{theme.main}"
55
+ style:--ec-grid-cols="{length(grid) * length(grid[0])}"
56
+ style:--ec-col-group-span="{length(grid[0])}"
57
+ style:--ec-col-width="{columnWidth ?? 'minmax(0, 1fr)'}"
58
+ style:--ec-slot-label-periodicity="{slotLabelPeriodicity}"
59
+ style:--ec-slot-height="{slotHeight}px"
60
+ style:--ec-header-height="{headerHeight}px"
61
+ style:--ec-sidebar-width="{sidebarWidth}px"
62
+ {@attach resizeObserver(reposition)}
63
+ >
64
+ <header bind:offsetHeight={headerHeight} class="{theme.header}">
65
+ <aside class="{theme.sidebar}" bind:offsetWidth={viewState.sidebarWidth}></aside>
66
+ <div class="{theme.grid}" role="row">
67
+ {#if header}
68
+ {@render header()}
69
+ {:else}
70
+ {#each grid[0] as {dayStart: date, disabled, highlight}, i}
71
+ <ColHead {date} colIndex={1 + i} {disabled} {highlight}>
72
+ <DayHeader {date}/>
73
+ </ColHead>
74
+ {/each}
75
+ {/if}
76
+ </div>
75
77
 
76
- {#if allDaySlot}
77
- <div class="{theme.allDay}">
78
- <aside class="{theme.sidebar}" {@attach contentFrom(allDayText)}></aside>
79
- <div class="{theme.grid}" role="row">
80
- {#each grid as days, i}
81
- {#each days as day, j}
82
- <Day {day} allDay noIeb={i + 1 === grid.length && j + 1 === days.length}/>
78
+ {#if allDaySlot}
79
+ <div class="{theme.allDay}">
80
+ <aside class="{theme.sidebar}" {@attach contentFrom(allDayText)}></aside>
81
+ <div class="{theme.grid}" role="row">
82
+ {#each grid as days, i}
83
+ {#each days as day, j}
84
+ <Day {day} allDay noIeb={i + 1 === length(grid) && j + 1 === length(days)}/>
85
+ {/each}
83
86
  {/each}
84
- {/each}
87
+ </div>
88
+ <div class="{theme.events}">
89
+ {#each allDayChunks as chunk, i (chunk.id)}
90
+ <!-- svelte-ignore binding_property_non_reactive -->
91
+ <AllDayEvent bind:this={refs[i]} {chunk}/>
92
+ {/each}
93
+ {#each allDayBgChunks as chunk (chunk.id)}
94
+ <AllDayEvent {chunk}/>
95
+ {/each}
96
+ {#each allDayIChunks as chunk}
97
+ <AllDayEvent {chunk}/>
98
+ {/each}
99
+ </div>
85
100
  </div>
86
- <div class="{theme.events}">
87
- {#each allDayChunks as chunk, i (chunk.id)}
88
- <!-- svelte-ignore binding_property_non_reactive -->
89
- <AllDayEvent bind:this={refs[i]} {chunk}/>
90
- {/each}
91
- {#each allDayBgChunks as chunk (chunk.id)}
92
- <AllDayEvent {chunk}/>
93
- {/each}
94
- {#each allDayIChunks as chunk}
95
- <AllDayEvent {chunk}/>
101
+ {/if}
102
+ </header>
103
+
104
+ <div class="{theme.body}" role="rowgroup">
105
+ <aside class="{theme.sidebar}" aria-hidden="true">
106
+ {#each slots as slot, i}
107
+ <div
108
+ class={[theme.slot, !i && theme.hidden]}
109
+ style:--ec-slot-label-periodicity={slot[2]}
110
+ >
111
+ <time
112
+ datetime="{slot[0]}"
113
+ {@attach contentFrom(slot[1])}
114
+ ></time>
115
+ </div>
116
+ {/each}
117
+ </aside>
118
+ <div class="{theme.grid}" role="row">
119
+ {#each grid as days, i}
120
+ {#each days as day, j}
121
+ <Day {day} noIeb={i + 1 === length(grid) && j + 1 === length(days)} noBeb/>
96
122
  {/each}
97
- </div>
123
+ {/each}
98
124
  </div>
99
- {/if}
100
- </header>
101
-
102
- <div class="{theme.body}" role="rowgroup">
103
- <aside class="{theme.sidebar}" aria-hidden="true">
104
- {#each slots as slot, i}
105
- <div
106
- class={[theme.slot, !i && theme.hidden]}
107
- style:--ec-slot-label-periodicity={slot[2]}
108
- >
109
- <time
110
- datetime="{slot[0]}"
111
- {@attach contentFrom(slot[1])}
112
- ></time>
113
- </div>
114
- {/each}
115
- </aside>
116
- <div class="{theme.grid}" role="row">
117
- {#each grid as days, i}
118
- {#each days as day, j}
119
- <Day {day} noIeb={i + 1 === grid.length && j + 1 === days.length} noBeb/>
125
+ <div class="{theme.events}">
126
+ {#each chunks as chunk (chunk.id)}
127
+ <Event {chunk}/>
120
128
  {/each}
121
- {/each}
122
- </div>
123
- <div class="{theme.events}">
124
- {#each chunks as chunk (chunk.id)}
125
- <Event {chunk}/>
126
- {/each}
127
- {#each bgChunks as chunk (chunk.id)}
128
- <Event {chunk}/>
129
- {/each}
130
- {#each iChunks as chunk}
131
- <Event {chunk}/>
132
- {/each}
129
+ {#each bgChunks as chunk (chunk.id)}
130
+ <Event {chunk}/>
131
+ {/each}
132
+ {#each iChunks as chunk}
133
+ <Event {chunk}/>
134
+ {/each}
135
+ </div>
133
136
  </div>
134
- </div>
135
137
 
136
- {#if showNowIndicator}
137
- {#if nowIndicator}
138
- {@render nowIndicator()}
139
- {:else}
140
- <NowIndicator days={grid[0]}/>
138
+ {#if showNowIndicator}
139
+ {#if nowIndicator}
140
+ {@render nowIndicator()}
141
+ {:else}
142
+ <NowIndicator days={grid[0]}/>
143
+ {/if}
141
144
  {/if}
142
- {/if}
143
- </section>
145
+ </section>
146
+ {/if}
@@ -12,6 +12,7 @@ export function createChunks(event, days, withId = true) {
12
12
  assign(chunk, {
13
13
  gridColumn,
14
14
  gridRow,
15
+ resource,
15
16
  top: (chunk.start - start) / 1000,
16
17
  height: (chunk.end - chunk.start) / 1000,
17
18
  maxHeight: (end - chunk.start) / 1000
@@ -1,6 +1,6 @@
1
1
  import {tick, untrack} from 'svelte';
2
2
  import {
3
- addDay, addDuration, cloneDate, createView, isFunction, prevClosestDay, subtractDay,
3
+ addDay, addDuration, cloneDate, createView, isFunction, prevClosestDay, setMidnight, subtractDay,
4
4
  toEventWithLocalDates, toViewWithLocalDates
5
5
  } from '#lib';
6
6
 
@@ -45,7 +45,7 @@ export function activeRange(mainState) {
45
45
  export function filteredEvents(mainState) {
46
46
  return () => {
47
47
  // Dependencies
48
- let {events, options: {eventFilter, eventOrder, filterEventsWithResources, resources}} = mainState;
48
+ let {events, options: {eventFilter, eventOrder, filterEventsWithResources, resources, view}} = mainState;
49
49
 
50
50
  let result = [...events];
51
51
 
@@ -91,8 +91,9 @@ export function viewDates(mainState) {
91
91
  let dates = [];
92
92
 
93
93
  untrack(() => {
94
- let date = cloneDate(activeRange.start);
95
- let end = cloneDate(activeRange.end);
94
+ // activeRange may be offset by hours due to slotMaxTime, so we set it to midnight
95
+ let date = setMidnight(cloneDate(activeRange.start));
96
+ let end = setMidnight(cloneDate(activeRange.end));
96
97
  while (date < end) {
97
98
  if (!hiddenDays.includes(date.getUTCDay())) {
98
99
  dates.push(cloneDate(date));
@@ -1,88 +1,140 @@
1
1
  import {getAbortSignal, tick, untrack} from 'svelte';
2
2
  import {
3
- assign, cloneDate, createDate, createEvents, datesEqual, isFunction, setMidnight, toISOString, toLocalDate,
4
- toViewWithLocalDates
3
+ assign, cloneDate, createDate, createEvents, createResources, datesEqual, empty, isArray, isFunction, setMidnight,
4
+ toISOString, toLocalDate, toViewWithLocalDates
5
5
  } from '#lib';
6
6
 
7
- export function loadEvents(mainState) {
8
- let fetching = 0;
7
+ export function loadEvents(mainState, loadingInvoker) {
9
8
  return () => {
10
9
  // Dependencies
11
- let {activeRange, fetchedRange, options: {events, eventSources, lazyFetching, loading}} = mainState;
10
+ let {activeRange, fetchedRange: {events: fetchedRange}, viewDates,
11
+ options: {events, eventSources, lazyFetching}} = mainState;
12
12
 
13
13
  untrack(() => {
14
- if (!eventSources.length) {
15
- mainState.events = events;
16
- }
17
- // Do not fetch if new range is within the previous one
18
- if (
19
- !fetchedRange.start ||
20
- fetchedRange.start > activeRange.start ||
21
- fetchedRange.end < activeRange.end ||
22
- !lazyFetching
23
- ) {
24
- // Call loading hook
25
- if (isFunction(loading) && !fetching) {
26
- loading(true);
14
+ load(
15
+ eventSources.map(source => isFunction(source.events) ? source.events : source),
16
+ events,
17
+ createEvents,
18
+ result => mainState.events = result,
19
+ activeRange,
20
+ fetchedRange,
21
+ viewDates,
22
+ true,
23
+ lazyFetching,
24
+ loadingInvoker
25
+ );
26
+ });
27
+ };
28
+ }
29
+
30
+ export function loadResources(mainState, loadingInvoker) {
31
+ return () => {
32
+ // Dependencies
33
+ let {activeRange, fetchedRange: {resources: fetchedRange}, viewDates,
34
+ options: {lazyFetching, refetchResourcesOnNavigate, resources}} = mainState;
35
+
36
+ untrack(() => {
37
+ load(
38
+ isArray(resources) ? [] : [resources],
39
+ resources,
40
+ createResources,
41
+ result => mainState.resources = result,
42
+ activeRange,
43
+ fetchedRange,
44
+ viewDates,
45
+ refetchResourcesOnNavigate,
46
+ lazyFetching,
47
+ loadingInvoker
48
+ );
49
+ });
50
+ };
51
+ }
52
+
53
+ function load(sources, defaultResult, parseResult, applyResult, activeRange, fetchedRange, viewDates, refetchOnNavigate, lazyFetching, loading) {
54
+ if (empty(viewDates)) {
55
+ return;
56
+ }
57
+ if (empty(sources)) {
58
+ applyResult(defaultResult);
59
+ return;
60
+ }
61
+ // Do not fetch if new range is within the previous one
62
+ if (
63
+ (refetchOnNavigate || !fetchedRange.start) &&
64
+ (
65
+ !lazyFetching ||
66
+ !fetchedRange.start ||
67
+ fetchedRange.start > activeRange.start ||
68
+ fetchedRange.end < activeRange.end
69
+ )
70
+ ) {
71
+ let result = [];
72
+ // Prepare handlers
73
+ let failure = e => loading.stop();
74
+ let success = data => {
75
+ result = result.concat(parseResult(data));
76
+ applyResult(result);
77
+ loading.stop();
78
+ };
79
+ // Prepare other stuff
80
+ let startStr = toISOString(activeRange.start)
81
+ let endStr = toISOString(activeRange.end);
82
+ // Loop over event sources
83
+ for (let source of sources) {
84
+ loading.start();
85
+ if (isFunction(source)) {
86
+ // Source as a function
87
+ let result = source(refetchOnNavigate ? {
88
+ start: toLocalDate(activeRange.start),
89
+ end: toLocalDate(activeRange.end),
90
+ startStr,
91
+ endStr
92
+ } : {}, success, failure);
93
+ if (result !== undefined) {
94
+ Promise.resolve(result).then(success, failure);
95
+ }
96
+ } else {
97
+ // Source as a JSON feed
98
+ // Prepare params
99
+ let params = isFunction(source.extraParams) ? source.extraParams() : assign({}, source.extraParams);
100
+ if (refetchOnNavigate) {
101
+ params.start = startStr;
102
+ params.end = endStr;
27
103
  }
28
- let stopLoading = () => {
29
- if (--fetching === 0 && isFunction(loading)) {
30
- loading(false);
31
- }
32
- };
33
- let events = [];
34
- // Prepare handlers
35
- let failure = e => stopLoading();
36
- let success = data => {
37
- events = events.concat(createEvents(data));
38
- mainState.events = events;
39
- stopLoading();
40
- };
41
- // Prepare other stuff
42
- let startStr = toISOString(activeRange.start)
43
- let endStr = toISOString(activeRange.end);
44
- // Loop over event sources
45
- for (let source of eventSources) {
46
- if (isFunction(source.events)) {
47
- // Events as a function
48
- let result = source.events({
49
- start: toLocalDate(activeRange.start),
50
- end: toLocalDate(activeRange.end),
51
- startStr,
52
- endStr
53
- }, success, failure);
54
- if (result !== undefined) {
55
- Promise.resolve(result).then(success, failure);
56
- }
57
- } else {
58
- // Events as a JSON feed
59
- // Prepare params
60
- let params = isFunction(source.extraParams) ? source.extraParams() : assign({}, source.extraParams);
61
- params.start = startStr;
62
- params.end = endStr;
63
- params = new URLSearchParams(params);
64
- // Prepare fetch
65
- let url = source.url, headers = {}, body;
66
- if (['GET', 'HEAD'].includes(source.method)) {
67
- url += (url.includes('?') ? '&' : '?') + params;
68
- } else {
69
- headers['content-type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
70
- body = String(params); // Safari 10.1 doesn't convert to string automatically
71
- }
72
- // Do the fetch
73
- fetch(url, {
74
- method: source.method, headers, body, signal: getAbortSignal(), credentials: 'same-origin'
75
- })
76
- .then(response => response.json())
77
- .then(success)
78
- .catch(failure);
79
- }
80
- ++fetching;
104
+ params = new URLSearchParams(params);
105
+ // Prepare fetch
106
+ let url = source.url, headers = {}, body;
107
+ if (['GET', 'HEAD'].includes(source.method)) {
108
+ url += (url.includes('?') ? '&' : '?') + params;
109
+ } else {
110
+ headers['content-type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
111
+ body = String(params); // Safari 10.1 doesn't convert to string automatically
81
112
  }
82
- // Save current range for future requests
83
- assign(fetchedRange, activeRange);
113
+ // Do the fetch
114
+ fetch(url, {
115
+ method: source.method, headers, body, signal: getAbortSignal(), credentials: 'same-origin'
116
+ })
117
+ .then(response => response.json())
118
+ .then(success)
119
+ .catch(failure);
84
120
  }
85
- });
121
+ }
122
+ // Save current range for future requests
123
+ assign(fetchedRange, activeRange);
124
+ }
125
+ }
126
+
127
+ export function createLoadingInvoker(options) {
128
+ let counter = 0;
129
+ function invoke(value) {
130
+ let {loading} = options;
131
+ if (isFunction(loading)) {
132
+ loading(value);
133
+ }
134
+ }
135
+ return {
136
+ start: () => ++counter === 1 && invoke(true),
137
+ stop: () => --counter === 0 && invoke(false)
86
138
  };
87
139
  }
88
140
 
@@ -1,6 +1,6 @@
1
1
  import {untrack} from 'svelte';
2
2
  import {
3
- createDate, createDateRange, createDuration, createEvents, createEventSources, createResources, entries,
3
+ createDate, createDateRange, createDuration, createEvents, createEventSources, createResources, entries, isArray,
4
4
  isFunction, keys, setMidnight
5
5
  } from '#lib';
6
6
  import {SvelteMap} from "svelte/reactivity";
@@ -55,6 +55,7 @@ function createOptions(plugins) {
55
55
  lazyFetching: true,
56
56
  loading: undefined,
57
57
  locale: undefined,
58
+ refetchResourcesOnNavigate: false,
58
59
  resources: [],
59
60
  selectable: false,
60
61
  theme: {
@@ -110,13 +111,13 @@ function createOptions(plugins) {
110
111
 
111
112
  function createParsers(plugins) {
112
113
  let parsers = {
113
- date: date => setMidnight(createDate(date)),
114
+ date: input => setMidnight(createDate(input)),
114
115
  duration: createDuration,
115
116
  events: createEvents,
116
117
  eventSources: createEventSources,
117
- hiddenDays: days => [...new Set(days)],
118
- highlightedDates: dates => dates.map(date => setMidnight(createDate(date))),
119
- resources: createResources,
118
+ hiddenDays: input => [...new Set(input)],
119
+ highlightedDates: input => input.map(item => setMidnight(createDate(item))),
120
+ resources: input => isArray(input) ? createResources(input) : input,
120
121
  validRange: createDateRange
121
122
  };
122
123
 
@@ -1,7 +1,9 @@
1
1
  import {SvelteMap} from 'svelte/reactivity';
2
2
  import {createDate, identity, intl, intlRange, setMidnight} from '#lib';
3
3
  import {optionsState} from './options.svelte.js';
4
- import {loadEvents, runDatesSet, runEventAllUpdated, runViewDidMount, setNowAndToday} from './effects.js';
4
+ import {
5
+ createLoadingInvoker, loadEvents, loadResources, runDatesSet, runEventAllUpdated, runViewDidMount, setNowAndToday
6
+ } from './effects.js';
5
7
  import {activeRange, currentRange, filteredEvents, view, viewDates, viewTitle} from './derived.js';
6
8
 
7
9
  export default class State {
@@ -18,11 +20,12 @@ export default class State {
18
20
  this.auxComponents = $state([]);
19
21
  this.currentRange = $derived.by(currentRange(this));
20
22
  this.activeRange = $derived.by(activeRange(this));
21
- this.fetchedRange = $state.raw({start: undefined, end: undefined});
23
+ this.fetchedRange = $state({events: {}, resources: {}});
22
24
  this.events = $state.raw([]);
23
25
  this.filteredEvents = $derived.by(filteredEvents(this));
24
26
  this.mainEl = $state();
25
27
  this.now = $state(createDate());
28
+ this.resources = $state.raw([]);
26
29
  this.today = $state(setMidnight(createDate()));
27
30
  this.intlEventTime = $derived.by(intlRange(this, 'eventTimeFormat'));
28
31
  this.intlDayHeader = $derived.by(intl(this, 'dayHeaderFormat'));
@@ -50,8 +53,10 @@ export default class State {
50
53
  }
51
54
 
52
55
  #initEffects() {
56
+ let loading = createLoadingInvoker(this.options);
53
57
  $effect.pre(setNowAndToday(this));
54
- $effect(loadEvents(this));
58
+ $effect(loadEvents(this, loading));
59
+ $effect(loadResources(this, loading));
55
60
  $effect(runDatesSet(this));
56
61
  $effect(runEventAllUpdated(this));
57
62
  $effect(runViewDidMount(this));