interq.ens.fr/www/js/calendar.js

1274 lines
33 KiB
JavaScript
Raw Normal View History

2024-01-05 17:05:23 +01:00
// Interval graph coloring algorithm, by Twal
class IntervalColoration {
2024-02-19 20:18:10 +01:00
constructor(intervals) {
this.intervals = intervals;
this.n = this.intervals.length;
this.computeInterferenceGraph();
this.computePEO();
this.computeColoration();
2024-01-05 17:05:23 +01:00
}
computeInterferenceGraph() {
2024-02-19 20:18:10 +01:00
this.adj = new Array(this.n);
for (let i = 0; i < this.n; ++i) {
this.adj[i] = [];
}
for (let i = 0; i < this.n; ++i) {
for (let j = 0; j < i; ++j) {
let inti = this.intervals[i];
let intj = this.intervals[j];
if (inti[0] < intj[1] && intj[0] < inti[1]) {
this.adj[i].push(j);
this.adj[j].push(i);
}
2024-01-05 17:05:23 +01:00
}
2024-02-19 20:18:10 +01:00
}
2024-01-05 17:05:23 +01:00
}
//Perfect elimination order using Maximum Cardinality Search
//Runs in O(n^2), could be optimized in O(n log n)
computePEO() {
2024-02-19 20:18:10 +01:00
let marked = new Array(this.n);
let nbMarkedNeighbor = new Array(this.n);
this.perm = new Array(this.n);
for (let i = 0; i < this.n; ++i) {
marked[i] = false;
nbMarkedNeighbor[i] = 0;
}
for (let k = this.n - 1; k >= 0; --k) {
let maxi = -1;
2024-01-05 17:05:23 +01:00
for (let i = 0; i < this.n; ++i) {
2024-02-19 20:18:10 +01:00
if (
!marked[i] &&
(maxi == -1 || nbMarkedNeighbor[i] >= nbMarkedNeighbor[maxi])
) {
maxi = i;
}
2024-01-05 17:05:23 +01:00
}
2024-02-19 20:18:10 +01:00
for (let i = 0; i < this.adj[maxi].length; ++i) {
nbMarkedNeighbor[this.adj[maxi][i]] += 1;
2024-01-05 17:05:23 +01:00
}
2024-02-19 20:18:10 +01:00
this.perm[maxi] = k;
marked[maxi] = true;
}
// console.log(this.perm);
2024-01-05 17:05:23 +01:00
}
computeColoration() {
2024-02-19 20:18:10 +01:00
this.colors = new Array(this.n);
let isColorUsed = new Array(this.n);
for (let i = 0; i < this.n; ++i) {
this.colors[i] = -1;
isColorUsed[i] = false;
}
for (let i = 0; i < this.n; ++i) {
let ind = this.perm[i];
for (let j = 0; j < this.adj[ind].length; ++j) {
let neigh = this.adj[ind][j];
if (this.colors[neigh] >= 0) {
isColorUsed[this.colors[neigh]] = true;
}
2024-01-05 17:05:23 +01:00
}
2024-02-19 20:18:10 +01:00
for (let j = 0; j < this.n; ++j) {
if (!isColorUsed[j]) {
this.colors[ind] = j;
break;
}
}
for (let j = 0; j < this.adj[ind].length; ++j) {
let neigh = this.adj[ind][j];
if (this.colors[neigh] >= 0) {
isColorUsed[this.colors[neigh]] = false;
}
2024-01-05 17:05:23 +01:00
}
2024-02-19 20:18:10 +01:00
}
2024-01-05 17:05:23 +01:00
}
}
// Based on https://stackoverflow.com/a/15289883
2024-02-19 20:18:10 +01:00
function computeDateDifferenceInHours(date1, date2) {
d1 = new Date(
date1.getYear(),
date1.getMonth(),
date1.getDate(),
date1.getHours(),
);
d2 = new Date(
date2.getYear(),
date2.getMonth(),
date2.getDate(),
date2.getHours(),
);
2024-01-05 17:05:23 +01:00
const msPerHour = 60 * 60 * 1000;
return Math.abs(d2.getTime() - d1.getTime()) / msPerHour;
}
class Calendar {
2024-02-19 20:18:10 +01:00
constructor(calendarParameters = {}) {
this.containerNode =
calendarParameters.containerNode !== undefined
? calendarParameters.containerNode
: $("#cal-container");
2024-01-05 17:05:23 +01:00
this.eventContainerNode = null;
this.timeSlotsContainerNode = null;
this.eventDetailsContainerNode = null;
2024-02-19 20:18:10 +01:00
this.startDate =
calendarParameters.startDate !== undefined
? calendarParameters.startDate
: new Date();
this.endDate =
calendarParameters.endDate !== undefined
? calendarParameters.endDate
: new Date(Date.now() + 24 * 60 * 60 * 1000);
2024-01-05 17:05:23 +01:00
this.nbHoursToDisplay = 0;
this.firstHourToDisplay = 0;
this.endHourToDisplay = 0;
this.events = [];
2024-02-19 20:18:10 +01:00
this.onlyDisplaySubscribedEvents =
calendarParameters.onlyDisplaySubscribedEvents !== undefined
? calendarParameters.onlyDisplaySubscribedEvents
: false;
this.groupEventsByLocation =
calendarParameters.groupEventsByLocation !== undefined
? calendarParameters.groupEventsByLocation
: true;
this.eventDetailURLFormat =
calendarParameters.eventDetailURLFormat !== undefined
? calendarParameters.eventDetailURLFormat
: "";
this.subscriptionURLFormat =
calendarParameters.subscriptionURLFormat !== undefined
? calendarParameters.subscriptionURLFormat
: "";
this.csrfToken =
calendarParameters.csrfToken !== undefined
? calendarParameters.csrfToken
: "";
2024-01-05 17:05:23 +01:00
// Map from locations to their CSS styles
this.locationStyles = new Map();
this.init();
}
2024-02-19 20:18:10 +01:00
init() {
2024-01-05 17:05:23 +01:00
this.updateHoursToDisplay();
this.createTimeSlotContainer();
this.createEventContainer();
this.createEventDetailsContainer();
this.updateTimeSlotContainerGridStyle();
this.updateEventContainerGridStyle();
this.createTimeSlots();
this.createEvents();
this.createLocationStyles();
this.applyLocationStylesAsCSS();
this.updateEventLocationStyleID();
//this.sortEventNodesByEndTimeAndLocation();
this.sortEventNodesByIntervalGraphColoring();
this.updateCalendarNodeHeight();
this.updateEventVisibilities();
this.initEventOverflowTooltips();
}
// Date change
2024-02-19 20:18:10 +01:00
setStartDate(newStartDate) {
2024-01-05 17:05:23 +01:00
this.startDate = newStartDate;
this.updateHoursToDisplay();
this.updateEventContainerGridStyle();
this.updateTimeSlots();
this.updateEventVisibilities();
this.updateCalendarNodeHeight();
this.sortEventNodesByIntervalGraphColoring();
this.startShowingEventOverflowTooltips();
}
2024-02-19 20:18:10 +01:00
setEndDate(newEndDate) {
2024-01-05 17:05:23 +01:00
this.endDate = newEndDate;
this.updateHoursToDisplay();
this.updateEventContainerGridStyle();
this.updateTimeSlots();
this.updateEventVisibilities();
this.updateCalendarNodeHeight();
this.sortEventNodesByIntervalGraphColoring();
this.startShowingEventOverflowTooltips();
}
2024-02-19 20:18:10 +01:00
updateHoursToDisplay() {
2024-01-05 17:05:23 +01:00
this.startHourToDisplay = this.startDate.getHours();
this.endHourToDisplay = this.endDate.getHours();
2024-02-19 20:18:10 +01:00
this.nbHoursToDisplay = Math.floor(
computeDateDifferenceInHours(this.startDate, this.endDate),
);
2024-01-05 17:05:23 +01:00
}
// Calendar container
2024-02-19 20:18:10 +01:00
updateCalendarNodeHeight() {
2024-01-05 17:05:23 +01:00
// Time slot hour row
let timeSlotHourRowHeight = $(".cal-time-slot-hour").outerHeight();
// Event grid
this.containerNode.css("height", "calc(100% )");
let eventContainerHeight = this.eventContainerNode
.css("grid-template-rows")
.split("px ")
.reduce((heightAccumulator, currentRowHeight) => {
return heightAccumulator + parseInt(currentRowHeight);
}, 0);
2024-02-19 20:18:10 +01:00
this.containerNode.css(
"height",
timeSlotHourRowHeight + eventContainerHeight,
);
2024-01-05 17:05:23 +01:00
}
// Time slots
2024-02-19 20:18:10 +01:00
createTimeSlotContainer() {
2024-01-05 17:05:23 +01:00
this.timeSlotsContainerNode = $("<div>")
.addClass("cal-time-slot-container")
.appendTo(this.containerNode);
}
2024-02-19 20:18:10 +01:00
createTimeSlots() {
2024-01-05 17:05:23 +01:00
// Populate the container hour by hour
let self = this;
2024-02-19 20:18:10 +01:00
function getHourStringToDisplay(index, hour) {
if (index === self.nbHoursToDisplay - 1 || hour === 23) {
2024-01-05 17:05:23 +01:00
return "";
}
if (hour >= 10) {
return hour + 1;
2024-02-19 20:18:10 +01:00
} else {
2024-01-05 17:05:23 +01:00
return "&nbsp;" + (hour + 1);
}
}
for (let i = 0; i < this.nbHoursToDisplay; i++) {
let hour = (this.startHourToDisplay + i) % 24;
// Time slot hour
let timeSlotHourNode = $("<div>")
.addClass("cal-time-slot-hour")
.css({
"grid-column-start": `${i + 1}`,
2024-02-19 20:18:10 +01:00
"grid-column-end": "span 1",
"grid-row-start": "1",
"grid-row-end": "1",
2024-01-05 17:05:23 +01:00
})
.html(getHourStringToDisplay(i, hour))
.prependTo(this.timeSlotsContainerNode);
// Time slot block
let timeSlotBlockNode = $("<div>")
.addClass("cal-time-slot")
.css({
"grid-column-start": `${i + 1}`,
2024-02-19 20:18:10 +01:00
"grid-column-end": "span 1",
"grid-row-start": "2",
"grid-row-end": "2",
2024-01-05 17:05:23 +01:00
})
.appendTo(this.timeSlotsContainerNode);
2024-02-19 20:18:10 +01:00
if (hour === 23) {
timeSlotHourNode.addClass("cal-last-hour");
timeSlotBlockNode.addClass("cal-last-hour");
}
2024-01-05 17:05:23 +01:00
2024-02-19 20:18:10 +01:00
if (hour === 0) {
timeSlotHourNode.addClass("cal-first-hour");
timeSlotBlockNode.addClass("cal-first-hour");
}
2024-01-05 17:05:23 +01:00
}
}
2024-02-19 20:18:10 +01:00
updateTimeSlotContainerGridStyle() {
this.timeSlotsContainerNode.css(
"grid-template-columns",
`repeat(${this.nbHoursToDisplay}, ${100 / this.nbHoursToDisplay}%)`,
);
2024-01-05 17:05:23 +01:00
}
2024-02-19 20:18:10 +01:00
updateTimeSlots() {
2024-01-05 17:05:23 +01:00
this.timeSlotsContainerNode.empty();
this.createTimeSlots();
this.updateTimeSlotContainerGridStyle();
}
2024-02-19 20:18:10 +01:00
getHourSlotWidth() {
2024-01-05 17:05:23 +01:00
return this.timeSlotsContainerNode.width() / this.nbHoursToDisplay;
}
// Events
2024-02-19 20:18:10 +01:00
createEventContainer() {
2024-01-05 17:05:23 +01:00
this.eventContainerNode = $("<div>")
.addClass("cal-event-container")
.appendTo(this.containerNode);
}
2024-02-19 20:18:10 +01:00
createEvents() {
2024-01-05 17:05:23 +01:00
// Move all event nodes into the event container
let eventElements = this.containerNode.find(".cal-event");
eventElements.appendTo(this.eventContainerNode);
// Create event objects from them all
for (let element of eventElements) {
let newEvent = new Event($(element), this);
this.events.push(newEvent);
}
}
2024-02-19 20:18:10 +01:00
updateEventContainerGridStyle() {
this.eventContainerNode.css(
"grid-template-columns",
`repeat(${this.nbHoursToDisplay}, ${100 / this.nbHoursToDisplay}%)`,
);
2024-01-05 17:05:23 +01:00
}
2024-02-19 20:18:10 +01:00
updateEventVisibilities() {
2024-01-05 17:05:23 +01:00
for (let event of this.events) {
event.updateVisibility();
}
}
// Event details
2024-02-19 20:18:10 +01:00
createEventDetailsContainer() {
2024-01-05 17:05:23 +01:00
this.eventDetailsContainerNode = $("<div>")
.addClass("cal-details-container")
.appendTo(this.containerNode);
}
// Location styles
2024-02-19 20:18:10 +01:00
createLocationStyles() {
2024-01-05 17:05:23 +01:00
let locationIndices = new Map();
for (let event of this.events) {
2024-02-19 20:18:10 +01:00
if (!locationIndices.has(event.location)) {
2024-01-05 17:05:23 +01:00
locationIndices.set(event.location, [...locationIndices.keys()].length);
}
}
let nbUniqueLocations = [...locationIndices.keys()].length;
let styleID = 0;
for (let [location, index] of locationIndices.entries()) {
let hue = (index / (nbUniqueLocations + 1)) * 255;
styleID += 1;
this.locationStyles.set(location, {
id: styleID,
normal: [
`background-color: hsl(${hue}, 40%, 80%);`,
`border-color: hsl(${hue}, 40%, 50%);`,
2024-02-19 20:18:10 +01:00
`color: #000;`,
2024-01-05 17:05:23 +01:00
],
hover: [
`background-color: hsl(${hue}, 55%, 85%);`,
`border-color: hsl(${hue}, 45%, 55%);`,
2024-02-19 20:18:10 +01:00
`color: #000;`,
2024-01-05 17:05:23 +01:00
],
subscribed: [
`background-color: hsl(${hue}, 75%, 75%);`,
`border-color: hsl(${hue}, 60%, 50%);`,
2024-02-19 20:18:10 +01:00
`color: #000;`,
2024-01-05 17:05:23 +01:00
],
selected: [
`background-color: hsl(${hue}, 45%, 50%);`,
`border-color: hsl(${hue}, 40%, 35%);`,
2024-02-19 20:18:10 +01:00
`color: #FFF;`,
],
2024-01-05 17:05:23 +01:00
});
}
}
2024-02-19 20:18:10 +01:00
applyLocationStylesAsCSS() {
2024-01-05 17:05:23 +01:00
let styleNode = $("<style>");
let styleNodeContent = "";
for (let styles of this.locationStyles.values()) {
let normalRules = styles.normal.join("\n");
styleNodeContent += `.cal-location-${styles.id} {${normalRules}}\n`;
let hoverRules = styles.hover.join("\n");
styleNodeContent += `.cal-location-${styles.id}:hover {${hoverRules}}\n`;
let subscribedRules = styles.subscribed.join("\n");
styleNodeContent += `.cal-location-${styles.id}.cal-event-subscribed {${subscribedRules}}\n`;
let selectedRules = styles.selected.join("\n");
styleNodeContent += `.cal-location-${styles.id}.cal-selected {${selectedRules}}\n`;
}
2024-02-19 20:18:10 +01:00
styleNode.html(styleNodeContent).appendTo($("head"));
2024-01-05 17:05:23 +01:00
}
2024-02-19 20:18:10 +01:00
updateEventLocationStyleID() {
2024-01-05 17:05:23 +01:00
for (let event of this.events) {
let style = this.locationStyles.get(event.location);
event.setLocationStyleID(style.id);
}
}
// Event node sorting
// The following method requires the IntervalColoration class,
// which provides the algorithm (see interval_coloring.js)
2024-02-19 20:18:10 +01:00
sortEventNodesByIntervalGraphColoring() {
2024-01-05 17:05:23 +01:00
let eventGroupIterator = [this.events];
// If required, group events by location
if (this.groupEventsByLocation) {
let locationsToEvents = new Map();
for (let event of this.events) {
let location = event.location;
2024-02-19 20:18:10 +01:00
if (!locationsToEvents.has(location)) {
2024-01-05 17:05:23 +01:00
locationsToEvents.set(location, [event]);
2024-02-19 20:18:10 +01:00
} else {
locationsToEvents.get(location).push(event);
2024-01-05 17:05:23 +01:00
}
}
eventGroupIterator = locationsToEvents.values();
}
// Assign a color to all events and a grid row to each event node,
// by applying the interval graph coloration algorithm
// to each subset of events with the same location
let currentLocationFirstGridRow = 1;
let eventsToColors = new Map();
for (let eventsAtSameLocation of eventGroupIterator) {
// Build intervals for each event
let intervals = [];
for (let event of eventsAtSameLocation) {
2024-02-19 20:18:10 +01:00
intervals.push([event.startDate.getTime(), event.endDate.getTime()]);
2024-01-05 17:05:23 +01:00
}
// Get the graph coloring
let intervalGraphColors = new IntervalColoration(intervals).colors;
// Assign a color to each event, and a grid row to each event node
let maximumColor = 0;
for (let i = 0; i < eventsAtSameLocation.length; i++) {
let event = eventsAtSameLocation[i];
let color = intervalGraphColors[i];
eventsToColors.set(event, color);
if (color > maximumColor) {
maximumColor = color;
}
event.node.css({
"grid-row-start": `${currentLocationFirstGridRow + color}`,
2024-02-19 20:18:10 +01:00
"grid-row-end": `${currentLocationFirstGridRow + color}`,
2024-01-05 17:05:23 +01:00
});
}
// Update the start row of the next location
currentLocationFirstGridRow += 1 + maximumColor;
// Sort the events by color
eventsAtSameLocation.sort((event1, event2) => {
return eventsToColors.get(event1) - eventsToColors.get(event2);
});
}
// Finally sort all event nodes (by (1) location and (2) color)
// Note: the container is detached from the DOM for better performances
this.eventContainerNode.detach();
for (let eventsAtSameLocation of eventGroupIterator) {
for (let event of eventsAtSameLocation) {
this.eventContainerNode.prepend(event.node);
}
}
this.eventContainerNode.appendTo(this.containerNode);
}
/*
sortEventNodesByEndTimeAndLocation () {
this.events.sort((event1, event2) => {
return event2.endDate.getTime() - event1.endDate.getTime();
});
this.events.sort((event1, event2) => {
return event2.location.localeCompare(event1.location);
});
this.eventContainerNode.detach();
for (let event of this.events) {
event.node.prependTo(this.eventContainerNode);
}
this.eventContainerNode.appendTo(this.containerNode);
}
*/
// Event overflow tooltip (using tipso library)
2024-02-19 20:18:10 +01:00
initEventOverflowTooltips() {
2024-01-05 17:05:23 +01:00
$(".cal-has-tooltip").tipso({
speed: 0,
delay: 20,
size: "cal_small",
2024-02-19 20:18:10 +01:00
background: "#000",
2024-01-05 17:05:23 +01:00
});
}
// Event filtering
2024-02-19 20:18:10 +01:00
showEventsNotSubscribedByUser() {
2024-01-05 17:05:23 +01:00
this.onlyDisplaySubscribedEvents = false;
this.updateEventVisibilities();
this.updateCalendarNodeHeight();
}
2024-02-19 20:18:10 +01:00
hideEventsNotSubscribedByUser() {
2024-01-05 17:05:23 +01:00
this.onlyDisplaySubscribedEvents = true;
this.updateEventVisibilities();
this.updateCalendarNodeHeight();
}
2024-02-19 20:18:10 +01:00
toggleEventsNotSubscribedByUser() {
2024-01-05 17:05:23 +01:00
if (this.onlyDisplaySubscribedEvents) {
this.showEventsNotSubscribedByUser();
2024-02-19 20:18:10 +01:00
} else {
2024-01-05 17:05:23 +01:00
this.hideEventsNotSubscribedByUser();
}
}
}
class Event {
2024-02-19 20:18:10 +01:00
constructor(eventNode, calendar) {
2024-01-05 17:05:23 +01:00
this.node = eventNode;
this.calendar = calendar;
this.details = null;
this.detailsNodeRemovalTimer = null;
this.id = null;
this.name = null;
this.startDate = null;
this.endDate = null;
this.location = null;
this.description = null;
this.hasPerms = false;
this.nbPerms = null;
this.minNbPerms = null;
this.maxNbPerms = null;
this.subscribedByUser = false;
this.tags = [];
this.displayed = false;
this.selected = false;
this.locationStyleID = 0;
// TODO: move this elsewhere
// Callback to display the details popup on click on the event node
this.showDetailPopupOnClickCallback = (event) => {
if ($(event.target).closest(".cal-event-details").length === 0) {
this.details.showPopup(event);
}
};
this.init();
}
2024-02-19 20:18:10 +01:00
init() {
2024-01-05 17:05:23 +01:00
this.parseAndSetID();
this.parseAndSetName();
this.parseAndSetDates();
this.parseAndSetLocation();
this.parseAndSetDescription();
this.parseAndSetPermRelatedFields();
this.parseAndSetTags();
this.addPermCounter();
this.updateOverflowTooltipContent();
this.createDetails();
this.updateVisibility();
}
// Event data parsers + setters
2024-02-19 20:18:10 +01:00
parseAndSetID() {
2024-01-05 17:05:23 +01:00
this.id = this.node.find(".cal-event-id").text();
}
2024-02-19 20:18:10 +01:00
parseAndSetName() {
2024-01-05 17:05:23 +01:00
this.name = this.node.find(".cal-event-name").text();
}
2024-02-19 20:18:10 +01:00
parseDate(dateString) {
2024-01-05 17:05:23 +01:00
let regex = /(\d+)\/(\d+)\/(\d+) (\d+)\:(\d+)/g;
let [day, month, year, hours, minutes] = regex
.exec(dateString)
.slice(1)
.map((intString) => {
return parseInt(intString);
});
2024-02-19 20:18:10 +01:00
return new Date(year, month - 1, day, hours, minutes);
2024-01-05 17:05:23 +01:00
}
2024-02-19 20:18:10 +01:00
parseAndSetStartDate() {
2024-01-05 17:05:23 +01:00
let startDateString = this.node.find(".cal-event-start-date").text();
let startDate = this.parseDate(startDateString);
this.startDate = startDate;
}
2024-02-19 20:18:10 +01:00
parseAndSetEndDate() {
2024-01-05 17:05:23 +01:00
let endDateString = this.node.find(".cal-event-end-date").text();
let endDate = this.parseDate(endDateString);
this.endDate = endDate;
}
2024-02-19 20:18:10 +01:00
parseAndSetDates() {
2024-01-05 17:05:23 +01:00
this.parseAndSetStartDate();
this.parseAndSetEndDate();
}
2024-02-19 20:18:10 +01:00
parseAndSetLocation() {
2024-01-05 17:05:23 +01:00
this.location = this.node.find(".cal-event-location").text();
}
2024-02-19 20:18:10 +01:00
parseAndSetDescription() {
2024-01-05 17:05:23 +01:00
this.description = this.node.find(".cal-event-description").html();
}
2024-02-19 20:18:10 +01:00
parseAndSetPermRelatedFields() {
let hasPerms = !!parseInt(this.node.find(".cal-event-has-perms").text());
2024-01-05 17:05:23 +01:00
if (hasPerms) {
this.hasPerms = true;
this.nbPerms = parseInt(this.node.find(".cal-event-nb-perms").text());
2024-02-19 20:18:10 +01:00
this.minNbPerms = parseInt(
this.node.find(".cal-event-min-nb-perms").text(),
);
this.maxNbPerms = parseInt(
this.node.find(".cal-event-max-nb-perms").text(),
);
2024-01-05 17:05:23 +01:00
2024-02-19 20:18:10 +01:00
this.subscribedByUser = !!parseInt(
this.node.find(".cal-event-subscribed").text(),
);
2024-01-05 17:05:23 +01:00
}
}
2024-02-19 20:18:10 +01:00
parseAndSetTags() {
this.node.find(".cal-event-tag").each((_, element) => {
this.tags.push(element.innerText);
});
2024-01-05 17:05:23 +01:00
}
// Event details
2024-02-19 20:18:10 +01:00
createDetails() {
2024-01-05 17:05:23 +01:00
this.details = new EventDetails(this);
}
2024-02-19 20:18:10 +01:00
startDisplayingDetailsPopupOnClick() {
2024-01-05 17:05:23 +01:00
this.node.on("click", this.showDetailPopupOnClickCallback);
}
2024-02-19 20:18:10 +01:00
stopDisplayingDetailsPopupOnClick() {
2024-01-05 17:05:23 +01:00
this.node.off("click", this.showDetailPopupOnClickCallback);
}
// Event node content
2024-02-19 20:18:10 +01:00
addPermCounter() {
if (!this.hasPerms) {
2024-01-05 17:05:23 +01:00
return;
}
let permCounterNode = $("<div>")
.addClass("cal-event-perm-count")
.appendTo(this.node);
this.updatePermCounter();
}
2024-02-19 20:18:10 +01:00
updatePermCounter() {
let permCounterNode = this.node
.find(".cal-event-perm-count")
2024-01-05 17:05:23 +01:00
.html(`<span>&#x1f464; ${this.nbPerms}/${this.maxNbPerms}</span>`);
if (this.minNbPerms > this.nbPerms) {
permCounterNode.addClass("cal-perms-missing");
2024-02-19 20:18:10 +01:00
} else {
2024-01-05 17:05:23 +01:00
permCounterNode.removeClass("cal-perms-missing");
}
}
// Grid position
// Assuming the events can appear in the calendar
2024-02-19 20:18:10 +01:00
getGridStartColumn() {
2024-01-05 17:05:23 +01:00
if (this.startDate.getTime() < this.calendar.startDate.getTime()) {
return 1;
}
2024-02-19 20:18:10 +01:00
return (
1 +
Math.floor(
computeDateDifferenceInHours(this.calendar.startDate, this.startDate),
)
);
2024-01-05 17:05:23 +01:00
}
// Assuming the events can appear in the calendar
2024-02-19 20:18:10 +01:00
getGridEndColumn() {
2024-01-05 17:05:23 +01:00
if (this.endDate.getTime() > this.calendar.endDate.getTime()) {
return 1 + this.calendar.nbHoursToDisplay;
}
let shiftedEndDate = new Date(this.endDate.getTime() + 1000 * 60 * 59);
2024-02-19 20:18:10 +01:00
return (
1 +
this.calendar.nbHoursToDisplay -
Math.ceil(
computeDateDifferenceInHours(shiftedEndDate, this.calendar.endDate),
)
);
2024-01-05 17:05:23 +01:00
}
2024-02-19 20:18:10 +01:00
updatePositionInGrid() {
2024-01-05 17:05:23 +01:00
// Align on the grid according to the hours
this.node.css({
"grid-column-start": `${this.getGridStartColumn()}`,
2024-02-19 20:18:10 +01:00
"grid-column-end": `${this.getGridEndColumn()}`,
2024-01-05 17:05:23 +01:00
});
// Add left and right padding according to the minutes
let columnWidth = this.calendar.getHourSlotWidth();
let startMinutes = this.startDate.getMinutes();
let endMinutes = this.endDate.getMinutes();
let marginLeft = 0;
2024-02-19 20:18:10 +01:00
if (
this.startDate.getTime() >= this.calendar.startDate.getTime() &&
startMinutes !== 0
) {
2024-01-05 17:05:23 +01:00
marginLeft = columnWidth * (startMinutes / 60);
}
let marginRight = 0;
2024-02-19 20:18:10 +01:00
if (
this.endDate.getTime() <= this.calendar.endDate.getTime() &&
endMinutes !== 0
) {
2024-01-05 17:05:23 +01:00
marginRight = columnWidth * ((60 - endMinutes) / 60);
}
this.node.css({
2024-02-19 20:18:10 +01:00
"margin-left": marginLeft,
"margin-right": marginRight,
2024-01-05 17:05:23 +01:00
});
}
// Event style
2024-02-19 20:18:10 +01:00
updateNodeStyle() {
2024-01-05 17:05:23 +01:00
if (this.subscribedByUser) {
this.node.addClass("cal-event-subscribed");
2024-02-19 20:18:10 +01:00
} else {
2024-01-05 17:05:23 +01:00
this.node.removeClass("cal-event-subscribed");
}
}
// Location style
2024-02-19 20:18:10 +01:00
setLocationStyleID(styleID) {
2024-01-05 17:05:23 +01:00
this.locationStyleID = styleID;
this.node.addClass(`cal-location-${styleID}`);
}
// Selection
2024-02-19 20:18:10 +01:00
select() {
2024-01-05 17:05:23 +01:00
this.selected = true;
this.node.addClass("cal-selected");
}
2024-02-19 20:18:10 +01:00
deselect() {
2024-01-05 17:05:23 +01:00
this.selected = false;
this.node.removeClass("cal-selected");
}
// Visibility
2024-02-19 20:18:10 +01:00
hide() {
2024-01-05 17:05:23 +01:00
this.stopDisplayingDetailsPopupOnClick();
this.node.hide();
this.displayed = false;
}
2024-02-19 20:18:10 +01:00
show() {
2024-01-05 17:05:23 +01:00
this.node.show();
this.updateNodeStyle();
this.updatePositionInGrid();
this.updateOverflowTooltipTriggering();
this.startDisplayingDetailsPopupOnClick();
this.displayed = true;
}
2024-02-19 20:18:10 +01:00
updateVisibility() {
2024-01-05 17:05:23 +01:00
// If required, hide events which are not subscribed by the current user
2024-02-19 20:18:10 +01:00
if (this.calendar.onlyDisplaySubscribedEvents && !this.subscribedByUser) {
2024-01-05 17:05:23 +01:00
this.hide();
return;
}
// Hide events which cannot apear in the calendar time span
2024-02-19 20:18:10 +01:00
if (
this.calendar.startDate.getTime() >= this.endDate.getTime() ||
this.calendar.endDate.getTime() <= this.startDate.getTime()
) {
2024-01-05 17:05:23 +01:00
this.hide();
return;
}
// Otherwise, show the current event
this.show();
}
// Overflow tooltip (using tipso library)
2024-02-19 20:18:10 +01:00
updateOverflowTooltipContent() {
this.node
.find(".cal-event-name")
2024-01-05 17:05:23 +01:00
.attr("data-tipso-titleContent", this.name)
.attr("data-tipso-content", this.location);
2024-02-19 20:18:10 +01:00
this.node
.find(".cal-event-location")
2024-01-05 17:05:23 +01:00
.attr("data-tipso-titleContent", this.name)
.attr("data-tipso-content", this.location);
}
2024-02-19 20:18:10 +01:00
updateOverflowTooltipTriggering() {
2024-01-05 17:05:23 +01:00
let eventNameNode = this.node.find(".cal-event-name");
let eventLocationNode = this.node.find(".cal-event-location");
let eventNameElement = eventNameNode[0];
let eventLocationElement = eventLocationNode[0];
2024-02-19 20:18:10 +01:00
if (
(eventNameElement !== undefined &&
eventNameElement.clientWidth < eventNameElement.scrollWidth) ||
(eventLocationElement !== undefined &&
eventLocationElement.clientWidth < eventLocationElement.scrollWidth)
) {
eventNameNode.addClass("cal-has-tooltip");
eventLocationNode.addClass("cal-has-tooltip");
} else {
2024-01-05 17:05:23 +01:00
eventNameNode.removeClass("cal-has-tooltip");
eventLocationNode.removeClass("cal-has-tooltip");
}
}
}
class EventDetails {
2024-02-19 20:18:10 +01:00
constructor(event) {
2024-01-05 17:05:23 +01:00
this.event = event;
this.node = null;
// TODO: move this elsewhere
// Callback to close the details popup on click outside the popup
this.closePopupOnClickOutsideCallback = (event) => {
if ($(event.target).closest(".cal-event-details").length === 0) {
this.hidePopup();
}
};
this.init();
}
2024-02-19 20:18:10 +01:00
init() {
2024-01-05 17:05:23 +01:00
// Create the container node
this.node = $("<div>")
.addClass("cal-event-details")
.appendTo(this.event.calendar.eventDetailsContainerNode)
.hide();
this.createAndAppendTitle();
this.createAndAppendCloseButton();
// Table of (label, value) details
2024-02-19 20:18:10 +01:00
let tableNode = $("<table>").appendTo(this.node);
2024-01-05 17:05:23 +01:00
2024-02-19 20:18:10 +01:00
function addRowToDetailTable(label, value) {
let rowNode = $("<tr>").addClass("cal-detail-list").appendTo(tableNode);
2024-01-05 17:05:23 +01:00
2024-02-19 20:18:10 +01:00
$("<td>").addClass("cal-detail-label").html(label).appendTo(rowNode);
2024-01-05 17:05:23 +01:00
2024-02-19 20:18:10 +01:00
$("<td>").addClass("cal-detail-value").html(value).appendTo(rowNode);
2024-01-05 17:05:23 +01:00
}
this.createAndAppendLocation(addRowToDetailTable);
this.createAndAppendStartDate(addRowToDetailTable);
this.createAndAppendEndDate(addRowToDetailTable);
this.createAndAppendDuration(addRowToDetailTable);
this.createAndAppendPermManagementArea();
this.createAndAppendDescription();
this.createAndAppendTags();
}
2024-02-19 20:18:10 +01:00
createAndAppendCloseButton() {
2024-01-05 17:05:23 +01:00
$("<button>")
.attr("type", "button")
.addClass("cal-detail-close-button")
.html("&#x2715;") // cancelation cross
.appendTo(this.node)
.on("click", (event) => {
this.hidePopup();
});
}
2024-02-19 20:18:10 +01:00
createAndAppendTitle() {
let eventDetailURL = this.event.calendar.eventDetailURLFormat.replace(
"999999",
this.event.id,
);
2024-01-05 17:05:23 +01:00
let linkToEventPageNode = $("<a>")
.attr("href", eventDetailURL)
.attr("target", "_blank")
.addClass("cal-detail-name")
.appendTo(this.node);
2024-02-19 20:18:10 +01:00
$("<h3>").html(this.event.name).appendTo(linkToEventPageNode);
2024-01-05 17:05:23 +01:00
}
2024-02-19 20:18:10 +01:00
createAndAppendLocation(addRowToDetailTable) {
2024-01-05 17:05:23 +01:00
addRowToDetailTable("Lieu", this.event.location);
}
2024-02-19 20:18:10 +01:00
createAndAppendStartDate(addRowToDetailTable) {
let startDateString = this.event.startDate.toLocaleDateString("fr-FR", {
year: "2-digit",
month: "2-digit",
day: "2-digit",
});
2024-01-05 17:05:23 +01:00
let startTimeString = this.event.startDate
2024-02-19 20:18:10 +01:00
.toLocaleDateString("fr-FR", { hour: "2-digit", minute: "2-digit" })
2024-01-05 17:05:23 +01:00
.slice(-5);
addRowToDetailTable("Début", `Le ${startDateString} à ${startTimeString}`);
}
2024-02-19 20:18:10 +01:00
createAndAppendEndDate(addRowToDetailTable) {
let endDateString = this.event.endDate.toLocaleDateString("fr-FR", {
year: "2-digit",
month: "2-digit",
day: "2-digit",
});
2024-01-05 17:05:23 +01:00
let endTimeString = this.event.endDate
2024-02-19 20:18:10 +01:00
.toLocaleDateString("fr-FR", { hour: "2-digit", minute: "2-digit" })
2024-01-05 17:05:23 +01:00
.slice(-5);
addRowToDetailTable("Fin", `Le ${endDateString} à ${endTimeString}`);
}
2024-02-19 20:18:10 +01:00
createAndAppendDuration(addRowToDetailTable) {
2024-01-05 17:05:23 +01:00
const msPerMinute = 60 * 1000;
const msPerHour = 60 * msPerMinute;
2024-02-19 20:18:10 +01:00
let durationInMs = Math.abs(
this.event.startDate.getTime() - this.event.endDate.getTime(),
);
2024-01-05 17:05:23 +01:00
let hours = Math.floor(durationInMs / msPerHour);
let minutes = Math.abs((durationInMs % msPerHour) / msPerMinute);
2024-02-19 20:18:10 +01:00
addRowToDetailTable(
"Durée",
hours !== 0 ? `${hours}h ${minutes}min` : `${minutes}min`,
);
2024-01-05 17:05:23 +01:00
}
2024-02-19 20:18:10 +01:00
createAndAppendPermManagementArea() {
if (!this.event.hasPerms) {
2024-01-05 17:05:23 +01:00
return;
}
let permAreaNode = $("<div>")
.addClass("cal-detail-perm-area")
.appendTo(this.node);
2024-02-19 20:18:10 +01:00
let permTitleNode = this.createPermManagementTitle().appendTo(permAreaNode);
2024-01-05 17:05:23 +01:00
2024-02-19 20:18:10 +01:00
let permCountNode =
this.createPermManagementCounter().appendTo(permAreaNode);
2024-01-05 17:05:23 +01:00
2024-02-19 20:18:10 +01:00
let permSubscribeButtonNode =
this.createPermManagementSwitchButton().appendTo(permAreaNode);
2024-01-05 17:05:23 +01:00
2024-02-19 20:18:10 +01:00
this.updatePermManagementArea();
2024-01-05 17:05:23 +01:00
}
2024-02-19 20:18:10 +01:00
updatePermManagementArea() {
2024-01-05 17:05:23 +01:00
let permAreaNode = this.node.find(".cal-detail-perm-area");
this.updatePermManagementCounter();
this.updatePermManagementSwitchButton();
// Additional warning message if there are too little perms,
// and if the user has not subscribed to this event
2024-02-19 20:18:10 +01:00
permAreaNode.find(".cal-detail-perm-nb-missing-perms").remove();
2024-01-05 17:05:23 +01:00
2024-02-19 20:18:10 +01:00
if (
!this.event.subscribedByUser &&
this.event.minNbPerms > this.event.nbPerms
) {
2024-01-05 17:05:23 +01:00
let nbMissingPerms = this.event.minNbPerms - this.event.nbPerms;
let optionnalPluralMark = nbMissingPerms > 1 ? "s" : "";
$("<p>")
.addClass("cal-detail-perm-nb-missing-perms")
2024-02-19 20:18:10 +01:00
.html(
`${nbMissingPerms} personne${optionnalPluralMark}</br>min. manquante${optionnalPluralMark} !`,
)
2024-01-05 17:05:23 +01:00
.appendTo(permAreaNode);
}
}
2024-02-19 20:18:10 +01:00
createPermManagementTitle() {
return $("<h4>").addClass("cal-detail-perm-title").html("Permanences");
2024-01-05 17:05:23 +01:00
}
2024-02-19 20:18:10 +01:00
createPermManagementCounter() {
2024-01-05 17:05:23 +01:00
return $("<span>")
.addClass("cal-detail-perm-count")
.html(`&#x1f464; ${this.event.nbPerms}/${this.event.maxNbPerms}`);
}
2024-02-19 20:18:10 +01:00
updatePermManagementCounter() {
2024-01-05 17:05:23 +01:00
let permCounterNode = this.node
.find(".cal-detail-perm-count")
.html(`&#x1f464; ${this.event.nbPerms}/${this.event.maxNbPerms}`);
if (this.event.minNbPerms > this.event.nbPerms) {
permCounterNode.addClass("cal-perms-missing");
2024-02-19 20:18:10 +01:00
} else if (this.event.nbPerms === this.event.maxNbPerms) {
2024-01-05 17:05:23 +01:00
permCounterNode.addClass("cal-perms-full");
}
}
2024-02-19 20:18:10 +01:00
createPermManagementSwitchButton() {
let buttonNode = $("<button>").addClass(
"cal-detail-perm-subscription-switch",
);
2024-01-05 17:05:23 +01:00
// On click, switch the subscription state
// and update objects related to the perm count
let event = this.event;
buttonNode.on("click", () => {
let goal = event.subscribedByUser ? "unenrol" : "enrol";
2024-02-19 20:18:10 +01:00
let url = event.calendar.subscriptionURLFormat.replace(
"999999",
event.id,
);
2024-01-05 17:05:23 +01:00
$.ajax(url, {
method: "POST",
dataType: "json",
headers: {
2024-02-19 20:18:10 +01:00
"X-CSRFToken": event.calendar.csrfToken,
2024-01-05 17:05:23 +01:00
},
data: {
2024-02-19 20:18:10 +01:00
goal: goal,
2024-01-05 17:05:23 +01:00
},
success: (jsonAnswer) => {
event.subscribedByUser = jsonAnswer.enrolled;
event.nbPerms = jsonAnswer.number;
this.updatePermManagementArea();
event.updatePermCounter();
event.updateNodeStyle();
event.updateOverflowTooltipContent();
},
error: () => {
2024-02-19 20:18:10 +01:00
alert(
"Erreur lors de l'inscription ou de la désinscription à cette permanence.",
);
},
2024-01-05 17:05:23 +01:00
});
});
return buttonNode;
}
2024-02-19 20:18:10 +01:00
updatePermManagementSwitchButton() {
2024-01-05 17:05:23 +01:00
let event = this.event;
let buttonNode = this.node.find(".cal-detail-perm-subscription-switch");
// Reset the button state
buttonNode.prop("disabled", false);
// Update its content and state
if (event.subscribedByUser) {
buttonNode.html("Se désinscrire");
2024-02-19 20:18:10 +01:00
} else {
2024-01-05 17:05:23 +01:00
buttonNode.html("S'inscrire");
// Disable the button if the maximum number of perms is already reached
if (event.nbPerms === event.maxNbPerms) {
buttonNode.prop("disabled", true);
}
}
}
2024-02-19 20:18:10 +01:00
createAndAppendDescription() {
2024-01-05 17:05:23 +01:00
$("<p>")
.addClass("cal-detail-description")
.html(this.event.description)
.appendTo(this.node);
}
2024-02-19 20:18:10 +01:00
createAndAppendTags() {
2024-01-05 17:05:23 +01:00
let tagContainerNode = $("<div>")
.addClass("cal-detail-tag-list")
.appendTo(this.node);
for (let tag of this.event.tags) {
$("<span>")
.addClass("cal-detail-tag")
.html(tag)
.appendTo(tagContainerNode);
}
}
2024-02-19 20:18:10 +01:00
setPopupPosition(mouseClickEvent) {
2024-01-05 17:05:23 +01:00
let calendarOffset = this.event.calendar.containerNode.offset();
let eventOffset = this.event.node.position();
let eventNodeHeight = this.event.node.outerHeight();
let detailNodeOffset = this.node.position();
let detailsNodeWidth = this.node.outerWidth();
let detailsNodeHeight = this.node.outerHeight();
2024-02-19 20:18:10 +01:00
let x = mouseClickEvent.pageX - detailsNodeWidth / 2 - calendarOffset.left;
2024-01-05 17:05:23 +01:00
let y = eventOffset.top + eventNodeHeight + 50;
// If the popup is too high to vertically fit in the window,
// it is displayed above the event instead
// let bottomMargin = 50; // px
// let detailsNodeBottom = y
// + calendarOffset.top
// + detailsNodeHeight;
// if (detailsNodeBottom > $(window).height() - bottomMargin) {
// y = eventOffset.top - detailsNodeHeight - 10;
// this.node.addClass("above-event")
// }
// If the popup is about to be displayed outside of
// the left/right side of the screen, correct its future x position
let absoluteX = x + calendarOffset.left;
let sideMargin = 20; // px
if (absoluteX < sideMargin) {
x += sideMargin - absoluteX;
2024-02-19 20:18:10 +01:00
} else if (absoluteX + detailsNodeWidth > $(window).width() - sideMargin) {
x -= absoluteX + detailsNodeWidth - ($(window).width() - sideMargin);
2024-01-05 17:05:23 +01:00
}
this.node.css({
2024-02-19 20:18:10 +01:00
left: x,
top: y,
2024-01-05 17:05:23 +01:00
});
}
2024-02-19 20:18:10 +01:00
showPopup(mouseClickEvent) {
2024-01-05 17:05:23 +01:00
if (this.node.is(":visible")) {
return;
}
this.event.select();
this.node.show();
this.setPopupPosition(mouseClickEvent);
// TODO: use a clean solution ;)
window.setTimeout(() => {
2024-02-19 20:18:10 +01:00
this.startHidingOnOutsideClick();
2024-01-05 17:05:23 +01:00
}, 20);
}
2024-02-19 20:18:10 +01:00
hidePopup() {
2024-01-05 17:05:23 +01:00
this.event.deselect();
this.stopHidingOnOutsideClick();
this.node.hide();
}
// TODO: define the callback elsewhere
2024-02-19 20:18:10 +01:00
startHidingOnOutsideClick() {
2024-01-05 17:05:23 +01:00
$(document).on("click", this.closePopupOnClickOutsideCallback);
}
2024-02-19 20:18:10 +01:00
stopHidingOnOutsideClick() {
2024-01-05 17:05:23 +01:00
$(document).off("click", this.closePopupOnClickOutsideCallback);
}
}