';
switch (this.contributionType) {
case 3 :
case 12 : if (this.isComplete) {
- entryContent += '
').attr({
+ 'type' : 'checkbox',
+ 'id' : checkboxId
+ }).addClass('form-check-input me-1')
+ .change(function(){
+ editEmailProgressDate($(this));
+ }),
+
+ dateString = $('
').addClass('form-check-label').attr('for', checkboxId).text(dateObj.date),
+
+ dateDiv = $('
').attr({
+ 'dateid' : dateObj.id,
+ 'datems' : dateObj.ms
+ })
+ .addClass('dialogListItem')
+ .append(dateString)
+ .prepend(checkbox)
+ .appendTo(list);
+
+ checkbox.prop('checked', checked);
+ return checkbox;
+}
+
+function sendProgressEmail() {
+ showConfirm(LABELS.PROGRESS_EMAIL_SEND_NOW_QUESTION, function() {
+ $.ajax({
+ dataType : 'json',
+ url : LAMS_URL + 'monitoring/emailProgress/sendLessonProgressEmail.do',
+ type: 'post',
+ cache : false,
+ data : {
+ 'lessonID' : lessonId
+ },
+ success : function(response) {
+ if ( response.error || ! response.sent > 0 )
+ showToast(LABELS.PROGRESS_EMAIL_SEND_FAILED+"\n"+(response.error ? response.error : ""));
+ else
+ showToast(LABELS.PROGRESS_EMAIL_SUCCESS.replace('[0]',response.sent));
+ }
+ });
+ });
+}
+
+function addEmailProgressDate() {
+ debugger;
+ var table = $('#emailProgressDialogTable', '#emailProgressDialog'),
+ list = $('.dialogList', table),
+ newDateMS = new tempusDominus.TempusDominus(document.getElementById('emaildatePicker')).viewDate;
+
+
+ if ( newDateMS != null ) {
+ if ( newDateMS.getTime() < Date.now() ) {
+ alert(LABELS.ERROR_DATE_IN_PAST);
+ } else {
+ var dateObj = { id: newDateMS.getTime(), date: getEmailDateString(newDateMS)},
+ checkbox = addCheckbox(dateObj, list, true);
+ editEmailProgressDate(checkbox); // update back end
+ addEmailProgressSeries(false, table);
+ }
+ } else {
+ alert(LABELS.PROGRESS_SELECT_DATE_FIRST);
+ }
+}
+
+
+
+function getEmailDateString(date) {
+ return date.toLocaleDateString('en', {year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: false });
+}
+
+function addEmailProgressSeries(forceQuestion, table) {
+ if ( ! table ) {
+ table = $('#emailProgressDialogTable', '#emailProgressDialog');
+ }
+ var list = $('.dialogList', table),
+ items = $('.dialogListItem', list);
+
+ if ( forceQuestion && items.length < 2 ) {
+ alert(LABELS.PROGRESS_ENTER_TWO_DATES_FIRST);
+ } else if ( items.length == 2 || forceQuestion ) {
+ var numDates = prompt(LABELS.PROGRESS_EMAIL_GENERATE_ONE+"\n\n"+LABELS.PROGRESS_EMAIL_GENERATE_TWO);
+ if ( numDates > 0 ) {
+ var dates=[];
+ var maxDate = 0;
+ items.each( function() {
+ var nextDate = $(this).attr('dateid');
+ dates.push($(this).attr('dateid'));
+ if ( maxDate < nextDate )
+ maxDate = nextDate;
+ });
+ if ( dates[1] < dates[0] ) {
+ var swap = dates[1];
+ dates[1] = dates[0];
+ dates[0] = swap;
+ }
+ var diff = dates[1] - dates[0];
+ if ( diff > 0 ) {
+ var genDateMS = maxDate;
+ for (var i = 0; i < numDates; i++) {
+ genDateMS = +genDateMS + +diff;
+ var genDateObj = { id: genDateMS, date: getEmailDateString(new Date(genDateMS))};
+ var checkbox = addCheckbox(genDateObj, list, false);
+ }
+ }
+ }
+ }
+ colorDialogList(table);
+}
+
function openGateNow(activityId) {
var data = {
'activityId' : activityId
@@ -129,6 +1100,1564 @@
});
}
+function closeGate(activityId) {
+ var data = {
+ 'activityId' : activityId
+ };
+ data[csrfTokenName] = csrfTokenValue;
+ $.ajax({
+ 'type' : 'post',
+ 'url' : LAMS_URL + 'monitoring/gate/closeGate.do',
+ 'data' : data,
+ 'success' : function(){
+ updateLessonTab();
+ }
+ });
+}
+//********** SEQUENCE TAB FUNCTIONS **********
+
+/**
+ * Sets up the sequence tab.
+ */
+function initSequenceTab(){
+ var learnerGroupDialogContents = $('#learnerGroupDialogContents');
+ $('#learnerGroupDialogForceCompleteButton, #learnerGroupDialogForceCompleteAllButton', learnerGroupDialogContents).click(function() {
+ var dialog = $('#learnerGroupDialog'),
+ // are we moving selected learners or all of learners who are currently in the activity
+ moveAll = $(this).attr('id') == 'learnerGroupDialogForceCompleteAllButton',
+ selectedLearners = moveAll ? null : $('.dialogList div.dialogListItemSelected', dialog),
+ // go to "force complete" mode, similar to dragging user to an activity
+ activityId = dialog.data('ajaxProperties').data.activityID,
+ dropArea = sequenceCanvas.add('#completedLearnersContainer');
+ dropArea.css('cursor', 'pointer')
+ .one('click', function(event) {
+ dropArea.off('click').css('cursor', 'default');
+ if (moveAll) {
+ // setting learners as 'true' is a special switch meaning "move all"
+ forceComplete(activityId, true, event.pageX, event.pageY);
+ } else {
+ var learners = [];
+ selectedLearners.each(function(){
+ var learner = $(this);
+ learners.push({
+ 'id' : learner.attr('userId'),
+ 'name' : learner.text()
+ });
+ });
+ forceComplete(activityId, learners, event.pageX, event.pageY);
+ }
+ });
+ dialog.modal('hide');
+
+ if (moveAll) {
+ alert(LABELS.FORCE_COMPLETE_CLICK.replace('[0]', ''));
+ } else {
+ var learnerNames = '';
+ selectedLearners.each(function(){
+ learnerNames += $(this).text() + ', ';
+ });
+ learnerNames = learnerNames.slice(0, -2);
+ alert(LABELS.FORCE_COMPLETE_CLICK.replace('[0]', '"' + learnerNames + '"'));
+ }
+ });
+
+ $('#learnerGroupDialogViewButton', learnerGroupDialogContents).click(function() {
+ var dialog = $('#learnerGroupDialog'),
+ selectedLearner = $('.dialogList div.dialogListItemSelected', dialog);
+ if (selectedLearner.length == 1) {
+ // open pop up with user progress in the given activity
+ openPopUp(selectedLearner.attr('viewUrl'), "LearnActivity", popupHeight, popupWidth, true);
+ }
+ });
+
+ $('#learnerGroupDialogEmailButton', learnerGroupDialogContents).click(function() {
+ var dialog = $('#learnerGroupDialog'),
+ selectedLearner = $('.dialogList div.dialogListItemSelected', dialog);
+ if (selectedLearner.length == 1) {
+ showEmailDialog(selectedLearner.attr('userId'));
+ }
+ });
+
+ $('#learnerGroupDialogCloseButton', learnerGroupDialogContents).click(function(){
+ $('#learnerGroupDialog').modal('hide');
+ });
+
+ // initialise lesson dialog
+ var learnerGroupDialog = showDialog('learnerGroupDialog',{
+ 'autoOpen' : false,
+ 'width' : 450,
+ 'height' : 450,
+ 'resizable' : true,
+ 'open' : function(){
+ // until operator selects an user, buttons remain disabled
+ $('button.learnerGroupDialogSelectableButton').blur().prop('disabled', true);
+ }
+ }, false);
+
+ $('.modal-body', learnerGroupDialog).empty().append(learnerGroupDialogContents.show());
+
+ // search for users with the term the Monitor entered
+ $('.dialogSearchPhrase', learnerGroupDialog).autocomplete({
+ 'source' : LAMS_URL + "monitoring/monitoring/autocomplete.do?scope=lesson&lessonID=" + lessonId,
+ 'delay' : 700,
+ 'select' : function(event, ui){
+ var phraseField = $(this),
+ dialog = $('#learnerGroupDialog');
+ // learner's ID in ui.item.value is not used here
+ phraseField.val(ui.item.label);
+ $('.dialogSearchPhraseClear', dialog).css('visibility', 'visible');
+ // reset to page 1
+ dialog.data('ajaxProperties').data.pageNumber = 1;
+ showLearnerGroupDialog();
+ return false;
+ }
+ })
+ // run the real search when the Monitor presses Enter
+ .keypress(function(e){
+ if (e.which == 13) {
+ var phraseField = $(this),
+ dialog = $('#learnerGroupDialog');
+
+ phraseField.autocomplete("close");
+ if (phraseField.val()) {
+ $('.dialogSearchPhraseClear', dialog).css('visibility', 'visible');
+ }
+ // reset to page 1
+ dialog.data('ajaxProperties').data.pageNumber = 1;
+ showLearnerGroupDialog();
+ }
+ });
+
+ // search for users with the term the Monitor entered
+ $("#sequenceSearchPhrase").autocomplete( {
+ 'source' : LAMS_URL + "monitoring/monitoring/autocomplete.do?scope=lesson&lessonID=" + lessonId,
+ 'delay' : 700,
+ 'response' : function(event, ui) {
+ $.each(ui.content, function(){
+ // only add portrait if user has got one
+ let valueParts = this.value.split('_');
+ this.value = valueParts[0];
+ // portrait div will be added as text, then in open() function below we fix it
+ this.portrait = definePortrait(valueParts.length > 1 ? valueParts[1] : null, this.value, STYLE_SMALL, true, LAMS_URL);
+ this.rawLabel = this.label;
+ this.label += this.portrait;
+ });
+ },
+ 'open' : function(event, ui) {
+ $('.ui-menu-item-wrapper', $(this).autocomplete( "widget" )).each(function(){
+ let menuItem = $(this);
+ // portrait, if exists, was added as text; now we make it proper html
+ menuItem.html(menuItem.text());
+ let portrait = menuItem.children('div');
+ if (portrait.length == 0) {
+ // no portrait, nothing to do
+ return;
+ }
+ // rearrange item contents
+ portrait.detach();
+ let label = $('
').text(menuItem.text());
+ // this extra class makes it a flex box
+ menuItem.empty().addClass('autocomplete-menu-item-with-portrait');
+ menuItem.append(label).append(portrait);
+ });
+ },
+ 'select' : function(event, ui){
+ // put the learner first name, last name and login into the box
+ $(this).val(ui.item.rawLabel);
+ // mark the learner's ID and make him highlighted after the refresh
+ sequenceSearchedLearner = ui.item.value;
+ refreshMonitor();
+ return false;
+ }
+ });
+
+ var forceBackwardsDialogContents = $('#forceBackwardsDialogContents');
+ showDialog('forceBackwardsDialog', {
+ 'autoOpen' : false,
+ 'modal' : true,
+ 'resizable' : true,
+ 'height' : 300,
+ 'width' : 400,
+ 'title' : LABELS.FORCE_COMPLETE_BUTTON
+ }, false);
+ // only need to do this once as then it updates the msg field directly.
+ $('.modal-body', '#forceBackwardsDialog').empty().append($('#forceBackwardsDialogContents').show());
+
+ $('#forceBackwardsRemoveContentNoButton', forceBackwardsDialogContents).click(function(){
+ var forceBackwardsDialog = $('#forceBackwardsDialog'),
+ learners = forceBackwardsDialog.data('learners'),
+ moveAll = learners === true;
+ forceCompleteExecute(moveAll ? null : learners,
+ moveAll ? forceBackwardsDialog.data('currentActivityId') : null,
+ forceBackwardsDialog.data('activityId'),
+ false);
+ forceBackwardsDialog.modal('hide');
+ });
+
+ $('#forceBackwardsRemoveContentYesButton', forceBackwardsDialogContents).click(function(){
+ var forceBackwardsDialog = $('#forceBackwardsDialog');
+ learners = forceBackwardsDialog.data('learners'),
+ moveAll = learners === true;
+ forceCompleteExecute(moveAll ? null : learners,
+ moveAll ? forceBackwardsDialog.data('currentActivityId') : null,
+ forceBackwardsDialog.data('activityId'),
+ true);
+ forceBackwardsDialog.modal('hide');
+ });
+
+ $('#forceBackwardsCloseButton', forceBackwardsDialogContents).click(function(){
+ $('#forceBackwardsDialog').modal('hide');
+ });
+
+ const learnerProgressUpdateSource = new EventSource(LAMS_URL + 'monitoring/monitoring/getLearnerProgressUpdateFlux.do?lessonId=' + lessonId);
+ learnerProgressUpdateSource.onmessage = function (event) {
+ if ("doRefresh" == event.data && $('#sequence-tab-content').length === 1){
+ updateSequenceTab();
+ }
+ }
+}
+
+function showIntroductionDialog(lessonId) {
+
+ showDialog('introductionDialog', {
+ 'height' : 450,
+ 'width' : Math.max(380, Math.min(800, $(window).width() - 60)),
+ 'resizable' : false,
+ 'title' : LABELS.LESSON_INTRODUCTION,
+ 'open' : function(){
+ $('iframe', this).attr('src', LAMS_URL + 'editLessonIntro/edit.do?lessonID='+lessonId);
+ $('iframe', this).css('height', '360px');
+ },
+ 'close' : function(){
+ closeIntroductionDialog()
+ }
+ }, false);
+}
+
+function closeIntroductionDialog() {
+ $('#introductionDialog').remove();
+}
+
+/**
+ * Updates learner progress in sequence tab according to respose sent to refreshMonitor()
+ */
+function updateSequenceTab() {
+ if (sequenceRefreshInProgress) {
+ return;
+ }
+ sequenceRefreshInProgress = true;
+
+ drawLessonCompletionChart();
+
+ sequenceCanvas = $('#sequenceCanvas');
+ sequenceCanvas.css('visibility', 'hidden');
+
+ if (originalSequenceCanvas) {
+ // put bottom layer, LD SVG
+ sequenceCanvas.html(originalSequenceCanvas);
+ } else {
+ var exit = loadLearningDesignSVG();
+ if (exit) {
+ // when SVG gets re-created, this update method will be run again
+ sequenceRefreshInProgress = false;
+ return;
+ }
+ }
+
+ // clear all learner icons
+ $('.learner-icon, .more-learner-icon', '#canvas-container').remove();
+
+ var sequenceTopButtonsContainer = $('#sequenceTopButtonsContainer');
+ if ($('img#sequenceCanvasLoading', sequenceTopButtonsContainer).length == 0){
+ $('#sequenceCanvasLoading')
+ .clone().appendTo(sequenceTopButtonsContainer)
+ .css('display', 'block');
+ }
+
+ $.ajax({
+ dataType : 'json',
+ url : LAMS_URL + 'monitoring/monitoring/getLessonProgress.do',
+ cache : false,
+ data : {
+ 'lessonID' : lessonId,
+ 'searchedLearnerId' : sequenceSearchedLearner
+ },
+ success : function(response) {
+ // activities have uiids but no ids, set it here
+ $.each(response.activities, function(){
+ $('g[uiid="' + this.uiid + '"]', sequenceCanvas).attr('id', this.id);
+ });
+
+ // remove the loading animation
+ $('img#sequenceCanvasLoading', sequenceTopButtonsContainer).remove();
+
+ var learnerCount = 0;
+ $.each(response.activities, function(index, activity){
+ var activityGroup = $('g[id="' + activity.id + '"]', sequenceCanvas),
+ isGate = [3,4,5,14].indexOf(activity.type) > -1;
+
+ learnerCount += activity.learnerCount;
+
+ if (isGate) {
+ var gateClosedIcon = activityGroup.find('.gateClosed');
+
+ if (activity.gateOpen && gateClosedIcon.length > 0) {
+ if (!gateOpenIconData) {
+ // if SVG is not cached, get it synchronously
+ $.ajax({
+ url : LAMS_URL + gateOpenIconPath,
+ async : false,
+ dataType : 'text',
+ success : function(response) {
+ gateOpenIconData = response;
+ }
+ });
+ }
+
+ $(gateOpenIconData).clone().attr({
+ x : gateClosedIcon.attr('x'),
+ y : gateClosedIcon.attr('y'),
+ width : gateClosedIcon.attr('width'),
+ height : gateClosedIcon.attr('height'),
+ }).appendTo(activityGroup)
+ .show();
+
+ gateClosedIcon.remove();
+ } else {
+ gateClosedIcon.show();
+ }
+ }
+
+ if (response.contributeActivities) {
+ $.each(response.contributeActivities, function(){
+ if (activity.id == this.activityID) {
+ activity.requiresAttention = true;
+ return false;
+ }
+ });
+ }
+
+ // put learner and attention icons on each activity shape
+ addActivityIcons(activity);
+ });
+
+ // modyfing SVG in DOM does not render changes, so we need to reload it
+ sequenceCanvas.html(sequenceCanvas.html());
+
+ // only now show SVG so there is no "jump" when resizing
+ sequenceCanvas.css('visibility', 'visible');
+
+ if (sequenceSearchedLearner != null && !response.searchedLearnerFound) {
+ // the learner has not started the lesson yet, display an info box
+ sequenceClearSearchPhrase();
+ showToast(LABELS.PROGRESS_NOT_STARTED);
+ }
+
+ var learnerTotalCount = learnerCount + response.completedLearnerCount;
+ // $('#learnersStartedPossibleCell').html('
'+learnerTotalCount + ' / ' + response.numberPossibleLearners+'');
+ addCompletedLearnerIcons(response.completedLearners, response.completedLearnerCount, learnerTotalCount);
+
+ $.each(response.activities, function(activityIndex, activity){
+ addActivityIconsHandlers(activity);
+
+ if (activity.url) {
+ var activityGroup = $('g[id="' + activity.id + '"]');
+ activityGroup.css('cursor', 'pointer');
+ dblTap(activityGroup, function(){
+ // double click on activity shape to open Monitoring for this activity
+ openPopUp(LAMS_URL + activity.url, "MonitorActivity", popupHeight, popupWidth, true, true);
+ });
+ }
+ });
+
+ // remove any existing popovers
+ $('.popover[role="tooltip"]').remove();
+
+ initializePortraitPopover(LAMS_URL, 'large', 'right');
+
+ // update the cache global values so that the contributions & the Live Edit buttons will update
+ lockedForEdit = response.lockedForEdit;
+ lockedForEditUserId = response.lockedForEditUserId;
+ lockedForEditUsername = response.lockedForEditUsername;
+ updateLiveEdit();
+
+ sequenceRefreshInProgress = false;
+ }
+ });
+}
+
+function updateLiveEdit() {
+ if ( liveEditEnabled ) {
+ if ( lockedForEdit ) {
+ if ( userId == lockedForEditUserId ) {
+ $("#liveEditButton").removeClass('btn-default');
+ $("#liveEditButton").addClass('btn-primary');
+ $("#liveEditButton").show();
+ $("#liveEditWarning").hide();
+ $("#liveEditWarning").text("");
+ } else {
+ $("#liveEditButton").removeClass('btn-primary');
+ $("#liveEditButton").addClass('btn-default');
+ $("#liveEditButton").hide();
+ $("#liveEditWarning").text(LABELS.LIVE_EDIT_WARNING.replace("%0",lockedForEditUsername));
+ $("#liveEditWarning").show();
+ }
+ } else {
+ $("#liveEditButton").removeClass('btn-primary');
+ $("#liveEditButton").addClass('btn-default');
+ $("#liveEditButton").show();
+ $("#liveEditWarning").hide();
+ $("#liveEditWarning").text("");
+ }
+ } else {
+ $("#liveEditButton").hide();
+ $("#liveEditWarning").hide();
+ }
+}
+
+function loadLearningDesignSVG() {
+ var exit = false;
+ // fetch SVG just once, since it is immutable
+ $.ajax({
+ dataType : 'text',
+ url : LAMS_URL + 'home/getLearningDesignThumbnail.do?',
+ async : false,
+ cache : false,
+ data : {
+ 'ldId' : ldId
+ },
+ success : function(response) {
+ originalSequenceCanvas = response;
+ sequenceCanvas = $('#sequenceCanvas').html(originalSequenceCanvas);
+ },
+ error : function(error) {
+ exit = true;
+ // the LD SVG is missing, try to re-generate it; if it is an another error, fail
+ if (error.status != 404) {
+ return;
+ }
+
+ // iframe just to load Authoring for a single purpose, generate the SVG
+ var frame = $('
').appendTo('body').css('visibility', 'hidden');
+ frame.on('load', function(){
+ // disable current onload handler as closing the dialog reloads the iframe
+ frame.off('load');
+
+ // call svgGenerator.jsp code to store LD SVG on the server
+ var win = frame[0].contentWindow || frame[0].contentDocument;
+ $(win.document).ready(function(){
+ // when LD opens, make a callback which save the thumbnail and displays it in current window
+ win.GeneralLib.openLearningDesign(ldId, function(){
+ result = win.GeneralLib.saveLearningDesignImage();
+ frame.remove();
+ if (result) {
+ // load the image again
+ updateSequenceTab();
+ }
+ });
+ });
+ });
+ // load svgGenerator.jsp to render LD SVG
+ frame.attr('src', LAMS_URL + 'authoring/generateSVG.do?selectable=false');
+ }
+ });
+
+ return exit;
+}
+
+
+/**
+ * Forces given learners to move to activity indicated on SVG by coordinated (drag-drop)
+ */
+function forceComplete(currentActivityId, learners, x, y) {
+ var foundActivities = [],
+ targetActivity = null,
+ // if "true", then we are moving all learners from the given activity
+ // otherwise it is a list of selected learners IDs
+ moveAll = learners === true;
+ // check all activities and "users who finished lesson" bar
+ $('g.svg-activity', sequenceCanvas).add('#completedLearnersContainer').each(function(){
+ // find which activity learner was dropped on
+ var act = $(this),
+ coord = {
+ 'x' : act.offset().left,
+ 'y' : act.offset().top
+ }
+ if (act.is('g')) {
+ var box = act[0].getBBox();
+ coord.width = box.width;
+ coord.height = box.height;
+ } else {
+ // end of lesson container
+ coord.width = act.width();
+ coord.height = act.height();
+ }
+
+ coord.x2 = coord.x + coord.width;
+ coord.y2 = coord.y + coord.height;
+
+ if (x >= coord.x && x <= coord.x2 && y >= coord.y && y <= coord.y2) {
+ foundActivities.push(act);
+ }
+ });
+
+ $.each(foundActivities, function(){
+ if (this.hasClass('svg-activity-floating')) {
+ // no force complete to support activities
+ targetActivity = null;
+ return false;
+ }
+ // the enveloping OptionalActivity has priority
+ if (targetActivity == null || this.hasClass('svg-activity-optional')) {
+ targetActivity = this;
+ }
+ });
+
+ if (!targetActivity) {
+ return;
+ }
+
+ var targetActivityId = null,
+ executeForceComplete = false,
+ isEndLesson = !targetActivity.is('g'),
+ learnerNames = '';
+
+ if (!moveAll) {
+ $.each(learners, function(){
+ learnerNames += this.name + ', ';
+ });
+ learnerNames = '"' + learnerNames.slice(0, -2) + '"';
+ }
+
+
+ if (isEndLesson) {
+ if (currentActivityId) {
+ showConfirm(LABELS.FORCE_COMPLETE_END_LESSON_CONFIRM.replace('[0]', learnerNames), function() {
+ forceCompleteExecute(moveAll ? null : learners, moveAll ? currentActivityId : null, targetActivityId, false);
+ });
+ }
+ return;
+ }
+
+ var targetActivityId = +targetActivity.attr('id');
+ if (currentActivityId != targetActivityId) {
+ var targetActivityName = targetActivity.hasClass('svg-activity-gate') ? "Gate" : targetActivity.find('.svg-activity-title-label').text(),
+ moveBackwards = currentActivityId == null;
+
+ // check if target activity is before current activity
+ if (currentActivityId) {
+ $.ajax({
+ dataType : 'text',
+ url : LAMS_URL + 'monitoring/monitoring/isActivityPreceding.do',
+ async : false,
+ cache : false,
+ data : {
+ 'activityA' : targetActivityId,
+ 'activityB' : currentActivityId
+ },
+ success : function(response) {
+ moveBackwards = response == 'true';
+ }
+ });
+ }
+
+ // check if the target activity was found or we are moving the learner from end of lesson
+ if (moveBackwards) {
+ // move the learner backwards
+ var msgString = LABELS.FORCE_COMPLETE_REMOVE_CONTENT
+ .replace('[0]', learnerNames).replace('[1]', targetActivityName);
+ $('#forceBackwardsMsg', '#forceBackwardsDialog').html(msgString);
+ $('#forceBackwardsDialog').data({
+ 'learners' : learners,
+ 'currentActivityId' : currentActivityId,
+ 'activityId': targetActivityId});
+ $('#forceBackwardsDialog').modal('show');
+ return;
+ }
+
+ // move the learner forward
+ showConfirm(LABELS.FORCE_COMPLETE_ACTIVITY_CONFIRM.replace('[0]', learnerNames).replace('[1]', targetActivityName), function() {
+ forceCompleteExecute(moveAll ? null : learners, moveAll ? currentActivityId : null, targetActivityId, false);
+ });
+ }
+}
+
+
+/**
+ * Tell server to force complete the learner.
+ */
+function forceCompleteExecute(learners, moveAllFromActivityId, activityId, removeContent) {
+ var learnerIds = '';
+ if (learners) {
+ $.each(learners, function() {
+ learnerIds += this.id + ',';
+ });
+ learnerIds = learnerIds.slice(0, -1);
+ }
+
+ var data={
+ 'lessonID' : lessonId,
+ // either we list selected learners to move
+ // or we move all learners from the given activity
+ 'learnerID' : learnerIds,
+ 'moveAllFromActivityID' : moveAllFromActivityId,
+ 'activityID' : activityId,
+ 'removeContent' : removeContent
+ };
+ data[csrfTokenName] = csrfTokenValue;
+
+ $.ajax({
+ url : LAMS_URL + 'monitoring/monitoring/forceComplete.do',
+ type : 'POST',
+ dataType : 'text',
+ cache : false,
+ data : data,
+ success : function(response) {
+ // inform user of result
+ showToast(response);
+
+ // progress changed, show it to monitor
+ refreshMonitor();
+ }
+ });
+}
+
+
+/**
+ * Draw user and attention icons on top of activities.
+ */
+function addActivityIcons(activity) {
+ if (activity.learnerCount == 0 && !activity.requiresAttention) {
+ return;
+ }
+
+ // fint the activity in SVG
+ var coord = getActivityCoordinates(activity);
+ if (!coord) {
+ return;
+ }
+
+ // add group of users icon
+ var learningDesignSvg = $('svg.svg-learning-design', sequenceCanvas),
+ isTool = activity.type == 1,
+ isGrouping = activity.type == 2,
+ // branching and gates require extra adjustments
+ isBranching = [10,11,12,13].indexOf(activity.type) > -1,
+ isGate = [3,4,5,14].indexOf(activity.type) > -1,
+ isContainer = [6,7].indexOf(activity.type) > -1,
+ activityGroup = $('g[id="' + activity.id + '"]', learningDesignSvg),
+ requiresAttentionIcon = activity.requiresAttention ?
+ $('
![]()
')
+ .attr({
+ 'id' : 'act' + activity.id + 'attention',
+ 'src' : LAMS_URL + 'images/exclamation.svg',
+ 'title' : LABELS.CONTRIBUTE_ATTENTION
+ })
+ .addClass('activity-requires-attention')
+ : null,
+ allLearnersIcon = activity.learnerCount > 0 ?
+ $('
')
+ .attr('id', 'act' + activity.id + 'learnerGroup')
+ .addClass('more-learner-icon')
+ : null;
+
+
+ if (isTool || isGrouping) {
+ if (activity.learnerCount > 0) {
+ // if learners reached the activity, make room for their icons: make activity icon and label smaller and move to top
+ $('svg', activityGroup).attr({
+ 'x' : coord.x + 20,
+ 'y' : coord.y + 3,
+ 'width' : '30px',
+ 'height': '30px'
+ });
+
+ // switch from wide banner to narrow one
+ $('.svg-tool-banner-narrow', activityGroup).show();
+ $('.svg-tool-banner-wide', activityGroup).hide();
+
+ $('.svg-activity-title-label', activityGroup).parent('foreignObject').remove();
+ $('
').text(activity.title.length < 20 ? activity.title : activity.title.substring(0, 20) + '...')
+ .attr({
+ 'x' : coord.x + 55,
+ 'y' : coord.y + 20
+ })
+ .addClass('svg-activity-title-label svg-activity-title-label-small')
+ .appendTo(activityGroup);
+
+ var learnersContainer = $('').addClass('learner-icon-container');
+ $('').append(learnersContainer).appendTo(activityGroup).attr({
+ 'x' : coord.x + 20,
+ 'y' : coord.y + 40,
+ 'width' : 184,
+ 'height' : 40
+ });
+
+ $.each(activity.learners, function(learnerIndex, learner){
+ if (learnerIndex >= 5 && activity.learnerCount > 6) {
+ return false;
+ }
+ $(definePortrait(learner.portraitId, learner.id, STYLE_SMALL, true, LAMS_URL))
+ .css({
+ 'left' : learnerIndex * (activity.learnerCount < 5 ? 46 : 28) + 'px',
+ 'z-index' : 100 + learnerIndex,
+ 'padding-top' : '2px'
+ })
+ .addClass('new-popover learner-icon')
+ .attr({
+ 'id' : 'act' + activity.id + 'learner' + learner.id,
+ 'data-id' : 'popover-' + learner.id,
+ 'data-toggle' : 'popover',
+ 'data-portrait' : learner.portraitId,
+ 'data-fullname' : getLearnerDisplayName(learner)
+ })
+ .appendTo(learnersContainer);
+ });
+
+ if (activity.learnerCount > 6) {
+ allLearnersIcon
+ .css({
+ 'left' : '140px',
+ 'z-index' : 108,
+ 'margin-top' : '1px'
+ })
+ .text('+' + (activity.learnerCount - 5))
+ .appendTo(learnersContainer);
+ }
+ }
+
+ if (requiresAttentionIcon) {
+ $('').append(requiresAttentionIcon).appendTo(activityGroup).attr({
+ 'x' : coord.x + 180,
+ 'y' : coord.y - 1,
+ 'width' : 20,
+ 'height' : 20
+ });
+ }
+ } else if (isGate) {
+ if (activity.learnerCount > 0) {
+ $('').append(allLearnersIcon).appendTo(activityGroup).attr({
+ 'x' : coord.x + 20,
+ 'y' : coord.y + 20,
+ 'width' : 40,
+ 'height' : 40
+ });
+ allLearnersIcon.text(activity.learnerCount);
+ }
+
+ if (requiresAttentionIcon) {
+ $('').append(requiresAttentionIcon).appendTo(activityGroup).attr({
+ 'x' : coord.x + 25,
+ 'y' : coord.y - 5,
+ 'width' : 20,
+ 'height' : 20
+ });
+ }
+ } else if (isBranching) {
+ if (activity.learnerCount > 0) {
+ $('').append(allLearnersIcon).appendTo(activityGroup).attr({
+ 'x' : coord.x,
+ 'y' : coord.y,
+ 'width' : 40,
+ 'height' : 40
+ });
+ allLearnersIcon.text(activity.learnerCount);
+ }
+
+ if (requiresAttentionIcon) {
+ $('').append(requiresAttentionIcon).appendTo(activityGroup).attr({
+ 'x' : coord.x + 8,
+ 'y' : coord.y - 28,
+ 'width' : 20,
+ 'height' : 20
+ });
+ }
+ } else if (isContainer) {
+ if (activity.learnerCount > 0) {
+ $('').append(allLearnersIcon).appendTo(activityGroup).attr({
+ 'x' : coord.x + coord.width - 20,
+ 'y' : coord.y - 17,
+ 'width' : 40,
+ 'height' : 40
+ });
+ allLearnersIcon.text(activity.learnerCount);
+ }
+
+ if (requiresAttentionIcon) {
+ $('').append(requiresAttentionIcon).appendTo(activityGroup).attr({
+ 'x' : coord.x,
+ 'y' : coord.y + 15,
+ 'width' : 20,
+ 'height' : 20
+ });
+ }
+ }
+}
+
+
+/**
+ * After SVG refresh, add click/dblclick/drag handlers to icons.
+ */
+function addActivityIconsHandlers(activity) {
+ if (activity.learnerCount == 0 && !activity.requiresAttention) {
+ return;
+ }
+
+ // gate activity does not allows users' view
+ var usersViewable = [3,4,5,14].indexOf(activity.type) == -1;
+
+ if (activity.learners){
+ $.each(activity.learners, function(learnerIndex, learner){
+ let learnerIcon = $('div#act' + activity.id + 'learner' + learner.id, sequenceCanvas)
+ .css('cursor', 'pointer')
+ // drag learners to force complete activities
+ .draggable({
+ 'appendTo' : '.svg-learner-draggable-area',
+ 'containment' : '.svg-learner-draggable-area',
+ 'distance' : 20,
+ 'scroll' : false,
+ 'cursorAt' : {'left' : 10, 'top' : 15},
+ 'helper' : function(){
+ return learnerIcon.clone();
+ },
+ 'stop' : function(event, ui) {
+ var learners = [{
+ 'id' : learner.id,
+ 'name' : getLearnerDisplayName(learner, true)
+ }];
+ // jQuery droppable does not work for SVG, so this is a workaround
+ forceComplete(activity.id, learners, ui.offset.left, ui.offset.top);
+ }
+ });
+
+ if (usersViewable) {
+ dblTap(learnerIcon, function(event){
+ // double click on learner icon to see activity from his perspective
+ var url = LAMS_URL + 'monitoring/monitoring/getLearnerActivityURL.do?userID='
+ + learner.id + '&activityID=' + activity.id + '&lessonID=' + lessonId;
+ openPopUp(url, "LearnActivity", popupHeight, popupWidth, true);
+ });
+ }
+
+ if (learner.id == sequenceSearchedLearner){
+ // do it here instead of addActivityIcons()
+ // as in that method the icons are added to the document yet
+ // and they have no offset for calculations
+ highlightSearchedLearner(learnerIcon);
+ }
+ });
+ }
+
+ if (activity.learnerCount > 0){
+ $('#act' + activity.id + 'learnerGroup', sequenceCanvas)
+ .click(function(event){
+ // double click on learner group icon to see list of learners
+ var ajaxProperties = {
+ url : LAMS_URL + 'monitoring/monitoring/getCurrentLearners.do',
+ data : {
+ 'activityID' : activity.id
+ }
+ };
+ showLearnerGroupDialog(ajaxProperties, activity.title, false, true, usersViewable, false);
+ });
+ }
+
+ if (activity.requiresAttention){
+ $('#act' + activity.id + 'attention', sequenceCanvas).click(function(event){
+ event.stopPropagation();
+ // switch to first tab where attention prompts are listed
+ if ($('#tblmonitor-tab-content').length == 0) {
+ // wer are in regular monitor, so switch to first tab to perform tasks
+ doSelectTab(1);
+ } else {
+ // we are in TBL mode, so switch back to regular monitor to perform tasks
+ switchToRegularMonitor(true);
+ }
+
+ });
+ }
+}
+
+
+/**
+ * Add learner icons in "finished lesson" bar.
+ */
+function addCompletedLearnerIcons(learners, learnerCount, learnerTotalCount) {
+ var iconsContainer = $('#completedLearnersContainer');
+
+ if (learners) {
+ // create learner icons, along with handlers
+ $.each(learners, function(learnerIndex, learner){
+ if (learnerIndex >= 23) {
+ // display only first few learners, not all of them
+ return false;
+ }
+
+ let icon = $(definePortrait(learner.portraitId, learner.id, STYLE_SMALL, true, LAMS_URL))
+ .addClass('new-popover learner-icon')
+ .attr({
+ 'id' : 'learner-complete-' + learner.id,
+ 'data-id' : 'popover-' + learner.id,
+ 'data-toggle' : 'popover',
+ 'data-portrait' : learner.portraitId,
+ 'data-fullname' : getLearnerDisplayName(learner)
+ })
+ // drag learners to force complete activities
+ .draggable({
+ 'appendTo' : '.svg-learner-draggable-area',
+ 'containment' : '.svg-learner-draggable-area',
+ 'distance' : 20,
+ 'scroll' : false,
+ 'cursorAt' : {'left' : 10, 'top' : 15},
+ 'helper' : function(){
+ // copy of the icon for dragging
+ return icon.clone();
+ },
+ 'stop' : function(event, ui) {
+ var learners = [{
+ 'id' : learner.id,
+ 'name' : getLearnerDisplayName(learner, true)
+ }];
+ // jQuery droppable does not work for SVG, so this is a workaround
+ forceComplete(null, learners, ui.offset.left, ui.offset.top);
+ }
+ })
+ .appendTo(iconsContainer);
+
+ if (learner.id == sequenceSearchedLearner){
+ highlightSearchedLearner(icon);
+ }
+ });
+
+
+ $('')
+ .addClass('more-learner-icon')
+ .text(learnerCount + '/' + learnerTotalCount)
+ .appendTo(iconsContainer)
+ .click(function(){
+ var ajaxProperties = {
+ url : LAMS_URL + 'monitoring/monitoring/getCurrentLearners.do',
+ data : {
+ 'lessonID' : lessonId
+ }
+ };
+ showLearnerGroupDialog(ajaxProperties, LABELS.LEARNER_FINISHED_DIALOG_TITLE, false, true, false, false);
+ });
+ }
+}
+
+
+/**
+ * Extracts activity using SVG attributes.
+ */
+function getActivityCoordinates(activity){
+ // fix missing coordinates, not set by Flash Authoring
+ if (!activity.x) {
+ activity.x = 0;
+ }
+ if (!activity.y) {
+ activity.y = 0;
+ }
+
+ var group = $('g[id="' + activity.id + '"]', sequenceCanvas);
+ if (group.length == 0) {
+ return;
+ }
+
+ return {
+ 'x' : +group.data('x'),
+ 'y' : +group.data('y'),
+ 'x2' : +group.data('x') + +group.data('width'),
+ 'y2' : +group.data('y') + +group.data('height'),
+ 'width' : +group.data('width'),
+ 'height': +group.data('height'),
+ }
+
+}
+
+
+/**
+ * Shows where the searched learner is.
+ */
+function highlightSearchedLearner(icon) {
+ // show the "clear" button
+ $('#sequenceSearchPhraseButton').prop('disabled', false);
+ $('#sequenceSearchPhraseIcon').hide();
+ $('#sequenceSearchPhraseClearIcon').show();
+
+ // border and z-index are manipulated via CSS
+ icon.addClass('learner-searched');
+
+ toggleInterval = setInterval(function(){
+ icon.toggle();
+ }, 500);
+
+ setTimeout(function(){
+ clearInterval(toggleInterval);
+ //if the search box was cleared during blinking, act accordingly
+ if (!sequenceSearchedLearner) {
+ icon.removeClass('learner-searched');
+ }
+ }, 3000);
+}
+
+
+/**
+ * Cancels the performed search.
+ */
+function sequenceClearSearchPhrase(refresh){
+ $('#sequenceSearchPhrase').val('');
+ $('#sequenceSearchPhraseButton').prop('disabled', true);
+ $('#sequenceSearchPhraseClearIcon').hide();
+ $('#sequenceSearchPhraseIcon').show();
+ $('#sequenceSearchedLearnerHighlighter').hide();
+ sequenceSearchedLearner = null;
+ if (refresh) {
+ refreshMonitor();
+ }
+}
+
+
+/**
+ * Shows Edit Class dialog for class manipulation.
+ */
+function showClassDialog(role){
+ // fetch available and already participating learners and monitors
+ if (!role) {
+ // first time show, fill both lists
+ fillClassList('Learner', false);
+ fillClassList('Monitor', true);
+
+ $('#classDialog').modal('show');
+ } else {
+ // refresh after page shift or search
+ fillClassList(role, role.toLowerCase() == 'monitor');
+ }
+}
+
+
+/**
+ * Fills class member list with user information.
+ */
+function fillClassList(role, disableCreator) {
+ var dialog = $('#classDialog'),
+ table = $('#class' + role + 'Table', dialog),
+ list = $('.dialogList', table).empty(),
+ searchPhrase = role == 'Learner' ? $('.dialogSearchPhrase', table).val().trim() : null,
+ ajaxProperties = dialog.data(role + 'AjaxProperties'),
+ users = null,
+ userCount = null;
+
+ if (!ajaxProperties) {
+ // initialise ajax config
+ ajaxProperties = {
+ dataType : 'json',
+ url : LAMS_URL + 'monitoring/monitoring/getClassMembers.do',
+ cache : false,
+ async : false,
+ data : {
+ 'lessonID' : lessonId,
+ 'role' : role.toUpperCase(),
+ 'pageNumber' : 1,
+ 'orderAscending' : true
+ }
+ };
+
+ dialog.data(role + 'AjaxProperties', ajaxProperties);
+ }
+
+ // add properties for this call only
+ if (!searchPhrase){
+ searchPhrase = null;
+ }
+ ajaxProperties.data.searchPhrase = searchPhrase;
+ ajaxProperties.success = function(response) {
+ users = response.users;
+ userCount = response.userCount;
+}
+
+ $.ajax(ajaxProperties);
+
+ // hide unnecessary controls
+ togglePagingCells(table, ajaxProperties.data.pageNumber, Math.ceil(userCount / 10));
+
+ $.each(users, function(userIndex, user) {
+ var checkboxId = 'class-list-' + role + '-' + user.id,
+ checkbox = $('').attr({
+ 'type' : 'checkbox',
+ 'id' : checkboxId
+ }).addClass('form-check-input me-1')
+ .change(function(){
+ editClassMember($(this));
+ }),
+
+ userDiv = $('').attr({
+ 'userId' : user.id
+ })
+ .addClass('dialogListItem')
+ .prepend($('').addClass('form-check-label').attr('for', checkboxId).text(getLearnerDisplayName(user)))
+ .prepend(checkbox)
+ .appendTo(list);
+
+ if (user.classMember) {
+ checkbox.prop('checked', 'checked');
+ if (user.readonly) {
+ // user creator must not be deselected
+ checkbox.attr('disabled', 'disabled');
+ }
+ }
+
+ if (disableCreator && user.lessonCreator) {
+ userDiv.addClass('dialogListItemDisabled');
+ } else {
+ userDiv.click(function(event){
+ if (event.target == this && !checkbox.is(':disabled')) {
+ checkbox.prop('checked', checkbox.is(':checked') ? null : 'checked');
+ checkbox.change();
+ }
+ })
+ }
+ });
+
+ colorDialogList(table);
+}
+
+/**
+ * Adds/removes a Learner/Monitor to/from the class.
+ */
+function editClassMember(userCheckbox){
+ var data={
+ 'lessonID' : lessonId,
+ 'userID' : userCheckbox.parent().attr('userId'),
+ 'role' : userCheckbox.closest('table').is('#classMonitorTable') ? 'MONITOR' : 'LEARNER',
+ 'add' : userCheckbox.is(':checked')
+ };
+ data[csrfTokenName] = csrfTokenValue;
+
+ $.ajax({
+ url : LAMS_URL + 'monitoring/monitoring/updateLessonClass.do',
+ type : 'POST',
+ cache : false,
+ data : data
+ });
+}
+
+/**
+ * Adds all learners to the class.
+ */
+function addAllLearners(){
+ showConfirm(LABELS.CLASS_ADD_ALL_CONFIRM, function() {
+ $.ajax({
+ url : LAMS_URL + 'monitoring/monitoring/addAllOrganisationLearnersToLesson.do',
+ type : 'POST',
+ cache : false,
+ data : {
+ 'lessonID' : lessonId
+ },
+ success : function(){
+ showToast(LABELS.CLASS_ADD_ALL_SUCCESS);
+ $('#classDialog').modal('hide');
+ }
+ });
+ });
+}
+
+/**
+ * Opens Authoring for live edit.
+ */
+function openLiveEdit(){
+ showConfirm(LABELS.LIVE_EDIT_CONFIRM, function() {
+ $.ajax({
+ dataType : 'text',
+ url : LAMS_URL + 'monitoring/monitoring/startLiveEdit.do',
+ cache : false,
+ async : false,
+ data : {
+ 'ldId' : ldId
+ },
+ success : function(response) {
+ if (response) {
+ showToast(response);
+ } else {
+ openAuthoring(ldId, lessonId);
+ }
+ }
+ });
+ });
+}
+
+/**
+ * Adjusts sequence canvas (SVG) based on space available in the dialog.
+ */
+function resizeSequenceCanvas(width, height){
+ var svg = $('svg.svg-learning-design', sequenceCanvas);
+
+ if (svg.length === 0){
+ // skip resizing if the SVG has not loaded (yet)
+ return;
+ }
+
+ var viewBoxParts = svg.attr('viewBox').split(' '),
+ svgHeight = +viewBoxParts[3],
+ sequenceCanvasHeight = learningDesignSvgFitScreen ? height - 140 : Math.max(svgHeight + 10, height - 140);
+
+ // By default sequenceCanvas div is as high as SVG, but for SVG vertical centering
+ // we want it to be as large as available space (iframe height minus toolbars)
+ // or if SVG is higher, then as high as SVG
+ sequenceCanvas.css({
+ 'height' : sequenceCanvasHeight + 'px'
+ });
+
+ // if we want SVG to fit screen, then we make it as wide & high as sequenceCanvas div
+ // but no more than extra 30% because small SVGs look weird if they are too large
+ if (learningDesignSvgFitScreen) {
+ var svgWidth = +viewBoxParts[2],
+ sequenceCanvasWidth = sequenceCanvas.width();
+ svg.attr({
+ 'preserveAspectRatio' : 'xMidYMid meet',
+ 'width' : Math.min(svgWidth * 1.3, sequenceCanvasWidth - 10),
+ 'height': Math.min(svgHeight * 1.3, sequenceCanvasHeight - 10)
+ })
+ }
+}
+
+function canvasFitScreen(fitScreen, skipResize) {
+ learningDesignSvgFitScreen = fitScreen;
+ if (!skipResize) {
+ updateSequenceTab();
+ }
+
+ if (fitScreen) {
+ $('#canvasFitScreenButton').hide();
+ $('#canvasOriginalSizeButton').show();
+ } else {
+ $('#canvasFitScreenButton').show();
+ $('#canvasOriginalSizeButton').hide();
+ }
+}
+
+
+/**
+ * Refreshes the existing progress bars.
+ */
+function updateLearnersTab(){
+ let learnersAccordion = $('#learners-accordion').empty(),
+ itemTemplate = $('.learners-accordion-item-template').clone().removeClass('learners-accordion-item-template d-none');
+
+ $.ajax({
+ 'url' : LAMS_URL + 'monitoring/monitoring/getLearnerProgressPage.do',
+ 'data': {
+ lessonID: lessonId
+ },
+ 'dataType' : 'json',
+ 'success' : function(response) {
+ let learnerProgressSource = null;
+
+ $(response.learners).each(function(){
+ let learner = this,
+ itemHeaderId = 'learners-accordion-heading-' + learner.id,
+ itemCollapseId = 'learners-accordion-collapse-' + learner.id,
+ item = itemTemplate.clone().data('user-id', learner.id).attr('id', 'learners-accordion-item-' + learner.id).appendTo(learnersAccordion),
+ portraitSmall = $(definePortrait(learner.portraitId, learner.id, STYLE_SMALL, true, LAMS_URL)).addClass('me-2'),
+ portraitLarge = learner.portraitId ? $(definePortrait(learner.portraitId, learner.id, STYLE_LARGE, false, LAMS_URL)) : null;
+
+
+ $('.accordion-header', item).attr('id', itemHeaderId)
+ .find('.accordion-button')
+ .attr('data-bs-target', '#' + itemCollapseId)
+ .attr('aria-controls', itemCollapseId)
+ .addClass(sequenceSearchedLearner == learner.id ? 'bg-primary' : '')
+ .html('' + learner.firstName + ' ' + learner.lastName + '')
+ .prepend(portraitSmall);
+ $('.learners-accordion-name', item).text(learner.firstName + ' ' + learner.lastName);
+ $('.learners-accordion-login', item).html('' + learner.login);
+ $('.learners-accordion-email', item).html('' + learner.email);
+ if (portraitLarge) {
+ $('.learners-accordion-portrait', item).append(portraitLarge);
+ }
+
+ $('.accordion-collapse', item).attr('id', itemCollapseId).attr('data-bs-parent', '#learners-accordion')
+ .on('show.bs.collapse', function () {
+ if (learnerProgressSource) {
+ try {
+ learnerProgressSource.close();
+ } catch(e) {
+ console.error(e);
+ }
+ }
+
+ let learnerId = $(this).closest('.accordion-item').data('user-id');
+ learnerProgressSource = new EventSource(LAMS_URL + 'learning/learner/getLearnerProgressUpdateFlux.do?lessonId='
+ + lessonId + '&userId=' + learnerId);
+
+ learnerProgressSource.onmessage = function (event) {
+ if ($('#learners-accordion-item-' + learnerId).length === 1) {
+ drawLearnerTimeline(learnerId, event.data);
+ }
+ }
+ });
+ });
+ }
+ });
+}
+
+function drawLearnerTimeline(learnerId, data) {
+ let item = $('#learners-accordion-item-' + learnerId),
+ timelineContainer = $('.vertical-timeline-container', item),
+ timeline = $('.vertical-timeline', timelineContainer).empty(),
+ noProgressLabel = $('.no-progress', item);
+
+ if (!data) {
+ noProgressLabel.show();
+ return;
+ }
+ noProgressLabel.hide();
+ data = JSON.parse(data);
+ let activityEntryTemplate = $('.learners-timeline-entry-template').clone().removeClass('learners-timeline-entry-template d-none');
+
+ $(data.activities).each(function(){
+ let activity = this,
+ entry = activityEntryTemplate.clone().appendTo(timeline),
+ icon = $('.timeline-icon', entry),
+ iconURL = null,
+ durationCell = $('.timeline-activity-duration', entry),
+ markCell = $('.timeline-activity-mark', entry);
+
+ $('.timeline-title', entry).text(activity.name);
+
+ switch(activity.status){
+ case 0: icon.addClass('border-primary activity-current');break;
+ case 1: icon.addClass('border-success activity-complete');break;
+ }
+
+ if (activity.iconURL) {
+ iconURL = activity.iconURL;
+ } else if (activity.type === 'g') {
+ iconURL = 'images/svg/gateClosed.svg';
+ } else if (activity.type === 'o') {
+ iconURL = 'images/svg/branchingStart.svg';
+ } else if (activity.isGrouping) {
+ iconURL = 'images/svg/grouping.svg';
+ }
+
+ if (iconURL) {
+ $('
').attr('src', LAMS_URL + iconURL).appendTo(icon);
+ }
+
+ if (typeof activity.mark !== 'undefined') {
+ markCell.text(activity.mark + (activity.maxMark ? ' / ' + activity.maxMark : ''));
+ } else {
+ markCell.closest('tr').remove();
+ }
+
+ if (activity.duration) {
+ durationCell.text(activity.duration);
+ } else {
+ durationCell.closest('tr').remove();
+ }
+ });
+
+ timelineContainer.show();
+}
+
+/**
+ * Clears previous run search for phrase, in Learners tab.
+ */
+function learnersClearSearchPhrase(){
+ $('#learnersSearchPhrase').val('').autocomplete("close");
+ loadLearnerProgressPage(1, '');
+ $('#learnersSearchPhraseClear').hide();
+}
+
+/**
+ * Clears previous run search for phrase, in Edit Class dialog.
+ */
+function classClearSearchPhrase(){
+ var dialog = $('#classDialog');
+ $('.dialogSearchPhrase', dialog).val('').autocomplete("close");
+ dialog.data('LearnerAjaxProperties').data.pageNumber = 1;
+ showClassDialog('Learner');
+ $('.dialogSearchPhraseClear', dialog).css('visibility', 'hidden');
+}
+
+
+/**
+ * Clears previous run search for phrase, in Learner Group dialogs.
+ */
+function learnerGroupClearSearchPhrase(){
+ var dialog = $('#learnerGroupDialog');
+ $('.dialogSearchPhrase', dialog).val('').autocomplete("close");
+ dialog.data('ajaxProperties').data.pageNumber = 1;
+ showLearnerGroupDialog();
+ $('.dialogSearchPhraseClear', dialog).css('visibility', 'hidden');
+}
+//********** GRADEBOOK TAB FUNCTIONS **********
+
+/**
+ * Inits Gradebook Tab.
+ */
+function initGradebookTab() {
+ /*
+ $.extend(true, $.jgrid.icons, {
+ fontAwesome6: $.extend(true, {}, $.jgrid.icons.fontAwesome, {
+ nav: { del: "fa-times" },
+ actions: { del: "fa-times" },
+ form: { del: "fa-times" }
+ })
+ $.extend(true, $.jgrid.icons.fontAwesome, {
+ common : "fa-solid"
+ });
+ });
+ */
+
+ $.extend(true, $.jgrid.guiStyles.bootstrap4, {
+ pager : {
+ pagerSelect : 'form-control-select'
+ },
+ searchToolbar : {
+ clearButton : 'btn btn-sm'
+ }
+ });
+
+ const gradebooksUpdateSource = new EventSource(LAMS_URL + 'monitoring/monitoring/getGradebookUpdateFlux.do?lessonId=' + lessonId);
+ gradebooksUpdateSource.onmessage = function (event) {
+ if ("doRefresh" == event.data && $('#gradebookDiv').length === 1){
+ let expandedGridIds = [],
+ userGrid = $('#userView'),
+ activityGrid = $('#activityView');
+ // do not update if grid is being edited by the teacher
+ if (userGrid.data('isCellEdited') === true || activityGrid.data('isCellEdited') === true) {
+ return;
+ }
+
+ $("tr:has(.sgexpanded)", userGrid).each(function () {
+ let num = $(this).attr('id');
+ expandedGridIds.push(num);
+ });
+ userGrid.data('expandedGridIds', expandedGridIds).trigger("reloadGrid");
+
+ expandedGridIds = [];
+ $("tr:has(.sgexpanded)", activityGrid).each(function () {
+ let num = $(this).attr('id');
+ expandedGridIds.push(num);
+ });
+ activityGrid.data('expandedGridIds', expandedGridIds).trigger("reloadGrid");
+ }
+ };
+}
+
+/**
+ * Refreshes Gradebook Tab.
+ */
+function updateGradebookTab() {
+ $("#gradebookLoading").show();
+ $("#gradebookDiv").load(LAMS_URL + 'gradebook/gradebookMonitoring.do?isTab=true&lessonID=' + lessonId, function() {
+ $("#gradebookLoading").hide();
+ });
+}
+
+function fixPagerInCenter(pagername, numcolshift) {
+ $('#'+pagername+'_right').css('display','inline');
+ if ( numcolshift > 0 ) {
+ var marginshift = - numcolshift * 12;
+ $('#'+pagername+'_center table').css('margin-left', marginshift+'px');
+ }
+}
+
+
+var popupWidth = 1280,
+ popupHeight = 720;
+
+// launches a popup from the page
+function launchPopup(url,title) {
+ var wd = null;
+ if(wd && wd.open && !wd.closed){
+ wd.close();
+ }
+
+ var left = ((screen.width / 2) - (popupWidth / 2));
+ var top = ((screen.height / 2) - (popupHeight / 2));
+
+ wd = window.open(url,title,'resizable,width='+popupWidth+',height='+popupHeight
+ +',scrollbars'
+ + ",top=" + top + ",left=" + left);
+ wd.window.focus();
+}
+
+/* gradebook dialog windows on the ipad do not update the grid width properly using setGridWidth. Calling this is
+-- setting the grid to parentWidth-1 and the width of the parent to parentWidth+1, leading to growing width window
+-- that overflows the dialog window. Keep the main grids slightly smaller than their containers and all is well.
+*/
+
+function resizeJqgrid(jqgrids) {
+ jqgrids.each(function(index) {
+ var gridId = $(this).attr('id');
+ var parent = jQuery('#gbox_' + gridId).parent();
+ var gridParentWidth = parent.width();
+ if ( parent.hasClass('grid-holder') ) {
+ gridParentWidth = gridParentWidth - 2;
+ }
+ jQuery('#' + gridId).setGridWidth(gridParentWidth, true);
+ });
+}
+
+
+/* Based on jqgrid internal functions */
+function displayCellErrorMessage(table, iRow, iCol, errorLabel, errorMessage, buttonText ) {
+ setTimeout(function () {
+ try {
+ var frozenRows = table.grid.fbRows,
+ tr = table.rows[iRow];
+ tr = frozenRows != null && frozenRows[0].cells.length > iCol ? frozenRows[tr.rowIndex] : tr;
+ var td = tr != null && tr.cells != null ? $(tr.cells[iCol]) : $(),
+ rect = td[0].getBoundingClientRect();
+ $.jgrid.info_dialog.call(table, errorLabel, errorMessage, buttonText, {left:rect.left-200, top:rect.top});
+ } catch (e) {
+ alert(errorMessage);
+ }
+ }, 50);
+}
+
+function blockExportButton(areaToBlock, exportExcelUrl) {
+ var token = new Date().getTime(),
+ area = $('#' + areaToBlock).css('cursor', 'wait'),
+ buttons = $('.btn', area).prop('disabled', true),
+ form = $('');
+
+ fileDownloadCheckTimer = window.setInterval(function () {
+ var cookieValue = $.cookie('fileDownloadToken');
+ if (cookieValue == token) {
+ //unBlock export button
+ window.clearInterval(fileDownloadCheckTimer);
+ $.cookie('fileDownloadToken', null); //clears this cookie value
+
+ area.css('cursor', 'auto');
+ buttons.prop('disabled', false);
+ form.remove();
+ }
+ }, 1000);
+
+ //dynamically create a form and submit it
+ form.attr("method", "post");
+ form.attr("action", exportExcelUrl);
+
+ var hiddenField = $('');
+ hiddenField.attr("type", "hidden");
+ hiddenField.attr("name", "downloadTokenValue");
+ hiddenField.attr("value", token);
+ form.append(hiddenField);
+
+ // The form needs to be a part of the document in order to be submitted
+ $(document.body).append(form);
+ form.submit();
+
+ return false;
+}
+
+//********** COMMON FUNCTIONS **********
+
+
+// generic function for opening a pop up
function openPopUp(url, title, h, w, status, forceNewWindow) {
var width = screen.width;
@@ -146,4 +2675,379 @@
+ ",resizable=yes,scrollbars=yes,status=" + status
+ ",menubar=no, toolbar=no"
+ ",top=" + top + ",left=" + left);
-}
\ No newline at end of file
+}
+
+/**
+ * Updates all changeable elements of monitoring screen.
+ */
+function refreshMonitor(){
+ let tabName = 'sequence';
+ if ($('#learners-accordion').length === 1) {
+ tabName = 'learners';
+ } else if ($('#gradebookDiv').length === 1){
+ tabName = 'gradebook';
+ }
+
+ if (tabName == 'sequence'){
+ updateSequenceTab();
+ } else if (tabName == 'learners'){
+ updateLearnersTab();
+ } else if (tabName == 'gradebook'){
+ updateGradebookTab();
+ }
+}
+
+
+/**
+ * Tells parent document to close this Monitor dialog.
+ */
+function closeMonitorLessonDialog(refresh) {
+ //check method is available in order to avoid JS errors (as it's not available from integrations)
+ if (typeof window.parent.closeDialog === "function") {
+ window.parent.closeDialog('dialogMonitorLesson' + lessonId, refresh);
+ }
+}
+
+/**
+ * Show a dialog with user list and optional Force Complete and View Learner buttons.
+ */
+function showLearnerGroupDialog(ajaxProperties, dialogTitle, allowSearch, allowForceComplete, allowView, allowEmail) {
+ var learnerGroupDialog = $('#learnerGroupDialog'),
+ learnerGroupList = $('.dialogList', learnerGroupDialog).empty(),
+ // no parameters provided? just work on what we saved
+ isRefresh = ajaxProperties == null,
+ learners = null,
+ learnerCount = null;
+
+ if (isRefresh) {
+ // ajax and other properties were saved when the dialog was opened
+ ajaxProperties = learnerGroupDialog.data('ajaxProperties');
+ allowForceComplete = learnerGroupDialog.data('allowForceComplete');
+ allowView = learnerGroupDialog.data('allowView');
+ allowEmail = learnerGroupDialog.data('allowEmail');
+ allowSearch = $('#learnerGroupSearchRow', learnerGroupDialog).is(':visible');
+ } else {
+ // add few standard properties to ones provided by method calls
+ ajaxProperties = $.extend(true, ajaxProperties, {
+ dataType : 'json',
+ cache : false,
+ async : false,
+ data : {
+ 'pageNumber' : 1,
+ 'orderAscending' : true
+ }
+ });
+
+ $('#learnerGroupSearchRow', learnerGroupDialog).css('display', allowSearch ? 'table-row' : 'none');
+ }
+
+ var pageNumber = ajaxProperties.data.pageNumber;
+
+ // set values for current variable instances
+ ajaxProperties.success = function(response) {
+ learners = response.learners;
+ learnerCount = response.learnerCount;
+ };
+
+ var searchPhrase = allowSearch ? $('.dialogSearchPhrase', learnerGroupDialog).val().trim() : null;
+ ajaxProperties.data.searchPhrase = searchPhrase;
+
+ // make the call
+ $.ajax(ajaxProperties);
+
+ // did all users already drift away to an another activity or there was an error?
+ // close the dialog and refresh the main screen
+ if (!learnerCount && !searchPhrase) {
+ if (isRefresh) {
+ learnerGroupDialog.modal('hide');
+ }
+ updateSequenceTab();
+ return;
+ }
+
+ // did some users already drift away to an another activity?
+ // move back until you get a page with any users
+ var maxPageNumber = Math.ceil(learnerCount / 10);
+ if (maxPageNumber > 0 && pageNumber > maxPageNumber) {
+ shiftLearnerGroupList(-1);
+ return;
+ }
+
+ // hide unnecessary controls
+ togglePagingCells(learnerGroupDialog, pageNumber, maxPageNumber);
+
+ $.each(learners, function(learnerIndex, learner) {
+ var viewUrl = allowView ? LAMS_URL + 'monitoring/monitoring/getLearnerActivityURL.do?userID='
+ + learner.id + '&activityID=' + ajaxProperties.data.activityID + '&lessonID=' + lessonId
+ : null,
+ learnerDiv = $('').attr({
+ 'userId' : learner.id,
+ 'viewUrl' : viewUrl
+ })
+ .addClass('dialogListItem')
+ .appendTo(learnerGroupList),
+ portraitDiv = $('').attr({
+ 'id': 'user-'+learner.id,
+ })
+ .addClass('roffset5')
+ .appendTo(learnerDiv);
+ addPortrait( portraitDiv, learner.portraitId, learner.id, 'small', true, LAMS_URL );
+ $('').html(getLearnerDisplayName(learner))
+ .addClass('portrait-sm-lineheight')
+ .appendTo(learnerDiv);
+
+ if (allowForceComplete || allowView || allowEmail) {
+ learnerDiv.click(function(event){
+ // select the learner
+ var learnerDiv = $(this),
+ selectedSiblings = learnerDiv.siblings('div.dialogListItem.dialogListItemSelected');
+ // enable buttons
+ $('button.learnerGroupDialogSelectableButton', learnerGroupDialog).prop('disabled', false);
+
+ if (allowForceComplete && (event.metaKey || event.ctrlKey)) {
+ var isSelected = learnerDiv.hasClass('dialogListItemSelected');
+ if (isSelected) {
+ // do not un-select last learner
+ if (selectedSiblings.length > 0) {
+ learnerDiv.removeClass('dialogListItemSelected')
+ }
+ } else {
+ learnerDiv.addClass('dialogListItemSelected');
+ }
+ if (selectedSiblings.length + (isSelected ? 0 : 1) > 1) {
+ // disable view button - only one learner can be viewed and multiple are selected
+ $('button#learnerGroupDialogViewButton', learnerGroupDialog).prop('disabled', true);
+ }
+ } else {
+ learnerDiv.addClass('dialogListItemSelected');
+ // un-select other learners
+ selectedSiblings.removeClass('dialogListItemSelected');
+ }
+ });
+ if (allowView){
+ dblTap(learnerDiv, function(){
+ // same as clicking View Learner button
+ openPopUp(viewUrl, "LearnActivity", popupHeight, popupWidth, true);
+ });
+ }
+ }
+ });
+
+ colorDialogList(learnerGroupDialog);
+
+ if (!isRefresh) {
+ // show buttons and labels depending on parameters
+ $('span#learnerGroupMultiSelectLabel, button#learnerGroupDialogForceCompleteButton, button#learnerGroupDialogForceCompleteAllButton', learnerGroupDialog)
+ .css('display', allowForceComplete ? 'inline' : 'none');
+ $('button#learnerGroupDialogViewButton', learnerGroupDialog)
+ .css('display', allowView ? 'inline' : 'none');
+ $('button#learnerGroupDialogEmailButton', learnerGroupDialog)
+ .css('display', allowEmail ? 'inline' : 'none');
+
+ $('.modal-title', learnerGroupDialog).text(dialogTitle);
+ learnerGroupDialog.data({
+ // save properties for refresh
+ 'ajaxProperties' : ajaxProperties,
+ 'allowForceComplete' : allowForceComplete,
+ 'allowView' : allowView,
+ 'allowEmail' : allowEmail
+ })
+ .modal('show');
+ }
+}
+
+
+/**
+ * Formats learner name.
+ */
+function getLearnerDisplayName(learner, raw) {
+ return raw ? learner.lastName + ', ' + learner.firstName + ' (' + learner.login + ')' + (learner.group ? ' - ' + learner.group : '')
+ : escapeHtml(learner.lastName) + ', ' + escapeHtml(learner.firstName) + ' (' + escapeHtml(learner.login) + ')'
+ + (learner.group ? ' - ' + escapeHtml(learner.group) : '');
+}
+
+
+/**
+ * Escapes HTML tags to prevent XSS injection.
+ */
+function escapeHtml(unsafe) {
+ if (unsafe == undefined) {
+ return "";
+ }
+
+ return unsafe
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+
+/**
+ * Change order of learner sorting in group dialog.
+ */
+function sortLearnerGroupList() {
+ var learnerGroupDialog = $('#learnerGroupDialog'),
+ sortIcon = $('td.sortCell span', learnerGroupDialog),
+ ajaxProperties = learnerGroupDialog.data('ajaxProperties'),
+ // reverse current order after click
+ orderAscending = !ajaxProperties.data.orderAscending;
+
+ if (orderAscending) {
+ sortIcon.removeClass('ui-icon-triangle-1-s').addClass('ui-icon-triangle-1-n');
+ } else {
+ sortIcon.removeClass('ui-icon-triangle-1-n').addClass('ui-icon-triangle-1-s');
+ }
+
+ ajaxProperties.data.orderAscending = orderAscending;
+ // refresh the list
+ showLearnerGroupDialog();
+ }
+
+/**
+ * Change order of learner sorting in Edit Class dialog.
+ */
+function sortClassList(role) {
+ var classDialog = $('#classDialog'),
+ table = $('#class' + role + 'Table', classDialog),
+ sortIcon = $('td.sortCell span', table),
+ ajaxProperties = classDialog.data(role + 'AjaxProperties'),
+ // reverse current order after click
+ orderAscending = !ajaxProperties.data.orderAscending;
+
+ if (orderAscending) {
+ sortIcon.removeClass('ui-icon-triangle-1-s').addClass('ui-icon-triangle-1-n');
+ } else {
+ sortIcon.removeClass('ui-icon-triangle-1-n').addClass('ui-icon-triangle-1-s');
+ }
+
+ ajaxProperties.data.orderAscending = orderAscending;
+ // refresh the list
+ showClassDialog(role);
+}
+
+/**
+ * Colours a list of users
+ */
+function colorDialogList(parent) {
+ $('.dialogList div.dialogListItem', parent).each(function(userIndex, userDiv){
+ // every odd learner has different background
+ $(userDiv).css('background-color', userIndex % 2 ? '#f5f5f5' : 'inherit');
+ });
+}
+
+/**
+* Change page in the group dialog.
+*/
+function shiftLearnerGroupList(shift) {
+ var learnerGroupDialog = $('#learnerGroupDialog'),
+ ajaxProperties = learnerGroupDialog.data('ajaxProperties'),
+ pageNumber = ajaxProperties.data.pageNumber + shift;
+ if (pageNumber < 0) {
+ pageNumber = 1;
+ }
+ ajaxProperties.data.pageNumber = pageNumber;
+ // refresh the dialog with new parameters
+ showLearnerGroupDialog();
+}
+
+/**
+* Change page in the Edit Class dialog.
+*/
+function shiftClassList(role, shift) {
+ var classDialog = $('#classDialog'),
+ ajaxProperties = classDialog.data(role + 'AjaxProperties'),
+ pageNumber = ajaxProperties.data.pageNumber + shift;
+ if (pageNumber < 0) {
+ pageNumber = 1;
+ }
+ ajaxProperties.data.pageNumber = pageNumber;
+ // refresh the dialog with new parameters
+ showClassDialog(role);
+}
+
+
+/**
+ * Hides/shows paging controls
+ */
+function togglePagingCells(parent, pageNumber, maxPageNumber) {
+ if (pageNumber + 10 <= maxPageNumber) {
+ $('td.pagePlus10Cell', parent).css('visibility', 'visible');
+ } else {
+ $('td.pagePlus10Cell', parent).css('visibility', 'hidden');
+ }
+ if (pageNumber - 10 < 1) {
+ $('td.pageMinus10Cell', parent).css('visibility', 'hidden');
+ } else {
+ $('td.pageMinus10Cell', parent).css('visibility', 'visible');
+ }
+ if (pageNumber + 1 <= maxPageNumber) {
+ $('td.pagePlus1Cell', parent).css('visibility', 'visible');
+ } else {
+ $('td.pagePlus1Cell', parent).css('visibility', 'hidden');
+ }
+ if (pageNumber - 1 < 1) {
+ $('td.pageMinus1Cell', parent).css('visibility', 'hidden');
+ } else {
+ $('td.pageMinus1Cell', parent).css('visibility', 'visible');
+ }
+ if (maxPageNumber < 2) {
+ $('td.pageCell', parent).css('visibility', 'hidden');
+ } else {
+ $('td.pageCell', parent).css('visibility', 'visible').text(pageNumber + ' / ' + maxPageNumber);
+ }
+}
+
+function showToast(text) {
+ let toast = $('#toast-template').clone().attr('id', null).appendTo('#toast-container');
+ toast.find('.toast-body', toast).text(text);
+ toast = new bootstrap.Toast(toast[0]);
+ toast.show();
+}
+
+function showConfirm(body, callback) {
+ let dialog = $('#confirmationDialog');
+ $('.modal-body', dialog).html(body);
+
+ $("#confirmationDialogConfirmButton").off('click').on("click", function(){
+ callback(true);
+ dialog.modal('hide');
+ });
+
+ $("#confirmationDialogCancelButton").off('click').on("click", function(){
+ dialog.modal('hide');
+ });
+
+ dialog.modal('show');
+}
+
+/**
+ * Works as dblclick for mobile devices.
+ */
+function dblTap(elem, dblClickFunction) {
+ // double tap detection on mobile devices; it works also for mouse clicks
+ // temporarly switched to click as jQuery mobile was removed for bootstrapping
+ elem.click(function(event) {
+ // was the second click quick enough after the first one?
+ var currentTime = new Date().getTime(),
+ tapLength = currentTime - lastTapTime;
+ lastTapTime = currentTime;
+
+ if (lastTapTarget && lastTapTarget.classList.contains('learner-icon') && tapLength < 10) {
+ // after clicking learner icon there is a propagation to activity, which must be ignored
+ // we can not stop propagation completetly as force complete stops working
+ return;
+ }
+
+ // is the second click on the same element as the first one?
+ if (event.currentTarget == lastTapTarget) {
+ if (tapLength < tapTimeout && tapLength > 0) {
+ event.preventDefault();
+ dblClickFunction(event);
+ }
+ }
+
+ lastTapTarget = event.currentTarget;
+ });
+}
Index: lams_monitoring/web/tblmonitor/tblmonitor.jsp
===================================================================
diff -u -r0f85998fe3695d15ad685d2564e0b12f331f5b7d -recc49cd6851b43f37ef02c2ddb85257096e2cf49
--- lams_monitoring/web/tblmonitor/tblmonitor.jsp (.../tblmonitor.jsp) (revision 0f85998fe3695d15ad685d2564e0b12f331f5b7d)
+++ lams_monitoring/web/tblmonitor/tblmonitor.jsp (.../tblmonitor.jsp) (revision ecc49cd6851b43f37ef02c2ddb85257096e2cf49)
@@ -12,12 +12,20 @@
+
@@ -177,12 +212,6 @@