Index: lams_central/web/css/progressBar.css =================================================================== RCS file: /usr/local/cvsroot/lams_central/web/css/progressBar.css,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ lams_central/web/css/progressBar.css 17 May 2013 09:36:17 -0000 1.1 @@ -0,0 +1,38 @@ +div#tooltip { + display: none; + position: absolute; + border: 1px solid gray; + background-color: rgb(246, 238, 191); + padding: 3px; + z-index: 5; + width: 100px; + font-size: 9px; +} + +.progressBarContainer { + overflow: auto; +} + +div.optionalActivity { + display: none; + position: absolute; + border: 1px solid black; + background-color: rgb(234, 249, 255); + z-index: 6; + width: 145px; +} + +div.optionalActivity table tr:first-child td { + border-top: none; + background-color: rgb(197, 212, 251); +} + +div.optionalActivity td { + border-top: 1px solid black; + cursor: pointer; + padding: 0px; +} + +div.optionalActivity td:hover { + background-color: rgb(246, 238, 191); +} \ No newline at end of file Index: lams_central/web/includes/javascript/progressBar.js =================================================================== RCS file: /usr/local/cvsroot/lams_central/web/includes/javascript/progressBar.js,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ lams_central/web/includes/javascript/progressBar.js 17 May 2013 09:36:17 -0000 1.1 @@ -0,0 +1,1022 @@ +// ------- GLOBAL VARIABLES ---------- +// IMPORTANT: set following variables on the page which imports this JS file +// var isHorizontalBar = false; +// var hasContentFrame = true; +// above settings are default for Learner page + +// colors used in shapes +// dark red +var COLOR_CURRENT_ACTIVITY = "rgb(187,0,0)"; +// dark blue +var COLOR_COMPLETED_ACTIVITY = "rgb(0,0,153)"; +// green +var COLOR_TOSTART_ACTIVITY = "rgb(0,153,0)"; +// black +var COLOR_STROKE_ACTIVITY = "rgb(0,0,0)"; +// red +var COLOR_GATE = "rgb(255,0,0)"; +// white +var COLOR_GATE_TEXT = "rgb(255,255,255)"; +// gray +var COLOR_COMPLEX_BACKGROUND = "rgb(153,153,153)"; + +// SVG paths for activity shapes +var PATH_SQUARE = " v16 h16 v-16 z"; +var PATH_BIG_SQUARE = " v26 h26 v-26 z"; +var PATH_CIRCLE = " m -8 0 a 8 8 0 1 0 16 0 a 8 8 0 1 0 -16 0"; +var PATH_QUARTER_CIRCLE = " a16 16 0 0 0 16 16 v-16 z"; +var PATH_TRIANGLE = " l8 16 l8 -16 z"; +var PATH_OCTAGON = " l-7 7 v12 l7 7 h12 l7 -7 v-12 l-7 -7 z"; + +var isPreview = false; +var controlFramePadding = 0; + +// ----- CONTROL FRAME & WINDOW MANIPULATION ----- + +// generic function for opening a pop up +function openPopUp(url, title, h, w, status) { + window.open(url, title, "HEIGHT=" + h + ",WIDTH=" + w + + ",resizable=yes,scrollbars=yes,status=" + status + + ",menubar=no, toolbar=no"); +} + +function openActivity(url) { + openPopUp(url, "LearnerActivity", 600, 800, "yes"); +} + +// loads a new activity to main content frame; alternative to opening in pop up +function loadFrame(url) { + $('#contentFrame').attr('src', url); +} + +// adjusts elements after window resize +function resizeElements() { + var width = $(window).width() - 160; + var height = $(window).height(); + + if (hasContentFrame) { + // resize main content frame + $('#contentFrame').css({ + 'width' : width + "px", + 'height' : height + "px", + 'position' : 'fixed' + }); + } + + if (!isHorizontalBar && progressPanelEnabled) { + if (hasContentFrame && !controlFramePadding) { + // calculate only once in the beginning + // there will be miscalculations when trying to repeat this + // in the middle of resizing + controlFramePadding = $('#controlFrame').outerHeight(true) + - $('#controlFrame').height(); + } + + // calculate immutable chunks and what is left goes for progress bar + var progressBarHeight = height - controlFramePadding; + $('.progressStaticHeight').each(function() { + var elem = $(this); + // are notebook and/or support activities hidden? + if (elem.is(':visible')) { + progressBarHeight -= elem.outerHeight(true); + } + }); + + $('#progressBarDiv').height(progressBarHeight); + } + + if (presenceEnabled) { + // resize chat frame only if it exists + resizeChat(); + } +} + +// double click triggers also single click event, this method helps +function handleClicks(elem, click, dblclick) { + if (click) { + elem.click(function(e) { + setTimeout(function() { + // if double clicked, just reduce the counter + if (elem.clickcounter) { + elem.clickcounter--; + } else { + // no double click, so execute + click.call(); + } + }, 300); + }); + } + if (dblclick) { + elem.dblclick(function() { + elem.clickcounter = 2; + dblclick.call(); + }); + } +} + +// ------------- RAPHAEL -------------- + +// This should be the super class for Activities, +// but it's hard to accomplish in JS. +// It is a set of common methods instead. +var ActivityUtils = { + // shape* methods are just preparing data, there is no actual drawing yet + shapeByStatus : function(activity) { + if (activity.status == 0) { + ActivityUtils.shapeCurrentActivity(activity); + } else if (activity.status == 1) { + ActivityUtils.shapeCompletedActivity(activity); + } else if (activity.status == 2) { + ActivityUtils.shapeAttemptedActivity(activity); + } else if (activity.status == 3) { + ActivityUtils.shapeToStartActivity(activity); + } + }, + + shapeCurrentActivity : function(activity) { + // dark red square + activity.path = 'M ' + (activity.middle - 8) + ' ' + activity.y + + PATH_SQUARE; + activity.fill = COLOR_CURRENT_ACTIVITY; + activity.stroke = COLOR_STROKE_ACTIVITY; + activity.statusTooltip = CURRENT_ACTIVITY_LABEL; + }, + + shapeCompletedActivity : function(activity) { + // dark blue circle + activity.path = 'M ' + activity.middle + ' ' + (activity.y + 8) + + PATH_CIRCLE; + activity.fill = COLOR_COMPLETED_ACTIVITY; + activity.stroke = COLOR_STROKE_ACTIVITY; + activity.statusTooltip = COMPLETED_ACTIVITY_LABEL; + }, + + shapeAttemptedActivity : function(activity) { + // green square with dark red arc + activity.path = 'M ' + (activity.middle - 8) + ' ' + activity.y + + PATH_SQUARE; + activity.fill = COLOR_TOSTART_ACTIVITY; + activity.stroke = COLOR_STROKE_ACTIVITY; + activity.statusTooltip = ATTEMPTED_ACTIVITY_LABEL; + + // this and similar methods are run when activity shape is drawn for + // real + activity.addDecoration = function(act) { + var paper = act.bar.paper; + act.decoration = paper.set(); + // get exact Y where inner shape was drawn + // it is different than activity.y in OptionalActivity + // because of gray square around it + var y = act.shape.attr('path')[0][2]; + var arc = paper.path('M ' + (act.middle - 8) + ' ' + y + + PATH_QUARTER_CIRCLE); + arc.attr({ + 'fill' : COLOR_CURRENT_ACTIVITY, + 'opacity' : 0, + 'cursor' : 'pointer' + }); + act.decoration.push(arc); + } + }, + + shapeToStartActivity : function(activity) { + // green triangle + activity.path = 'M ' + (activity.middle - 8) + ' ' + activity.y + + PATH_TRIANGLE; + activity.fill = COLOR_TOSTART_ACTIVITY; + activity.stroke = COLOR_STROKE_ACTIVITY; + activity.statusTooltip = TOSTART_ACTIVITY_LABEL; + }, + + shapeGateActivity : function(activity) { + // red octagon for STOP road sign + activity.path = 'M ' + (activity.middle - 6) + ' ' + activity.y + + PATH_OCTAGON; + activity.fill = COLOR_GATE; + + activity.addDecoration = function(act) { + var paper = act.bar.paper; + act.decoration = paper.set(); + + // should be internationalised? + var text = paper.text(act.middle, act.y + (isHorizontalBar ? 16 : 13), 'STOP'); + text.attr({ + 'opacity' : 0, + 'font-size' : 9, + 'font' : 'sans-serif', + 'stroke' : COLOR_GATE_TEXT, + 'cursor' : 'pointer' + }); + act.decoration.push(text); + + if (act.status == 0) { + // add dark red edge when current activity + act.statusTooltip = CURRENT_ACTIVITY_LABEL; + + var edge = paper.path(act.path); + edge.attr({ + 'opacity' : 0, + 'stroke' : COLOR_CURRENT_ACTIVITY, + 'stroke-width' : 3, + 'cursor' : 'pointer' + }); + act.decoration.push(edge); + } else { + act.statusTooltip = TOSTART_ACTIVITY_LABEL; + } + } + }, + + shapeComplexActivityContainer : function(activity) { + var addDecoration = activity.addDecoration; + activity.addDecoration = function(act) { + // run previous addDecoration(), for example defined in Attempted + // Activity + if (addDecoration) { + addDecoration(act); + } + + var paper = act.bar.paper; + // gray square in background + var square = paper.path('M ' + (act.middle - 13) + ' ' + act.y + + PATH_BIG_SQUARE); + square.attr({ + 'opacity' : 0, + 'fill' : COLOR_COMPLEX_BACKGROUND, + 'cursor' : 'pointer' + }); + + // inform that it goes behind, not to front like other decoration + act.decorationWraps = true; + square.decorationWraps = true; + + if (!act.decoration) { + act.decoration = paper.set(); + } + act.decoration.push(square); + } + }, + + // return some attributes in Raphael consumable way + getShapeAttributes : function(activity) { + return { + 'path' : activity.path, + 'fill' : activity.fill, + 'stroke' : activity.stroke, + 'cursor' : 'pointer' + } + }, + + // does the actual drawing, based on info in Activity object + drawActivity : function(activity, quick, isLast) { + activity.bar.activities[activity.index] = activity; + // all elements that activity consists of + // so they all can be moved at once + var paper = activity.bar.paper; + activity.elements = paper.set(); + // only now do the read drawing, add event handlers etc. + activity.shape = paper.path(activity.path); + // add Activity attributes + activity.shape.attr(ActivityUtils.getShapeAttributes(activity)); + activity.elements.push(activity.shape); + var isLarger = activity.isComplex || activity.type == 'g'; + + // label underneath the shape + var label = null; + if (isHorizontalBar) { + label = paper.text(activity.middle, + 40 + (activity.index % 2 == 0 ? 0 : 15), + activity.name); + } else { + label = paper.text(activity.middle, + 43 + 60 * activity.index + (isLarger ? 10 : 0), + activity.name); + } + activity.elements.push(label); + + if (!isLast) { + // line between activities; last activity does not have it + var line = null; + if (isHorizontalBar) { + line = paper.path('M ' + (activity.middle + 15) + ' 18 h 30'); + } else { + line = paper.path('M ' + activity.middle + ' ' + + (50 + 60 * activity.index + (isLarger ? 10 : 0)) + + ' v ' + (isLarger ? 20 : 30)); + } + + activity.elements.push(line); + } + + if (!quick) { + // slowly show the activity + activity.elements.forEach(function(elem) { + // hide first + elem.attr('opacity', 0); + // show in 1 second + elem.animate({ + 'opacity' : 1 + }, 1000, "linear"); + }); + } + + // add additional elements + ActivityUtils.addDecoration(activity, null, quick); + // add hover, click etc. handlers + ActivityUtils.addEffects(activity); + }, + + // adds handlers to activity for mouse interactions + // long method with simple actions + addEffects : function(activity) { + // remove any existing handlers + ActivityUtils.removeHover(activity.shape); + if (activity.shape.events) { + while (activity.shape.events.length) { + // iterate over any handlers bound + activity.shape.events.pop().unbind(); + } + } + + var mouseover = function(e, x, y) { + // add glowing effect on hover + if (activity.decorationWraps) { + activity.decoration.forEach(function(elem) { + // check which decoration element should glow + if (elem.decorationWraps) { + // glow the wrapping decoration element + // for example gray square in Optonal Activity container + // is bigger than inner activity shape, so it should + // glow + activity.shape.glowRef = elem.glow({ + color : elem.attr('fill') + }); + return false; + } + }); + } else { + activity.shape.glowRef = activity.shape.glow({ + color : activity.shape.attr('fill') + }); + } + + // add tooltip + var tooltipText = '' + activity.name + '
' + + activity.statusTooltip; + // move to proper place and show + $('#tooltip').stop(true, true).css("left", x + 10).css("top", y + 20) + .html(tooltipText).delay(1000).fadeIn(); + } + + var mouseout = function() { + // remove glow + ActivityUtils.removeHover(activity.shape); + } + + var isSupportActivity = activity instanceof SupportActivity; + var dblclick = activity.url ? function() { + // open pop up if it is a support or completed activity + if (isSupportActivity + || activity.status == 1 + || (!hasContentFrame && activity.status <= 2)) { + openActivity(activity.url); + + if (isSupportActivity) { + // do not ask server, just mark the activity as attempted + activity.transformToAttempted(); + } + } else { + loadFrame(activity.url); + } + } : null; + + var click = activity.isComplex ? function() { + // show complex (Optional, Branching) activity inner content + ActivityUtils.showComplexContent(activity); + } : null; + + // assign handlers + activity.shape.hover(mouseover, mouseout); + handleClicks(activity.shape, click, dblclick); + if (activity.decoration) { + // add handlers not only to shape, but also to all decoration + // elements + activity.decoration.forEach(function(elem) { + elem.hover(mouseover, mouseout); + handleClicks(elem, click, dblclick); + }); + } + }, + + // remove glow when mouse leaves shape + removeHover : function(shape) { + if (shape.glowRef) { + shape.glowRef.remove(); + shape.glowRef = null; + } + $('#tooltip').stop(true, true).fadeOut(); + }, + + // copy important properties and morph visible elements + transform : function(sourceActivity, targetActivity) { + var gotCompleted = false; + + // modify only if anything changed + if (sourceActivity.status != targetActivity.status) { + // was just completed + gotCompleted = targetActivity.status == 1; + + sourceActivity.path = targetActivity.path; + sourceActivity.fill = targetActivity.fill; + sourceActivity.stroke = targetActivity.stroke; + sourceActivity.url = targetActivity.url; + sourceActivity.status = targetActivity.status; + sourceActivity.statusTooltip = targetActivity.statusTooltip; + sourceActivity.addDecoration = targetActivity.addDecoration; + + // transform current shape to the new one + ActivityUtils.animate(sourceActivity, sourceActivity.decoration); + } + + var isCurrent = targetActivity.status == 0; + if (sourceActivity.childActivities) { + // run for all inner activities (Optional, Branching) + $ + .each( + sourceActivity.childActivities, + function(childActivityIndex, childActivity) { + var targetChildActivity = targetActivity.childActivities[childActivityIndex]; + // if child activity is current, parent activity + // is current as well + isCurrent |= targetChildActivity.status == 0; + ActivityUtils.transform(childActivity, + targetChildActivity); + }); + } + + if (isCurrent) { + // shows box with inner activities, if not open yet + ActivityUtils.showComplexContent(sourceActivity); + if (sourceActivity.toggleChildren) { + // complex sequence just became current, show it + sourceActivity.toggleChildren('open'); + } + // close box with inner activities, if finished + } else if (gotCompleted) { + if (sourceActivity.isComplex) { + ActivityUtils.hideOtherComplexContent(); + } else if (sourceActivity.toggleChildren) { + sourceActivity.toggleChildren('close'); + } + } + }, + + animate : function(activity, oldDecoration) { + if (activity.shape) { + // remove old decoration and start showin new one + ActivityUtils.addDecoration(activity, oldDecoration, false); + // transform the shape + activity.shape.animate(ActivityUtils.getShapeAttributes(activity), + 2000, 'linear', function() { + if (!(activity instanceof OptionalActivity)) { + // inner activities do not have glow and tooltip + // effects + ActivityUtils.addEffects(activity); + } + }); + } + }, + + // adds additional elements to activity basic shape + // quick is for inital drawing, no nice effect is needed + addDecoration : function(activity, oldDecoration, quick) { + if (oldDecoration) { + // hide existing decoration + oldDecoration.forEach(function(elem) { + if (activity.elements) { + activity.elements.exclude(elem); + } + elem.animate({ + 'opacity' : 0 + }, quick ? 0 : 1000, 'linear', function() { + elem.remove(); + }); + }); + } + + // run function that draws decoration + if (activity.addDecoration) { + var animation = Raphael.animation({ + 'opacity' : 1 + }, quick ? 0 : 1000, "linear"); + + activity.addDecoration(activity); + activity.decoration + .forEach(function(elem) { + if (activity.elements) { + activity.elements.push(elem); + } + if (elem.decorationWraps) { + // decoration element is bigger that activity shape, + // put it in background + elem.toBack(); + } else { + elem.toFront(); + } + + elem.animate(animation.delay(oldDecoration ? 1000 + : undefined)); + }); + } + }, + + // hide all Optional Activities, except for the given one + hideOtherComplexContent : function(currentOptionalContentId) { + $('div.optionalActivity').each(function(index, contentDiv) { + var content = $(contentDiv); + if (content.attr('id') != currentOptionalContentId) { + content.slideUp(currentOptionalContentId ? 'fast' : 'slow'); + } + }); + }, + + // draw box with inner activities + showComplexContent : function(activity) { + if (activity.isComplex) { + // hide glow if shown (IE) + ActivityUtils.removeHover(activity.shape); + // remove other boxes, if shown + ActivityUtils + .hideOtherComplexContent(activity.optionalContent ? activity.optionalContent + .attr('id') + : null); + + if (!activity.optionalContent) { + // build box HTML + var containerName = 'optionalActivityContent' + activity.id; + activity.optionalContent = $('
').attr('id', + containerName).addClass('optionalActivity').css({ + // a little higher than activity, to cover it + 'top' : $(activity.shape.node).offset().top - 8, + 'left' : $(activity.shape.node).offset().left - 65, + 'height' : 27 * activity.childActivities.length - 1 + }).appendTo('#' + activity.bar.containerId); + + var optionalContentTable = $('') + .appendTo(activity.optionalContent); + + ActivityUtils.addChildActivitiesRows(activity, + optionalContentTable, activity.optionalContent, false); + } + + activity.optionalContent.slideDown('slow'); + } + }, + + addChildActivitiesRows : function(activity, parent, container, isNested) { + var isCurrent = false; + + $.each(activity.childActivities, function(childActivityIndex, childActivity) { + var row = $(''); + var parentId = null; + if (isNested) { + // second tier, a part of optional sequence or + // branching + parentId = $('td', parent).attr('id'); + // find last activity from sequence and put + // current one after it to keep ordering + row.insertAfter($('td[id^=' + parentId + ']', + container).last().parent()); + isCurrent |= childActivity.status == 0; + } else { + parentId = container.attr('id'); + row.appendTo(parent); + } + + var cellId = parentId + 'child' + + childActivityIndex; + var cell = $('
').attr('id', cellId).appendTo( + row); + if (isNested) { + cell.hide(); + } + + // each row has its own paper + var paper = childActivity.bar.paper = Raphael( + cellId, 145, 23); + // draw the inner activity + childActivity.shape = paper + .path(childActivity.path); + childActivity.shape.attr(ActivityUtils + .getShapeAttributes(childActivity)); + ActivityUtils.addDecoration(childActivity, null, + true); + + var label = paper.text(35, childActivity.y + 11, + // add dash before name + (isNested ? '- ' : '') + childActivity.name) + // align to left + .attr('text-anchor', 'start'); + // fix a bug in FF layout + $('tspan', label.node).attr('dy', 0); + + var click = null; + if (!isNested) { + // only first tier inner activities + if (childActivityIndex == 0) { + click = function() { + // first row is the parent activity + // itself; hide content box when clicked + container.slideUp(); + } + } else if (childActivity.childActivities) { + // show/hide 2nd tier inner activities + childActivity.toggleChildren = function( + forceCommand) { + if (cell.is(':visible')) { + var childCells = $('td[id^=' + + cellId + 'child]', parent); + var isOpen = childCells + .is(':visible'); + if (!forceCommand + || (isOpen ? forceCommand == 'close' + : forceCommand == 'open')) { + var containerHeightDelta = 27 * childCells.length; + childCells.toggle(); + // resize inner content box + container.height(container.height() + + (isOpen ? -containerHeightDelta + : containerHeightDelta)); + } + } + } + + click = function() { + // show 2nd tier when 1st tier activity + // is clicked + childActivity.toggleChildren(); + } + } + } + var dblclick = function() { + if (childActivity.url) { + if (childActivity.status == 1) { + openActivity(childActivity.url); + } else { + loadFrame(childActivity.url); + } + } + } + handleClicks(cell, click, dblclick); + + if (childActivity.childActivities) { + isCurrent |= ActivityUtils + .addChildActivitiesRows(childActivity, + row, container, true); + if (isCurrent && childActivity.toggleChildren) { + childActivity.toggleChildren('open'); + } + } + }); + + return isCurrent; + }, + + // replace single Branching activity with list of branch activities + expandBranch : function(bar, branchIndex, branchActivities) { + // hide any boxes obstructing the view + ActivityUtils.hideOtherComplexContent(); + + var activityShift = branchActivities.length - 1; + // how many pixels move subsequent activities down + var yShift = 60 * activityShift; + // activity just after branching + var afterBranchActivity = null; + // move down existing activities that come after Branching; start with + // the last one + var activities = bar.activities; + for ( var activityIndex = activities.length - 1; activityIndex > branchIndex; activityIndex--) { + afterBranchActivity = activities[activityIndex]; + activities[activityIndex + activityShift] = afterBranchActivity; + afterBranchActivity.y += yShift; + afterBranchActivity.path = Raphael.transformPath( + afterBranchActivity.path, 'T0,' + yShift); + afterBranchActivity.elements.forEach(function(elem) { + var y = elem.attr('y'); + var targetProperties = null; + // text, rectangles etc. have 'y', paths have 'path' + if (y) { + targetProperties = { + 'y' : elem.attr('y') + yShift + }; + } else { + var path = elem.attr('path'); + targetProperties = { + 'path' : Raphael.transformPath(path, 'T0,' + yShift) + }; + } + elem.animate(targetProperties, 2000, "linear"); + }); + } + + // smoothly remove Branching activity + activities[branchIndex].elements.forEach(function(elem) { + elem.animate({ + 'opacity' : 0 + }, 2000, "linear", function() { + elem.remove(); + }); + }); + + // create branch activities structures + for ( var activityIndex = 0; activityIndex < branchActivities.length; activityIndex++) { + var activityData = branchActivities[activityIndex]; + var activity = new Activity(bar, activityIndex + branchIndex, + activityData.id, activityData.type, activityData.name, + activityData.status, activityData.url, + activityData.childActivities); + activities[activityIndex + branchIndex] = activity; + if (activity.status == 0) { + currentActivityIndex = activityIndex; + } + } + + // smoothly draw branch activities + setTimeout( + function() { + for ( var activityIndex = 0; activityIndex < branchActivities.length; activityIndex++) { + ActivityUtils + .drawActivity( + activities[activityIndex + branchIndex], + false, + !afterBranchActivity + && activityIndex == branchActivities.length - 1); + } + }, 2000); + } +} + +// main activities +function Activity(bar, index, id, type, name, status, url, childActivitiesData) { + this.bar = bar; + this.index = index; + this.id = id; + this.type = type; + this.name = name; + this.status = status; + this.url = url; + + // Optional Activities, Optional Sequences or Branching in preview mode + this.isComplex = type == 'o' || (isPreview && type == 'b'); + + if (isHorizontalBar) { + this.middle = 48 + 60 * index; + this.y = 10; + } else { + // X positioning + this.middle = 70; + // 20 is the first line segment and following activities take 60 px each + // (together with following vertical line) + this.y = 20 + 60 * index; + } + + // first draw the inner shape, then put back the realY for background gray + // square + var finalY = this.y; + if (!isHorizontalBar && this.isComplex) { + this.y += 5; + } + + if (type == 'g') { + // gate activity + if (isHorizontalBar) { + this.y -= 5; + } + ActivityUtils.shapeGateActivity(this); + } else { + ActivityUtils.shapeByStatus(this); + } + + // special behaviour for complex activities + if (this.isComplex) { + this.y = finalY - (isHorizontalBar ? 5 : 0); + ActivityUtils.shapeComplexActivityContainer(this); + + this.childActivities = [ new OptionalActivity(bar, name, status, url) ]; + var childActivities = this.childActivities; + $.each(childActivitiesData, function(childActivityIndex, + childActivityData) { + childActivities[childActivityIndex + 1] = new OptionalActivity(bar, + childActivityData.name, childActivityData.status, + childActivityData.url, childActivityData.childActivities, + false); + }); + } +} + +// Support (floating) activities are show in separate box and behave differently +function SupportActivity(bar, index, name, status, url) { + this.bar = bar; + this.name = name; + this.status = status; + this.url = url; + + this.middle = 24; + this.y = 17 + 33 * index; + + if (status <= 2) { + ActivityUtils.shapeAttemptedActivity(this); + } else if (status == 3) { + ActivityUtils.shapeToStartActivity(this); + } + this.statusTooltip = SUPPORT_ACTIVITY_LABEL; + + this.transformToAttempted = function() { + var oldDecoration = this.decoration; + ActivityUtils.shapeAttemptedActivity(this); + this.statusTooltip = SUPPORT_ACTIVITY_LABEL; + + ActivityUtils.animate(this, oldDecoration); + } +} + +// Optional and Branching inner activities +function OptionalActivity(bar, name, status, url, childActivitiesData, isNested) { + this.bar = bar; + this.name = name; + this.status = status; + this.url = url; + + this.middle = isNested ? 22 : 15; + this.y = 5; + + ActivityUtils.shapeByStatus(this); + + // if Sequence or Branching, this is the 2nd tier of inner activities + if (childActivitiesData) { + this.childActivities = []; + var childActivities = this.childActivities; + $.each(childActivitiesData, function(childActivityIndex, + childActivityData) { + childActivities[childActivityIndex] = new OptionalActivity(bar, + childActivityData.name, childActivityData.status, + childActivityData.url, null, true); + }); + } +} + +// refresh progress bar on first/next activity load +function fillProgressBar(barId) { + var bar = bars[barId]; + if (!bar) { + // bar must be initialised first! + return false; + } + + $.ajax({ + url : LAMS_URL + 'monitoring/monitoring.do', + data : { + 'method' : 'getLearnerProgressJSON', + 'lessonID' : lessonId, + 'userID' : bar.userId + }, + cache : false, + dataType : 'json', + success : function(result) { + // if nothing changed, don't do any calculations + if (!bar.currentActivityId + || result.currentActivityId != bar.currentActivityId) { + bar.currentActivityId = result.currentActivityId; + isPreview = result.isPreview; + + var paper = bar.paper; + if (!paper) { + // create paper only the first time + paper = bar.paper = Raphael(bar.containerId, + isHorizontalBar ? 40 + 60 * result.activities.length : 140, + isHorizontalBar ? 60 : 60 * result.activities.length); + // first line on the top + paper.path(isHorizontalBar ? 'M 0 18 h 35' + : 'M 70 0 v 20'); + } + + // we need this to scroll to the current activity + var currentActivityIndex = 0; + + for (var activityIndex = 0; activityIndex < result.activities.length; activityIndex++) { + var activityData = result.activities[activityIndex]; + // prepare the Activity descriptor, but do not draw + // yet + var activity = new Activity(bar, activityIndex, + activityData.id, activityData.type, + activityData.name, activityData.status, + activityData.url, + activityData.childActivities); + if (activity.status == 0) { + currentActivityIndex = activityIndex; + } + + var activities = bar.activities; + if (!activities) { + activities = bar.activities = []; + } + + var existingActivity = activities[activityIndex]; + if (existingActivity) { + // if in preview mode, always display all inner + // activities, i.e. never expand + if (!isPreview && existingActivity.type == 'b' + && existingActivity.id != activity.id) { + + var branchActivityId = activityIndex; + var afterBranchActivityId = activityIndex + 1 < activities.length ? activities[activityIndex + 1].id + : null; + var branchActivities = [ activity ]; + activityIndex++; + + // find which activities are new (branch) + // and which ones already existed + while (activityIndex < result.activities.length) { + activityData = result.activities[activityIndex]; + var activity = new Activity(bar, + activityIndex, activityData.id, + activityData.type, + activityData.name, + activityData.status, + activityData.url, + activityData.childActivities); + if (activity.id == afterBranchActivityId) { + // prepare for the next big loop + // iteration, which executes + // normally + activityIndex--; + break; + } else { + branchActivities.push(activity); + activityIndex++; + } + } + + // resize main paper to accomodate new + // activities + paper.setSize(140, 60 * (activities.length + + branchActivities.length - 1)); + ActivityUtils.expandBranch(bar, + branchActivityId, branchActivities); + } else { + // refresh existing bar, transform + // activities if needed + ActivityUtils.transform(existingActivity, + activity); + } + } else { + // draw new activity + ActivityUtils + .drawActivity( + activity, + true, + activityIndex == result.activities.length - 1); + } + } + + // draw support activities if they exist + if (result.support + && !supportSeparatorRow.is(':visible')) { + supportSeparatorRow.show(); + supportPart.height(17 + 33 * result.support.length) + .show(); + + // separate paper for Suppor Activities frame + var supportPaper = Raphael('supportPart'); + + $.each(result.support, function(activityIndex, + activityData) { + var activity = new SupportActivity( + supportPaper, activityIndex, + activityData.name, activityData.status, + activityData.url); + activity.shape = supportPaper + .path(activity.path); + activity.shape.attr(ActivityUtils + .getShapeAttributes(activity)); + ActivityUtils.addDecoration(activity, null, + true); + ActivityUtils.addEffects(activity); + supportPaper.text(90, 24 + 33 * activityIndex, + activity.name); + }); + } + + resizeElements(); + // scroll to the current activity + if (!isHorizontalBar) { + $('#' + bar.containerId).scrollTop( + Math.max((currentActivityIndex - 1) * 60,0) + ); + } + } + } + }); + + return true; +} \ No newline at end of file Index: lams_learning/src/java/org/lamsfoundation/lams/learning/web/action/LearnerAction.java =================================================================== RCS file: /usr/local/cvsroot/lams_learning/src/java/org/lamsfoundation/lams/learning/web/action/LearnerAction.java,v diff -u -r1.50 -r1.51 --- lams_learning/src/java/org/lamsfoundation/lams/learning/web/action/LearnerAction.java 5 Dec 2012 14:01:17 -0000 1.50 +++ lams_learning/src/java/org/lamsfoundation/lams/learning/web/action/LearnerAction.java 17 May 2013 09:36:08 -0000 1.51 @@ -26,7 +26,6 @@ import java.io.IOException; import java.io.PrintWriter; -import java.util.List; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -36,11 +35,8 @@ import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; -import org.apache.tomcat.util.json.JSONException; -import org.apache.tomcat.util.json.JSONObject; import org.lamsfoundation.lams.learning.service.ICoreLearnerService; import org.lamsfoundation.lams.learning.service.LearnerServiceProxy; -import org.lamsfoundation.lams.learning.web.bean.ActivityURL; import org.lamsfoundation.lams.learning.web.util.ActivityMapping; import org.lamsfoundation.lams.learning.web.util.LearningWebUtil; import org.lamsfoundation.lams.learningdesign.Activity; @@ -78,7 +74,7 @@ * @since 3/03/2005 * @version 1.1 * - * ----------------XDoclet Tags-------------------- + * ----------------XDoclet Tags-------------------- * * @struts:action path="/learner" parameter="method" validate="false" * @struts:action-forward name="displayActivity" path="/DisplayActivity.do" @@ -134,15 +130,15 @@ *

* * @param mapping - * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to - * send the end-user. + * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to send + * the end-user. * * @param form - * The ActionForm class that will contain any data submitted by the end-user via a form. + * The ActionForm class that will contain any data submitted by the end-user via a form. * @param request - * A standard Servlet HttpServletRequest class. + * A standard Servlet HttpServletRequest class. * @param response - * A standard Servlet HttpServletResponse class. + * A standard Servlet HttpServletResponse class. * @return An ActionForward class that will be returned to the ActionServlet indicating where the user is to go * next. * @@ -195,15 +191,15 @@ *

* * @param mapping - * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to - * send the end-user. + * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to send + * the end-user. * * @param form - * The ActionForm class that will contain any data submitted by the end-user via a form. + * The ActionForm class that will contain any data submitted by the end-user via a form. * @param request - * A standard Servlet HttpServletRequest class. + * A standard Servlet HttpServletRequest class. * @param response - * A standard Servlet HttpServletResponse class. + * A standard Servlet HttpServletResponse class. * @return An ActionForward class that will be returned to the ActionServlet indicating where the user is to go * next. * @@ -263,15 +259,15 @@ * component. * * @param mapping - * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to - * send the end-user. + * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to send + * the end-user. * * @param form - * The ActionForm class that will contain any data submitted by the end-user via a form. + * The ActionForm class that will contain any data submitted by the end-user via a form. * @param request - * A standard Servlet HttpServletRequest class. + * A standard Servlet HttpServletRequest class. * @param response - * A standard Servlet HttpServletResponse class. + * A standard Servlet HttpServletResponse class. * @return An ActionForward class that will be returned to the ActionServlet indicating where the user is to go * next. * @@ -365,15 +361,15 @@ *

* * @param mapping - * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to - * send the end-user. + * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to send + * the end-user. * * @param form - * The ActionForm class that will contain any data submitted by the end-user via a form. + * The ActionForm class that will contain any data submitted by the end-user via a form. * @param request - * A standard Servlet HttpServletRequest class. + * A standard Servlet HttpServletRequest class. * @param response - * A standard Servlet HttpServletResponse class. + * A standard Servlet HttpServletResponse class. * @return An ActionForward class that will be returned to the ActionServlet indicating where the user is to go * next. * @throws IOException @@ -385,8 +381,6 @@ LearnerAction.log.debug("Getting Flash progress data..."); } - - FlashMessage message = null; try { @@ -402,20 +396,20 @@ message = new FlashMessage("getFlashProgressData", learnerProgress); } catch (Exception e) { - message = handleException(e, "getFlashProgressData", LearnerServiceProxy.getLearnerService(getServlet() - .getServletContext())); + message = handleException(e, "getFlashProgressData", + LearnerServiceProxy.getLearnerService(getServlet().getServletContext())); } String wddxPacket = WDDXProcessor.serialize(message); if (LearnerAction.log.isDebugEnabled()) { LearnerAction.log.debug("Sending learner progress data to flash:" + wddxPacket); } - // LDEV-2835 + // LDEV-2835 response.addHeader("Pragma", "no-cache"); - response.addHeader("Cache-Control", "no-cache"); - response.addDateHeader("Expires", System.currentTimeMillis() - LamsDispatchAction.HEADER_EXPIRES_VALUE); + response.addHeader("Cache-Control", "no-cache"); + response.addDateHeader("Expires", System.currentTimeMillis() - LamsDispatchAction.HEADER_EXPIRES_VALUE); - response.getWriter().print(wddxPacket); + response.getWriter().print(wddxPacket); // don't need to return a action forward because it sent the wddx packet // back already. @@ -429,15 +423,15 @@ *

* * @param mapping - * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to - * send the end-user. + * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to send + * the end-user. * * @param form - * The ActionForm class that will contain any data submitted by the end-user via a form. + * The ActionForm class that will contain any data submitted by the end-user via a form. * @param request - * A standard Servlet HttpServletRequest class. + * A standard Servlet HttpServletRequest class. * @param response - * A standard Servlet HttpServletResponse class. + * A standard Servlet HttpServletResponse class. * @return An ActionForward class that will be returned to the ActionServlet indicating where the user is to go * next. * @throws IOException @@ -461,8 +455,8 @@ message = new FlashMessage("getLearnerActivityURL", activityDTO); } catch (Exception e) { - message = handleException(e, "getLearnerActivityURL", LearnerServiceProxy.getLearnerService(getServlet() - .getServletContext())); + message = handleException(e, "getLearnerActivityURL", + LearnerServiceProxy.getLearnerService(getServlet().getServletContext())); } String wddxPacket = WDDXProcessor.serialize(message); @@ -500,14 +494,14 @@ * Flash packet. * * @param mapping - * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to - * send the end-user. + * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to send + * the end-user. * @param form - * The ActionForm class that will contain any data submitted by the end-user via a form. + * The ActionForm class that will contain any data submitted by the end-user via a form. * @param request - * A standard Servlet HttpServletRequest class. + * A standard Servlet HttpServletRequest class. * @param response - * A standard Servlet HttpServletResponse class. + * A standard Servlet HttpServletResponse class. * @return An ActionForward class that will be returned to the ActionServlet indicating where the user is to go * next. * @throws IOException @@ -526,79 +520,19 @@ return mapping.findForward("displayProgress"); } - - @SuppressWarnings("unchecked") - public ActionForward displayProgressJSON(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) throws JSONException, IOException { - Integer learnerId = LearningWebUtil.getUserId(); - Long lessonId = WebUtil.readLongParam(request, AttributeNames.PARAM_LESSON_ID); - ICoreLearnerService learnerService = LearnerServiceProxy.getLearnerService(getServlet().getServletContext()); - Object[] ret = learnerService.getStructuredActivityURLs(learnerId, lessonId); - - JSONObject responseJSON = new JSONObject(); - responseJSON.put("currentActivityId", ret[1]); - responseJSON.put("isPreview", ret[2]); - for (ActivityURL activity : (List) ret[0]) { - if (activity.getFloating()) { - // these are support activities - for (ActivityURL childActivity : activity.getChildActivities()) { - responseJSON.append("support", activityToJSON(childActivity, null)); - } - } else { - responseJSON.append("activities", activityToJSON(activity, (Long) ret[1])); - } - } - - response.setContentType("application/json;charset=utf-8"); - response.getWriter().print(responseJSON.toString()); - - return null; - } - - private JSONObject activityToJSON(ActivityURL activity, Long currentActivityId) throws JSONException { - JSONObject activityJSON = new JSONObject(); - activityJSON.put("id", activity.getActivityId()); - activityJSON.put("name", activity.getTitle()); - activityJSON.put("status", activity.getActivityId().equals(currentActivityId) ? 0 : activity.getStatus()); - - if (activity.getUrl() != null) { - activityJSON.put("url", activity.getUrl()); - } - - String actType = activity.getType().toLowerCase(); - String type = "a"; - if (actType.contains("gate")) { - type = "g"; - } else if (actType.contains("options")) { - type = "o"; - } else if (actType.contains("branching")) { - type = "b"; - } - - activityJSON.put("type", type); - - if (activity.getChildActivities() != null) { - for (ActivityURL childActivity : activity.getChildActivities()) { - activityJSON.append("childActivities", activityToJSON(childActivity, currentActivityId)); - } - } - - return activityJSON; - } - /** * Forces a move to a destination Activity in the learning sequence, returning a WDDX packet * * @param mapping - * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to - * send the end-user. + * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to send + * the end-user. * @param form - * The ActionForm class that will contain any data submitted by the end-user via a form. + * The ActionForm class that will contain any data submitted by the end-user via a form. * @param request - * A standard Servlet HttpServletRequest class. + * A standard Servlet HttpServletRequest class. * @param response - * A standard Servlet HttpServletResponse class. + * A standard Servlet HttpServletResponse class. * @return An ActionForward class that will be returned to the ActionServlet indicating where the user is to go * next. * @throws IOException @@ -638,14 +572,14 @@ * returning a WDDX packet. * * @param mapping - * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to - * send the end-user. + * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to send + * the end-user. * @param form - * The ActionForm class that will contain any data submitted by the end-user via a form. + * The ActionForm class that will contain any data submitted by the end-user via a form. * @param request - * A standard Servlet HttpServletRequest class. + * A standard Servlet HttpServletRequest class. * @param response - * A standard Servlet HttpServletResponse class. + * A standard Servlet HttpServletResponse class. * @return An ActionForward class that will be returned to the ActionServlet indicating where the user is to go * next. * @throws IOException @@ -744,14 +678,14 @@ * Flash packet. * * @param mapping - * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to - * send the end-user. + * An ActionMapping class that will be used by the Action class to tell the ActionServlet where to send + * the end-user. * @param form - * The ActionForm class that will contain any data submitted by the end-user via a form. + * The ActionForm class that will contain any data submitted by the end-user via a form. * @param request - * A standard Servlet HttpServletRequest class. + * A standard Servlet HttpServletRequest class. * @param response - * A standard Servlet HttpServletResponse class. + * A standard Servlet HttpServletResponse class. * @return An ActionForward class that will be returned to the ActionServlet indicating where the user is to go * next. * @throws IOException Index: lams_learning/web/main.jsp =================================================================== RCS file: /usr/local/cvsroot/lams_learning/web/main.jsp,v diff -u -r1.23 -r1.24 --- lams_learning/web/main.jsp 9 May 2013 07:53:55 -0000 1.23 +++ lams_learning/web/main.jsp 17 May 2013 09:36:08 -0000 1.24 @@ -30,6 +30,7 @@ + <fmt:message key="learner.title" /> @@ -40,31 +41,39 @@ + + + @@ -254,7 +264,7 @@ @@ -288,7 +298,15 @@
- + + + + +
+ + +
@@ -324,6 +342,9 @@
+ +
+ \ No newline at end of file Index: lams_monitoring/web/css/monitorLesson.css =================================================================== RCS file: /usr/local/cvsroot/lams_monitoring/web/css/monitorLesson.css,v diff -u -r1.2 -r1.3 --- lams_monitoring/web/css/monitorLesson.css 1 Apr 2013 18:03:58 -0000 1.2 +++ lams_monitoring/web/css/monitorLesson.css 17 May 2013 09:36:21 -0000 1.3 @@ -1,5 +1,4 @@ /********** GENERAL/DIALOG STYLES **********/ - div#tabs { width: 768px; height: 574px; @@ -79,9 +78,7 @@ cursor: pointer; } - /********** LESSON TAB STYLES **********/ - div#tabLesson { height: 540px; overflow: auto; @@ -154,9 +151,7 @@ border: none; } - /********** SEQUENCE TAB STYLES **********/ - div#sequenceCanvas { text-align: center; height: 478px; @@ -180,4 +175,21 @@ cursor: default; vertical-align: top; margin-right: 5px; +} + +/********** LEARNERS TAB STYLES **********/ +td.progressBarLabel { + padding: 0; + background-color: #D0E5F5; +} + +td.progressBarLabel div { + float: left; + padding: 10px 0 4px 20px; + font-weight: bold; +} + +td.progressBarLabel a { + float: right; + margin: 6px 5px 4px 0; } \ No newline at end of file Index: lams_monitoring/web/includes/javascript/monitorLesson.js =================================================================== RCS file: /usr/local/cvsroot/lams_monitoring/web/includes/javascript/monitorLesson.js,v diff -u -r1.5 -r1.6 --- lams_monitoring/web/includes/javascript/monitorLesson.js 30 Apr 2013 09:24:16 -0000 1.5 +++ lams_monitoring/web/includes/javascript/monitorLesson.js 17 May 2013 09:36:21 -0000 1.6 @@ -9,6 +9,7 @@ classLearner : false, classMonitor : false }; +var bars = {}; //********** LESSON TAB FUNCTIONS ********** @@ -415,8 +416,8 @@ var selectedLearner = $('#learnerGroupList div.dialogListItemSelected'); if (selectedLearner.length == 1) { // open pop up with user progress in the given activity - openWindow(LAMS_URL - + selectedLearner.attr('viewUrl'), "LearnActivity", 800, 600); + openPopUp(LAMS_URL + + selectedLearner.attr('viewUrl'), "LearnActivity", 600, 800, true); } } }, @@ -467,7 +468,7 @@ var activityGroup = $('rect[id="act' + activity.id + '"]', sequenceCanvas).parent(); activityGroup.css('cursor', 'pointer').dblclick(function(){ // double click on activity shape to open Monitoring for this activity - openWindow(LAMS_URL + activity.url, "MonitorActivity", 900, 720); + openPopUp(LAMS_URL + activity.url, "MonitorActivity", 720, 900, true); }); } }); @@ -609,7 +610,7 @@ .dblclick(function(event){ // double click on learner icon to see activity from his perspective event.stopPropagation(); - openWindow(LAMS_URL + learner.url, "LearnActivity", 800, 600); + openPopUp(LAMS_URL + learner.url, "LearnActivity", 600, 800, true); }).css('cursor', 'pointer') // drag learners to force complete activities .draggable({ @@ -774,7 +775,7 @@ } var userDiv = $('
').attr({ - 'userId' : user.id, + 'userId' : user.id }) .addClass('dialogListItem') .text(getLearnerDisplayName(user)) @@ -796,13 +797,50 @@ //********** LEARNERS TAB FUNCTIONS ********** +var learnerProgressCellsTemplate = null; -function initLearnersTab(){ + +function updateLearnersTab(response){ + if (!learnerProgressCellsTemplate) { + learnerProgressCellsTemplate = + '
;11;
' + + '' + + EXPORT_PORTFOLIO_LABEL + + '' + + TIME_CHART_LABEL + + ''; + } + + if (response.learners) { + $.each(response.learners, function(){ + var barId = 'bar' + this.id; + var bar = bars[barId]; + if (!bar) { + bar = bars[barId] = { + 'userId' : this.id, + 'containerId' : 'progressBar' + this.id + }; + + var learnerProgressCellsInstance = learnerProgressCellsTemplate + .replace(/;00;/g, this.id) + .replace(/;11;/g, getLearnerDisplayName(this)); + $(learnerProgressCellsInstance).appendTo('#tabLearnersTable'); + } + + fillProgressBar(barId); + }); + } } - - //********** COMMON FUNCTIONS ********** /** @@ -860,11 +898,14 @@ 'lessonID' : lessonId }, success : function(response) { - // update lesson tab widgets (state, number of learners etc.) + // update Lesson tab widgets (state, number of learners etc.) updateLessonTab(response); - // update learner progress in sequence tab + // update learner progress in Sequence tab updateSequenceTab(response); + + // update learner progress in Learners tab + updateLearnersTab(response); } }); } @@ -897,7 +938,7 @@ }) .dblclick(function(){ // same as clicking View Learner button - openWindow(LAMS_URL + learner.url, "LearnActivity", 800, 600); + openPopUp(LAMS_URL + learner.url, "LearnActivity", 600, 800, true); }); } }); @@ -909,8 +950,7 @@ .dialog('option', { 'title' : dialogTitle, - 'activityId' : activityId, - + 'activityId' : activityId }) .dialog('open'); } @@ -963,12 +1003,6 @@ } -function openWindow(url, title, width, height) { - window.open(url, title, "width=" + width + ",height=" + height - + ",resizable=yes,scrollbars=yes,status=yes,menubar=no,toolbar=no"); -} - - /** * Makes a XML element with given attributes. * jQuery does not work well with SVG in Chrome, so all this manipulation need to be done manually.