@event-calendar/core
Version:
Full-sized drag & drop event calendar with resource & timeline views
218 lines (201 loc) • 8.3 kB
JavaScript
import {derived, get, readable, writable} from 'svelte/store';
import {
addDay, addDuration, assign, cloneDate, createDate, createEvents, createView, DAY_IN_SECONDS, debounce,
isFunction, nextClosestDay, prevClosestDay, setMidnight, subtractDay, toISOString, toLocalDate
} from '#lib';
export function dayGrid(state) {
return derived(state.view, $view => $view?.startsWith('dayGrid'));
}
export function activeRange(state) {
return derived(
[state._currentRange, state.firstDay, state.slotMaxTime, state._dayGrid],
([$_currentRange, $firstDay, $slotMaxTime, $_dayGrid]) => {
let start = cloneDate($_currentRange.start);
let end = cloneDate($_currentRange.end);
if ($_dayGrid) {
// First day of week
prevClosestDay(start, $firstDay);
nextClosestDay(end, $firstDay);
} else if ($slotMaxTime.days || $slotMaxTime.seconds > DAY_IN_SECONDS) {
addDuration(subtractDay(end), $slotMaxTime);
let start2 = subtractDay(cloneDate(end));
if (start2 < start) {
start = start2;
}
}
return {start, end};
}
);
}
export function currentRange(state) {
return derived(
[state.date, state.duration, state.firstDay],
([$date, $duration, $firstDay]) => {
let start = cloneDate($date), end;
if ($duration.months) {
start.setUTCDate(1);
} else if ($duration.inWeeks) {
// First day of week
prevClosestDay(start, $firstDay);
}
end = addDuration(cloneDate(start), $duration);
return {start, end};
}
);
}
export function viewDates(state) {
return derived([state._activeRange, state.hiddenDays], ([$_activeRange, $hiddenDays]) => {
let dates = [];
let date = setMidnight(cloneDate($_activeRange.start));
let end = setMidnight(cloneDate($_activeRange.end));
while (date < end) {
if (!$hiddenDays.includes(date.getUTCDay())) {
dates.push(cloneDate(date));
}
addDay(date);
}
if (!dates.length && $hiddenDays.length && $hiddenDays.length < 7) {
// Try to move the date
state.date.update(date => {
while ($hiddenDays.includes(date.getUTCDay())) {
addDay(date);
}
return date;
});
dates = get(state._viewDates);
}
return dates;
});
}
export function viewTitle(state) {
return derived(
[state.date, state._activeRange, state._intlTitle, state._dayGrid],
([$date, $_activeRange, $_intlTitle, $_dayGrid]) => {
return $_dayGrid
? $_intlTitle.formatRange($date, $date)
: $_intlTitle.formatRange($_activeRange.start, subtractDay(cloneDate($_activeRange.end)));
}
);
}
export function view(state) {
return derived([state.view, state._viewTitle, state._currentRange, state._activeRange], args => createView(...args));
}
export function events(state) {
let _events = writable([]);
let abortController;
let fetching = 0;
let debounceHandle = {};
derived(
[state.events, state.eventSources, state._activeRange, state._fetchedRange, state.lazyFetching, state.loading],
(values, set) => debounce(() => {
let [$events, $eventSources, $_activeRange, $_fetchedRange, $lazyFetching, $loading] = values;
if (!$eventSources.length) {
set($events);
return;
}
// Do not fetch if new range is within the previous one
if (!$_fetchedRange.start || $_fetchedRange.start > $_activeRange.start || $_fetchedRange.end < $_activeRange.end || !$lazyFetching) {
if (abortController) {
// Abort previous request
abortController.abort();
}
// Create new abort controller
abortController = new AbortController();
// Call loading hook
if (isFunction($loading) && !fetching) {
$loading(true);
}
let stopLoading = () => {
if (--fetching === 0 && isFunction($loading)) {
$loading(false);
}
};
let events = [];
// Prepare handlers
let failure = e => stopLoading();
let success = data => {
events = events.concat(createEvents(data));
set(events);
stopLoading();
};
// Prepare other stuff
let startStr = toISOString($_activeRange.start)
let endStr = toISOString($_activeRange.end);
// Loop over event sources
for (let source of $eventSources) {
if (isFunction(source.events)) {
// Events as a function
let result = source.events({
start: toLocalDate($_activeRange.start),
end: toLocalDate($_activeRange.end),
startStr,
endStr
}, success, failure);
if (result !== undefined) {
Promise.resolve(result).then(success, failure);
}
} else {
// Events as a JSON feed
// Prepare params
let params = isFunction(source.extraParams) ? source.extraParams() : assign({}, source.extraParams);
params.start = startStr;
params.end = endStr;
params = new URLSearchParams(params);
// Prepare fetch
let url = source.url, headers = {}, body;
if (['GET', 'HEAD'].includes(source.method)) {
url += (url.includes('?') ? '&' : '?') + params;
} else {
headers['content-type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
body = String(params); // Safari 10.1 doesn't convert to string automatically
}
// Do the fetch
fetch(url, {method: source.method, headers, body, signal: abortController.signal, credentials: 'same-origin'})
.then(response => response.json())
.then(success)
.catch(failure);
}
++fetching;
}
// Save current range for future requests
$_fetchedRange.start = $_activeRange.start;
$_fetchedRange.end = $_activeRange.end;
}
}, debounceHandle, state._queue),
[]
).subscribe(_events.set);
return _events;
}
export function filteredEvents(state){
let view;
state._view.subscribe($_view => view = $_view);
let debounceHandle = {};
return derived(
[state._events, state.eventFilter],
(values, set) => debounce(() => {
let [$_events, $eventFilter] = values;
set(
isFunction($eventFilter)
? $_events.filter((event, index, events) => $eventFilter({
event,
index,
events,
view
}))
: $_events
);
}, debounceHandle, state._queue),
[]
);
}
export function now() {
return readable(createDate(), set => {
let interval = setInterval(() => {
set(createDate());
}, 1000);
return () => clearInterval(interval);
});
}
export function today(state) {
return derived(state._now, $_now => setMidnight(cloneDate($_now)));
}