Customizing the Canvas LMS Rich Content Editor
In my current role at Webster University as Director of Instructional and Creative Design Services, we’ve created a design system that informs styleguides across the University’s online courses, which are taught in Canvas. We’ve also created reusable interactive objects such as tabbed interfaces, accordions, and slideshows. Much of this requires custom HTML, CSS, and Javascript. We’ve built a few tools to help create these experiences. The Canvas LMS Rich Content WYSIWYG editor is used to place in the content and edit them. The challenge is that customized, and in our case highly customized, CSS and Javascript isn’t viewable through the editor. At least, not in it’s default state.
We recently decided to take on the task of loading our custom CSS and Javascript into the editor. We’ve been successful in our initial testing. We still have a little ways to go to make it perfect, but I thought I’d share what we’ve learned so far and how we did it, in case it helps anybody else trying to do the same.
The Challenge
First, we defined out challenge. How might we display our content in the Canvas Canvas Rich Content Editor in a way that reflects how it looks when it’s published? As an example, we routinely use display components that we call “cards.” These are styled display components intended to draw attention to small bits of content or information. They have a title and a space for limited content. As a rule, they do not display more than 3 wide on a screen, and are mobile responsive, so that they stack when viewed on mobile.
We wanted to be able to display these cards in the editor, so that the user could see what they were editing, and how it would look on the screen. Cards are just one of the many content customizations that we wanted to be able to display. We chose to focus on cards because they use Canvas’ implementation of Bootstrap Grid, which is a little different than what you include from the CDN. We figured focusing on the cards, while taking into account the rest of the styleguides, would place us further along the path to implementation.
The Solution
We started with a post from 2021 on the Canvas community. In this post, user TimJensenUSC dropped some Javascript that could be used to potentially customize the CSS that the Canvas Rich Content Editor uses to display content.
This code essentially looks for existence of an editor, and if it’s there, appends some new <link> and <script> tags to the page loaded within the current view of the page through Rich Content Editor. It doesn’t place them on the page itself, it just appends it to the page that loads in the editor.
The javascript written in that community post is minimized and uses a ternary function, so to make it easier to read for those of us who don’t read and write minified and ternary, we rewrote the function. Here’s the resulting script:
(function() {
'use strict';
// Define a regular expression to check if the current page's URL ends with "/edit"
const pageRegexCSEV = new RegExp('/edit$');
const pageMatchesCSEV = pageRegexCSEV.exec(window.location.pathname);
// Only proceed if the current page matches the regex (i.e., it is an "edit" page)
if (pageMatchesCSEV) {
console.log('Init- CSEV');
// Function to find the necessary elements asynchronously
async function getBits() {
// Helper function to check if a specific element has loaded
function elemLoaded(type) {
return new Promise(function(resolve, reject) {
let i = 20; // Start with a delay of 20 ms
let t = setInterval(function() {
// Determine the element to check based on the "type" argument
let elem;
if (type === 'frame') {
// Check for different elements related to frames and the editor instance
elem = document.getElementById('wiki_page_body_ifr');
if (!elem) {
elem = document.getElementById('assignment_description_ifr');
}
if (!elem) {
elem = document.getElementsByClassName('tox-edit-area__iframe')[0];
}
if (!elem) {
elem = document.getElementsByClassName('tox-editor-container')[1];
}
} else {
// Check for the CSS link element if type is not 'frame'
elem = document.head.querySelector('link[href*="YOUR CSS FILE LOCATION"]');
}
console.log('CSEV- checking for:', type);
// Check if the element exists
let l = (elem !== null && elem !== undefined);
if (l) {
// If found, clear the interval and resolve the promise with the element
clearInterval(t);
console.log('CSEV- ' + type + ' elem found.');
resolve(elem);
} else if (i >= 3000) { // Timeout after 3 seconds
// If not found within the timeout period, clear the interval and reject
clearInterval(t);
console.log('CSEV- ' + type + ' elem not found.');
reject();
}
i += 15; // Increment the interval delay by 15 ms for each check
}, i);
});
}
// Wrapper functions to specifically check for the iframe and CSS elements
function getIFrame() {
return elemLoaded('frame');
}
function getCSS() {
return elemLoaded('css');
}
// Wait for both the CSS file and the iframe to be loaded
const cssFile = await getCSS();
const editorFrameHead = await getIFrame();
// Return the loaded elements
return {
editorFrameHead: editorFrameHead,
cssFile: cssFile
};
}
// Function to load the CSS into the iframe once elements are found
async function loadCSS(bits) {
console.log('CSEV- append CSS to FRAME');
// Create a new <link> element to include the CSS file
let cssLinkTag = document.createElement('link');
cssLinkTag.rel = 'stylesheet';
cssLinkTag.media = 'screen';
cssLinkTag.crossorigin = 'anonymous';
cssLinkTag.href = bits.cssFile.href;
// Append the CSS file to the iframe's document head
bits.editorFrameHead.contentDocument.head.appendChild(cssLinkTag);
// Create a new <script> element to include the CSS file
let jsLinkTag = document.createElement('script');
jsLinkTag.src = 'YOUR CUSTOM JAVASCRIPT FILE LOCATION';
let bsLinkTag = document.createElement('link');
bsLinkTag.rel = 'stylesheet';
bsLinkTag.media = 'screen';
bsLinkTag.crossorigin = 'anonymous';
bsLinkTag.href = 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css';
let bsScriptTag = document.createElement('script');
bsScriptTag.src = 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js'
let jqLinkTag = document.createElement('script');
jqLinkTag.src = 'https://code.jquery.com/jquery-3.7.1.js';
// Append the CSS file to the iframe's document head
bits.editorFrameHead.contentDocument.head.appendChild(cssLinkTag);
bits.editorFrameHead.contentDocument.head.appendChild(jqLinkTag);
bits.editorFrameHead.contentDocument.head.appendChild(bsLinkTag);
bits.editorFrameHead.contentDocument.head.appendChild(bsScriptTag);
bits.editorFrameHead.contentDocument.head.appendChild(jsLinkTag);
// create a new style element
var style = document.createElement('style');
// set the style type to "text/css"
//style.type = 'text/css';
// define the CSS rules as a string
var cssString = '.mce-content-body { width: 100% !important; }';
// add the CSS rules to the style element
style.appendChild(document.createTextNode(cssString));
// append the style element to the head element of the document
bits.editorFrameHead.contentDocument.head.appendChild(style);
}
// Function to handle any errors that occur in loading
function failureCallback(e) {
console.log('failureCallback: ' + e);
}
// Run the process: get elements and load CSS if successful, handle failure otherwise
getBits()
.then(bits => loadCSS(bits))
.catch(failureCallback);
}
})();
Essentially, we rewrote the ternary function as an if/else statement, then added <script>, <link>, and <style> tags on the page that’s loaded in the editor using appendChild. This is what loads your custom CSS and javascript into the Canvas RCE.
To test, rather than create a new .js file and load it into our stack on our test or beta instances of Canvas, we simply run this script in the Google Console when we’re on a page with the Canvas RCE loaded onto it.
Once we did this, our custom Javascript functioned and our CSS loaded into the editor. There are a few bugs to still work out, we’ll likely make some changes to the script itself, and we had to make a few adjustments to our own custom styles (which didn’t affect the viewing of the content), but overall we feel like we’re off to a great start.
What’s Next
We’ll continue extending and refining our script. We’ll also continue testing this on a variety of course pages with different features loaded onto them. We’ll make the needed adjustments, and we’ll deploy to our test instance, where we’ll conduct wider testing. Once we’ve worked out bugs, and are confident that we’re ready for production, we’ll deploy this script onto our production instance of Canvas. Once deployed, we’ll add this to our periodic testing for customized code and styles.
I’d love to see Canvas formally implement a solution at some level that would allow for customizing the CSS that’s loaded into the Canvas Rich Content Editor. Given that the editor is a customized instance of TinyMCE, and Canvas Themes with customized CSS are already pointing to a url to load custom CSS, it would be nice to see Canvas pass that custom CSS link as a variable to the editor instance init for custom CSS. This would load an organization’s custom CSS straight into the Canvas RCE. There are likely use cases and challenges that I’m not taking into account, but it would be nice to see.
Until then, we’ll extend the Canvas Rich Content Editor this way.