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

How to remove Curve Grades in gradebook?

Hi

In Gradebook, we would like to remove the "Curve Grades" item, how can we do it? Please see the attached screenshot. I can remove the item in the console, but it does not work after I put it in the global Javascript

Please advise.

li

6 Replies
James
Navigator II

julielkz,

Try this:

(function() {
'use strict';

const pageRegex = new RegExp('^/courses/[0-9]+/gradebook/?$');
if (!pageRegex.test(window.location.pathname) || !document.body.classList.contains('gradebook')) {
return;
}

const observer = new MutationObserver(hideCurveGrades);
observer.observe(document.body, { 'childList': true });

function hideCurveGrades(mutations) {
if (mutations.length === 1 && mutations[0].addedNodes.length === 1) {
const el = mutations[0].addedNodes[0].querySelector('span[data-menu-item-id="curve-grades"]');
if (el) {
const item = el.closest('li');
if (item) {
item.style.display = 'none';
}
}
}
}
})();‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

The element.closest() isn't supported with Internet Explorer 11, but Canvas doesn't support that browser either.

The "Curve Grades" isn't in the DOM until you click on the vertical ellipsis to open the menu. It's attached and removed from the body element. I added a mutationObserver to watch the body. Thankfully, it was a direct child of the body element and so there's not a lot of traffic -- not like if I had to watch the subtree. The mutationObserver needs to stay active since that item keeps getting added and removed from the body.

When the menu is added, it is a single mutation with a single added node. Even those the mutations didn't happen, I narrowed down the search by only looking for those that matched. Then I search for data-menu-item-id="curve-grades", which is the list item we need to remove.

Then I traverse up the DOM to find the list item that it is contained under and hide that list item. I could have removed it completely with some additional logic, but hopefully hiding it is sufficient.

You can't use CSS alone to do this as there is no ancestor CSS selector. You need to match off span[data-menu-item-id="curve-grades"], but then hide the list element that precedes it by several levels.

Thanks James, I will give a try today. 

Hi  @James ,

I remove the option by simply adding an EventListener (Focusin) to the gradebook as follow

document.addEventListener('focusin', function (event) {	
$("span[data-menu-item-id='curve-grades']").closest('li').remove();
})


Do you know if using MutationObserver as you describe above would be better in term of browser performance?

I’m new to Javascript. I hope you could shed light on this matter. Thank you!

 @jerry_nguyen  

One way to answer that question is to add a console.log('x'); line inside your code and inside mine (first line inside the hideCurveGrades function). The 'x' doesn't have to be x, it's just something to show what happens. Then go into the developer tools on the browser and look at the console as you start to do things in the gradebook.

Your code fires every time an element is about to receive focus. That means that every time you move to a different cell in the gradebook it fires. And it fires multiple times for each of them as well. It fires when you click on any of the menu items like 'gradebook', 'view', or 'actions'. It fires twice when you click inside the search box. It fires when you click on the Courses from the global navigation. 

On the other hand, mine waits for things that happen that might actually invoke the menu with the curve grades. This includes clicking on gradebook, view, actions, and the search box, the same as yours. But it doesn't fire when changing cells within the gradebook. It does not fire when you click on Courses from the global navigation.

I cannot tell if you are only running this code on the gradebook page or on every page, but if you're not limiting it to just the gradebook, then it is definitely a bad idea since you're never going to find those items except on the gradebook page, but you're continually checking for them.

If we're talking about just within the gradebook page, it looks like mine is going to have better performance because it's more selective. Your approach wastes CPU cycles checking for a non-existent span for most of those fires while I'm checking for specific things that may result in a hit and ignoring all of the ones that won't. You're using jQuery and I'm using pure JavaScript so there's less overhead to mine despite yours being shorter. You're searching the entire document for a span that matches while I'm only checking the part that has actually changed for a matching span.

Yours looks simpler because jQuery is masking the failed find for you while I have code to narrow down where it looks and to make sure it finds it before it deletes it.

I was just learning JavaScript a few years ago (maybe 5 now). Thankfully, I didn't have legacy code stuck in my mind from years of programming, so I went to the specs on the Mozilla Developer Network to see how it was supposed to be done. The more and more I do, the less and less I use jQuery. It makes my code appear longer and more complicated but it also removes any dependency on a third party JS library.

