react-schedule-view
Version:
A zero-dependency, fully customizable component for displaying schedules in a daily or week format
539 lines (512 loc) • 25.3 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var jsxRuntime = require('react/jsx-runtime');
var react = require('react');
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function __read(o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
}
function __spreadArray(to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
}
var clamp = function (value, min, max) {
return Math.min(Math.max(value, min), max);
};
var numToHHMM = function (num) {
var hours = Math.floor(num) % 12;
if (hours === 0) {
hours = 12;
}
var minutes = Math.round((num % 1) * 60);
return "".concat(hours, ":").concat(minutes < 10 ? "0" : "").concat(minutes, " ").concat(num >= 12 && num < 24 ? "PM" : "AM");
};
var numToHH = function (num) {
var hours = Math.floor(num) % 12;
if (hours === 0) {
hours = 12;
}
return "".concat(hours, " ").concat(num >= 12 && num < 24 ? "PM" : "AM");
};
var timeRangeFormatter = function (startTime, endTime) {
return "".concat(numToHHMM(startTime), " - ").concat(numToHHMM(endTime));
};
/**
* Given a background color and two foreground color options, returns the
* foreground color with the best contrast ratio to the background color.
*/
var testContrast = function (bg, fg1, fg2, skewReturnLighter) {
if (skewReturnLighter === void 0) { skewReturnLighter = 0; }
var bgLuminance = clamp(getLuminance(bg) - skewReturnLighter, 0, 1);
var fg1Luminance = getLuminance(fg1);
var fg2Luminance = getLuminance(fg2);
var fg1Contrast = getContrast(bgLuminance, fg1Luminance);
var fg2Contrast = getContrast(bgLuminance, fg2Luminance);
return fg1Contrast > fg2Contrast ? fg1 : fg2;
};
/**
* Returns the contrast ratio between two luminances
*/
var getContrast = function (bgLuminance, fgLuminance) {
return Math.abs(bgLuminance - fgLuminance);
};
/**
* Returns the luminance value of a CSS color
*/
var getLuminance = function (color) {
var rgb = cssColorToRGB(color);
var r = rgb[0] / 255;
var g = rgb[1] / 255;
var b = rgb[2] / 255;
var luminance = Math.sqrt(0.299 * Math.pow(r, 2) + 0.587 * Math.pow(g, 2) + 0.114 * Math.pow(b, 2));
return luminance;
};
/**
* Converts a CSS color string to RGB values
*/
var cssColorToRGB = function (color) {
var isBrowser = typeof window !== "undefined";
if (!color || !isBrowser)
return [0, 0, 0];
var canvas = document.createElement("canvas");
canvas.width = 1;
canvas.height = 1;
var ctx = canvas.getContext("2d");
if (!ctx)
throw new Error("Could not get canvas context");
ctx.fillStyle = color;
ctx.fillRect(0, 0, 1, 1);
var data = ctx.getImageData(0, 0, 1, 1).data;
return [data[0], data[1], data[2]];
};
// https://github.com/30-seconds/30-seconds-of-code/blob/master/snippets/RGBToHSL.md
var RGBToHSL = function (_a) {
var _b = __read(_a, 3), r = _b[0], g = _b[1], b = _b[2];
r /= 255;
g /= 255;
b /= 255;
var l = Math.max(r, g, b);
var s = l - Math.min(r, g, b);
var h = s
? l === r
? (g - b) / s
: l === g
? 2 + (b - r) / s
: 4 + (r - g) / s
: 0;
return [
60 * h < 0 ? 60 * h + 360 : 60 * h,
100 * (s ? (l <= 0.5 ? s / (2 * l - s) : s / (2 - (2 * l - s))) : 0),
(100 * (2 * l - s)) / 2,
];
};
var googleColors = {
greyGridline: "#DADCE0",
greyTimeLabel: "#70757a",
greyDayLabel: "#3c4043",
blue: "#5186EC",
red: "#C3281C",
yellow: "#EEC04B",
purple: "#8332A4",
green: "#397D49",
indigo: "#4153AF",
pink: "#C63461",
lavender: "#AF9FD7",
orange: "#E35C33",
brown: "#74574A",
};
var calculateTextColor = function (event, theme) {
var _a;
var defaultColor = typeof theme.defaultTileColor === "function"
? theme.defaultTileColor(event)
: theme.defaultTileColor;
return testContrast((_a = event.color) !== null && _a !== void 0 ? _a : defaultColor, "white", "black", 0.3);
};
var googleTheme = {
style: {
root: { fontFamily: "Roboto, Helvetica, Arial, sans-serif" },
dayLabels: {
color: googleColors.greyDayLabel,
textTransform: "uppercase",
},
timeScaleLabels: {
color: googleColors.greyTimeLabel,
fontSize: "0.7rem",
},
majorGridlinesBorder: "1px solid ".concat(googleColors.greyGridline),
minorGridlinesBorder: "1px dotted ".concat(googleColors.greyGridline),
verticalGridlinesBorder: "1px solid ".concat(googleColors.greyGridline),
eventTiles: function (event, theme) { return ({
color: calculateTextColor(event, theme),
}); },
},
hourHeight: "46px",
minorGridlinesPerHour: 0,
timeRangeFormatter: timeRangeFormatter,
defaultTileColor: googleColors.blue,
timeFormatter: numToHH,
};
var DEFAULT_THEME = googleTheme;
var ThemeContext = react.createContext(DEFAULT_THEME);
var DayLabels = function (props) {
var dayNames = props.dayNames;
var theme = react.useContext(ThemeContext);
return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: dayNames.map(function (dayName, i) {
var _a;
return (jsxRuntime.jsx("div", __assign({ style: __assign({ gridColumn: "".concat(i + 2), gridRow: "1", padding: "0.5rem", textAlign: "center" }, (_a = theme.style) === null || _a === void 0 ? void 0 : _a.dayLabels) }, { children: dayName }), i));
}) }));
};
/**
* Determines if the start/end times of two events overlap if their
* total duration is greater than their outer duration.
*
* @returns true / false if the events overlap
*/
var eventsOverlap = function (event1, event2) {
var totalDuration = event1.endTime - event1.startTime + (event2.endTime - event2.startTime);
var outerDuration = Math.max(event1.endTime, event2.endTime) -
Math.min(event1.startTime, event2.startTime);
return outerDuration < totalDuration;
};
/**
* Determines the optimal layout for displaying events on a grid without overlapping
* and while maximizing the size of each event
*
* @returns an array of groups of events annotated with size/position of each event
*/
function positionEventsOnGrid(params) {
var events = params.events, viewStartTime = params.viewStartTime, viewEndTime = params.viewEndTime, subdivisionsPerHour = params.subdivisionsPerHour;
var timeToRowNum = function (time) {
var hourIndex = time - viewStartTime;
var viewDuratinon = viewEndTime - viewStartTime;
return (2 + Math.round(subdivisionsPerHour * clamp(hourIndex, 0, viewDuratinon)));
};
var eventGroups = [];
var tempGroup = [];
var groupEndTime = Number.NEGATIVE_INFINITY;
events
.filter(function (event) { return event.startTime < viewEndTime && event.endTime > viewStartTime; })
.sort(function (a, b) {
if (a.startTime === b.startTime)
return a.endTime - b.endTime;
return a.startTime - b.startTime;
})
.forEach(function (event) {
// If event does not overlap with any events from the current group, start a new group
if (tempGroup.length && event.startTime >= groupEndTime) {
eventGroups.push(__spreadArray([], __read(tempGroup), false));
tempGroup = [];
groupEndTime = Number.NEGATIVE_INFINITY;
}
groupEndTime = Math.max(groupEndTime, event.endTime);
var didPlace = false;
// Place the event in the leftmost track it fits in
tempGroup.forEach(function (track) {
if (!didPlace && event.startTime >= track[track.length - 1].endTime) {
track.push(event);
didPlace = true;
}
});
// If the event doesn't fit in any existing tracks, put it in a new one
if (!didPlace) {
tempGroup.push([event]);
}
});
eventGroups.push(__spreadArray([], __read(tempGroup), false));
if (eventGroups.length === 0 || eventGroups[0].length === 0) {
return [];
}
return eventGroups.map(function (group) {
var groupStartTime = Math.max(viewStartTime, group[0][0].startTime);
var groupEndTime = Math.min(viewEndTime, group.reduce(function (acc, curr) {
return Math.max(acc, curr[curr.length - 1].endTime);
}, Number.NEGATIVE_INFINITY));
var groupEndRow = timeToRowNum(groupEndTime);
var groupStartRow = timeToRowNum(groupStartTime);
var positionedEvents = group.flatMap(function (track, col) {
col += 1;
return track.map(function (event) {
// Expand the event right as long as it doesn't overlap with any other events
var endCol = col + 1;
for (var i = col; i < group.length; i++) {
if (group[i].find(function (e) { return eventsOverlap(e, event); }))
break;
endCol++;
}
var row = 1 + Math.max(0, timeToRowNum(event.startTime) - groupStartRow);
var endRow = 1 +
Math.min(timeToRowNum(event.endTime), groupEndRow) -
groupStartRow;
return {
col: col,
endCol: endCol,
row: row,
endRow: endRow,
event: event,
};
});
});
return {
totalCols: group.length,
groupStartRow: groupStartRow,
groupEndRow: groupEndRow,
positionedEvents: positionedEvents,
};
});
}
var SUBDIVISIONS_PER_HOUR = 12;
var EventRectangles = function (props) {
var daySchedules = props.daySchedules, viewStartTime = props.viewStartTime, viewEndTime = props.viewEndTime, handleEventClick = props.handleEventClick;
var theme = react.useContext(ThemeContext);
var defaultTileColor = theme.defaultTileColor, timeRangeFormatter = theme.timeRangeFormatter, style = theme.style, CustomTileComponent = theme.customTileComponent, ThemeTileContent = theme.themeTileContent;
return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: daySchedules.map(function (day, dayIndex) {
return positionEventsOnGrid({
subdivisionsPerHour: SUBDIVISIONS_PER_HOUR,
events: day.events,
viewStartTime: viewStartTime,
viewEndTime: viewEndTime,
}).map(function (_a, groupIndex) {
var totalCols = _a.totalCols, groupStartRow = _a.groupStartRow, groupEndRow = _a.groupEndRow, positionedEvents = _a.positionedEvents;
return (jsxRuntime.jsx("div", __assign({ style: {
display: "grid",
gridTemplateColumns: "repeat(".concat(totalCols, ", 1fr)"),
gridTemplateRows: "repeat(".concat(groupEndRow - groupStartRow, ", 1fr)"),
gridColumn: "".concat(dayIndex + 2),
gridRow: "".concat(groupStartRow, " / ").concat(groupEndRow),
marginRight: "5%",
} }, { children: positionedEvents.map(function (_a, eventIndex) {
var _b;
var col = _a.col, endCol = _a.endCol, row = _a.row, endRow = _a.endRow, event = _a.event;
var defaultColor = typeof defaultTileColor === "function"
? defaultTileColor(event)
: defaultTileColor;
var color = (_b = event.color) !== null && _b !== void 0 ? _b : defaultColor;
return (jsxRuntime.jsx("div", __assign({ style: __assign({ gridColumn: "".concat(col, " / ").concat(endCol), gridRow: "".concat(row, " / ").concat(endRow), overflow: "hidden", cursor: handleEventClick ? "pointer" : "default", marginInline: "0.5px", marginBottom: "1px", borderRadius: "5px", padding: "5px", backgroundColor: color, color: testContrast(color, "white", "black") }, (typeof (style === null || style === void 0 ? void 0 : style.eventTiles) === "function"
? style.eventTiles(event, theme)
: style === null || style === void 0 ? void 0 : style.eventTiles)), onClick: function () { return (handleEventClick !== null && handleEventClick !== void 0 ? handleEventClick : (function () { }))(event); } }, { children: CustomTileComponent ? (jsxRuntime.jsx(CustomTileComponent, { event: event })) : ThemeTileContent ? (jsxRuntime.jsx(ThemeTileContent, { event: event })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", __assign({ style: { fontWeight: "bold" } }, { children: event.title })), jsxRuntime.jsx("div", __assign({ style: {
fontSize: "0.8rem",
marginTop: "0.25rem",
} }, { children: timeRangeFormatter(event.startTime, event.endTime) })), jsxRuntime.jsx("div", __assign({ style: {
fontSize: "0.8rem",
marginTop: "0.25rem",
} }, { children: event.description }))] })) }), "".concat(dayIndex, "-").concat(groupIndex, "-").concat(eventIndex)));
}) }), groupIndex));
});
}) }));
};
var HorizontalLines = function (props) {
var borderStyle = props.borderStyle, spacing = props.spacing, numLines = props.numLines, _a = props.offset, offset = _a === void 0 ? 0 : _a;
return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: new Array(numLines).fill(0).map(function (_, i) {
return (jsxRuntime.jsx("div", { style: {
gridColumn: "2 / -1",
gridRow: "".concat(i * (Math.round(spacing) + 1) + offset + 1),
borderTop: borderStyle,
} }, i));
}) }));
};
var Gridlines = function (props) {
var numHours = props.numHours, numDays = props.numDays;
var theme = react.useContext(ThemeContext);
var minorGridlinesPerHour = theme.minorGridlinesPerHour, style = theme.style;
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [minorGridlinesPerHour > 0 && (jsxRuntime.jsx(HorizontalLines, { numLines: numHours * minorGridlinesPerHour + 1, offset: 1, spacing: SUBDIVISIONS_PER_HOUR / minorGridlinesPerHour - 1, borderStyle: style === null || style === void 0 ? void 0 : style.minorGridlinesBorder })), jsxRuntime.jsx(HorizontalLines, { numLines: numHours + 1, offset: 1, spacing: SUBDIVISIONS_PER_HOUR - 1, borderStyle: style === null || style === void 0 ? void 0 : style.majorGridlinesBorder }), new Array(numDays - 1).fill(0).map(function (_, i) {
return (jsxRuntime.jsx("div", { style: {
gridColumn: "".concat(i + 2),
gridRow: "2 / span ".concat(numHours * SUBDIVISIONS_PER_HOUR),
borderRight: style === null || style === void 0 ? void 0 : style.verticalGridlinesBorder,
} }, i));
})] }));
};
var TimeLabels = function (props) {
var numHours = props.numHours, viewStartTime = props.viewStartTime;
var theme = react.useContext(ThemeContext);
return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: new Array(numHours + 1).fill(0).map(function (_, i) {
var _a;
return (jsxRuntime.jsx("div", __assign({ style: __assign({ gridColumn: "1", gridRow: "".concat(i * SUBDIVISIONS_PER_HOUR + 2), textAlign: "right", paddingRight: "1rem" }, (_a = theme.style) === null || _a === void 0 ? void 0 : _a.timeScaleLabels) }, { children: jsxRuntime.jsx("div", __assign({ style: { position: "relative", top: "-0.6em" } }, { children: theme.timeFormatter(viewStartTime + i) })) }), i));
}) }));
};
// Credit github.com/voodoocreation
var isObject = function (obj) {
if (typeof obj === "object" && obj !== null) {
if (typeof Object.getPrototypeOf === "function") {
var prototype = Object.getPrototypeOf(obj);
return prototype === Object.prototype || prototype === null;
}
return Object.prototype.toString.call(obj) === "[object Object]";
}
return false;
};
var deepMerge = function () {
var objects = [];
for (var _i = 0; _i < arguments.length; _i++) {
objects[_i] = arguments[_i];
}
return objects.reduce(function (result, current) {
Object.keys(current).forEach(function (key) {
if (Array.isArray(result[key]) && Array.isArray(current[key])) {
result[key] = deepMerge.options.mergeArrays
? Array.from(new Set(result[key].concat(current[key])))
: current[key];
}
else if (isObject(result[key]) && isObject(current[key])) {
result[key] = deepMerge(result[key], current[key]);
}
else {
result[key] = current[key];
}
});
return result;
}, {});
};
var defaultOptions = {
mergeArrays: true,
};
deepMerge.options = defaultOptions;
deepMerge.withOptions = function (options) {
var objects = [];
for (var _i = 1; _i < arguments.length; _i++) {
objects[_i - 1] = arguments[_i];
}
deepMerge.options = __assign({ mergeArrays: true }, options);
var result = deepMerge.apply(void 0, __spreadArray([], __read(objects), false));
deepMerge.options = defaultOptions;
return result;
};
var AppleEventTile = function (props) {
var event = props.event;
var theme = react.useContext(ThemeContext);
// Using useEffect here to ensure color is converted on client, not SSR, since `document` must be available
var _a = __read(react.useState(), 2), colorRGB = _a[0], setColorRGB = _a[1];
react.useEffect(function () {
var _a;
var defaultColor = typeof theme.defaultTileColor === "function"
? theme.defaultTileColor(event)
: theme.defaultTileColor;
var color = (_a = event.color) !== null && _a !== void 0 ? _a : defaultColor;
setColorRGB(cssColorToRGB(color));
}, []);
if (!colorRGB)
return jsxRuntime.jsx(jsxRuntime.Fragment, {});
var colorHSL = RGBToHSL(colorRGB);
var mediumLightnessColorString = "hsl(".concat(colorHSL[0], ", ").concat(colorHSL[1], "%, 50%)");
var darkLightnessColorString = "hsl(".concat(colorHSL[0], ", ").concat(colorHSL[1], "%, 30%)");
return (jsxRuntime.jsx("div", __assign({ style: {
padding: "5px",
backgroundColor: "rgba(".concat(colorRGB[0], ", ").concat(colorRGB[1], ", ").concat(colorRGB[2], ", 0.3)"),
height: "100%",
borderLeft: "4px solid ".concat(mediumLightnessColorString),
color: darkLightnessColorString,
} }, { children: theme.themeTileContent ? (theme.themeTileContent({ event: event })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", __assign({ style: {
fontSize: "0.8rem",
fontWeight: "lighter",
} }, { children: theme.timeRangeFormatter(event.startTime, event.endTime) })), jsxRuntime.jsx("div", __assign({ style: { fontWeight: "bold", fontSize: "0.8rem", lineHeight: 1.2 } }, { children: event.title })), jsxRuntime.jsx("div", __assign({ style: {
fontSize: "0.8rem",
marginTop: "0.25rem",
} }, { children: event.description }))] })) })));
};
var appleColors = {
greyBlackLabel: "#272727",
greyLabel: "#C0C0C0",
greyGridline: "#E5E5E5",
blue: "#4FACF2",
red: "#EB446A",
orange: "#F19937",
yellow: "#F9D64A",
green: "#6BD35F",
purple: "#C07ADB",
brown: "#A78E6D",
};
var appleTheme = {
style: {
root: { fontFamily: "Roboto, Helvetica, Arial, sans-serif" },
dayLabels: {
color: appleColors.greyBlackLabel,
},
timeScaleLabels: {
color: appleColors.greyLabel,
fontSize: "0.7rem",
},
majorGridlinesBorder: "1px solid ".concat(appleColors.greyGridline),
minorGridlinesBorder: "1px dotted ".concat(appleColors.greyGridline),
verticalGridlinesBorder: "1px solid ".concat(appleColors.greyGridline),
eventTiles: {
padding: 0,
marginRight: "2px",
marginBlock: "1px",
backgroundColor: "unset",
},
},
hourHeight: "46px",
minorGridlinesPerHour: 0,
timeRangeFormatter: function (startTime, _endTime) {
return startTime % 1 === 0 ? numToHH(startTime) : numToHHMM(startTime);
},
defaultTileColor: appleColors.blue,
timeFormatter: function (time) { return (time === 12 ? "Noon" : numToHH(time)); },
customTileComponent: AppleEventTile,
};
var themes = {
google: googleTheme,
apple: appleTheme,
};
var colors = {
google: googleColors,
apple: appleColors,
};
var createTheme = function (baseTheme, partialTheme) {
var selectedBaseTheme = typeof baseTheme === "string" ? themes[baseTheme] : baseTheme;
return deepMerge.withOptions({ mergeArrays: false }, selectedBaseTheme, partialTheme);
};
var ScheduleView = function (props) {
var daySchedules = props.daySchedules, _a = props.viewStartTime, viewStartTime = _a === void 0 ? 0 : _a, _b = props.viewEndTime, viewEndTime = _b === void 0 ? 24 : _b, handleEventClick = props.handleEventClick, _c = props.theme, themeOrName = _c === void 0 ? DEFAULT_THEME : _c;
var theme = typeof themeOrName === "string" ? themes[themeOrName] : themeOrName;
var hourHeight = theme.hourHeight, style = theme.style;
var numHours = viewEndTime - viewStartTime;
var numDays = daySchedules.length;
return (jsxRuntime.jsx(ThemeContext.Provider, __assign({ value: theme }, { children: jsxRuntime.jsxs("div", __assign({ style: __assign({ display: "grid", gridTemplateColumns: "auto repeat(".concat(numDays, ", 1fr)"), gridTemplateRows: "auto repeat(".concat(numHours * SUBDIVISIONS_PER_HOUR, ", calc(").concat(hourHeight, " / ").concat(SUBDIVISIONS_PER_HOUR, "))") }, style === null || style === void 0 ? void 0 : style.root) }, { children: [jsxRuntime.jsx(Gridlines, { numHours: numHours, numDays: numDays }), jsxRuntime.jsx(DayLabels, { dayNames: daySchedules.map(function (day) { return day.name; }) }), jsxRuntime.jsx(TimeLabels, { viewStartTime: viewStartTime, numHours: numHours }), jsxRuntime.jsx(EventRectangles, { daySchedules: daySchedules, viewStartTime: viewStartTime, viewEndTime: viewEndTime, handleEventClick: handleEventClick })] })) })));
};
exports.SUBDIVISIONS_PER_HOUR = SUBDIVISIONS_PER_HOUR;
exports.ScheduleView = ScheduleView;
exports.ThemeContext = ThemeContext;
exports.colors = colors;
exports.createTheme = createTheme;
exports.themes = themes;
//# sourceMappingURL=index.js.map