datatracker/ietf/static/ietf/js/edit-meeting-schedule.js
Ole Laursen ab178173f4 Fix bug in showing the time of other sessions of the same group in the
new meeting schedule editor
 - Legacy-Id: 18022
2020-06-22 12:31:39 +00:00

428 lines
15 KiB
JavaScript

jQuery(document).ready(function () {
let content = jQuery(".edit-meeting-schedule");
function failHandler(xhr, textStatus, error) {
let errorText = error;
if (xhr && xhr.responseText)
errorText += "\n\n" + xhr.responseText;
alert("Error: " + errorText);
}
let sessions = content.find(".session");
let timeslots = content.find(".timeslot");
let days = content.find(".day-flow .day");
// hack to work around lack of position sticky support in old browsers, see https://caniuse.com/#feat=css-sticky
if (content.find(".scheduling-panel").css("position") != "sticky") {
content.find(".scheduling-panel").css("position", "fixed");
content.css("padding-bottom", "14em");
}
function findTimeslotsOverlapping(intervals) {
let res = [];
timeslots.each(function () {
var timeslot = jQuery(this);
let start = timeslot.data("start");
let end = timeslot.data("end");
for (let i = 0; i < intervals.length; ++i) {
if (end >= intervals[i][0] && intervals[i][1] >= start) {
res.push(timeslot);
break;
}
}
});
return res;
}
// selecting
function selectSessionElement(element) {
if (element) {
sessions.not(element).removeClass("selected");
jQuery(element).addClass("selected");
showConstraintHints(element);
let sessionInfoContainer = content.find(".scheduling-panel .session-info-container");
sessionInfoContainer.html(jQuery(element).find(".session-info").html());
sessionInfoContainer.find("[data-original-title]").tooltip();
sessionInfoContainer.find(".time").text(jQuery(element).closest(".timeslot").data('scheduledatlabel'));
sessionInfoContainer.find(".other-session").each(function () {
let scheduledAt = sessions.filter("#session" + this.dataset.othersessionid).closest(".timeslot").data('scheduledatlabel');
let timeElement = jQuery(this).find(".time");
if (scheduledAt)
timeElement.text(timeElement.data("scheduled").replace("{time}", scheduledAt));
else
timeElement.text(timeElement.data("notscheduled"));
});
}
else {
sessions.removeClass("selected");
showConstraintHints();
content.find(".scheduling-panel .session-info-container").html("");
}
}
function showConstraintHints(selectedSession) {
let sessionId = selectedSession ? selectedSession.id.slice("session".length) : null;
// hints on the sessions
sessions.find(".constraints > span").each(function () {
if (!sessionId) {
jQuery(this).removeClass("would-violate-hint");
return;
}
let sessionIds = this.dataset.sessions;
if (!sessionIds)
return;
let wouldViolate = sessionIds.split(",").indexOf(sessionId) != -1;
jQuery(this).toggleClass("would-violate-hint", wouldViolate);
});
// hints on timeslots
timeslots.removeClass("would-violate-hint");
if (selectedSession) {
let intervals = [];
timeslots.filter(":has(.session .constraints > span.would-violate-hint)").each(function () {
intervals.push([this.dataset.start, this.dataset.end]);
});
let overlappingTimeslots = findTimeslotsOverlapping(intervals);
for (let i = 0; i < overlappingTimeslots.length; ++i)
overlappingTimeslots[i].addClass("would-violate-hint");
// check room sizes
let attendees = +selectedSession.dataset.attendees;
if (attendees) {
timeslots.not(".would-violate-hint").each(function () {
if (attendees > +jQuery(this).closest(".timeslots").data("roomcapacity"))
jQuery(this).addClass("would-violate-hint");
});
}
}
}
content.on("click", function (event) {
if (jQuery(event.target).is(".session-info-container") || jQuery(event.target).closest(".session-info-container").length > 0)
return;
selectSessionElement(null);
});
sessions.on("click", function (event) {
event.stopPropagation();
selectSessionElement(this);
});
if (ietfData.can_edit) {
// dragging
sessions.on("dragstart", function (event) {
event.originalEvent.dataTransfer.setData("text/plain", this.id);
jQuery(this).addClass("dragging");
selectSessionElement(this);
});
sessions.on("dragend", function () {
jQuery(this).removeClass("dragging");
});
sessions.prop('draggable', true);
// dropping
let dropElements = content.find(".timeslot .drop-target,.unassigned-sessions .drop-target");
dropElements.on('dragenter', function (event) {
if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session")
return;
event.preventDefault(); // default action is signalling that this is not a valid target
jQuery(this).parent().addClass("dropping");
});
dropElements.on('dragover', function (event) {
// we don't actually need this event, except we need to signal
// that this is a valid drop target, by cancelling the default
// action
event.preventDefault();
});
dropElements.on('dragleave', function (event) {
// skip dragleave events if they are to children
if (event.originalEvent.currentTarget.contains(event.originalEvent.relatedTarget))
return;
jQuery(this).parent().removeClass("dropping");
});
dropElements.on('drop', function (event) {
jQuery(this).parent().removeClass("dropping");
let sessionId = event.originalEvent.dataTransfer.getData("text/plain");
if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session")
return;
let sessionElement = sessions.filter("#" + sessionId);
if (sessionElement.length == 0)
return;
event.preventDefault(); // prevent opening as link
if (sessionElement.parent().is(this))
return;
let dropElement = jQuery(this);
let dropParent = dropElement.parent();
function done(response) {
if (response != "OK") {
failHandler(null, null, response);
return;
}
dropElement.append(sessionElement); // move element
updateCurrentSchedulingHints();
if (dropParent.hasClass("unassigned-sessions"))
sortUnassigned();
}
if (dropParent.hasClass("unassigned-sessions")) {
jQuery.ajax({
url: ietfData.urls.assign,
method: "post",
timeout: 5 * 1000,
data: {
action: "unassign",
session: sessionId.slice("session".length)
}
}).fail(failHandler).done(done);
}
else {
jQuery.ajax({
url: ietfData.urls.assign,
method: "post",
data: {
action: "assign",
session: sessionId.slice("session".length),
timeslot: dropParent.attr("id").slice("timeslot".length)
},
timeout: 5 * 1000
}).fail(failHandler).done(done);
}
});
}
// hints for the current schedule
function updateSessionConstraintViolations() {
// do a sweep on sessions sorted by start time
let scheduledSessions = [];
sessions.each(function () {
let timeslot = jQuery(this).closest(".timeslot");
if (timeslot.length == 1)
scheduledSessions.push({
start: timeslot.data("start"),
end: timeslot.data("end"),
id: this.id.slice("session".length),
element: jQuery(this),
timeslot: timeslot.get(0)
});
});
scheduledSessions.sort(function (a, b) {
if (a.start < b.start)
return -1;
if (a.start > b.start)
return 1;
return 0;
});
let currentlyOpen = {};
let openedIndex = 0;
for (let i = 0; i < scheduledSessions.length; ++i) {
let s = scheduledSessions[i];
// prune
for (let sessionIdStr in currentlyOpen) {
if (currentlyOpen[sessionIdStr].end <= s.start)
delete currentlyOpen[sessionIdStr];
}
// expand
while (openedIndex < scheduledSessions.length && scheduledSessions[openedIndex].start < s.end) {
let toAdd = scheduledSessions[openedIndex];
currentlyOpen[toAdd.id] = toAdd;
++openedIndex;
}
// check for violated constraints
s.element.find(".constraints > span").each(function () {
let sessionIds = this.dataset.sessions;
let violated = sessionIds && sessionIds.split(",").filter(function (v) {
return (v != s.id
&& v in currentlyOpen
// ignore errors within the same timeslot
// under the assumption that the sessions
// in the timeslot happen sequentially
&& s.timeslot != currentlyOpen[v].timeslot);
}).length > 0;
jQuery(this).toggleClass("violated-hint", violated);
});
}
}
function updateTimeSlotDurationViolations() {
timeslots.each(function () {
let total = 0;
jQuery(this).find(".session").each(function () {
total += +jQuery(this).data("duration");
});
jQuery(this).toggleClass("overfull", total > +jQuery(this).data("duration"));
});
}
function updateAttendeesViolations() {
sessions.each(function () {
let roomCapacity = jQuery(this).closest(".timeslots").data("roomcapacity");
if (roomCapacity && this.dataset.attendees)
jQuery(this).toggleClass("too-many-attendees", +this.dataset.attendees > +roomCapacity);
});
}
function updateCurrentSchedulingHints() {
updateSessionConstraintViolations();
updateAttendeesViolations();
updateTimeSlotDurationViolations();
}
updateCurrentSchedulingHints();
// sorting unassigned
function sortArrayWithKeyFunctions(array, keyFunctions) {
function compareArrays(a, b) {
for (let i = 1; i < a.length; ++i) {
let ai = a[i];
let bi = b[i];
if (ai > bi)
return 1;
else if (ai < bi)
return -1;
}
return 0;
}
let arrayWithSortKeys = array.map(function (a) {
let res = [a];
for (let i = 0; i < keyFunctions.length; ++i)
res.push(keyFunctions[i](a));
return res;
});
arrayWithSortKeys.sort(compareArrays);
return arrayWithSortKeys.map(function (l) {
return l[0];
});
}
function sortUnassigned() {
let sortBy = content.find("select[name=sort_unassigned]").val();
function extractId(e) {
return e.id.slice("session".length);
}
function extractName(e) {
return e.querySelector(".session-label").innerHTML;
}
function extractParent(e) {
return e.querySelector(".session-parent").innerHTML;
}
function extractDuration(e) {
return +e.dataset.duration;
}
function extractComments(e) {
return e.querySelector(".session-info .comments") ? 0 : 1;
}
let keyFunctions = [];
if (sortBy == "name")
keyFunctions = [extractName, extractDuration, extractId];
else if (sortBy == "parent")
keyFunctions = [extractParent, extractName, extractDuration, extractId];
else if (sortBy == "duration")
keyFunctions = [extractDuration, extractParent, extractName, extractId];
else if (sortBy == "comments")
keyFunctions = [extractComments, extractParent, extractName, extractDuration, extractId];
let unassignedSessionsContainer = content.find(".unassigned-sessions .drop-target");
let sortedSessions = sortArrayWithKeyFunctions(unassignedSessionsContainer.children(".session").toArray(), keyFunctions);
for (let i = 0; i < sortedSessions.length; ++i)
unassignedSessionsContainer.append(sortedSessions[i]);
}
content.find("select[name=sort_unassigned]").on("change click", function () {
sortUnassigned();
});
sortUnassigned();
// toggling visible sessions by session parents
let sessionParentInputs = content.find(".session-parent-toggles input");
function updateSessionParentToggling() {
let checked = [];
sessionParentInputs.filter(":checked").each(function () {
checked.push(".parent-" + this.value);
});
sessions.not(".untoggleable").filter(checked.join(",")).show();
sessions.not(".untoggleable").not(checked.join(",")).hide();
}
sessionParentInputs.on("click", updateSessionParentToggling);
updateSessionParentToggling();
// toggling visible timeslots
let timeslotGroupInputs = content.find("#timeslot-group-toggles-modal .modal-body input");
function updateTimeslotGroupToggling() {
let checked = [];
timeslotGroupInputs.filter(":checked").each(function () {
checked.push("." + this.value);
});
timeslots.filter(checked.join(",")).removeClass("hidden");
timeslots.not(checked.join(",")).addClass("hidden");
days.each(function () {
jQuery(this).toggle(jQuery(this).find(".timeslot:not(.hidden)").length > 0);
});
}
timeslotGroupInputs.on("click change", updateTimeslotGroupToggling);
updateTimeslotGroupToggling();
// session info
content.find(".session-info-container").on("mouseover", ".other-session", function (event) {
sessions.filter("#session" + this.dataset.othersessionid).addClass("highlight");
}).on("mouseleave", ".other-session", function (event) {
sessions.filter("#session" + this.dataset.othersessionid).removeClass("highlight");
});
});