UNPKG

@event-calendar/core

Version:

Full-sized drag & drop event calendar with resource & timeline views

218 lines (201 loc) 8.3 kB
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))); }