feedback1.js - JavaScript functions supporting immediate feedback to in-browser questions

 
"use strict";

Feedback for interactive questions

These routines support Interactive questions, by showing feedback, grading questions, and saving/restoring question state.

Question classes

These classes all provide this.id and this.restoreState, in addition to calling saveState when their value changes.

Take a <input type="text"> and augment it to make it a TextQuestion.

function TextQuestion(

The id (name) of this input in the DOM.

  id) {

    var this_ = document.getElementById(id);
    this_.id = id;

The order on the web page is always text input, check button, feedback span. Place feedback in the feedback span.

    this_.feedbackSpan = this_.nextElementSibling.nextElementSibling;
    this_.restoreState = function(answer, points, pointsPossible, feedback) {
        this.value = answer;
        updateFeedback(this.feedbackSpan, points, pointsPossible, feedback);
    };

Check to see if the text submitted to a question is correct, providing feedback.

    this_.onTextSubmitted = function(event=null) {

When the user presses Enter...

        if ( (event.type == "keyup" && event.keyCode == 13) ||

...or clicks the Check button,

          (event.type == "click") ) {

Save and grade the answer provided.

            gradeAnswer(id, this.value);
        }
    };
    return this_;
}
 

Augment the “Check” <input type="button"> with Question abilities. This provides a central container for gathering the results from a group of checkboxes.

function CheckboxQuestion(

See id.

  id) {

    var this_ = document.getElementById(id);
    this_.id = id;

The structure of the HTML is:

<ul>
  <li><label>...</label></li> repeating
  <li>
    <input type="button">  <-- this_
    <span></span> <-- Feedback goes inside this span.
    this_.feedbackSpan = this_.nextElementSibling;
    this_.restoreState = function(answer, points, pointsPossible, feedback) {
        var checkboxes = $(this.parentNode.parentNode.parentNode).find('input[type=checkbox]');
        checkboxes.each(function(i) { this.checked = answer[i] === '1'; } );
        updateFeedback(this.feedbackSpan, points, pointsPossible, feedback);
    };
 

Check if the answer submitted to this group of checkboxes is correct, providing feedback.

    this_.onCheckboxSumitted = function() {

Retrieve an array of checkboxes associated with this_ (the “Check” button).

        var checkboxes = $(this.parentNode.parentNode.parentNode).find('input[type=checkbox]');
        var state = (jQuery.map(checkboxes, function(obj) { return obj.checked ? '1' : '0'; })).join('')
        gradeAnswer(id, state);
    };

    return this_;
}
 

onRadioChanged

This function is invoked by a radio button’s onchange handler. It:

  1. Updates the RadiobuttonQuestion class (the <ul>) isCorrect and state attributes.
  2. Shows feedback for this radio button and hides feedback for all others.
  3. Unchecks the other radio buttons.

The expected HTML structure is:

<ul id="xxx">   <- This is the RadiobuttonQuestion.
  <li>
    <label>
      <input type="radio">
      text, other stuff
function onRadioChanged(this_) {

Hide and uncheck other selections in this group of radio buttons.

    var ul = this_.parentNode.parentNode.parentNode;
    $(ul).find('input[type=radio]').each(
        function(index) {

If this radio button wasn’t clicked...

            if (this !== this_) {

...deselect it, since they aren’t in groups.

                this.checked = false;
            } else {

...otherwise, it was clicked. Save its index as the state.

                gradeAnswer(ul.id, index);
            }
        } );
}
 

Since there are many radio buttons, it makes more sense to store state in the containing <ul>, then have the buttons update that. See onRadioChanged.

function RadiobuttonQuestion(

See id.

  id) {

    var this_ = document.getElementById(id);
    this_.id = id;

The structure of the HTML is:

<ul> <-- this_
  <li><label>...</label></li> repeating
  <li><span></span></li> <-- Feedback goes inside this span.
    this_.feedbackSpan = this_.lastElementChild;
    this_.restoreState = function(answer, points, pointsPossible, feedback) {

Check the appropriate radio button.

        var rb = $(this).find('input[type=radio]')[answer]
        rb.checked = true;
        onRadioChanged(rb);
    };

    return this_;
}

TestButtonQuestion

Handle a click on the “Test” button.

