Canvas and Mastery are experiencing issues due to an ongoing AWS incident. Follow the status at AWS Health Dashboard and Instructure Status Page
Good Day,
Let me start off by thanking those who contribute to these discussion boards. I've been here quite a bit over the last few months and have found a lot of extremely helpful information.
I'm somewhat new to Canvas development but am hoping someone can help me. We have a sub-account for our school that we would like to have custom help links made available to students. After reviewing the help documentation on customizing the Canvas global help menu...we thought we were in luck. Unfortunately, links in this help menu propagated throughout the entire Canvas system at the "institution" level and not customizable at the sub-account level.
To get around this issue, and because we want to train our students and faculty to reference the global Canvas help link, I have written a simple JavaScript to add our school-specific help links to the Canvas global help menu by targeting id="help_tray". The solution is working just fine in desktop mode.
Unfortunately, there are no human-readable ID or class names to anchor this functionality to when the interface switches to mobile view. I've looked through the structures created and it looks like everything is being generated dynamically (vue.js?). Class names and IDs like the following "MlJlv_bGBk MlJlv_ycrn MlJlv_fFWz" are everywhere. As confusing as these are, It does seem like these class names and IDs stay the same...at least when I closed my Canvas window and opened a new one the IDs and class names were the same for the elements I was investigating.
I have two questions:
1. Do these machine-generated element IDs and class names stay the same and, if so, how long do these names persist?
2. If these class names and IDs are constantly changing, what can I reference within the interface so that we can customize help links for our school?
And a critical request:
I'm not sure if these posts are reviewed by Instructure. If so...please find a way to surface useful anchors such as ID's or class names that can be relied upon for schools to customize interface options within Canvas. Our faculty use the bare minimum of features in Canvas-mainly course downloads and grading. Faculty, staff and students have all commented on how frustrating and confusing the interface is and that the overall experience of using Canvas is not that enjoyable. In the short time our Digital Education team has been working with the school, we have been able to transform the experience through careful use of some of the core Canvas features along with customized styles and user interface tools enabled by API calls and JavaScript. Being able to extend the administrative UI would go a long way towards smoothing out user experience pain.
Thanks to all for your insight.
Hi Bill,
First off; I'm glad that you find these forums to be helpful. It is still amazing to me how many people are willing to come here and share and help each other.
Unfortunately I do not know the answer to the question. However I did share a link to your question into the Canvas Developers group where I am hoping that someone with a lot more technical savvy than myself will see it.
Thank you, Scott! I appreciate you sharing the link. I'm very new to these forums and am still not sure where I should post questions.
Hey Bill,
We're happy to have you here! Sort of the nature of how this platform works is that you want to do like you did and post in the Q&A area like you did but then it doesn't also hurt to share your question into other areas where like minded people might be frequenting. So you might post something here but also share it into the instructional design or Canvas admins group for example
Hi wnewhous,
I'll add @James and any other Canvas Developers too.
Those class name are generated with React and webpack, they change with every Canvas release, don't use them.
The specificity of some React components with those classes is difficult and sometimes impossible. I've found instead of classes that using CSS/DOM selectors and JS defined provides better luck. We poke at Instructure to make these things easier and sometimes specifically request an #id (like #nav-tray-portal) on an element, but the end user experience is usually sidelined to internal development and probably resources.
I've hacked the hell out of Canvas Themes with JS and CSS. A lot can be done, some can't. In Cascading Stylesheets/Javascript from Main Account to Sub Accounts I showed how we use global JavaScript combined with sub account Javascript to pass params and settings at different levels. I mention this, and I'm not sure if much has changed in the help menu since, so there could be alternative routes to develop this, but we kinda used reverse logic and used the available features. We put all our custom help links, including those for specific sub accounts in the global help menu using that feature. Then, global JS, has the method for removing links and watching for the AJAX call, and stores any links that need to be removed globally. Sub account JS then has any links for that sub account that should remain, using the link's href to keep the ones for that sub account.
We developed that solution before we used Mutation Observers.
If you dig enough, you'll see the problem is that when you click on the Help Tray it sends an AJAX request to /help_links, so the links don't exist on the page when your code is trying to modify them. If you use mutation observers, you can probably inject new items. In Admin Sub Account Menu, I inject links to a React Tray, and copy the dynamically generated class names from a sibling. And in Admin Tray - Sub Account Menu, @James was crucial in helping me get the right Mutation Observer for watching the Nav Tray mutations.
You can always share you code here, use the syntax highlighter feature under More. Code posts are reviewed by moderators before being publicly visible.
Additional resources, maybe some examples of selectors and work arounds.
https://community.canvaslms.com/docs/DOC-17354-awesome-canvaslms#CanvasThemes
Canvancements - Canvas Enhancements
GitHub - robert-carroll/ccsd-canvas: CanvasLMS - Theme hacks and feature augmentation
Thank you so much for all the helpful information. Looks like I have more good threads to review
I had some suspicions that the class names were being dynamically generated during some build/page rendering process. I know enough about React.js to get myself into trouble (it is one of those framework/environments that's on my lest to learn more about).
When I started poking around in Canvas' dynamically-generated mobile menu DOM structure, I noticed some "random-looking" element IDs and was hopeful I could anchor off those. Unfortunately, reloading the page created completely different IDs!
The solution I landed on is based on two things. 1) Each button has a unique text label. 2) Menu items that operate a submenu leverage a unique ARIA-controls label to control the visibility of that submenu DOM element. The JavaScript below targets the Help menu item by name (string search) and then captures the ARIA-controls label of the Help menu item (during the click event) which gives us all the information we need to add our new menu item (in this case, prepending to the top of the list).
$("body").on("click", "button", function () {
// Check that the button is the "Help" button and check if the global nav is mobile
if (($(this).is(':contains("Help")')) && $(".ic-brand-mobile-global-nav-logo").length) {
// Capture ARIA selector of button
var aria_selector = $(this).attr('aria-controls');
// Check for link already added
if ($("#umsn_sphfcc_added").length == 0) {
// Timer to ensure page is rendered before adding new link
setTimeout(function () {
// Select associated help menu and add link
$("#" + aria_selector + " ul").prepend("your_link_here");
}, 500);
}
}
});
Even though we only have access to sub-account CSS/JavaScript customization, this solution has been working very nicely for us so far. That said, I'm sure there are more elegant solutions out there and am certainly open to any suggestions or improvements 🙂
Thanks again for all your generous insight and shared collaborative spirit!
Here's the argument against setTimeout, and for Mutation Observers from @James
how-to-adapt-to-the-undocumented-javascript-loading-sequence-changes#comment-149412
I'm a bit occupied this afternoon, but I'm open to working on a solution together using Mutation Observers so we can formalize some code for the community.
I tinkered with this last night instead of doing the Grand Canyon puzzle or playing video games, honestly just as fun.
After writing a few other global nav hacks this wasn't too bad. With a bit of polishing it might be useful for others. I took a few ideas from past hacks and your original post and thought up some options. Here's what I have so far.
please don't use this in production yet, it requires testing
General Warning/Considerations for use: The user, no matter what role, has absolutely zero idea what sub account they are currently in. This might be why Canvas hasn't provided this feature. If you pepper different links all over different sub accounts your users may have an inconsistent experience. ex: When we add links to our sub accounts, we do 1 extra link at the root of the the organizations sub account, for us, that's a school (we have 350). We have department sub accounts within, but the whole school gets 1 extra help link, not different links at different levels.
for global themes javascript
[edit: older snippet removed]
for sub account themes javascript
[edit: older snippet removed]
Roles can now be passed with each link. This reduces code for setup.
Added some additional logic and error handling.
// global or sub account javascript
// Canvas Global Nav Custom Help Links or How To Add Custom Help Links to Sub Accounts
// working example
// please don't use this in production yet
(d => {
'use strict';
const help_tray = 'help_tray',
mark = 'custom-help-link',
watch_tray_portal = (mtx, observer) => {
let portal = d.getElementById('nav-tray-portal');
if (!portal) {
if (typeof observer === 'undefined') {
var obs = new MutationObserver(watch_tray_portal);
obs.observe(d.body, {
'childList': true
});
}
return;
}
if (typeof observer !== 'undefined') {
observer.disconnect();
}
let tray = new MutationObserver(check_tray);
tray.observe(portal, {
'childList': true,
'subtree': true
});
},
check_tray = () => {
let tray_is_open = d.getElementById(help_tray);
let any_custom_links = d.querySelectorAll(`#${help_tray} ul li.${mark}`);
if (tray_is_open && any_custom_links.length == 0) {
insert_links();
// observer.disconnect();
}
},
insert_links = () => {
if (!d.getElementById(help_tray) || typeof linksForGlobalNavCustomHelpTray === 'undefined') return;
let links = linksForGlobalNavCustomHelpTray;
for (let i = 0; i < links.length; i++) {
let link = links[i];
// if roles aren't set, add for everyone, otherwise skip link if user doesn't have set rules
if (typeof links.roles !== 'undefined')
if (!link.roles.some(a => ENV.current_user_roles.includes(a))) continue;
try {
// going to insert the custom link before this one
// if position isn't set, insert them at the top
let target_sel = link.position ? `nth-of-type(${link.position})` : `first-child`,
// if the target li doesn't exist, default it to the top
target_li = d.querySelector(`#${help_tray} ul li:${target_sel}`) || d.querySelector(`#${help_tray} ul li:first-child`);
// clone the target li
let new_help_item = target_li.cloneNode(true),
new_help_item_link = new_help_item.querySelector('a'),
new_help_item_desc = new_help_item.querySelector('div');
// remove canvas flags like (NEW), if we cloned it
if (new_help_item.querySelector('span[aria-hidden]'))
new_help_item.querySelector('span[aria-hidden]').remove();
// update with current custom link
new_help_item_link.href = link.href;
new_help_item.target = link.target;
new_help_item_link.textContent = link.title;
new_help_item_desc.textContent = link.desc;
// mark the item for the observer
new_help_item.classList.add(mark);
// insert the new help link
target_li.before(new_help_item);
} catch (e) {
console.log(e);
}
}
};
watch_tray_portal();
})(document);
const linksForGlobalNavCustomHelpTray = [];
// sub account javascript
// additional links for sub account help tray
linksForGlobalNavCustomHelpTray.push({
href: "#sub_account_help_link",
title: "Additional Link for All Roles",
desc: "Adding this from Javascript",
target: '',
position: 2,
//roles: [] // not set, defaults to all
}, {
href: "#link_for_students",
title: "Additional Link For Students",
desc: "This one too",
target: '_blank',
position: 3,
roles: ['student']
}, {
href: "#link_for_admin",
title: "Link For Teachers & Admin",
desc: "also this",
target: '_blank',
position: 8,
roles: ['teacher', 'admin']
});
I would not use the text of the link as a way of selecting content if there is another way to do so. The text varies based on someone's language. As long as everyone was using the same language for Canvas, it should be okay.
In the case of the help menu, I would base it off the href for the link. The bonus is that you can do that with a CSS selector, while you cannot for text. I think I've seen Robert do something with xpath that can do that. There's also a note that xpath support varies widely by browser and while Firefox supports it, there's no plan to improve it. Regardless, you're back to the first issue that the text may vary.
If the text you're looking for is set by you and you don't support internationalization, then it may also be okay to select, but the href route is still faster.
Let's say that my help dialog looks like this, where the last entry is a link to office.com
This CSS selector will find that anchor element for the Student Email.
#help_tray li a[href="https://office.com"]
Now what I need to do is find the parent list item element.
The underlying HTML for that entry in the DOM looks like this and this is inside of a unordered list element which is inside a div with an id of help_tray.
<li class="fOyUs_bGBk dxCCp_bGBk dxCCp_fLbg dxCCp_ycrn dxCCp_cfzP dxCCp_bCcs">
<span direction="row" wrap="no-wrap" class="fOyUs_bGBk fOyUs_desw bDzpk_bGBk bDzpk_oDLF bDzpk_fZWR bDzpk_qOas">
<span class="fOyUs_bGBk dJCgj_bGBk" style="min-width: 100%; flex-basis: 100%;">
<a target="_blank" rel="noopener" href="https://office.com" role="button" tabindex="0" class="fOyUs_bGBk fbyHH_bGBk fbyHH_bSMN">Student Email</a>
<div class="cjUyb_bGBk cjUyb_doqw cjUyb_eQnG">Check your student email in Office 365's Outlook.</div>
</span>
<span class="fOyUs_bGBk dJCgj_bGBk"></span>
</span>
</li>
Those classes are not static between builds, which used to be every three weeks, then went to once a month, and now with the COVID-19 thing might be changing every week. In other words, most definitely do not rely on them.
Now, let's say that you want to insert your own link first and you want it to look like all the others. The first thing to do is to find a link that is styled the way that you want you link to work. That probably means not having "new" or "featured", but just a regular link. Let's say the "report a problem" link. The href for it is #create_link.
Here is something that I threw together very quickly. It should only be called after you have verified that the help_tray div exists via a mutation observer and possibly even that it's displayed to keep Canvas from overwriting your changes -- you would have to play around with that to make sure it's right.
(function() {
'use strict';
const helptray = document.getElementById('help_tray');
const helplinks = helptray.querySelector('ul');
const firstlink = helptray.querySelector('ul>li');
const problemlink = helptray.querySelector('a[href="#create_ticket"]');
const problem = problemlink.closest('li');
const node = problem.cloneNode(true);
const nodelink = node.querySelector('a[href="#create_ticket"]');
const description = node.querySelector('a[href="#create_ticket"] + div');
nodelink.href = 'https://google.com';
nodelink.textContent = 'Google It';
description.textContent = 'If you cannot figure it out, Google it.';
helplinks.insertBefore(node, firstlink);
})();
What this does is to find the Report a Problem link based off the href rather than the text so that it isn't dependent upon the language selected by the user.
Then it clones that node using a deep clone. We now have an identical copy of that node.
We're going to change the href and text of the link and the text of the div that immediately follows the link. This holds the description.
In my example, I'm giving students the best help I can -- telling them to Google It.
After that is changed, it inserts the node before the top help link. If there's ever more than one list in the help_tray, you could use problemlink.closest('ul') to make sure you get the right one. Older browsers may need a polyfill for closest(), but those browsers aren't supported by Canvas anyway.
When I run this, my help menu now looks like this.
If I knew, and could guarantee, what the first link was going to be, I could have just used it to clone and saved some steps, but I don't know when Canvas is going to put something at the top of the help links.
I apologize for the poor formatting on the code, I was typing it into the browser's console and it was acting really slow on me. I guess I had too many Chrome windows open (or maybe just the wrong ones). I need to reboot my computer but I didn't want to do it while writing your message.
I wonder how different these discussions might go if there was no delay in moderation? I posted at 12:04, and the refresh showed me you liked my earlier reply, and I thought I should tell him I just posted and send you the example, and then I continued to think, he's +2 hours and has to be going to bed, but you spent another 40 minutes with that.
I've recently switched to using Firefox for dev. Using Firefox Dev (or Chrome Canary), gives me an additional working space. I found last night that Firefox deals with crashed tabs due to Mutation Observers much better than Chrome. Chrome the browser dies, requires force quit. Firefox, just close the tab!
insert_links() with deep clone, much better.
insert_links = () => {
if (!d.getElementById(help_tray) || typeof linksForGlobalNavCustomHelpTray === 'undefined') return;
linksForGlobalNavCustomHelpTray.forEach(link => {
// going to insert the custom link before this one
// if position isn't set, insert them at the top
let target_sel = link.position ? `nth-of-type(${link.position})` : `first-child`,
target_li = d.querySelector(`#${help_tray} ul li:${target_sel}`);
// clone the target li
let new_help_item = target_li.cloneNode(true),
new_help_item_link = new_help_item.querySelector('a'),
new_help_item_desc = new_help_item.querySelector('a + div');
// update with current custom link
new_help_item_link.href = link.href;
new_help_item_link.textContent = link.title;
new_help_item_desc.textContent = link.desc;
// tag the item for the observer
new_help_item.classList.add(tag);
// insert the new help link
target_li.before(new_help_item);
});
};
WOW! There are a lot of excellent ideas here...I feel a guilty for not responding sooner. Unfortunately, I don't have access to the main Canvas account, so can't implement JavaScript customization at that level. I may be able to convince our ITS folks to do that at a later date and intend to work that angle. As you can imagine, they are very protective over making changes at the institutional level, which is a level of caution I completely understand.
If you don't mind, I'd like to pull your snippets apart over the next few days and refine my original code snippet with some of your ideas. (I'm currently tasked with building out 3 courses with an end of month deadline). I'll post my derivative as soon as I have it updated. It's definitely helpful to have the ability to customize global navigation at the sub account level.
carroll-ccsd, I noticed the interesting "exist or not exist" link behavior when I was initially adding our custom help items while logged in as administrator for our sub-account. I am somewhat concerned about that behavior but it might be okay. When I logged in as a test student, the link showed up everywhere. As far as I understand, our students will "only" be enrolled under our school's sub account, so should see the links consistently. Of course, I'm still new to customizing Canvas as well as fully understanding how our school uses it, so will have to just test things to see what works and check in with our student users to verify. I have to admit my ignorance of the existence of mutation observers, which is a far better solution than a timer (shows how old-school my hacking skills really are I suppose).
@jamesjonespa , you make a very good point about not using the menu name. I will admit that I was looking for an expedient solution rather than a robust one (which must also be obvious given my use of a timer to add the item). We don't internationalize our Canvas interface at the moment but that doesn't mean it won't happen in the future. So it makes perfect sense to restructure the query in a way that doesn't anchor off the name.
As a side note, I'm thinking about installing a version of Canvas on my local machine so that I can test things more rapidly. Is this something you both have gone through the trouble to do or is it a waste of time? Part of why I'm interested in doing this is that it would give me access to the main account admin features, which would allow me to better understand the ins and outs of Canvas and how we can best customize at the sub-account level.
Thank you both again for all your insight
I'll post my revised code once it's complete and will try out what you have provided when I have a chance.
I've not had much luck installing Canvas on my own machine. I've tried several times I wasn't able to get it fully working. One thing I did get working was to use the Bitnami Canvas LMS Stack. They have a virtual machine that made it a snap to install without installing a bunch of other dependencies.
This stay at home thing has my sleep cycle completely messed up and the kids are getting to bed later each night as well. They have been getting to bed after midnight and since I need some quiet to do things, I'm staying up well into the wee hours. I think I've been getting to bed by 6 or 7 am the last few days and then sleeping until 11 am.
The #create_ticket was an example, it was meant to be something that the site knew that they had that looked like what they wanted the new one to look like. Lack of sleep sometimes causes me to be more confusing than normal.
Weird about MutationObservers crashing. I haven't had that problem, but I haven't had an opportunity to develop much recently, either. I find I don't like the Firefox development tools as much as Chrome, it's harder for me to get done what I need. That might be familiarity at this point because I started off in Firefox but it crashed so much I switched to Chrome and have been there for several years.
Development time is minimal. I'm still scrambling to get content online for my students. Our governor just announced today what we all suspected was coming -- our 2nd grader isn't going back to school this year and finding quiet time with them in the house is a challenge. That and trying to solve differential equation problems in a 2'x3' whiteboard is very restrictive. As I'm writing my students for next week -- 3 more weeks -- just 3 more weeks (plus finals). I turned down the opportunity to teach in the summer as we've gone completely online for that too and I need a break. The original plan was to take the summer off and possibly come see you like we had planned last year, but we're not doing that trip anymore.
Did you get the message the other day from GitHub about private repositories with unlimited collaborators? I immediately thought of the project we worked on together and how it might have gone faster -- although it was a fun night with the quasi-real-time editing. I've also got some Canvancement code that isn't quite ready for release but I could use a few people to beta test. Right now, it's only on my machine so if it dies, so does my work.
With the toddler, we're up at 6AM every day, or whatever time she picks, sometimes 5:59, sometimes 5:30. Bed time is a matter of how much we chose to suffer the next day, and whether that time spent was enjoyable or productive.
Our district is distance learning for the remainder of the year.
Firefox/Chrome... maybe it's a OSX symptom. Chrome loves to hog CPU/RAM on Mac.
I did get the GitHub announcement, but I haven't looked further.
I'd be happy to test any Canvancements. I have a module mashup hack that's gonna need some testing soon too.
I would be sunk without a good backup. Not everything is in git, but everything is backed up with Time Machine, so files can be recovered that I edited, and the entire computer can be restored. This is actually how I rebuild my laptop, it's just a clone.
// ps. there's a moderated code post lingering above
To interact with Panda Bot, our automated chatbot, you need to sign up or log in:
Sign inTo interact with Panda Bot, our automated chatbot, you need to sign up or log in:
Sign in
This discussion post is outdated and has been archived. Please use the Community question forums and official documentation for the most current and accurate information.