Hi James,

Thank you for your thorough explanation and the time you put in to write it. I really appropriated it.

By adding console.log to the "Focusin" eventListener, I know that it runs every time I move my cursor on top of a link. 

Through Chrome's Performance Monitor, I can see that using MutationObserver is much better performance and use less CPU (About 30%).

I didn't know pure JS would have less overhead, I use jQuery because it's so much easier to write. But I would consider converting my code to pure JS.

Thanks again James, this is very useful.

By the way, do you know if I can use MutationObserver to detect Gradebook view change? (E.g. scrolling horizontally and vertically) 

 @jerry_nguyen ,

Yes, you can do this with Mutation Observers, but probably not in the way you are thinking.

A MutationObserver detects a change to underlying DOM. In general, the DOM doesn't necessarily change when you scroll, but it turns out that they do in the Canvas gradebook. 

At least they sort of change. The addition or removal of information from the screen triggers a DOM change. That's not the same as scrolling; it's scrolling enough to bring something into the display or take it out of the display. That may be good enough for you.

What you want to look at is div.viewport_1 > div.canvas_1. The selector .canvas_1 appears to be enough to select it.

With horizontal scrolling all of the children of that element. When vertical scrolling, only some of the items change. The vertical scrolling can be detected by observing childList, but to get the horizontal scrolling, you need to add subtree as it doesn't happen directly to a child of .canvas_1.

Adding subtree makes it costly as it now fires for every change within the gradebook, not just those related to scrolling. Clicking on a cell to enter a grade will fire it. So we want to avoid subtree if we can.

We can do that by adding a mutation observer that watches the first row of the gradebook with selector .canvas_1 > div

Here is a code fragment that will do that.

(function(){
'use strict';
const hEl = document.querySelector('.canvas_1 > div');
if (hEl) {
const hObserver = new MutationObserver(() => {console.log('Horizontal Scroll');});
hObserver.observe(hEl, {'childList' : true});
}
const vEl = document.querySelector('.canvas_1');
if (vEl) {
const vObserver = new MutationObserver(() => {console.log('Vertical Scroll');});
vObserver.observe(vEl, {'childList' : true});
}
})();

Here's the problem, though. An element on the .canvas_1 class doesn't exist when the page is loaded. That means that you cannot attach a Mutation Observer to it until it is present. This gets into where I have used Mutation Observers before -- waiting for an element to be present on the page.

To do this, you need to find an element that is present on the page. Viewing the source (Ctrl+U) shows you what is delivered with the page and you can rest assured that it is in the DOM to listen to.


It looks like #gradebook_grid is as close as you're going to get.

Watching the #gradebook_grid element and waiting for .canvas_1 > div to appear under it isn't going to get it exactly, though. I had vertical scrolls show up as additional students were loaded before I ever scrolled. However, the .canvas_1>div element is removed and recreated after the observer was attached and so it's not listening to the final version. I determined this because the code above works when it is entered from the console (after the page is fully rendered) but it does not work when added to a script that runs automatically and waits for it appear. The vertical scroll detection works, but the horizontal one never fires.

A likely candidate is because the first student loaded isn't necessarily the first student displayed (depending on your sort order), but that has to be taken care of.

You would need some way of detecting once the gradebook was completely ready to watch. More likely, you would need to watch .canvas_1 to see if the whole thing is re-rendered, wiping out the >div portion that you were watching.

Now we have a mutation observer to wait for .canvas_1 to appear and then we can attach a listener to .canvas_1 to see when the div elements under it change to detect a possible vertical scroll (or perhaps just loading of additional students) and to see when the div elements under it completely change so that we can add an observer to look for horizontal scrolling.

A much easier way of determining scrolling is by adding an eventListener to the div.viewport_1 element. Element: scroll event - Web APIs | MDN 

You will still need to use a mutation observer to wait for the .viewport_1 element to become present on the page. You will also want to request an animation frame as detailed in the link provided.

What looks to be the easiest way right now is to use a Mutation Observer to wait for .viewport_1 to be ready and then a scroll Event Listener once .viewport_1 is ready.