Index: lams_central/web/author2.jsp =================================================================== diff -u -r06e2eedee66e1ee3087000324a326111dccdbcba -r8068bd165e981855f43bdeb2358e2a6b3654020c --- lams_central/web/author2.jsp (.../author2.jsp) (revision 06e2eedee66e1ee3087000324a326111dccdbcba) +++ lams_central/web/author2.jsp (.../author2.jsp) (revision 8068bd165e981855f43bdeb2358e2a6b3654020c) @@ -67,6 +67,18 @@
+ Optional +
+
 
+
+ +
+
+
+
Flow
 
@@ -393,6 +405,37 @@
+ +
+ + + + + + + + + + + + + +
+ Title: + + +
+ Min activities: + + +
+ Max activities: + + +
+
+ +
@@ -404,8 +447,9 @@
-
+
+
Index: lams_central/web/includes/javascript/authoring/authoringActivity.js =================================================================== diff -u -r06e2eedee66e1ee3087000324a326111dccdbcba -r8068bd165e981855f43bdeb2358e2a6b3654020c --- lams_central/web/includes/javascript/authoring/authoringActivity.js (.../authoringActivity.js) (revision 06e2eedee66e1ee3087000324a326111dccdbcba) +++ lams_central/web/includes/javascript/authoring/authoringActivity.js (.../authoringActivity.js) (revision 8068bd165e981855f43bdeb2358e2a6b3654020c) @@ -34,7 +34,6 @@ this.id = +id; this.uiid = +uiid || ++layout.ld.maxUIID; this.toolContentID = toolContentID; - this.type = 'tool'; this.toolID = +toolID; this.title = title; this.supportsOutputs = supportsOutputs; @@ -58,8 +57,7 @@ this.groupingID = +groupingID; this.groupingUIID = +groupingUIID || ++layout.ld.maxUIID; this.uiid = +uiid || ++layout.ld.maxUIID; - this.type = 'grouping'; - this.title = title; + this.title = title || 'Grouping'; this.groupingType = groupingType || 'random'; this.groupDivide = groupDivide || 'groups'; this.groupCount = +groupCount || 2; @@ -84,7 +82,6 @@ GateActivity : function(id, uiid, x, y, gateType) { this.id = +id; this.uiid = +uiid || ++layout.ld.maxUIID; - this.type = 'gate'; this.gateType = gateType || 'permission'; this.transitions = { 'from' : [], @@ -102,7 +99,6 @@ * Either branching or converge point. */ BranchingEdgeActivity : function(id, uiid, x, y, title, branchingType, branchingActivity) { - this.type = 'branchingEdge'; this.transitions = { 'from' : [], 'to' : [] @@ -154,7 +150,48 @@ /** + * Constructor for an Optional Activity. + */ + OptionalActivity : function(id, uiid, x, y, title, minActivities, maxActivities) { + DecorationLib.Container.call(this, title || 'Optional Activity'); + + this.id = +id; + this.uiid = +uiid || ++layout.ld.maxUIID; + this.minActivities = minActivities || 0; + this.maxActivities = maxActivities || 0; + this.transitions = { + 'from' : [], + 'to' : [] + }; + + this.loadPropertiesDialogContent = PropertyLib.optionalActivityProperties; + + this.draw = ActivityLib.draw.optionalActivity; + this.draw(x, y); + }, + + + /** + * Constructor for a Floating Activity. + */ + FloatingActivity : function(id, uiid, x, y) { + DecorationLib.Container.call(this, 'Support Activity'); + + this.id = +id; + this.uiid = +uiid || ++layout.ld.maxUIID; + + this.draw = ActivityLib.draw.floatingActivity; + this.draw(x, y); + + // there can only be one + layout.floatingActivity = this; + }, + + + /** * Mehtods for drawing various kinds of activities. + * They are not defined in constructors so there is always a reference to an existing method, + * not a separate definition for each object instance. */ draw : { @@ -329,6 +366,79 @@ this.items.shape = shape; ActivityLib.activityHandlersInit(this); + }, + + + optionalActivity : function(x, y, ignoredParam1, ignoredParam2, childActivities) { + if (x == undefined || y == undefined) { + // if no new coordinates are given, just redraw the activity + x = this.items.shape.getBBox().x; + y = this.items.shape.getBBox().y; + } + + // either check what children are on canvas or use the priovided parameter + if (childActivities) { + this.childActivities = childActivities; + } + + if (this.childActivities && this.childActivities.length > 0) { + // draw one by one, vertically + var activityY = y + 30, + allElements = paper.set(), + optionalActivity = this; + $.each(this.childActivities, function(){ + this.parentActivity = optionalActivity; + this.draw(x + 20, activityY); + activityY = this.items.shape.getBBox().y2 + 10; + allElements.push(this.items.shape); + }); + // area containing all drawn child activities + var box = allElements.getBBox(); + + this.drawContainer(x, y, box.x2 + 20, box.y2 + 20, layout.colors.optionalActivity); + } else { + this.drawContainer(x, y, x + 50, y + 70, layout.colors.optionalActivity); + } + + // allow transition drawing and other activity behaviour + this.items.shape.unmousedown().mousedown(HandlerLib.activityMousedownHandler); + + this.items.data('parentObject', this); + }, + + + floatingActivity : function(x, y, ignoredParam1, ignoredParam2, childActivities) { + if (x == undefined || y == undefined) { + // if no new coordinates are given, just redraw the activity + x = this.items.shape.getBBox().x; + y = this.items.shape.getBBox().y; + } + + // either check what children are on canvas or use the priovided parameter + if (childActivities) { + this.childActivities = childActivities; + } + + if (this.childActivities && this.childActivities.length > 0) { + // draw one by one, horizontally + var activityX = x + 20, + allElements = paper.set(), + floatingActivity = this; + $.each(this.childActivities, function(){ + this.parentActivity = floatingActivity; + this.draw(activityX, y + 30); + activityX = this.items.shape.getBBox().x2 + 10; + allElements.push(this.items.shape); + }); + // area containing all drawn child activities + var box = allElements.getBBox(); + + this.drawContainer(x, y, box.x2 + 20, box.y2 + 20, layout.colors.optionalActivity); + } else { + this.drawContainer(x, y, x + 50, y + 70, layout.colors.optionalActivity); + } + + this.items.data('parentObject', this); } }, @@ -347,7 +457,8 @@ 'cursor' : 'pointer' }); - if (activity.type == 'branchingEdge' && activity.branchingActivity.end) { + if (activity instanceof ActivityLib.BranchingEdgeActivity + && activity.branchingActivity.end) { // highligh branching edges on hover activity.branchingActivity.start.items.hover(HandlerLib.branchingEdgeMouseoverHandler, HandlerLib.branchingEdgeMouseoutHandler); @@ -361,7 +472,7 @@ * Deletes the given activity. */ removeActivity : function(activity, forceRemove) { - if (!forceRemove && activity.type == 'branchingEdge'){ + if (!forceRemove && activity instanceof ActivityLib.BranchingEdgeActivity){ // user removes one of the branching edges, so remove the whole activity if (confirm('Are you sure you want to remove the whole branching activity?')){ var otherEdge = activity.isStart ? activity.branchingActivity.end @@ -372,26 +483,46 @@ } } - // remove the transitions - // need to use slice() to copy the array as it gets modified in removeTransition() - $.each(activity.transitions.from.slice(), function() { - ActivityLib.removeTransition(this); - }); - $.each(activity.transitions.to.slice(), function() { - ActivityLib.removeTransition(this); - }); + if (activity instanceof ActivityLib.FloatingActivity) { + layout.floatingActivity = null; + // re-enable the button + $('#floatingActivityButton').attr('disabled', null) + .css('opacity', 1); + } else { + // remove the transitions + // need to use slice() to copy the array as it gets modified in removeTransition() + $.each(activity.transitions.from.slice(), function() { + ActivityLib.removeTransition(this); + }); + $.each(activity.transitions.to.slice(), function() { + ActivityLib.removeTransition(this); + }); + + // remove the activity from reference tables + layout.activities.splice(layout.activities.indexOf(activity), 1); + if (layout.items.copiedActivity = activity) { + layout.items.copiedActivity = null; + } + if (activity instanceof ActivityLib.GroupingActivity) { + $.each(layout.activities, function(){ + if (activity == this.grouping) { + this.grouping = null; + this.draw(); + } + }); + } + } - // remove the activity from reference tables - layout.activities.splice(layout.activities.indexOf(activity), 1); - if (layout.items.copiedActivity = activity) { - layout.items.copiedActivity = null; + // remove the activity from parent activity + if (activity.parentActivity) { + activity.parentActivity.childActivities.splice(layout.parentActivity.childActivities.indexOf(activity), 1); } - if (activity.type == 'grouping') { - $.each(layout.activities, function(){ - if (activity == this.grouping) { - this.grouping = null; - this.draw(); - } + + // remove child activities + if (activity instanceof ActivityLib.OptionalActivity + || activity instanceof ActivityLib.FloatingActivity) { + $.each(activity.childActivities, function(){ + ActivityLib.removeActivity(this); }); } @@ -404,19 +535,30 @@ * Draws a transition between two activities. */ addTransition : function(fromActivity, toActivity, redraw, id, uiid, title) { + if (toActivity.parentActivity){ + toActivity = toActivity.parentActivity; + } + if (fromActivity.parentActivity){ + fromActivity = fromActivity.parentActivity; + } + if (toActivity instanceof ActivityLib.FloatingActivity + || fromActivity instanceof ActivityLib.FloatingActivity){ + return; + } + // only converge points are allowed to have few inbound transitions if (!redraw && toActivity.transitions.to.length > 0 - && !(toActivity.type == 'branchingEdge' && !toActivity.isStart)) { + && !(toActivity instanceof ActivityLib.BranchingEdgeActivity && !toActivity.isStart)) { alert('Transition to this activity already exists'); return; } // user chose to create outbound transition from an activity that already has one if (!redraw && fromActivity.transitions.from.length > 0 - && !(fromActivity.type == 'branchingEdge' && fromActivity.isStart) - && !(toActivity.type == 'branchingEdge' && toActivity.isStart)) { + && !(fromActivity instanceof ActivityLib.BranchingEdgeActivity && fromActivity.isStart) + && !(toActivity instanceof ActivityLib.BranchingEdgeActivity && toActivity.isStart)) { if (confirm('Transition from this activity already exists.\n' + 'Do you want to create branching here?')) { ActivityLib.addBranching(fromActivity, toActivity); @@ -436,7 +578,7 @@ } }); - if (!branch && fromActivity.type == 'branchingEdge' && fromActivity.isStart) { + if (!branch && fromActivity instanceof ActivityLib.BranchingEdgeActivity && fromActivity.isStart) { // create a new branch branch = new ActivityLib.BranchActivity(null, null, null, fromActivity.branchingActivity); } @@ -491,7 +633,7 @@ convergeActivity1 = convergeActivity1.transitions.from[0].toActivity; }; - if (toActivity2.type == 'branchingEdge' && toActivity2.isStart) { + if (toActivity2 instanceof ActivityLib.BranchingEdgeActivity && toActivity2.isStart) { // there is already a branching activity, reuse existing items branchingEdgeStart = toActivity2; branchingEdgeEnd = toActivity2.branchingActivity.end; @@ -542,6 +684,7 @@ 'stroke-dasharray' : '-' }); object.items.resizeButton.show(); + object.items.resizeButton.toFront(); object.items.selectEffect = true; // also select encapsulated activities @@ -550,7 +693,9 @@ object.items.fitButton.show(); $.each(childActivities, function(){ - ActivityLib.addSelectEffect(this, false); + if (!this.parentActivity) { + ActivityLib.addSelectEffect(this, false); + } }); } } else if (object instanceof ActivityLib.Transition) { @@ -605,6 +750,7 @@ if (object) { if (object.items.selectEffect) { + // different effects for different types of objects if (object instanceof DecorationLib.Region) { object.items.shape.attr({ 'stroke' : 'black', @@ -661,17 +807,69 @@ /** * Drop the dragged activity on the canvas. */ - dropActivity : function(activity) { - // redraw transitions - $.each(activity.transitions.from.slice(), function(){ - ActivityLib.addTransition(activity, this.toActivity, true); + dropActivity : function(activity, x, y) { + if (!(activity instanceof DecorationLib.Container)) { + // check if it was removed from an Optional or Floating Activity + if (activity.parentActivity) { + var childActivities = DecorationLib.getChildActivities(activity.parentActivity.items.shape); + if ($.inArray(activity, childActivities) == -1) { + activity.parentActivity.draw(); + ActivityLib.redrawTransitions(activity.parentActivity); + activity.parentActivity = null; + } + } + + // check if it was added to an Optional or Floating Activity + var container = layout.floatingActivity && layout.floatingActivity.items.shape.isPointInside(x, y) + ? layout.floatingActivity : null; + if (!container) { + $.each(layout.activities, function(){ + if (this instanceof ActivityLib.OptionalActivity && this.items.shape.isPointInside(x, y)) { + container = this; + return false; + } + }); + } + if (container) { + if ($.inArray(activity, container.childActivities) == -1) { + $.each(activity.transitions.from, function(){ + ActivityLib.removeTransition(this); + }); + $.each(activity.transitions.to, function(){ + ActivityLib.removeTransition(this); + }); + + // for properties dialog to reload + ActivityLib.removeSelectEffect(container); + + container.childActivities.push(activity); + container.draw(null, null, null, null, childActivities); + ActivityLib.redrawTransitions(container); + } + } + } + + ActivityLib.redrawTransitions(activity); + + $.each(layout.regions, function(){ + // redraw all annotation regions so they are pushed to back + this.draw(); }); - $.each(activity.transitions.to.slice(), function(){ - ActivityLib.addTransition(this.fromActivity, activity, true); - }); }, + redrawTransitions : function(activity) { + if (activity.transitions) { + $.each(activity.transitions.from.slice(), function(){ + ActivityLib.addTransition(activity, this.toActivity, true); + }); + $.each(activity.transitions.to.slice(), function(){ + ActivityLib.addTransition(this.fromActivity, activity, true); + }); + } + }, + + /** * Open separate window with activity authoring on double click. */ @@ -755,7 +953,7 @@ // include the first activity var branchLength = 1, activity = this.transitionFrom.toActivity; - if (activity.type == 'branchingEdge' + if (activity instanceof ActivityLib.BranchingEdgeActivity && branchingActivity == activity.branchingActivity){ // branch with no activities return true; @@ -764,7 +962,7 @@ while (activity.transitions.from.length > 0) { activity = activity.transitions.from[0].toActivity; // check if reached the end of branch - if (activity.type == 'branchingEdge') { + if (activity instanceof ActivityLib.BranchingEdgeActivity) { break; } else { branchLength++; Index: lams_central/web/includes/javascript/authoring/authoringDecoration.js =================================================================== diff -u -r06e2eedee66e1ee3087000324a326111dccdbcba -r8068bd165e981855f43bdeb2358e2a6b3654020c --- lams_central/web/includes/javascript/authoring/authoringDecoration.js (.../authoringDecoration.js) (revision 06e2eedee66e1ee3087000324a326111dccdbcba) +++ lams_central/web/includes/javascript/authoring/authoringDecoration.js (.../authoringDecoration.js) (revision 8068bd165e981855f43bdeb2358e2a6b3654020c) @@ -5,16 +5,28 @@ var DecorationLib = { /** - * Constructor for region annotation. + * Abstract class for Region, Optional and Floating Activities. */ - Region : function(x, y, width, height, color, title) { + Container : function(title) { this.title = title; + this.childActivities = []; + this.drawContainer = DecorationLib.methods.container.draw; + this.fit = DecorationLib.methods.container.fit; + }, + + /** + * Constructor for region annotation. + */ + Region : function(x, y, x2, y2, color, title) { + DecorationLib.Container.call(this, title); + // we don't use it for region + this.childActivities = null; + this.draw = DecorationLib.methods.region.draw; - this.fit = DecorationLib.methods.region.fit; this.loadPropertiesDialogContent = PropertyLib.regionProperties; - this.draw(x, y, width, height, color); + this.draw(x, y, x2, y2, color); }, @@ -33,76 +45,46 @@ methods : { - - /** - * Region methods - */ - region : { - draw : function(x, y, x2, y2, color, title){ + container : { + + draw : function(x, y, x2, y2, color){ // check for new coordinates or just take them from the existing shape - var box = this.items && this.items.shape ? this.items.shape.getBBox() : null, + var box = this.items ? this.items.shape.getBBox() : null, x = x ? x : box.x, y = y ? y : box.y, // take into account minimal size of rectangle - x2 = x2 ? (x2 < x + 20 ? x + 20 : x2) : x + box.width, - y2 = y2 ? (y2 < y + 20 ? y + 20 : y2) : y + box.height, + x2 = x2 ? Math.max(x2, x + 20) : x + box.width, + y2 = y2 ? Math.max(y2, y + 20) : y + box.height, color = color ? color : this.items.shape.attr('fill'); - if (box) { - this.items.remove(); - } + if (box) { + this.items.remove(); + } + + // the label + this.items = paper.set(); + if (this.title) { + var label = paper.text(x + 7, y + 10, this.title) + .attr('text-anchor', 'start') + .toBack(); + this.items.push(label); - // the label - this.items = paper.set(); - if (this.title) { - var label = paper.text(x + 7, y + 10, this.title) - .attr('text-anchor', 'start') - .toBack(); - this.items.push(label); - } - - // the rectangle - this.items.shape = paper.path('M {0} {1} L {2} {1} L {2} {3} L {0} {3} z', x, y, x2, y2) - .attr({ - 'fill' : color, - 'cursor' : 'pointer' - }) - .mousedown(HandlerLib.regionMousedownHandler) - .click(HandlerLib.itemClickHandler) - .toBack(); - this.items.push(this.items.shape); - - this.items.fitButton = paper.circle(x2 - 10, y + 10, 5) - .attr({ - 'stroke' : null, - 'fill' : 'blue', - 'cursor' : 'pointer', - 'title' : 'Fit' - }) - .click(function(event){ - event.stopImmediatePropagation(); - var region = this.data('parentObject'); - region.fit(); - ActivityLib.addSelectEffect(region, true); - }) - .hide(); - this.items.push(this.items.fitButton); - - this.items.resizeButton = paper.path(Raphael.format('M {0} {1} v {2} h -{2} z', - x2, y2 - 15, 15)) - .attr({ - 'stroke' : null, - 'fill' : 'blue', - 'cursor' : 'se-resize' - }) - .mousedown(HandlerLib.resizeRegionStartHandler) - .hide(); - this.items.push(this.items.resizeButton); - - this.items.data('parentObject', this); - }, - - + // make sure title fits + x2 = Math.max(x2, label.getBBox().x2 + 5); + } + + // the rectangle + this.items.shape = paper.path('M {0} {1} L {2} {1} L {2} {3} L {0} {3} z', x, y, x2, y2) + .attr({ + 'fill' : color, + 'cursor' : 'pointer' + }) + .mousedown(HandlerLib.containerMousedownHandler) + .click(HandlerLib.itemClickHandler) + .toBack(); + this.items.push(this.items.shape); + }, + /** * Adjust the annotation so it envelops its child activities and nothing more. */ @@ -111,7 +93,7 @@ if (childActivities.length == 0) { return; } - + ActivityLib.removeSelectEffect(this); var allElements = paper.set(); @@ -126,7 +108,47 @@ } }, + /** + * Region methods + */ + region : { + draw : function(x, y, x2, y2, color){ + this.drawContainer(x, y, x2, y2, color); + + var box = this.items.shape.getBBox(); + + this.items.fitButton = paper.circle(box.x2 - 10, box.y + 10, 5) + .attr({ + 'stroke' : null, + 'fill' : 'blue', + 'cursor' : 'pointer', + 'title' : 'Fit' + }) + .click(function(event){ + event.stopImmediatePropagation(); + var region = this.data('parentObject'); + region.fit(); + ActivityLib.addSelectEffect(region, true); + }) + .hide(); + this.items.push(this.items.fitButton); + + this.items.resizeButton = paper.path(Raphael.format('M {0} {1} v {2} h -{2} z', + box.x2, box.y2 - 15, 15)) + .attr({ + 'stroke' : null, + 'fill' : 'blue', + 'cursor' : 'se-resize' + }) + .mousedown(HandlerLib.resizeRegionStartHandler) + .hide(); + this.items.push(this.items.resizeButton); + + this.items.data('parentObject', this); + } + }, + /** * Label methods */ @@ -171,19 +193,25 @@ /** - * Get activities enveloped by given shape (usually annotation region) + * Get activities enveloped by given container */ getChildActivities : function(shape){ var result = []; $.each(layout.activities, function(){ - var activityBox = this.items.shape.getBBox(); - - if (shape.isPointInside(activityBox.x, activityBox.y) - && shape.isPointInside(activityBox.x2, activityBox.y2)) { - result.push(this); + if (shape != this.items.shape) { + var activityBox = this.items.shape.getBBox(); + + if (shape.isPointInside(activityBox.x, activityBox.y) + && shape.isPointInside(activityBox.x2, activityBox.y2)) { + result.push(this); + } } }); + var parentObject = shape.data('parentObject'); + if (parentObject && !(parentObject instanceof DecorationLib.Region)) { + parentObject.childActivities = result; + } return result; }, @@ -199,4 +227,9 @@ layout.labels.splice(layout.labels.indexOf(label), 1); label.items.remove(); } -}; \ No newline at end of file +}; + +// set prototype hierarchy +DecorationLib.Region.prototype = new DecorationLib.Container; +ActivityLib.OptionalActivity.prototype = new DecorationLib.Container; +ActivityLib.FloatingActivity.prototype = new DecorationLib.Container; \ No newline at end of file Index: lams_central/web/includes/javascript/authoring/authoringGeneral.js =================================================================== diff -u -r06e2eedee66e1ee3087000324a326111dccdbcba -r8068bd165e981855f43bdeb2358e2a6b3654020c --- lams_central/web/includes/javascript/authoring/authoringGeneral.js (.../authoringGeneral.js) (revision 06e2eedee66e1ee3087000324a326111dccdbcba) +++ lams_central/web/includes/javascript/authoring/authoringGeneral.js (.../authoringGeneral.js) (revision 8068bd165e981855f43bdeb2358e2a6b3654020c) @@ -11,6 +11,7 @@ 'drawMode' : false, // 'isZoomed' : false, 'activities' : null, + 'floatingActivity' : null, 'regions' : null, 'labels' : null, 'items' : { @@ -65,7 +66,8 @@ 'transition' : 'rgb(119,126,157)', 'binActive' : 'red', 'selectEffect' : 'blue', - 'annotation' : 'yellow' + 'annotation' : 'yellow', + 'optionalActivity' : 'rgb(194,213,254)' }, }; @@ -141,9 +143,14 @@ var toolID = draggable.draggable.attr('toolId'), x = draggable.offset.left + canvas.scrollLeft() - canvas.offset().left, y = draggable.offset.top + canvas.scrollTop() - canvas.offset().top, - label = $('div', draggable.draggable).text(); - - layout.activities.push(new ActivityLib.ToolActivity(null, null, null, toolID, x, y, label)); + label = $('div', draggable.draggable).text(), + activity = new ActivityLib.ToolActivity(null, null, null, toolID, x, y, label), + translatedEvent = ActivityLib.translateEventOnCanvas(event), + eventX = translatedEvent[0], + eventY = translatedEvent[1]; + + layout.activities.push(activity); + ActivityLib.dropActivity(activity, eventX, eventY); } }); } @@ -489,7 +496,7 @@ // Sequence Activity, i.e. a branch case 8: $.each(layout.activities, function(){ - if (this.type == 'branchingEdge' + if (this instanceof ActivityLib.BranchingEdgeActivity && activityData.parentActivityID == this.branchingActivity.id) { // create a branch inside the branching activity this.branchingActivity.branches.push( @@ -555,9 +562,9 @@ var groupedActivity = this.activity, groupingID = this.groupingID; $.each(layout.activities, function(){ - if (this.type == 'grouping' && groupingID == this.groupingID) { + if (this instanceof ActivityLib.GroupingActivity && groupingID == this.groupingID) { // add reference and redraw the grouped activity - if (groupedActivity.type == 'branchingEdge') { + if (groupedActivity instanceof ActivityLib.BranchingEdgeActivity) { groupedActivity.branchingActivity.grouping = this; } else { groupedActivity.grouping = this; @@ -577,15 +584,15 @@ branch = null; $.each(layout.activities, function(){ // is it the branch we're looking for? - if (this.type == 'branchingEdge' && this.isStart) { + if (this instanceof ActivityLib.BranchingEdgeActivity && this.isStart) { $.each(this.branchingActivity.branches, function(){ if (branchUIID == this.uiid) { branch = this; return false; } }); // is it the grouping we're looking for - } else if (this.type == 'grouping') { + } else if (this instanceof ActivityLib.GroupingActivity) { $.each(this.groups, function(){ if (groupUIID == this.uiid) { group = this; @@ -611,7 +618,7 @@ // draw starting and ending transitions in branches $.each(layout.activities, function(){ - if (this.type == 'branchingEdge' && this.isStart) { + if (this instanceof ActivityLib.BranchingEdgeActivity && this.isStart) { var branchingActivity = this.branchingActivity; $.each(branchingActivity.branches, function(){ var branch = this, @@ -635,7 +642,7 @@ // find which activities the transition belongs to $.each(layout.activities, function(){ var activity = this, - isBranching = activity.type == 'branchingEdge'; + isBranching = activity instanceof ActivityLib.BranchingEdgeActivity; // check if transition IDs match either a plain activity or a complex one if (isBranching ? !activity.isStart && transition.fromActivityID == activity.branchingActivity.id @@ -717,14 +724,14 @@ 'groupingSupportType' : 2, 'applyGrouping' : isGrouped, 'groupingUIID' : isGrouped ? activity.grouping.groupingUIID : null, - 'createGroupingUIID' : activity.type == 'grouping' ? activity.groupingUIID : null, + 'createGroupingUIID' : activity instanceof ActivityLib.GroupingActivity ? activity.groupingUIID : null, 'parentActivityID' : null, 'parentUIID' : null, 'libraryActivityUIImage' : iconPath, 'xCoord' : activityBox.x, 'yCoord' : activityBox.y, 'activityTitle' : activity.title, - 'activityCategoryID' : activity.type == 'tool' ? + 'activityCategoryID' : activity instanceof ActivityLib.ToolActivity ? layout.toolMetadata[toolID].activityCategoryID : 1, 'activityTypeID' : activityTypeID, @@ -746,7 +753,7 @@ }); }); - if (activity.type == 'grouping') { + if (activity instanceof ActivityLib.GroupingActivity) { // create a list of groupings var groups = [], groupingType = null; Index: lams_central/web/includes/javascript/authoring/authoringHandler.js =================================================================== diff -u -r06e2eedee66e1ee3087000324a326111dccdbcba -r8068bd165e981855f43bdeb2358e2a6b3654020c --- lams_central/web/includes/javascript/authoring/authoringHandler.js (.../authoringHandler.js) (revision 06e2eedee66e1ee3087000324a326111dccdbcba) +++ lams_central/web/includes/javascript/authoring/authoringHandler.js (.../authoringHandler.js) (revision 8068bd165e981855f43bdeb2358e2a6b3654020c) @@ -105,6 +105,15 @@ items.isDragged = true; items.attr('cursor', 'move'); + var parentObject = draggedElement.data('parentObject'); + sticky = parentObject && (parentObject instanceof ActivityLib.OptionalActivity + || parentObject instanceof ActivityLib.FloatingActivity); + if (sticky) { + $.each(parentObject.childActivities, function(){ + this.items.hide(); + }); + } + canvas.mousemove(function(event) { HandlerLib.dragItemsMoveHandler(items, event, startX, startY); }); @@ -153,7 +162,7 @@ * Start drawing a transition. */ drawTransitionStartHandler : function(activity, event, x, y) { - if (activity.fromTransition && activity.type != 'branchingEdge') { + if (activity.fromTransition && !(activity instanceof ActivityLib.BranchingEdgeActivity)) { alert('Transition from this activity already exists'); return; } @@ -274,7 +283,7 @@ // immediatelly show which activities will be enveloped var childActivities = DecorationLib.getChildActivities(data.shape); $.each(layout.activities, function(){ - if ($.inArray(this, childActivities) > -1){ + if (!this.parentActivity && $.inArray(this, childActivities) > -1){ ActivityLib.addSelectEffect(this, false); } else { ActivityLib.removeSelectEffect(this); @@ -386,13 +395,16 @@ ActivityLib.removeActivity(activity); } else { HandlerLib.dropObject(activity); - ActivityLib.dropActivity(activity); + + var translatedEvent = ActivityLib.translateEventOnCanvas(event), + endX = translatedEvent[0], + endY = translatedEvent[1]; + ActivityLib.dropActivity(activity, endX, endY); } } // start dragging the activity HandlerLib.dragItemsStartHandler(activity.items, this, mouseupHandler, event, x, y); } - }, @@ -449,26 +461,33 @@ /** - * Starts dragging a region + * Starts dragging a container */ - regionMousedownHandler : function(event, x, y){ + containerMousedownHandler : function(event, x, y){ if (layout.drawMode || (event.originalEvent ? event.originalEvent.defaultPrevented : event.defaultPrevented)){ return; } - var region = this.data('parentObject'); + var container = this.data('parentObject'); // allow transition dragging var mouseupHandler = function(event){ if (HandlerLib.isElemenentBinned(event)) { - // if the region was over rubbish bin, remove it - DecorationLib.removeRegion(region); + // if the container was over rubbish bin, remove it + if (container instanceof DecorationLib.Region) { + DecorationLib.removeRegion(container); + } else { + ActivityLib.removeActivity(container); + } } else { - HandlerLib.dropObject(region); + HandlerLib.dropObject(container); + if (container instanceof ActivityLib.FloatingActivity) { + ActivityLib.dropActivity(container); + } } } - HandlerLib.dragItemsStartHandler(region.items, this, mouseupHandler, event, x, y); + HandlerLib.dragItemsStartHandler(container.items, this, mouseupHandler, event, x, y); }, Index: lams_central/web/includes/javascript/authoring/authoringMenu.js =================================================================== diff -u -r06e2eedee66e1ee3087000324a326111dccdbcba -r8068bd165e981855f43bdeb2358e2a6b3654020c --- lams_central/web/includes/javascript/authoring/authoringMenu.js (.../authoringMenu.js) (revision 06e2eedee66e1ee3087000324a326111dccdbcba) +++ lams_central/web/includes/javascript/authoring/authoringMenu.js (.../authoringMenu.js) (revision 8068bd165e981855f43bdeb2358e2a6b3654020c) @@ -90,7 +90,7 @@ x = translatedEvent[0] - 47, y = translatedEvent[1] - 2; - layout.activities.push(new ActivityLib.GroupingActivity(null, null, x, y, 'Grouping')); + layout.activities.push(new ActivityLib.GroupingActivity(null, null, x, y)); HandlerLib.resetCanvasMode(true); }); @@ -148,6 +148,65 @@ /** + * Creates a new optional activity. + */ + addOptionalActivity : function() { + HandlerLib.resetCanvasMode(); + + var dialog = layout.items.infoDialog.text('Click to add an optional activity container.'); + dialog.dialog('open'); + + canvas.css('cursor', 'pointer').click(function(event){ + dialog.text(''); + dialog.dialog('close'); + + + var translatedEvent = ActivityLib.translateEventOnCanvas(event), + x = translatedEvent[0], + y = translatedEvent[1]; + + HandlerLib.resetCanvasMode(true); + + layout.activities.push(new ActivityLib.OptionalActivity(null, null, x, y)); + }); + }, + + + /** + * Creates a new floating activity. + */ + addFloatingActivity : function() { + if (layout.floatingActivity) { + // there can be only one + return; + } + HandlerLib.resetCanvasMode(); + + var dialog = layout.items.infoDialog.text('Click to add a support activity container.'); + dialog.dialog('open'); + + canvas.css('cursor', 'pointer').click(function(event){ + dialog.text(''); + dialog.dialog('close'); + + + var translatedEvent = ActivityLib.translateEventOnCanvas(event), + x = translatedEvent[0], + y = translatedEvent[1]; + + HandlerLib.resetCanvasMode(true); + + // do not add it to layout.activities as it behaves differently + new ActivityLib.FloatingActivity(null, null, x, y); + + // there can be only one, so disable the button + $('#floatingActivityButton').attr('disabled', 'disabled') + .css('opacity', 0.2); + }); + }, + + + /** * Creates a new transition. */ addTransition : function() { @@ -314,17 +373,19 @@ + 'Do you want to continue?')) { return; } - - // just to refresh the state of canvas - HandlerLib.resetCanvasMode(true); if (layout.activities.length == 0) { // no activities, nothing to do return; } + // just to refresh the state of canvas + HandlerLib.resetCanvasMode(true); + // activities are arranged in a grid var row = 0, + // for special cases when row needs to shifted more + forceRowY = null, column = 0, // check how many columns current paper can hold maxColumns = Math.floor((paper.width - layout.conf.arrangeHorizontalPadding) @@ -334,15 +395,21 @@ // check how many rows current paper can hold maxRows = Math.floor((paper.height - layout.conf.arrangeVerticalPadding) / layout.conf.arrangeVerticalSpace), - // make a shallow copy of activities array - activitiesCopy = layout.activities.slice(), + // a shallow copy of activities array without inner activities + activitiesCopy = [], // just to speed up processing when there are only activities with no transitions left onlyDetachedLeft = false; + + $.each(layout.activities, function(){ + if (!this.parentActivity){ + activitiesCopy.push(this); + } + }); // branches will not be broken into few rows; if they are long, paper will be resized // find the longes branch to find the new paper size $.each(layout.activities, function(){ - if (this.type == 'branchingEdge' && this.isStart) { + if (this instanceof ActivityLib.BranchingEdgeActivity && this.isStart) { // refresh branching metadata ActivityLib.updateBranchesLength(this.branchingActivity); // add start and end edges to the result @@ -352,6 +419,12 @@ } } }); + + // check how many child activities are in Floating Activity, if any + if (layout.floatingActivity && layout.floatingActivity.childActivities.length > subsequenceMaxLength) { + subsequenceMaxLength = childActivities.length; + } + // resize paper horizontally, if needed if (subsequenceMaxLength > maxColumns) { maxColumns = subsequenceMaxLength; @@ -400,7 +473,7 @@ // crawl through a sequence of activities while (activity) { - if (activity.type == 'branchingEdge') { + if (activity instanceof ActivityLib.BranchingEdgeActivity) { if (activity.isStart) { // draw branching edges straight away and remove them from normall processing var branchingActivity = activity.branchingActivity, @@ -409,7 +482,7 @@ complex = { end : end }, - // can the whole branching fit in curren canvas width? + // can the whole branching fit in current canvas width? branchingFits = column + branchingActivity.longestBranchLength + 2 <= maxColumns; if (!branchingFits) { // start branching from the left side of canvas @@ -465,21 +538,39 @@ var x = layout.conf.arrangeHorizontalPadding + column * layout.conf.arrangeHorizontalSpace, y = layout.conf.arrangeVerticalPadding + row * layout.conf.arrangeVerticalSpace; - if (activity.type == 'gate') { + if (activity instanceof ActivityLib.GateActivity) { // adjust placement for gate activity, so it's in the middle of its cell x += 57; y += 10; + } else if (activity instanceof ActivityLib.OptionalActivity){ + x -= 20; } activity.draw(x, y); // remove the activity so we do not process it twice activitiesCopy.splice(activitiesCopy.indexOf(activity), 1); + + // learn where a tall Optional Activity has its end + // and later start drawing activities lower than in the next row + if (activity instanceof ActivityLib.OptionalActivity && activity.childActivities.length > 1) { + var activityEndY = activity.items.shape.getBBox().y2; + if (!forceRowY || activityEndY > forceRowY) { + forceRowY = activityEndY; + } + } } // find the next row and column column = (column + 1) % maxColumns; if (column == 0) { row++; + // if an Optional Activity forced next activities to be drawn lower than usual + if (forceRowY) { + while (forceRowY > layout.conf.arrangeVerticalPadding + 10 + row * layout.conf.arrangeVerticalSpace) { + row++; + } + forceRowY = null; + } } // does the activity has further activities? @@ -531,6 +622,15 @@ }; }; + if (layout.floatingActivity) { + row++; + column = 0; + var x = layout.conf.arrangeHorizontalPadding, + y = layout.conf.arrangeVerticalPadding - 30 + row * layout.conf.arrangeVerticalSpace; + + layout.floatingActivity.draw(x, y); + } + // redraw transitions one by one $.each(layout.activities, function(){ $.each(this.transitions.from.slice(), function(){ @@ -544,13 +644,15 @@ * Removes existing activities and prepares canvas for a new sequence. */ newLearningDesign : function(force, soft){ + // force means that user should not be asked for confirmation. if (!force && (layout.activities.length > 0 || layout.regions.length > 0 || layout.labels.length > 0) && !confirm('Are you sure you want to remove all existing elements?')){ return; } + // soft means that data is manually reset, instead of simply reloading the page. if (soft) { $('#ldDescriptionFieldTitle').text(''); CKEDITOR.instances['ldDescriptionFieldDescription'].setData(null); @@ -561,6 +663,7 @@ layout.activities = []; layout.regions = []; layout.labels = []; + layout.floatingActivity = null; if (paper) { paper.clear(); @@ -599,7 +702,7 @@ return; } // only tool activities can be copied (todo?) - if (activity.type != 'tool') { + if (!(activity instanceof ActivityLib.ToolActivity)) { alert('Sorry, you can not paste this type of activity'); return; } Index: lams_central/web/includes/javascript/authoring/authoringProperty.js =================================================================== diff -u -r06e2eedee66e1ee3087000324a326111dccdbcba -r8068bd165e981855f43bdeb2358e2a6b3654020c --- lams_central/web/includes/javascript/authoring/authoringProperty.js (.../authoringProperty.js) (revision 06e2eedee66e1ee3087000324a326111dccdbcba) +++ lams_central/web/includes/javascript/authoring/authoringProperty.js (.../authoringProperty.js) (revision 8068bd165e981855f43bdeb2358e2a6b3654020c) @@ -405,6 +405,67 @@ /** + * Properties dialog content for Optional Activity. + */ + optionalActivityProperties : function() { + var activity = this, + content = activity.propertiesContent; + + activity.minActivities = Math.min(activity.minActivities, activity.childActivities.length); + activity.maxActivities = Math.min(activity.maxActivities, activity.childActivities.length); + + if (!content) { + // first run, create the content + content = activity.propertiesContent = $('#propertiesContentOptionalActivity').clone().attr('id', null) + .show().data('parentObject', activity); + $('.propertiesContentFieldTitle', content).val(activity.title); + + $('input', content).change(function(){ + // extract changed properties and redraw the transition + var content = $(this).closest('.dialogContainer'), + activity = content.data('parentObject'), + newTitle = $('.propertiesContentFieldTitle', content).val(); + if (newTitle != activity.title) { + activity.title = newTitle; + activity.draw(); + } + }); + + $('.propertiesContentFieldOptionalActivityMin', content).spinner({'min' : 0}) + .on('spinchange', function(){ + var value = +$(this).val(); + activity.minActivities = Math.min(value, activity.childActivities.length); + if (value != activity.minActivities) { + $(this, content).spinner('value', activity.minActivities); + } + if (activity.minActivities > activity.maxActivities) { + $('.propertiesContentFieldOptionalActivityMax', content).spinner('value', value); + } + $('.propertiesContentFieldOptionalActivityMax', content).spinner('option', 'min', value); + }); + + + $('.propertiesContentFieldOptionalActivityMax', content).spinner({'min' : 0}) + .on('spinchange', function(){ + var value = +$(this).val(); + activity.maxActivities = Math.min(value, activity.childActivities.length); + if (value != activity.maxActivities) { + $(this, content).spinner('value', activity.maxActivities); + } + }); + } + + $('.propertiesContentFieldOptionalActivityMin', content).spinner('value', activity.minActivities) + .spinner('option', 'max', activity.childActivities.length); + $('.propertiesContentFieldOptionalActivityMax', content).spinner('value', activity.maxActivities) + .spinner('option', { + 'min' : activity.minActivities, + 'max' : activity.childActivities.length + }); + }, + + + /** * Properties dialog content for regions (annotations). */ regionProperties : function() { @@ -474,7 +535,7 @@ var emptyOption = $('