Progress bar in Instructure Canvas

WARNING: Note that themes allow you to upload .css and .js files for both the web and app versions of your Canvas site BUT the app does not provide JQuery so you’ll need a plain JS version of your code for the app…or pull in JQuery.

The Bootstrap progress bar doesn’t appear to be available within pages. Instead, use the JQueryUI progress bar (for the web version of Canvas anyway):

I have styled this in one of our courses as follows.

HTML

<div class="ui-progressbar ui-widget ui-widget-content ou-ProgressBar module_6" style="width: 100%; height: 15px;" role="progressbar" aria-valuemax="100" aria-valuemin="0" aria-valuenow="19">
    <div class="ui-progressbar-value ui-widget-header ui-corner-left ou-ProgressBarBar" style="width: 19%;"></div>
</div>

CSS

.ou-ProgressBar {
 height: 15px;
 margin-bottom: 3em;
}

.ou-ProgressBar.module_6 .ou-ProgressBarBar {
 border: 1px solid #640D14;
 background-image: linear-gradient(to bottom, #640D14, #640D14) !important;
}

TL;DR

In fact, I actually use JS to insert the HTML above based upon a div like this:

HTML

<div id="module_6" class="ou-insert-progress-bar">19</div>

JS

if($('div.ou-insert-progress-bar').length){
    $('div.ou-insert-progress-bar').each( function(index) {
        //get data from insert-progress-bar div
        var value = $(this).text();
        var className = $(this).attr('id');
        //UC first letter
        var viewName = className.toLowerCase().replace(/\b[a-z]/g, function(letter) {
            return letter.toUpperCase();
        });
        viewName = viewName.replace(/_/g, ' ');
        //create progress bar and title
        $(this).after(''+
            '<h4 class="ou-space-before-progress-bar">Current position in ' + viewName + ':</h4>' +
            '<div class="ui-progressbar ui-widget ui-widget-content ui-corner-all ou-ProgressBar ' + className + '" style="width: 100%; height: 15px;" role="progressbar" aria-valuemax="100" aria-valuemin="0" aria-valuenow="'+ value +'">' +
                ' <div class="ui-progressbar-value ui-widget-header ui-corner-left ou-ProgressBarBar" style="width: '+ value +'%;"></div>' +
            '</div>' +
            '');
    });
}
//End by deleting div.insert-progress-bars
$('div.ou-insert-progress-bar').remove();

Far TL;DR

If you also want this to appear in the app (where neither JQuery nor JQuery UI are available), you’ll need to use the plain JS version of the above and update the styles:

JS

//From: https://stackoverflow.com/questions/4793604/how-to-insert-an-element-after-another-element-in-javascript-without-using-a-lib
function insertAfter(newNode, referenceNode) {
    referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
}

/* Trying plain JS so that it works in the app as well */
function domReady () {
	//get all elements with classname ou-insert-progress-bar
	var progressBarPlaceholders = document.getElementsByClassName('ou-insert-progress-bar');
	Array.prototype.forEach.call(progressBarPlaceholders, function(progressBarPlaceholder) {
		var value = progressBarPlaceholder.innerHTML;
		var className = progressBarPlaceholder.id;
		//UC first letter
		var viewName = className.toLowerCase().replace(/\b[a-z]/g, function(letter) {
			return letter.toUpperCase();
		});
		//replace underscore with space
		viewName = viewName.replace(/_/g, ' ');
		//create our new element
		var progressBarContainer = document.createElement("div");
                progressBarContainer.innerHTML = ''+
                    '<h4 class="ou-space-before-progress-bar">Current position in ' + viewName + ':</h4>' +
                    '<div class="ou-ProgressBar ' + className + '" style="width: 100%; height: 15px;" role="progressbar" aria-valuemax="100" aria-valuemin="0" aria-valuenow="'+ value +'">' +
                    ' <div class="ou-ProgressBarBar" style="width: '+ value +'%;"></div>' +
                    '</div>';
                //insert it after the placeholder using the function insertAfter
                insertAfter(progressBarContainer, progressBarPlaceholder);
                //now delete the placeholder
                progressBarPlaceholder.parentNode.removeChild(progressBarPlaceholder);
       });
}
//Function to work out when the DOM is ready: https://stackoverflow.com/questions/1795089/how-can-i-detect-dom-ready-and-add-a-class-without-jquery/1795167#1795167
// Mozilla, Opera, Webkit 
if ( document.addEventListener ) {
	document.addEventListener( "DOMContentLoaded", function(){
		document.removeEventListener( "DOMContentLoaded", arguments.callee, false);
		domReady();
	}, false );
// If IE event model is used
} else if ( document.attachEvent ) {
	// ensure firing before onload
	document.attachEvent("onreadystatechange", function(){
		if ( document.readyState === "complete" ) {
			document.detachEvent( "onreadystatechange", arguments.callee );
			domReady();
		}
	});
}

CSS

.ou-ProgressBar {
  height: 15px;
  margin-bottom: 3em;
  width: 100%;
  border-radius: 3px;
  border: 1px solid #aaa;
  background: #fff;
  overflow: hidden;
}

.ou-ProgressBarBar {
  height: 100%;
}

.ou-ProgressBar.welcome .ou-ProgressBarBar {
  border: 1px solid #D89E17;
  background-image: linear-gradient(to bottom, #D89E17, #D89E17) !important;
}

Self-test questions in Moodle

In an earlier post I outlined plans for migration of MedLearn to Moodle. The technical work is now done except for editor plugins. The migration is well underway.

The tricky bit was getting the self test questions to work. The purpose of self test questions is to enliven content and enhance learning. They are not intended to be an assessment, although it may be useful to record the answers.

The Quiz activity provides the best range of question types but it also forces questions into an assessment with a very specific workflow (start attempt; answer questions; finish; submit; review). We wanted a much looser workflow which did not present questions as a quiz. The answer (suggested by Tim Hunt) was to embed a preview question in an iFrame. We chose the Book module for the content into which the questions were embedded.

Students log in to our VLE (Sakai). They then launch an LTI tool which automatically provisions them in Moodle. They are assigned an adapted set of permissions to avoid the issues highlighted by Tim Hunt in this post. The LTI provider plugin does all the provisioning, including the assignment of the role with the permissions.

Questions are inserted into the book content using Generico filter and Atto plugin. I’ve defined a template which just takes the question number and applies the correct iFrame. A Javascript frame resize script triggers on load (and on change) so that the question appears seamlessly in the content. The plugin also has an Atto editor plugin to simplify editing.

I have modified the preview PHP so that the title and config have been stripped off, leaving just bare question with answer box and ‘check’ button. The preview PHP still accepts the querystring configuration so I passed &correctness=0&marks=0&markdp=0&feedback=1&generalfeedback=1&rightanswer=1&history=0 to it in the Generico template.

The script tag to load the JavaScript resize code is in AdditionalHTML config.

We’ve also made the content more mobile friendly using Bas Brand’s Bootstrap  3 theme and more semantically relevant HTML5 tags such as <figure>, <cite>, <dfn> and <dl>. We are about to start authoring Atto plugins to support these tags. JQuery is used to for image swaps, replacing the mouseovers and flash which aren’t mobile friendly.

The only missing feature which would be really nice to have is a way of browsing the questions from the question bank for insertion. It’s currently a little clunky to find the question ID for use in the Generico Atto plugin.

My thanks go to Tim Hunt, Juan Leyva (LTI Provider) and Justin Hunt (Generico) for making the tools which underpin this integration.

Google Maps need resizing after being “shown” with JavaScript/JQuery

If you have a Google Map on a page that, for example, starts off hidden and is shown using JavaScript (inc. JQuery or another library), it seems to only load the top left map tile. In order to fix this, you need to resize the map after showing it, using the following line:

google.maps.event.trigger(map, 'resize');    //map is your google.maps.Map object

This is mentioned in the resize event in the Map class documentation.

Ajax form submission from a JQuery UI modal dialog in CakePHP

I just wanted to share with you two approaches to bringing up an add or edit form in a JQuery UI modal dialog and closing the dialog on a successful submit or keeping it open and displaying errors in case of validation or other save problems.

A non-Cake approach

Ingredients

CakePHP 2+
JQuery 1.9+
JQuery UI
JQuery Form Plugin

The Views

Main page

This is the page from which we want to launch our modal add or edit dialog:

<!-- overlayed element -->
<div id="dialogModal">
     <!-- the external content is loaded inside this tag -->
     <div class="contentWrap"></div>
</div>
...
<div class="actions">
    <ul>
        <li>
            <?php echo $this->Html->link(__('Add user', true), array("controller"=>"users", "action"=>"add"), array("class"=>"overlay", "title"=>"Add User"));
        </li>
    </ul>
</div>
...
<script>
$(document).ready(function() {
    //prepare the dialog
    $( "#dialogModal" ).dialog({
        autoOpen: false,
        show: {
            effect: "blind",
            duration: 500
            },
        hide: {
            effect: "blind",
            duration: 500
            },
        modal: true
        });
    //respond to click event on anything with 'overlay' class
    $(".overlay").click(function(event){
        event.preventDefault();
        $('#contentWrap').load($(this).attr("href"));  //load content from href of link
        $('#dialogModal').dialog('option', 'title', $(this).attr("title"));  //make dialog title that of link
        $('#dialogModal').dialog('open');  //open the dialog
        });
    });
</script>

Our add or edit form

This is shown in the dialog

echo $this->Form->create('User');
echo $this->Form->input('name');
echo $this->Form->end('Submit');
<script>
$('#UserAddForm').ajaxForm({ 
    target: '#contentWrap', 
    resetForm: false, 
    beforeSubmit: function() { 
        $('#contentWrap').html('Loading...'); 
        }, 
    success: function(response) { 
        if (response=="saved")) { 
            $('#dialogModal').dialog('close');  //close containing dialog    
            location.reload();  //if you want to reload parent page to show updated user
            } 
        } 
    });
</script>

Controller

For our add or edit action

function add() {
    ...
    if (!empty($this->request->data)) {
        $this->User->create();
        if ($this->User->save($this->request->data)){ 
            return "saved"; 
            } 
        }
    ...
    }

 

A more Cakey approach

With thanks to RXC on StackOverflow

Ingredients

CakePHP 2+
JQuery 1.9+
JQuery UI

The Views

Main page

This is the same as above

Our add or edit form

echo $this->Form->create('User');
echo $this->Form->input('name');
echo $this->Js->submit('Save', array(  //create 'ajax' save button
    'update' => '#contentWrap'  //id of DOM element to update with selector
    ));
if (false != $saved){ //will only be true if saved OK in controller from ajax save above
    echo "<script>
        $('#dialogModal').dialog('close');  //close containing dialog         
        location.reload();  //if you want to reload parent page to show updated user
    </script>";
    }
echo $this->Form->end();
echo $this->Js->writeBuffer(); //assuming this view is rendered without the default layout, make sure you write out the JS buffer at the bottom of the page

Controller

For our add or edit action

function add() {
    ...
    $this->set('saved', false); //false by default - controls closure of overlay in which this is opened
    if (!empty($this->request->data)) {
        $this->User->create();
        if ($this->User->save($this->request->data)){ 
            $this->set('saved', true); //only set true if data saves OK
            } 
        }
    ...
    }

Hope that’s helpful. Would be very interested in alternatives that have worked for you…and corrections to inevitable errors above.