/gadget

The in-browser Python editor used in CY105

Primary LanguageCSS

Canvas Gadgets

Click here for a demo

Gadgets let students create, run, and test Python programs in their browser. Gadgets run Skulpt, a Javascript implementation of Python 3 and is specifically designed to run in the Canvas LMS.

Configuring Canvas

The gadget renderer must be executed on every page containing a gadget. The easiest way to do this in Canvas is to add the renderer script to your school's theme.

Go to Admin -> Themes and open your school's theme in the Theme Editor. Click the Upload tab. Create a new JavaScript file with the code below or add it to your existing JavaScript file:

$.getScript('https://usma-eecs.github.io/gadget/canvas/gadget.bundle.js');

Create a new CSS file with the code below or add it to your existing CSS file:

@import url("https://usma-eecs.github.io/gadget/canvas/gadget.css");

You will also need to copy the templates folder in this repo to your Course Files directory in Canvas. If you cloned the course from an old Gadget-enabled course, then it should already be there. Note: It is important that the folder is names templates.

Gadget serialization

Gadgets are serialized as an HTML <div> with the gadget class. Each file in the editor is saved in a <pre>. Here's an example:

<div class="personal gadget">
  <pre id="main.py"># every gadget must have a main.py</pre>
  <pre id="instructions.md">
    # Instructions
    An instructions tab will appear with HTML instructions rendered from markdown
  <pre id="tests.py">
    # put unit tests in tests.py
  </pre>
  <pre id="secret.py" class="hidden">
    # any file with the hidden class is only visible when editing the gadget
  </pre>
</div>

Personal gadgets

Personal gadgets can be customized and saved on a per-user basis. Each user will get a copy of the personal gadget in their personal Canvas files area under the gadget folder. To make a gadget a personal gadget, add the personal class to the gadget <div> like so:

<div class="personal gadget">
  <pre id="main.py"># this is your personal gadget!</pre>
</div>

Building the Canvas integration bundle

To update gadget.bundle.js, install Node 14.6.0 or greater and run the following:

$ cd webpack
$ npm install
$ npx webpack 

This will pack everything in webpack/src and any dependecies in webpack/package.json into a bundle in the canvas directory in the project root.

Development Mode

To configure Chrome to render gadgets using your webpack development server instead of the production copy in Github, do the following:

  1. Clone the project, open the webpack folder, type npm install, then npm start to start the webpack development server.
  2. Open Canvas in Chrome and head to a page with a gadget then open Developer Tools.
  3. Go to Sources tab, then the Overrides sub-tab and add an overrides folder.
  4. Go to the Sources tab then the Page sub-tab and find the Javascript integration hook. It's usually on a server starting with instructure-uploads. Change the hook to load the bundle from your development server. It should look something like this when you're done:
$.getScript('http://localhost:9292/canvas/gadget.bundle.js');

If you configured overrides correctly, you should be able to save your change and it will persist.

Now you can edit the files in webpack/src and see those changes in Canvas. When you're done, re-build the bundle and push it to Github.

How do gadgets work?

When your school's theme is loaded, the gadget renderer is executed. It uses the monitoring library to monitor for <div class="gadgets"> on the page. When it finds one, it instantiates a new <iframe> outside the document.body. The iframe is here so that it isn't saved into the TinyMCE editor and it is outside the area being monitored for new gadgets for performance.

The iframe contains the actual gadget editor, which lives in this repository. Since the iframe needs to access Canvas (to read and write gadgets) we need a way to get around cross-origin restrictions. Previous iterations of the gadget used the postMessage protocol, but that was complicated and couldn't reliably save. Instead, the iframe is created with no src and its contents are dynamically inserted from a template using JavaScript. The template has a base_uri that points to this repo where all of its dependent code is loaded from. If Canvas ever tightens up their Content Security Policy, this could very well break.

Once the iframe is loaded, it places itself over its respective gadget <div> and continually monitors the <div> so its size and location match the <div>'s. Note that the renderer only instantiates iframes for gadgets that are actually visible on the screen. This makes page loading much faster and is freindlier to LockDown browser, but does cause a noticeable delay in the rendering of gadgets.

As edits are made to the code, changes are saved back to the original gadget <div> (with about a 1 second debounce).