cancel
Showing results for 
Search instead for 
Did you mean: 
Surveyor

Dynamically Customize Canvas Global Help Menu

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.

15 Replies
Community Team
Community Team

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.

Surveyor

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

Adventurer II

Hi wnewhous,

I'll add james@richland.edu‌ 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 Menujames@richland.edu‌ 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 Smiley Happy

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@richland.edu

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.

Adventurer II

wnewhous

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.

  • Main function self executes and can be added to global Canvas JavaScript, alone or appended with a set of links to add. Obviously, adding links is a default Canvas feature. Adding the function globally lets you add links with sub account javascript with less copy/paste
  • Optionally, if you're only modifying 1 or 2 sub accounts, you can use a file in each sub account
  • Sub account Javascript is easy, just push the extra links into the array and done
  • Easily add links based on user role
  • I added the ability to set the position of the item in the menu, if it's not set the link defaults to the top
  • I copied the classes from the next li, but I still need to evaluate what's left to make those right in different viewports and finish the missing classes

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']
});
Navigator

wnewhous,

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

345121_pastedImage_4.png

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.

345122_pastedImage_5.png

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.