0) { + html += ' indent' + indent; + } + html += '">' + this.text + end; + return html; + }; + + // Answer a text representation (i.e. code) of this codeline + ParsonsCodeline.prototype.asText = function() { + var text = ''; + var indent = this.indent + this.block.indent; + for (var i = 0; i < indent; i++) { + // four spaces for each indent + text += ' '; + } + text += this.text; + return text; + }; + + // Create a code block object based on the codestring + // widget: the ParsonsWidget + // index: index of the block (could be an array) + // lines: an array of ParsonsCodeline + // indent: how indented is the code based on spaces + // distractor: boolean as to whether it is not part of the solution + // paired: boolean whether this distractor should be paired with last valid line + var ParsonsCodeblock = function(codestring, widget) { + this.widget = widget; + this.lines = []; + this.indent = 0; + if (codestring) { + var code = codestring; + var options = {}; + // Figure out options based on the #option and #option=value syntax + // Remove the options from the code + code = code.replace(/#(\w+)=(\w+)/, function(mystring, arg1, arg2) { + options[arg1] = arg2; + return "" + }); + code = code.replace(/#(\w+)/, function(mystring, arg1) { + options[arg1] = true; + return "" + }); + + // Based on the options, determine the distractors + if (options["paired"]) { + // paired distractor + delete options["paired"]; + this.distractor = true; + this.paired = true; + } else if (options["distractor"]) { + // distractor + delete options["distractor"]; + this.distractor = true; + this.paired = false; + } else { + // This line is part of the solution + this.distractor = false; + this.paired = false; + } + + //Report unused options + for (var option in options) { + console.log(option + " is not a valid #option for a code block"); + } + + code = code.split(/\\n/); + for (var i = 0; i < code.length; i++) { + code[i] = new ParsonsCodeline(code[i], this); + } + this.lines = code; + } + }; + + // Used to normalize indents + // Part of initialization + ParsonsCodeblock.prototype.addIndentsTo = function(array) { + for (var i = 0; i < this.lines.length; i++) { + var value = this.indent + this.lines[i].indent; + if ($.inArray(value, array) == -1) { + array.push(value); + } + } + }; + + // Normalize indents based on array of indents + // Part of initialization + ParsonsCodeblock.prototype.normalizeIndents = function(array) { + var minIndent = 1000; + for (var i = 0; i < this.lines.length; i++) { + var value = this.indent + this.lines[i].indent; + value = array.indexOf(value); + this.lines[i].indent = value; + minIndent = Math.min(minIndent, value); + } + this.indent = minIndent; + for (i = 0; i < this.lines.length; i++) { + this.lines[i].indent = this.lines[i].indent - minIndent; + } + }; + + // Answer a string that represents this codeblock for saving + ParsonsCodeblock.prototype.hash = function() { + var hash = ""; + if (this.index.constructor === Array) { + for (var i = 0; i < this.index.length; i++) { + hash += this.index[i] + "_"; + } + } else { + hash += this.index + "_"; + } + hash += this.viewIndent; + return hash; + }; + + // Answer an HTML representation of this codeblock + ParsonsCodeblock.prototype.asHTML = function() { + var html = '
'+this.options.trash_label+'
':'') + - this.codeLinesToHTML(trashIDs, this.options.trashId); - $("#" + this.options.trashId).html(html); - html = (this.options.solution_label?''+this.options.solution_label+'
':'') + - this.codeLinesToHTML(solutionIDs, this.options.sortableId); - $("#" + this.options.sortableId).html(html); - } else { - html = this.codeLinesToHTML(solutionIDs, this.options.sortableId); - $("#" + this.options.sortableId).html(html); - } - - if (window.prettyPrint && (typeof(this.options.prettyPrint) === "undefined" || this.options.prettyPrint)) { - prettyPrint(); - } - - var that = this; - var sortable = $("#ul-" + this.options.sortableId).sortable( - { - start : function() { that.clearFeedback(); }, - stop : function(event, ui) { - if ($(event.target)[0] != ui.item.parent()[0]) { - return; - } - that.updateIndent(ui.position.left - ui.item.parent().position().left, - ui.item[0].id); - that.updateHTMLIndent(ui.item[0].id); - that.addLogEntry({type: "moveOutput", target: ui.item[0].id}, true); - }, - receive : function(event, ui) { - var ind = that.updateIndent(ui.position.left - ui.item.parent().position().left, - ui.item[0].id); - that.updateHTMLIndent(ui.item[0].id); - that.addLogEntry({type: "addOutput", target: ui.item[0].id}, true); - }, - grid : that.options.can_indent ? [that.options.x_indent, 1 ] : false - }); - sortable.addClass("output"); - if (this.options.trashId) { - var trash = $("#ul-" + this.options.trashId).sortable( - { - connectWith: sortable, - start: function() { that.clearFeedback(); }, - receive: function(event, ui) { - that.getLineById(ui.item[0].id).indent = 0; - that.updateHTMLIndent(ui.item[0].id); - that.addLogEntry({type: "removeOutput", target: ui.item[0].id}, true); - }, - stop: function(event, ui) { - if ($(event.target)[0] != ui.item.parent()[0]) { - // line moved to output and logged there - return; - } - that.addLogEntry({type: "moveInput", target: ui.item[0].id}, true); - } - }); - sortable.sortable('option', 'connectWith', trash); - } - // Log the original codelines in the exercise in order to be able to - // match the input/output hashes to the code later on. We need only a - // few properties of the codeline objects - var bindings = []; - for (var i = 0; i < this.modified_lines.length; i++) { - var line = this.modified_lines[i]; - bindings.push({code: line.code, distractor: line.distractor}) - } - this.addLogEntry({type: 'init', time: new Date(), bindings: bindings}); - }; - - - window['ParsonsWidget'] = ParsonsWidget; - } + // Return a codeblock that corresponds to the hash + ParsonsWidget.prototype.blockFromHash = function(hash) { + var split = hash.split("_"); + var block = this.blocks[Number(split[0])]; + if (this.options.noindent) { + block.viewIndent = 0; + } else { + block.viewIndent = Number(split[1]); + } + return block; + }; + + // Return an array of codeblocks that corresponds to the hash + ParsonsWidget.prototype.blocksFromHash = function(hash) { + var split; + if (hash === "-" || hash === "" || hash === null) { + split = []; + } else { + split = hash.split("-"); + } + var blocks = []; + for (var i = 0; i < split.length; i++) { + blocks.push(this.blockFromHash(split[i])); + } + return blocks; + }; + + // Update the HTML based on hashes + // Called from local storage + ParsonsWidget.prototype.createHTMLFromHashes = function(sourceHash, answerHash) { + var sourceBlocks = this.blocksFromHash(sourceHash); + var answerBlocks = this.blocksFromHash(answerHash); + this.createView(sourceBlocks, answerBlocks); + }; + + // Log the activity to the server + ParsonsWidget.prototype.log = function(activity) { + var act = activity + "|" + this.sourceHash() + "|" + this.answerHash(); + var divid = this.problem.divid; + this.problem.logBookEvent({ + "event" : "parsons", + "act" : act, + "div_id" : divid + }); + } + + // Return a block object by the full id including id prefix + ParsonsWidget.prototype.getBlockById = function(id) { + for (var i = 0; i < this.blocks.length; i++) { + var block = this.blocks[i]; + if (block.id == id) { + return block; + } + } + return undefined; + }; + + // Retrieve the codelines based on what is in the DOM + ParsonsWidget.prototype.getModifiedCode = function(search_string) { + var codeLines = []; + var that = this; + $(search_string + " div").each(function(idx, i) { + var domItem = $(i); + var lineItem = that.getBlockById(domItem[0].id); + codeLines.push(lineItem); + }); + return codeLines; + }; + + // Return array of codeblocks based on what is in the answer field + ParsonsWidget.prototype.answerBlocks = function() { + var that = this; + var answerBlocks = []; + $("#" + this.options.answerId + " div").each(function(idx, i) { + answerBlocks.push(that.getBlockById($(i)[0].id)); + }); + return answerBlocks; + }; + + // Return array of codelines based on what is in the answer field + ParsonsWidget.prototype.answerLines = function() { + var that = this; + var answerLines = []; + var blocks = this.answerBlocks(); + for (var i = 0; i < blocks.length; i++) { + var block = blocks[i]; + for (var j = 0; j < block.lines.length; j++) { + answerLines.push(block.lines[j]); + } + } + return answerLines; + }; + + // Return array of codelines based on what is in the solution + ParsonsWidget.prototype.solutionLines = function() { + var solutionLines = []; + for (var i = 0; i < this.solution.length; i++) { + var lines = this.solution[i].lines; + for (var j = 0; j < lines.length; j++) { + solutionLines.push(lines[j]); + } + } + return solutionLines; + }; + + // Grade the answer compared to the solution + ParsonsWidget.prototype.getFeedback = function() { + this.grader.grade(); + this.feedback_exists = true; + }; + + // Clear any feedback from the answer area + ParsonsWidget.prototype.clearFeedback = function() { + if (this.feedback_exists) { + $("#" + this.options.answerId).removeClass("incorrect correct"); + var blocks = $("#" + this.options.answerId + " div"); + blocks.removeClass("correctPosition incorrectPosition incorrectIndent"); + $("#" + this.options.feedbackId).hide(); + } + this.feedback_exists = false; + }; + + // A function for returning a shuffled version of an array + ParsonsWidget.prototype.shuffled = function(array) { + var currentIndex = array.length; + var returnArray = array.slice(); + var temporaryValue, randomIndex; + // While there remain elements to shuffle... + while (0 !== currentIndex) { + // Pick a remaining element... + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex -= 1; + // And swap it with the current element. + temporaryValue = returnArray[currentIndex]; + returnArray[currentIndex] = returnArray[randomIndex]; + returnArray[randomIndex] = temporaryValue; + } + return returnArray; + }; + + // Based on the movingId, etc., establish the moving state + // rest = not moving + // source = moving inside source area + // answer = moving inside answer area + // moving = moving outside areas + ParsonsWidget.prototype.movingState = function() { + if (this.movingId == undefined) { + return "rest"; + } + var moving = $("#" + this.movingId); + var x = this.movingX; + var y = this.movingY; + // Check if in answer area + var left = this.answerArea.offset().left; + var right = left + this.answerArea.outerWidth(); + var top = this.answerArea.offset().top; + var bottom = top + this.answerArea.outerHeight(); + if (x >= left && (x <= right) && (y >= top) && (y <= bottom)) { + return "answer"; + } + // Check if in source area + left = this.sourceArea.offset().left; + right = left + this.sourceArea.outerWidth(); + top = this.sourceArea.offset().top; + bottom = top + this.sourceArea.outerHeight(); + if (x >= left && (x <= right) && (y >= top) && (y <= bottom)) { + return "source"; + } + return "moving"; + } + + // Update the ParsonsWidget view + // This occurs when dragging the moving tile + ParsonsWidget.prototype.updateView = function() { + // Based on the new and the old state, figure out what to update + var state = this.state; + var newState = this.movingState(); + var updateSource = true; + var updateAnswer = true; + var updateMoving = newState == "moving"; + if (state == newState) { + if (newState == "rest") { + updateSource = false; + updateAnswer = false; + } else if (newState == "source") { + updateAnswer = false; + } else if (newState == "answer") { + updateSource = false; + } else if (newState == "moving") { + updateAnswer = false; + updateSource = false; + } + } + var moving = undefined; + var movingHeight; + if (this.movingId !== undefined) { + moving = $("#" + this.movingId); + // Must get height here as detached items don't have height + movingHeight = moving.outerHeight(true); + moving.detach(); + } + + var positionTop, width; + var that = this; + var baseWidth = this.areaWidth - 22; + + // Update the Source Area + if (updateSource) { + positionTop = 0; + if (newState == "source") { + var hasInserted = false; + var x = this.movingX - this.sourceArea.offset().left - baseWidth / 2 - 11; + var y = this.movingY - this.sourceArea.offset().top; + $("#" + this.options.sourceId + " div").each(function(idx, i) { + item = $(i); + if (item[0].id !== "") { + if (!hasInserted) { + if (y - positionTop < (movingHeight + item.outerHeight(true)) / 2) { + hasInserted = true; + moving.insertBefore("#" + item[0].id); + moving.css({ + 'left' : x, + 'top' : y - movingHeight / 2, + 'width' : baseWidth, + 'z-index' : 2 + }); + positionTop = positionTop + movingHeight; + } + } + item.css({ + 'left' : 0, + 'top' : positionTop, + 'width' : baseWidth, + 'z-index' : 1 + }); + positionTop = positionTop + item.outerHeight(true); + } + }); + if (!hasInserted) { + moving.appendTo("#" + this.options.sourceId); + moving.css({ + 'left' : x, + 'top' : y - moving.outerHeight(true) / 2, + 'width' : baseWidth, + 'z-index' : 2 + }); + } + } else { + $("#" + this.options.sourceId + " div").each(function(idx, i) { + item = $(i); + if (item[0].id !== "") { + item.css({ + 'left' : 0, + 'top' : positionTop, + 'width' : baseWidth, + 'z-index' : 1 + }); + positionTop = positionTop + item.outerHeight(true); + } + }); + } + } + + // Update the Answer Area + if (updateAnswer) { + var block, indent; + positionTop = 0; + width = this.areaWidth + this.indent * this.options.x_indent - 22; + var that = this; + if (newState == "answer") { + var hasInserted = false; + var x = this.movingX - this.answerArea.offset().left - baseWidth / 2 - 11; + movingIndent = Math.round(x / this.options.x_indent); + if (movingIndent < 0) { + movingIndent = 0; + } else if (movingIndent > this.indent) { + movingIndent = this.indent; + } else { + x = movingIndent * this.options.x_indent; + } + var y = this.movingY - this.answerArea.offset().top; + block = this.getBlockById(this.movingId); + block.viewIndent = movingIndent; + $("#" + this.options.answerId + " div").each(function(idx, i) { + item = $(i); + if (item[0].id !== "") { + if (!hasInserted) { + if (y - positionTop < (movingHeight + item.outerHeight(true)) / 2) { + hasInserted = true; + moving.insertBefore("#" + item[0].id); + moving.css({ + 'left' : x, + 'top' : y - movingHeight / 2, + 'width' : baseWidth, + 'z-index' : 2 + }); + positionTop = positionTop + movingHeight; + } + } + block = that.getBlockById(item[0].id); + indent = block.viewIndent * that.options.x_indent; + item.css({ + 'left' : indent, + 'top' : positionTop, + 'width' : width - indent, + 'z-index' : 1 + }); + positionTop = positionTop + item.outerHeight(true); + } + }); + if (!hasInserted) { + moving.appendTo("#" + this.options.answerId); + moving.css({ + 'left' : x, + 'top' : y - moving.outerHeight(true) / 2, + 'width' : baseWidth, + 'z-index' : 2 + }); + } + } else { + $("#" + this.options.answerId + " div").each(function(idx, i) { + item = $(i); + if (item[0].id !== "") { + block = that.getBlockById(item[0].id); + indent = block.viewIndent * that.options.x_indent; + item.css({ + 'left' : indent, + 'top' : positionTop, + 'width' : width - indent, + 'z-index' : 1 + }); + positionTop = positionTop + item.outerHeight(true); + } + }); + } + } + + // Update the Moving Area + if (updateMoving) { + moving.appendTo("#" + this.options.sourceId); + moving.css({ + 'left' : this.movingX - this.sourceArea.offset().left - (moving.outerWidth(true) / 2), + 'top' : this.movingY - this.sourceArea.offset().top - (movingHeight / 2), + 'width' : baseWidth, + 'z-index' : 2 + }); + } + + state = newState; + this.state = state; + }; + + // Reset the view based on this.blocks accounting for + // * shorten to the distractors to maxdist size + // * if an order is specified, then use that + // * else shuffle the blocks randomly, accounting for paired distractors + // * call createView with the shuffled blocks in the source field + ParsonsWidget.prototype.resetView = function() { + var blocks = [], i, aBlock; + for (i = 0; i < this.blocks.length; i++) { + blocks.push(this.blocks[i]); + } + + // Trim the distractors (if necessary) + if (this.options.maxdist !== undefined) { + var distractorIDs = []; + for (i = 0; i < blocks.length; i++) { + distractorIDs.push(blocks[i].id); + } + if (this.options.maxdist < distractorIDs.length) { + distractorIDs = this.shuffled(distractorIDs); + distractorIDs = distractorIDs.slice(0, this.options.maxdist - 1); + var trimmed = []; + for (i = 0; i < blocks.length; i++) { + aBlock = blocks[i]; + if (aBlock.distractor) { + if ($.inArray(aBlock.id, distractorIDs)) { + trimmed.push(aBlock); + } + } else { + trimmed.push(aBlock); + } + } + blocks = trimmed; + } + } + + // Reorder the sourceBlock + var sourceBlocks = []; + if (this.options.order === undefined) { + // Shuffle, respecting paired distractors + var chunks = [], chunk = []; + $.each(blocks, function(index, item) { + if (item.paired) { + chunk.push(item); + } else { + chunk = []; + chunk.push(item); + chunks.push(chunk); + } + }); + chunks = this.shuffled(chunks); + for (var c = 0; c < chunks.length; c++) { + chunk = chunks[c]; + if (chunk.length > 1) { + // shuffle paired distractors + chunk = this.shuffled(chunk); + for (i = 0; i < chunk.length; i++) { + sourceBlocks.push(chunk[i]); + } + } else { + sourceBlocks.push(chunk[0]); + } + } + } else { + // Use the specified order to create the sourceBlocks + // Note that any lines not specified in the order are deleted + var order = this.options.order; + for (i = 0; i < order.length; i++) { + for (var j = 0; j < blocks.length; j++) { + if (blocks[j].index === order[i]) { + sourceBlocks.push(blocks[j]); + } + } + } + } + this.createView(sourceBlocks, []); + }; + + // Based on the blocks, create the view and insert it into the DOM + ParsonsWidget.prototype.createView = function(sourceBlocks, answerBlocks) { + var html, i; + if (this.options.sourceId) { + // Add source area + html = 'Drag from here
'; + html += 'Drop blocks here
'; + html += '
+
%(qnumber)s: %(instructions)s%(code)s
'''
self.options['divid'] = self.arguments[0]
self.options['qnumber'] = self.getNumber()
self.options['instructions'] = ""
self.options['code'] = self.content
-
- if 'maxdist' not in self.options:
- self.options['maxdist'] = '5'
+
+ if 'maxdist' in self.options:
+ self.options['maxdist'] = ' data-maxdist="' + self.options['maxdist'] + '"'
+ else:
+ self.options['maxdist'] = ''
+ if 'order' in self.options:
+ self.options['order'] = ' data-order="' + self.options['order'] + '"'
+ else:
+ self.options['order'] = ''
+ if 'noindent' in self.options:
+ self.options['noindent'] = ' data-noindent="true"'
+ else:
+ self.options['noindent'] = ''
+ if 'language' in self.options:
+ self.options['language'] = ' data-language="' + self.options['language'] + '"'
+ else:
+ self.options['language'] = ''
+
+
if '-----' in self.content:
index = self.content.index('-----')
self.options['instructions'] = "\n".join(self.content[:index])