Index: lams_central/web/css/progressBar.css =================================================================== diff -u --- lams_central/web/css/progressBar.css (revision 0) +++ lams_central/web/css/progressBar.css (revision 9438790401b957cdd70a4d6ab2a76da70176c6eb) @@ -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 =================================================================== diff -u --- lams_central/web/includes/javascript/progressBar.js (revision 0) +++ lams_central/web/includes/javascript/progressBar.js (revision 9438790401b957cdd70a4d6ab2a76da70176c6eb) @@ -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 =================================================================== diff -u -r8634f7029afb87182dfccccd53fca4b82df4a04c -r9438790401b957cdd70a4d6ab2a76da70176c6eb --- lams_learning/src/java/org/lamsfoundation/lams/learning/web/action/LearnerAction.java (.../LearnerAction.java) (revision 8634f7029afb87182dfccccd53fca4b82df4a04c) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/web/action/LearnerAction.java (.../LearnerAction.java) (revision 9438790401b957cdd70a4d6ab2a76da70176c6eb) @@ -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/css/main.css =================================================================== diff -u -rc124a53ce3fc13192de91c935a2cc88a4a532cc9 -r9438790401b957cdd70a4d6ab2a76da70176c6eb --- lams_learning/web/css/main.css (.../main.css) (revision c124a53ce3fc13192de91c935a2cc88a4a532cc9) +++ lams_learning/web/css/main.css (.../main.css) (revision 9438790401b957cdd70a4d6ab2a76da70176c6eb) @@ -55,7 +55,6 @@ div#progressBarDiv { background-color: rgb(219,230,252); - overflow: auto; text-align: center; } @@ -122,41 +121,6 @@ padding: 0px 4px 4px 0px } -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; -} - -div.optionalActivity { - display: none; - position : absolute; - left : 5px; - 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; -} - -div.optionalActivity td:hover { - background-color: rgb(246,238,191); -} - .ui-layout-pane { padding: 0px !important; } Index: lams_learning/web/includes/javascript/main.js =================================================================== diff -u -rd1756abe2c97e06d69ce5069f4c973d21a7d1904 -r9438790401b957cdd70a4d6ab2a76da70176c6eb --- lams_learning/web/includes/javascript/main.js (.../main.js) (revision d1756abe2c97e06d69ce5069f4c973d21a7d1904) +++ lams_learning/web/includes/javascript/main.js (.../main.js) (revision 9438790401b957cdd70a4d6ab2a76da70176c6eb) @@ -1,64 +1,12 @@ -// ------- GLOBAL VARIABLES ---------- +// ----- CONTROL FRAME & WINDOW MANIPULATION ----- -// 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"; - -// other variables -var paper = null; -var controlFramePadding = null; -var currentActivityId = null; -var isPreview = false; -var activities = []; - -// ----- CONTROL FRAME & WINDOW MANIPULATION ----- - -// generic function for opening a pop up -function openPopUp(args, title, h, w, status) { - window.open(args, title, "HEIGHT=" + h + ",WIDTH=" + w - + ",resizable=yes,scrollbars=yes,status=" - + status + ",menubar=no, toolbar=no"); -} - function exportPortfolio(){ openPopUp(APP_URL + "exportWaitingPage.jsp?mode=learner&lessonID=" + lessonId, "ExportPortfolioLearner", 410,640, "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); -} - function viewNotebookEntries(){ openPopUp(APP_URL + "notebook.do?method=viewAll&lessonID=" + lessonId, "Notebook", @@ -82,43 +30,6 @@ top.window.close(); } -// adjusts elements after window resize -function resizeElements() { - var width = $(window).width() - 160; - var height = $(window).height(); - // resize main content frame - $('#contentFrame').css({ - 'width' : width + "px", - 'height' : height + "px", - 'position' : 'fixed' - }); - - if (progressPanelEnabled) { - if (!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(); - } -} - // open/close Notebook or Support Activity frames function toggleBarPart(name) { var part = $('#' + name + 'Part'); @@ -145,790 +56,4 @@ } }); return formFilled; -} - -// 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 = LABEL_CURRENT_ACTIVITY; - }, - - 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 = LABEL_COMPLETED_ACTIVITY; - }, - - 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 = LABEL_ATTEMPTED_ACTIVITY; - - // this and similar methods are run when activity shape is drawn for real - activity.addDecoration = function(act) { - act.decoration = act.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 = act.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 = LABEL_TOSTART_ACTIVITY; - }, - - 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) { - act.decoration = activity.paper.set(); - - // should be internationalised? - var text = act.paper.text(act.middle, act.y + 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 = LABEL_CURRENT_ACTIVITY; - - var edge = act.paper.path(act.path); - edge.attr({ - 'opacity' : 0, - 'stroke' : COLOR_CURRENT_ACTIVITY, - 'stroke-width' : 3, - 'cursor' : 'pointer' - }); - act.decoration.push(edge); - } else { - act.statusTooltip = LABEL_TOSTART_ACTIVITY; - } - } - }, - - shapeComplexActivityContainer : function(activity) { - var addDecoration = activity.addDecoration; - activity.addDecoration = function(act) { - // run previous addDecoration(), for example defined in Attempted Activity - if (addDecoration) { - addDecoration(act); - } - - // gray square in background - var square = act.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 = act.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) { - activities[activity.index] = activity; - // all elements that activity consists of, so they all can be moved at once - activity.elements = activity.paper.set(); - // only now do the read drawing, add event handlers etc. - activity.shape = activity.paper.path(activity.path); - // add Activity attributes - activity.shape.attr(ActivityUtils.getShapeAttributes(activity)); - activity.elements.push(activity.shape); - // label underneath the shape - var label = paper.text(activity.middle, 43 + 60 * (activity.index - 1) + activity.height, - activity.name); - activity.elements.push(label); - if (!isLast) { - // line between activities; last activity does not have it - var line = paper.path("M " + activity.middle + " " + (50 + 60 * (activity.index - 1) + activity.height) - + " v" + (90 - activity.height)); - 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 - tooltipDiv.stop(true, true).css("left", 30).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) { - 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; - } - tooltipDiv.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.height = targetActivity.height; - 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.y; - activity.optionalContent = $('
') - .attr('id', containerName) - .addClass('optionalActivity') - .css({ - // a little higher than activity, to cover it - 'top' : $(activity.shape.node).offset().top - 8, - 'height' : 27 * activity.childActivities.length - 1 - }) - .appendTo('#progressBarDiv'); - - 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 - childActivity.paper = Raphael(cellId, 145, 23); - // draw the inner activity - childActivity.shape = childActivity.paper.path(childActivity.path); - childActivity.shape.attr(ActivityUtils.getShapeAttributes(childActivity)); - ActivityUtils.addDecoration(childActivity, null, true); - var label = childActivity.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(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 - 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(paper, 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(paper, index, id, type, name, status, url, childActivitiesData) { - this.paper = paper; - 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'); - // X positioning - this.middle = 70; - this.height = 60; - // 20 is the first line segment and following activities take 60 px each - // (together with following vertical line) - this.y = 20 + this.height * index; - - // first draw the inner shape, then put back the realY for background gray square - var finalY = this.y; - if (this.isComplex) { - this.y += 5; - } - - if (type == 'g') { - // gate activity - this.height = 70; - ActivityUtils.shapeGateActivity(this); - } else { - ActivityUtils.shapeByStatus(this); - } - - - // special behaviour for complex activities - if (this.isComplex) { - this.height = 70; - this.y = finalY; - ActivityUtils.shapeComplexActivityContainer(this); - - this.childActivities = [new OptionalActivity(name, status, url)]; - var childActivities = this.childActivities; - $.each(childActivitiesData, function(childActivityIndex, childActivityData) { - childActivities[childActivityIndex + 1] = new OptionalActivity(childActivityData.name, - childActivityData.status, childActivityData.url, - childActivityData.childActivities, false); - }); - } -} - -// Support (floating) activities are show in separate box and behave differently -function SupportActivity(paper, index, name, status, url) { - this.paper = paper; - 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 = LABEL_SUPPORT_ACTIVITY; - - this.transformToAttempted = function(){ - var oldDecoration = this.decoration; - ActivityUtils.shapeAttemptedActivity(this); - this.statusTooltip = LABEL_SUPPORT_ACTIVITY; - - ActivityUtils.animate(this, oldDecoration); - } -} - -// Optional and Branching inner activities -function OptionalActivity(name, status, url, childActivitiesData, isNested) { - 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(childActivityData.name, - childActivityData.status, childActivityData.url, null, true); - }); - } -} - - -// refresh progress bar on first/next activity load -function fillProgressBar() { - $.ajax({ - url : APP_URL + 'learner.do', - data : { - 'method' : 'displayProgressJSON', - 'lessonID' : lessonId - }, - cache : false, - dataType : 'json', - success : function(result) { - // if nothing changed, don't do any calculations - if (!currentActivityId || result.currentActivityId != currentActivityId) { - currentActivityId = result.currentActivityId; - isPreview = result.isPreview; - - if (!paper) { - // create paper only the first time - paper = Raphael('progressBarDiv', 140, 60*result.activities.length); - // first line on the top - paper.path("M70 0 v20"); - } - - // 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(paper, activityIndex, activityData.id, - activityData.type, activityData.name, - activityData.status, activityData.url, - activityData.childActivities); - if (activity.status == 0) { - currentActivityIndex = activityIndex; - } - - 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(paper, 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(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'); - - jQuery.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 - $('#progressBarDiv').scrollTop(Math.max((currentActivityIndex-1)*60, 0)); - } - } - }); } \ No newline at end of file Index: lams_learning/web/main.jsp =================================================================== diff -u -r2c442242c90c2509ed564fda945f01ff422e102b -r9438790401b957cdd70a4d6ab2a76da70176c6eb --- lams_learning/web/main.jsp (.../main.jsp) (revision 2c442242c90c2509ed564fda945f01ff422e102b) +++ lams_learning/web/main.jsp (.../main.jsp) (revision 9438790401b957cdd70a4d6ab2a76da70176c6eb) @@ -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