function onTestButtonClicked(this_) {

Tell the user we’re building.

    this_.feedbackSpan.value = "Building...";

Gather all code snippets.

    var code_snippets = $.map($('textarea.code_snippet'), function(obj, index) {
        return obj.value; });

Send them as JSON.

    gradeAnswer(this_.id, JSON.stringify(code_snippets));
}
 

The code in all other textareas defines the state of a TestButtonQuestions.

function TestButtonQuestion(

See id.

  id) {

    var this_ = document.getElementById(id);
    this_.id = id;

The structure of the HTML is:

<textarea class="code_snippet">code</textarea>
Other stuff.
<textarea class="code_snippet">code</textarea>
Other stuff.
...and so on for all code_snippet textareas.

<input type="button"> <-- this_
<br />
<textarea>Result from testing...</textarea>

This isn’t a span, it’s a textarea. But, this matches the required naming convention for feedback objects.

    this_.feedbackSpan = this_.nextElementSibling.nextElementSibling;
    this_.restoreState = function(answer, points, pointsPossible, feedback) {

Restore the feedback.

        updateFeedback(this_.feedbackSpan, points, pointsPossible, feedback)
 

Restore previous answers:

Find all code snippet textareas.

        var tb = $('textarea.code_snippet');

The answer is a JSON-encoded array. Unpack it.

        var answer_array = JSON.parse(answer);

Assign the textarea contents to each of the given strings.

        for (var index = 0; index < tb.length && index < answer_array.length; ++index) {
            tb[index].value = answer_array[index];
        }
    };

    return this_;
}

Utility functions

Change the color and visibility of the feedback for an answer.

function updateFeedback(feedbackSpan, points, pointsPossible, feedback) {

Some so-called feedbackSpans are really textareas – handle this case.

    if (feedbackSpan.type === "textarea") {
        feedbackSpan.value = feedback;

Scroll the text to the end, showing the useful part.

        feedbackSpan.scrollTop = feedbackSpan.scrollHeight;
    } else {
        feedbackSpan.innerHTML = feedback;
        MathJax.Hub.Queue(["Typeset", MathJax.Hub, feedbackSpan]);
    }
    feedbackSpan.style = points == pointsPossible ? 'color: green;' : 'color: red;';
}

Persistent storage

See https://developer.mozilla.org/en-US/docs/Web/API/Window/location and https://developer.mozilla.org/en-US/docs/Web/API/Location.

var getStateAddress = window.location.origin + window.location.pathname + '/questions';
 

Save the answer then grade it.

function gradeAnswer(id, answer) {

It would be nice (but incorrect) to write var data = {id: [state, isCorrect]};. The somewhat obscure var data = {[id]: [[state], [isCorrect]}; would work.

    var data = {};
    data[id] = answer;
    $.getJSON(getStateAddress, data)
        .done(function (jsonState) {
            var [points, pointsPossible, feedback] = jsonState[id];
            updateFeedback(document.getElementById(id).feedbackSpan, points, pointsPossible, feedback);
        });
}
 

Restore the state of the questions, if it exists and is accessible.

function restoreState() {

Only restore if there are questions on this page.

    if (typeof questionList !== "undefined") {
        $.getJSON(getStateAddress).done(restoreFromState);
    }
}

function restoreFromState(stateSource) {

Restore each question’s state...

    for (var question of questionList) {
        var state = stateSource[question.id];
        if (state) {
            var [answer, points, pointsPossible, feedback] = state;
            question.restoreState(answer, points, pointsPossible, feedback);
        }
    }
}
$(window).ready(restoreState);

Commenting UI

When the user clicks the commenting links at the bottom of each page, enable the appropriate commenting and provide brief instructions.

function enableAddComment() {

The app object is defined by Configure Annotator. Use it here. Eventually, modify Annotator to make this a user-only comment.

    app.start()

Provide a bit of explanation. Again, see the text at Configure Annotator.

    $('#comment_explanation').css({'display': 'inline'});
    $('#comment_action').html('add a comment to your textbook.');
}
 

See enableAddComment for an explanation of how this works.

function enableAskQuestion() {
    app.start()

    $('#comment_explanation').css({'display': 'inline'});
    $('#comment_action').html('ask your instructor a question about the highlighted text.');
}
 

See enableAddComment for an explanation of how this works.

function enableReportProblem() {
    app.start()

    $('#comment_explanation').css({'display': 'inline'});
    $('#comment_action').html('report an error in the highlighted text to the authors.');
}