cancel
Showing results for 
Search instead for 
Did you mean: 

Admin Tray - Sub Account Menu

robotcars
Community Champion
16 63 5,102

CHANGELOG

Sep 19, 2018 > Sep 21, 2018 12:28 PM

Jun 6, 2018

May 31, 2018

 

README

If you're a system administrator of a Canvas LMS instance with a deep organization of sub accounts you have inevitably found yourself in one of Dante's 9 Circles of Hell, repetitively clicking Sub Accounts at each and every level as you drill down to where you need to go. Here at CCSD we have over 8,000 sub accounts, and yet this only affects 5 people. So we will share this to help anyone else who is trapped.

The JavaScript and CSS files here add a directory style recursive HTML menu to the Canvas global navigation admin tray...

In Pictures

CollapsedExpandedSearch
Admin Tray CollapsedAdmin Tray ExpandedAdmin Tray Search

Configuration & Setup

Zero to little configuration is needed for admintray-subaccmenu.js, so I won't even bother explaining subacctray.cfg

Option 1

  1. Host the file admintray-subaccmenu.js on a secure server (https) or somewhere like Amazon S3
  2. Copy/append the contents of admintray-subaccmenu.inc.js to your Canvas Theme JavaScript file, point the URL to the hosted file.

Option 2 (Quick Start)

  1. Just copy the example snippet below that uses this repo's version and RawGit   to your Canvas Theme JavaScript file. However, please note that the RawGit URL points to the repo source and any changes to it may affect your usage later. I recommend hosting your own for stability, but this can get you started.

Once loaded the script will initialize an API call (to /api/v1/accounts/self/sub_accounts) and loop through x/100, collecting every sub account in your instance, compile a recursive HTML unordered list and store it your browser's localStorage.

Depending on the number of your sub accounts and internet speed, this will take a moment, be patient for the first load, open the tray and wait for it to show up. This takes us about 45-60 seconds on production. You will see a loading indicator in the Admin Tray while it compiles.

Features & Use

Instance Independent

There are some users (like Instructure Canvas Remote Admins) who may login to multiple Canvas Instances/institutions, so each menu will be stored in localStorage based on the uniqueness of x.instructure.com, including x.beta.instructure.com, x.test.instructure.com, and xyz.instructure.com.

For most users and institutions this will go mostly unnoticed unless you have different sub accounts between your Production, Test and Beta environments. I made this update to help those users it will affect. I also personally hate copying, pasting or replicating files just to change 1 line. This update allows 1 file to be hosted on a CDN like Amazon S3 and simply change the var show_results_parent value in admintray-subaccmenu.inc.js or leave it blank.

Therefore an already minified file has been provided in the repo.

Alphabetical Sorting

Yup! As far as I know, the Canvas API does not allow sorting API calls alphabetically during pagination. The entire stack of sub accounts is sorted prior to building the menu.

Localized Search

Using JavaScript/jQuery to search within the stored HTML, preventing further API calls. You can search the entire sub account menu by name.

Search Result - Prefix Parent Account (Skip-to-Depth Display)

I don't know what else to call it, so here is an explanation.

Suppose your directory structure looks something like the following, where (#) is the depth of the account.

  • High Schools (1)
    • George Washington HS (2)
      • Course Shells (3)
      • SIS Courses (3)
        • English (4)
        • Math (4)
        • Science (4)
  • Middle Schools (1)
    • Betsy Ross MS (2)
      • Course Shells (3)
      • SIS Courses (3)
        • English (4)
        • Math (4)
        • Science (4)

When we search for Science and get multiple sub accounts of 'Science', we can't identify which one we want to choose.

  1. Science
  2. Science

So if we map the results depth of 4 to it's parent depth at 2 (instead of 3, SIS Courses) we can get a result like:

  1. George Washington HS > Science
  2. Betsy Ross MS > Science

And both are links to their respective account.

Examples

// one skip
show_results_parent = { 4:2 }
// expected result:
// George Washington HS > Science

// or multiple skips, must be unique
show_results_parent = { 4:2, 3:2 }
// expected results:
// (4:2) George Washington HS > Science
// (3:2) George Washington HS > SIS Courses‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

Note: If you don't define the skip, it will not display a parent

Reload

The ↻ button at the bottom right of the menu will clear the menu from localStorage and recompile it for the current Canvas instance. Use this when you or other admins have added or removed sub accounts.

User Impact

As mentioned above, CCSD has 5 system admins, with 40,000 Employees and over 300,000 Students. Be kind to your users, use the snippet below to reduce the impact of your admin-only tools. This is included in the repo as admintray-subaccmenu.inc.js

if (ccsd.util.hasAnyRole('admin','root_admin')) {
// used for search results, result:parent, see documentation for more details
var show_results_parent = {}
// async load the sub account menu script
$.ajax({
url: 'https://cdn.rawgit.com/robert-carroll/ccsd-canvas/master/admintray-subaccmenu/admintray-subaccmenu.m...',
dataType: 'script',
cache: true,
data: {skipd: JSON.stringify(show_results_parent)}
})
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

User Script

At the suggestion and some coaching by  @James , I have created a User Script version. This is useful for those admins that can't or don't want to install this script in their Canvas Theme/Global JavaScript. This script is identical to the global JavaScript file, except that the CSS will be added to the DOM by the script and has the various userscript requirements at the top for Tampermonkey.

  1. You will need to install and enable the Tampermonkey browser extension
  2. Install the UserScript version of admintray-subaccmenu.user.js

Known Issues

If you are not a root admin of your Canvas Instance, you will need to set (1) sub account in the root setting. At this time you cannot add multiple sub accounts. I am planning to fix this.

Code repo here...

ccsd-canvas/admintray-subaccmenu at master · robert-carroll/ccsd-canvas · GitHub

Credits for Blog Post Banner Image

Public domain

Chart of Hell
File:Sandro Botticelli - La Carte de l'Enfer.jpg - Wikimedia Commons
 

Botticelli : de Laurent le Magnifique à Savonarole : catalogue de l'exposition à Paris, Musée du Luxembourg, du 1er octobre 2003 au 22 février 2004 et à Florence, Palazzo Strozzi, du 10 mars au 11 juillet 2004. Milan : Skira editore, Paris : Musée du Luxembourg, 2003. ISBN 9788884915641

{{PD-US}}
This media file is in the public domain in the United States. This applies to U.S. works where the copyright has expired, often because its first publication occurred prior to January 1, 1923. See this page for further explanation.

63 Comments
Boekenoogen
Community Contributor

Thanks for sharing this code.  I totally understand where you are coming from regarding the sub accounts. We have many different sub accounts with in our college. I will have my staff look at this and see if we can use it. 

James
Community Champion

carroll-ccsd,

Impressive contribution.

Something that might work well in your case, since you only have 5 people affected, would be making it a userscript to be executed from within Tampermonkey. Then you can install it for those five people without having to add more to the global custom JavaScript. That would also avoid the extra call to the CDN to get the information as the require line makes a local copy.

And if you install Tampermonkey, then you can also add in some of the other cool things that people are doing with userscripts without having to put them in the global file.

robotcars
Community Champion

Thank you  @James ‌,

That's a great suggestion and I will discuss that with our team and look into including some additional information about userscripts in the repository.

straussi
Community Champion

I wanted to add for those of us with only a hundred or so sub-accounts, please go and vote for cms_hickss idea to make sub-accounts a link in the new course search.

robotcars
Community Champion

I've updated the repo code with new features. A reset button and loading indicator. If you use the new source, please update both the CSS and JavaScript theme files.

However, just this once, you will have to manually clear the menu from localStorage via the Javascript Console in your browser(s). web development - How do I open the JavaScript console in different browsers? - Webmasters Stack Exc... 

// type the following, then press enter
localStorage.removeItem('adm_tray_sub_acc_menu')‍‍‍
// then SHIFT+REFRESH, to reload CSS and JavaScript

If you notice any issues please let me know.

robotcars
Community Champion

For the past year Deactivated user, an amazing Product Engagement Consultant from Canvas, was helping out our district and department, going to schools and training, and providing some awesome feedback and details on things I didn't know! Mike is now a Remote Admin for multiple Canvas instances and started trying to use this hack this week and found a few difficulties. I like Mike, so we worked through those problems and I've posted an update to the repo.

First

That root_account_id patterns are more different than I thought, and that self can be used instead of a root account id for API calls. I've also grabbed the root account id during the first API call for recursion instead of having it statically set by the user.

See lines 167 & 184

Now, there is no need to make any changes to admintray-subaccmenu.js, and a minified version is available in the repo.

Second

The first solution made the need for user configuration pretty unnecessary. The next step was allowing Mike to have a menu for every Canvas Instance he's an admin of, a problem, because we only stored one menu. Now menu's are stored independently by changing the localStorage key to location.host+'_subacc_tray'

See line 17

Third

The only setting now is the option for handling Parent Display of a Search Result. This option had to move to the snippet that loads the main script from an external source. So show_results_parent replaces skipd and is sent via query string to the script.

ccsd-canvas/admintray-subaccmenu.inc.js at master · robert-carroll/ccsd-canvas · GitHub

Full source and README ccsd-canvas/admintray-subaccmenu at master · robert-carroll/ccsd-canvas · GitHub 

As always, if you have any questions (or maybe I need to clarify something) please let me know.

mcowen
Instructure
Instructure

Robert gives me too much credit on his code. I just did a little consulting/troubleshooting/beta testing.  This is a really cool tool! Great job, Robert!

robotcars
Community Champion

Our email has 27 threads... Great Collaboration!

Thanks Mike!

robotcars
Community Champion

Gonna try something new...

BETA TEST USER SCRIPT/NEW FEATURES

Ever since  @James  and I did some collaboration on Custom JavaScript for Admin/Courses Page, I've been tinkering with making a user script version of this. Wasn't the first time he nudged me to do so either.

Anyway, the changes I've been working on include:

  • Using a new DOM ID that Canvas has added since this was first released.
  • Caching the data in the event the user interrupts the build process*
  • Improving the loading indicator by adding a counter of the API pagination
    it's not a progress bar, but if you can do the math, you'll know how far it has left to go
  • CSS is added inline with a simple function at the bottom for user script support vs CDN hosting
  • update... additionally, #comment-125539 includes new changes

*The cache process, is for instances with a large number of sub accounts. As previously mentioned we have over 8000 sub accounts, that requires 80+ api requests with 100 records each. This can take considerable amount of time. If the user interrupts this process by using Canvas and clicking to a new page, the process is interrupted and the whole thing starts over. Extremely annoying right? This version collects the API data in local storage after each pagination and waits until the whole process is complete to build the HTML list. If the process is interrupted it continues on the next page, with the next pagination request to the API.

For this user script, set the SKIP DEPTH value on line 291.

You will need to install TamperMonkey to make this work.

Please run only on TEST or BETA - don't use on production until l release all updated versions.

Please provide any feedback or comments, results etc. 

CODE HERE [redacted beta script, see title post]

Please bookmark this thread if you use the code I provide here, so you get updates in your inbox.

James
Community Champion

I wrote this huge message and then right before I sent it, I found out that if you include recursive=1 on the request, it doesn't send you the last Link, just the next one. That nullifies everything I wrote about speeding up the process. Still, other people may be able to benefit from it, so I'm going to leave it in here. Depending on how the subaccount structure is setup, it would mean writing your own recursion, leading to probably more than 80 API calls, so it may not be a speed improvement and would definitely make it harder. Too bad, it could have really sped up things.

This part does not apply to sub_accounts with recursive=true, but it works with lists of courses and lists of students and other places. It did reveal out a problem with the intra-group peer review code that isn't a problem there, but it could be for someone using the code for something else without a last header link.

*The cache process, is for instances with a large number of sub accounts. As previously mentioned we have over 8000 sub accounts, that requires 80+ api requests with 100 records each. This can take considerable amount of time. If the user interrupts this process by using Canvas and clicking to a new page, the process is interrupted and the whole thing starts over. Extremely annoying right? This version collects the API data in local storage after each pagination and waits until the whole process is complete to build the HTML list. If the process is interrupted it continues on the next page, with the next pagination request to the API

Interested in speeding up this initial load? I don't have anywhere close to that many sub accounts for testing purposes, but I have done some testing with making the calls in parallel and it really speeds things up.

When I wrote the Assigning Intra-Group Peer Reviews‌ script (source on GitHub), I circumvented the sequential nature by looking for pagination that followed the form you're using here. It looks at the last Link header to determine how many pages there are and then tries to fetch them all at once. Because most browsers limit you to 5-6 concurrent requests, it doesn't hit the timeout threshold, but you load the data a lot faster because you're making 6 fetches in the time it took to do 1 before.  I've found that requests seem to come back faster with smaller per_page settings. If you know you have a huge number, like you do, it may be quicker at 100, but if someone has a smaller number, it might load faster with say 50. I might be imagining that, too. I did some testing with multi-curl in PHP that is here somewhere in the Community (I think it was on a discussion regarding a Google Sheet and a faster way to get information, but I might be remembering incorrectly). It didn't run in the browser so it didn't have the automatic limiting and it was possible to exceed the rate limiting.

Since I don't have many subaccounts, I tested it with courses and just loaded 6266 course records, 50 at a time, including enrollments in the course in 39.014 seconds. That took 126 fetches. Once it was in the Canvas cache, it only took 13.795 seconds. That cache makes it hard to do benchmark testing. When I set per_page = 100, it took 11.650 seconds when fetched from the cache. I modified it to only return courses with student enrollments in an effort to get around the cache. That took 17.855 seconds at 100 per page to fetch 5086 records and 11.062 the second time. I don't know if there was a partial cache hit.  This is over DSL, so it may be faster from a school. I kind of skimmed the x-rate-limit-remaining and saw it dropped from 700 to 691, so nowhere close to using too many resources.

To make sure that something wasn't in the cache, I switched the query to look up the students in our student resources course (every student in there). That took 11.064 seconds to fetch 6243 records at 100 per page (564 records per second). I then switched to another course with only 2646 records and loaded them 50 at time and it took 5.063 seconds (523 records per second). I'm wondering if that 39 s at the beginning was a hiccup. I can't test for sure until I know that whatever I have is out of the Canvas cache.

Enough of the speed improvements that won't be happening in this particular project. On to other comments.

Modern browsers have WebAPI helpers that include fetch(), so you don't have to use jQuery's. When I looked into JavaScript promises as I was developing it, they didn't really have kind words about jQuery or other implementations of deferred. I will say that the error checking is probably not up to snuff in that code I wrote, it was one of the first things I did with promises and was happy that I got it to work at all.

I notice that you're converting ID and parentID into integers. Given the discussion we had about long IDs and being too big for JavaScript to handle, is there a reason these need to be integers rather than strings? Are you doing any math with them? I remember you saying you might need to change your code, so I thought I'd mention it. I also remember  the Accept: application/json+canvas-string-ids comment on the front page of the API documentation and how I probably need to start using those myself.

James
Community Champion

carroll-ccsd,

I'm probably thinking too much about the part of the script that works correctly. Again, you may have already done this, I haven't dissected the code.

What if ...

  1. You add an Event Listener to the id=global_nav_accounts_link. As soon as that is triggered, you check to see if it needs to fetch sub-account information through the API and if so, then start doing that. Execute Canvas' default action on that link. Better yet, do what Canvas does, which is start the download on a mouse-over so that information is ready (or nearly ready) as soon as someone clicks on the link.
  2. You fetch the sub-account list without recursion for the root account. Then you recursively fetch all of the sub-account information for each of the sub-accounts off the root account.

Why?

The first item allows you to prefetch the account information rather than waiting for the dialog box to appear. That gives it a head start and makes it appear peppier. This is what Canvas needs to do with the Course Search page, which takes way too long to load.

The second item allows you to quickly populate the initial list of sub-accounts and then fetch the rest in the background. It might only take a second to get the list that appears on the first screen then the rest loads in the background. The calls to each GET must be made sequentially, waiting for one to finish before the next, but we could start calls to all of the sub-accounts in parallel, let the browser dole out 5-6 at a time, and sequentially process each of them. A queuing process that used less than the max available might be a better approach (harder to write though) so that some connections were available for the browser to do other things. But the speed improvement might be so much that people can deal with the wait. There is no indicator within the list sub-accounts API endpoint that it has any children, so you may be needlessly making calls to fetch the sub-account information when there isn't any you don't already have (see Canvas Training in your screenshots). For you, at least, it looks like most of them have sub-sub-accounts. There will be more API calls this way (guaranteed), but since you have relatively few sub-accounts (direct children of root), hopefully it won't be so many more that the concurrent calls comes out faster in the end.

As each one comes sub-account returns its data, you can add it to the master list and modify the entry on the tray to reflect the status.

Of course, once all of this is loaded into the local storage, then there's no need for this. This is all to just improve that initial load.

If my curiosity seems to be in the wrong place, it's because the Mutation Observer seems more straight forward to me. I've not tested any of this, of course, just watched what happens as I click around. This is what that limited testing suggests.

  1. Make sure the id=nav-tray-portal div is available. If not, put a mutation observer on the body with childList:true until it appears and then disconnect once it's there.
  2. Attach a mutation observer to id=nav-tray-portal with childList:true. It never seems to be destroyed although the things inside of it are, but only once per pop-out or close. This will let you know whether it has opened or closed.
  3. That does not trigger a mutation when someone has the tray open and goes to another tray -- example, click on Account and then click on Admin. You would need to add subtree:true to get that. However, because Canvas is using React IDs for things, it's seems difficult to tell when the account navigation link has been opened other than by looking for an anchor element with an href="https://community.canvaslms.com/accounts". That is certainly doable with a query selector of '#nav-tray-portal a[href="https://community.canvaslms.com/accounts"]'. I started to write about Event Listeners, but the more I wrote, the more I think adding subtree:true is probably the easier route to go. 

When using a query selector to look for the accounts, it might be useful to do some testing. For example, if you use '#nav-tray-portal span div div ul li a[href="https://community.canvaslms.com/account"]' as the selector is it faster because of some short-circuit analysis or is it longer because it's more complex? The nav-tray-portal ID is required otherwise the Admin link itself matches.

To answer that my own question about speed, I went back to my benchmark testing from last night and tried three different approaches. For the third approach, I used a getElementById to get the nav-tray-portal element and then did a querySelector on that. All of these are 1 million times with either the admin tray, courses tray, or profile tray open, and finally with the tray closed. All of these times are in milliseconds and done with Chrome. Firefox is probably going to be slower based off last night's testing.

SelectorAdminCoursesProfileClosed
#nav-tray-portal span div div ul li a[href="https://community.canvaslms.com/accounts"]792111899279
#nav-tray-portal a[href="https://community.canvaslms.com/account"]587112299480
a[href="https://community.canvaslms.com/account"] after getElementById('nav-tray-portal');61311431021102

It looks like the best one to use is the second one. It was the fastest when it mattered and within a few ms over the course of 1 million queries when it didn't.

I would probably not look through every mutation checking for that one magic bullet, though. It may depend on how many times the subtree gets the observer triggered, but React normally writes the whole thing at one time and so there are just two -- one to destroy things and one to create everything, but once everything is there, you can do your thing. In other words, I would not pass the mutations to the callback, but just check for the existence of the right element.

There's a pitfall, but it's easily handled. Every thing you write to the tray triggers the mutation observer again. That means you'll need to check for the existence of your information right off the bat, and if it's there, return. I would add an ID to your block to look for that. It seems, without testing, that getElementById() should be faster than getElementsByClassName().

robotcars
Community Champion

Lot to think about... maybe you can help me with the mutation alone, before I look into the rest of your suggestions.

BTW, the whole thing starts with the execution of subacctray.init() L292, collecting and compiling the list in the background without any user action, if it's already built, it's available when the tray is opened.

I've been working on the mutation today to prevent the buildup and add a disconnect.

I'm currently stuck on not listening to other trays after activating the listener on nav-tray-portal.

edited before moderated...

Solved that issue, but I think I need to remove the onlick event listener when the tray is open.

Here's the observer I'm working on, compared to L167 of the current version.

(function() {
    'use strict';
    var subacctray = {};
    subacctray.trigger = function() {
        console.log('trigger')
        const cb = function() {
           
            var tray_content = document.querySelectorAll('#nav-tray-portal span');
            // close tray
            if(tray_content.length == 0) {
                console.log('close admin tray');
                observer.disconnect();
                console.log('disconnect');
                return subacctray.trigger;
            }
            // open tray
            var admin_tray = document.querySelector('div.tray-with-space-for-global-nav a[href="https://community.canvaslms.com/accounts"]');
            if(admin_tray != null) {
                console.log('open admin tray')
            }
        };
        const watch = document.getElementById('nav-tray-portal');
        const observer = new MutationObserver(cb);
        observer.observe(watch, { childList: true, subtree: true });
    };
    document.getElementById('global_nav_accounts_link'). = subacctray.trigger;
})();

Line 26 is supposed to include an on-click, but jive really hates words that start with 'on'. 

James
Community Champion

The quick way to fix those on removals is to not use those functions. New standards prefer event listeners and dispatchEvent('click'). But Jive really needs to fix this. They're probably saying "we're removing dangerous javascript to keep bad things from happening", but I think their code is a little too greedy. When it's enclosed in a code block marked as JavaScript it might be necessary.


Okay, that minimizes the need for increasing the speed since it's already done. There's really only a noticeable lag when someone goes to a fresh browser version and immediately goes to the Admin menu.

I will see what I can do with the mutation thing. My plan makes perfect sense to me so it shouldn't take long to test whether the initial version, but I'm probably missing something and things are always harder than they look. I'm trying to take the day to get caught up / get ahead on school work so I'll try to work it in between breaks in that.

James
Community Champion

carroll-ccsd,

 

I decided to take a short break from grading. It came together even faster than I thought it would and that was with me writing it with a promise at first and then rewriting it without them. It turns out that there are relatively few mutations that are sent (2 or 3 depending on what's going on), so it might be quicker to iterate through them, but probably not. Whether or not, I still think the logic is clearer just by checking for the existence of the href.

The watchTray() logic could be incorporated into an anonymous function on line 28, I just separated it so it was easier to follow. You can also add a parameter of mutations to the function if you really want to see what mutations are being sent, but it wasn't necessary once I figured it out so I removed it. You'll want to add your logic to make sure that your code isn't the ones making the mutations. That could be as simple as another querySelector. Also, I stored the document.querySelector() to a variable, but you should be able to just use if (document.querySelector(...)) {}. I sometimes don't take the most efficient way so that I can understand it better later on.

Obviously line 38 doesn't stay in the final code.

Here it is as a user script so you can test it easily.

// ==UserScript==
// @name        Navigation Tray Watcher
// @namespace   https://github.com/jamesjonesmath/canvancement
// @description Watch for navigation tray to pop out
// @include     https://*.instructure.com/*
// @version     1
// @grant       none
// ==/UserScript==
(function() {
  'use strict';

  checkTray();

  function checkTray(mutations, observer) {
    var el = document.getElementById('nav-tray-portal');
    if (!el) {
      if (typeof observer === 'undefined') {
        var obs = new MutationObserver(checkTray);
        obs.observe(document.body, {
          'childList' : true
        });
      }
      return;
    }
    if (typeof observer !== 'undefined') {
      observer.disconnect();
    }
    var tray = new MutationObserver(watchTray);
    tray.observe(el, {
      'childList' : true,
      'subtree' : true
    });
  }
 
  function watchTray() {
    var isMatch = document.querySelector('#nav-tray-portal a[href="https://community.canvaslms.com/accounts"]');
    if (isMatch) {
      console.log('Do your thing');
    }
  }

})();‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
James
Community Champion

I'm also noticing that Jive is now recognizing URLs inside code blocks. That's almost as bad as censoring any word beginning with on.

James
Community Champion

I also went in and modified the watchTray() function to check for all three common trays that might need modified. This is mostly for other people who might stumble across this thread later on.

function watchTray() {
  // Most people will only want to use one of these
  var isProfile = document.querySelector('#nav-tray-portal a[href="https://community.canvaslms.com/profile"]');
  var isAdmin = document.querySelector('#nav-tray-portal a[href="https://community.canvaslms.com/accounts"]');
  var isCourses = document.querySelector('#nav-tray-portal a[href="https://community.canvaslms.com/courses"]');
  if (isProfile) {
    console.log('Profile Tray');
  }
  if (isAdmin) {
    console.log('Admin Tray');
  }
  if (isCourses) {
    console.log('Courses Tray');
  }
}

The Profile tray is the tricky one. They might do a removeChild() after the addChild() so the profile link is there but you get another mutation right away. The node that is being removed looks like it's a person's initials (JJ for me), so it looks like it might be when the avatar comes in -- if it comes in. As if the fact that your code is modifying the tray isn't enough, that's another reason why it's important to check to make sure your logic isn't running twice.

PS - I'm being moderated so this post won't make much sense until the code I shared is released from the moderation queue.

robotcars
Community Champion

Thanks for the thoughts and the pastebin, I will check it out in a bit. Just got back from Costco, which on Sunday is like watching pigeons at the park jump all over each other for scraps of bread... :smileycry:

I have to try and get caught up with some house projects for a few hours, those drawer pulls etc are piling up waiting for my curious coding adventures.

Bobby2
Community Coach
Community Coach

carroll-ccsdhis was worth reading even if just for the opening paragraph. Dante's 9 Circles of Hell - made me giggle.

robotcars
Community Champion

That is pretty fantastic sir! When you first stated mutations on an , would be suspect for problems, I was concerned about the observer listening to every mutation all the time, waiting for the tray, and wasn't sure about how it would only track the correct tray. Something I've wrapped my head about this past week, is that with a mutation observer we are not watching them happen, we are accessing them after they've already happened. Thus, why a selector should be all that's necessary, the element already exists, and iterating through mutations is probably unnecessary. When I first started looking at mutations via iteration, I found the Node.isConnected property, and figured some might not be connected when I get to them, don't believe that to be the case anymore.

I like the usage of href, especially since nav-tray-portal is always available now.

I was originally using document.querySelector('#nav-tray-portal :first-of-type h2')

and checking for 'Admin', which works, but ignores internationalization, /accounts is probably the route for every language.

robotcars
Community Champion

Jive's prejudice here is unbalanced and inconsistent. We used [edit that which we speak of was removed here] all over Customizing Add People Dialog with Custom Javascript, even in the code samples, but it removed it from the sample above.

IMO, the syntax highlighting is a feature to improve the quality of a post by adding contextual value to the syntax, when provided. The syntax highlighter breaks apart the code into formatted html. Unless I'm missing something else, someone would need a highly suspect browser/extension for code in the post to be executed as a script. 

Furthermore, since Jive isn't going to prevent something like this...

function watchTray() {
// Most people will only want to use one of these
var isProfile = document.querySelector('#nav-tray-portal a[href="https://community.canvaslms.com/profile"]');
var isAdmin = document.querySelector('#nav-tray-portal a[href="https://community.canvaslms.com/accounts"]');
var isCourses = document.querySelector('#nav-tray-portal a[href="https://community.canvaslms.com/courses"]');
if (isProfile) {
console.log('Profile Tray');
}
if (isAdmin) {
console.log('Admin Tray');
}
if (isCourses) {
console.log('Courses Tray');
}
}

...there's little difference between what I can copy out of VSCode in full HTML formatting vs the Syntax highlighter.

289978_Screen Shot 2018-09-03 at 2.21.47 PM.png289951_Screen Shot 2018-09-03 at 2.19.29 PM.png

I'm also noticing that Jive is now recognizing URLs inside code blocks. That's almost as bad as censoring any word beginning with on.

The link parser in the code sample seems like a lazy/untested css selector to me.

PS - I'm being moderated so this post won't make much sense until the code I shared is released from the moderation queue.

Maybe your posts work different than mine, but from what I've tested, when my posts are waiting for moderation they aren't visible at all, to anyone but me.

With all that stated, the moderation feature should almost be a post evaluation not an approval to post, but I get why it's important.

James
Community Champion

I also thought of internationalization when I went with the href attribute. Supporting non-US English is always a bonus.

I think the mutations are live, meaning you could manipulate the items in there, but they should occur after they are in the DOM. If you read one place, it says they are generated when the DOM changes. However -- James' commentary --but because of the way JavaScript works, they would get placed on the stack and have to wait their turn to get executed.  That means that they're not delivered until later. They're not going to interrupt the running code in the middle of a block. You also get a bunch of mutations at a time, which would suggest that it was saving them up for something to happen, perhaps the current block to happen?

I did finally find a reasonably authoritative source while responding to this message. Maybe I should have responded to this post months ago before it was written?

The MDN page on MutationObserver provides a More in-depth discussion link (emphasis mine):

The key advantage to this new specification over the deprecated DOM Mutation Events spec is one of efficiency. If you are observing a node for changes, your callback will not be fired until the DOM has finished changing. When the callback is triggered, it is supplied a list of the changes to the DOM, which you can then loop through and choose to react to.

Of course, that authoritative source goes on to start the very next paragraph as

This also means that any code you write will need to process the observer results in order to react to the changes you are looking for.

That would certainly make people think they would need to iterate through the mutations to look for the one they want.

In many cases, what I was trying to accomplish was a triggered replacement for waitForKeyElements that used polling with setInterval() to wait for a specific time before something would appear. But I can still just check to see if the item appeared. My early mutation observers weren't like that. I went through looking for a specific mutation -- or often just that a mutation, any mutation, occurred because I knew the first mutation was the one that put the element I needed in there. The easiest way to check for the existence of an arbitrary element is document.querySelector().

The trick is to get the attach the mutation to the element closest to the element experiencing the change. Ideally, it would be the parent and you could do childList without subtree, but that's not always possible. In this case, we were never getting more than 3 mutations at a time so iterative checking may be quicker, but more complicated to code the check for. That means easier to fix if something changes in the future.

James
Community Champion

There are different areas in the Community and different coaches have the ability to see the posts. Kona isn't a moderator for Canvas Developers, so she can't see them. Even when I provided her with a direct link of the post that needed releasing, she still couldn't see it to release it. I think people like Scott, Renee, and Stefanie can see all of the groups.

I wonder if the sanitation happens upon creation. That would explain why old content shows but recent ones don't allow variants of on. I might have recently tried to edit an old blog post to make a change and I think it was gone there, but it had been there.

robotcars
Community Champion

Edit: Because moderated posts can't keep up with continuing to code... the disconnect question below here is irrelevant, but I'll keep the post here unedited for knowledge sake. I found it's not necessary in the snippet below, but when moving it into the rest of subacctray 'Do your thing' fires twice. You probably knew that...


Been playing with this for a bit and comparing to mine, it's probably entirely unnecessary to identify when the tray is closed, which I've been trying to handle.

Looking at... if(!el) {} and if (typeof observer !== 'undefined') {}

...and I'm just curious if you have found an instance where nav-tray-portal doesn't exist? I tested for this quite a bit, and then using your user script, I thought maybe I'd catch React dropping it in after page load, but that doesn't seem to happen either. If it's as dependable as document.body, do we need the extra lines? Is there a page I'm not thinking about where there is never a Global Navigation? How important is it that we check for this? I feel like Canvas gave us this element so we don't observe on document.body.

Does it ever disconnect()? Maybe it doesn't need to?

Excuse my altering the example, just trying to work with the same format it's going into, and someone recently told me they rarely ever use code examples verbatim in their own code.

This seems as dependable as your example, provided nav-tray-portal always exists and it doesn't disconnect()?

(function () {
    'use strict';

    var subacctray = {};
    subacctray.checkTray = function()  {
        var portal = document.getElementById('nav-tray-portal');
        if(portal) {
            var tray = new MutationObserver(subacctray.watchTray);
            tray.observe(portal, { 'childList': true, 'subtree': true });
        }
    };
    subacctray.watchTray = function() {
        var admin_tray = document.querySelector('#nav-tray-portal a[href="https://community.canvaslms.com/accounts"]');
        if (admin_tray) {
            console.log('Do your thing');
        }
    };
    subacctray.checkTray();
})();‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

I've been reading this Stack thread trying to figure out what's best, clean code - Is redundant condition checking against best practices? - Software Engineering Stack Ex... 
possibly resorting to 'A check too much won't hurt, one too less might.'

James
Community Champion

Once again, the message I was responding to disappeared before I could hit send. It's about the revised version you are working with. Here are my thoughts ... out of context.

nav-tray-portal isn't supplied with the HTML delivered, so I check to make sure it's there before doing anything with it. It may be overkill on my part, but it may also be a timing issue. Timing issues are difficult to check. It may always be there for me but not for someone else. Let me explain why it's probably not necessary but it's not a bad idea -- especially if you're trying to produce code that others can use as an example rather than code you can specifically use in your application.

If you are running a user script, you can control when the script runs and one of the options is at the start of the page load rather than after the document is ready. In that case, it wouldn't exist yet.

If you're running under global custom JavaScript,then some of the scripts are loaded in the header and are blocking so the page won't be ready until they load. There are additional scripts loaded at the bottom with a defer attribute so they can be loaded at any time after the page is ready. I haven't gone through the testing to see whether it's a script in the header or a script in the body that adds the nav-tray-portal.

I did disable the cache in Chrome developer tools and told it to throttle with a slow 3G connection. The nav-tray-portal appeared just before the document was ready and my script was called. It appears that it is always there in this case.

You can take it out and probably be safe in this application.

If you leave it in and the nav-tray-portal is already there, the mutation observer on document.body will never get added in the first place, it will just fall through to the code below it -- the mutation observer on the tray.

My code is also relying on nav-tray-portal never being destroyed. If nav-tray-portal wasn't reliably there, I might have to keep the one on document.body to look for nav-tray-portal.

If you are going to rely on nav-tray-portal existing, then the if() block in line 7 isn't necessary. If you say, "but it might not be there so I want to make sure so I don't get an error", then you understand the reason I did the check for existence of nav-tray-portal at the beginning and if it's not there added the mutation observer on document.body. Once I've passed that test, it's there. If it's a fluke case and for some reason it's not there when the code starts executing, then your code will never run. If you're going all-in and trust that it will always exist, then go all in. If your code is there to make sure that it isn't destroyed at some point, then you'll need to add the mutation observer to the document.body and not disconnect it. In watching what was happening, I can see that nav-tray-portal doesn't currently (could change next release) get destroyed, but it's much harder to see the timing when it's being created because it is almost instantaneous with the document being ready and the script kicking in.

As a side note, I would have rather written the code with ES6 stuff, it's just that Eclipse won't format it if I do and code formatting is important to me.

Beyond the purpose of the specific case and referring to your other goal -- to get badges by documenting mutation observers -- then I want to show how to nest mutation observers in case the thing you have to wait on one thing to appear before you can do something with it. I think I had to do this with the sort the find rubrics script.

robotcars
Community Champion

:smileyconfused: That's totally my fault, I released that post, and it was immediately approved, then I went to edit it and it went up for moderation. So I deleted it, and tried to make it post without moderation again. You're referring to #comment-125516

Not much else to say once you see that comment, because I retracted most of it. As I've continued to work with this, as global and as a user script I'm continuing as you've shown above. Especially since I'm trying to make this code usable as a user script or global script with the exception of the addCSS() function vs a .css file in global. Based on what you've said it's safer to keep it in tact.

Badges were a bit of a joke, because I rarely seem to spend entire weekends trying to solve the same problem, with the exception of mutations. I think you hit the stop button on that treadmill!

James
Community Champion

I figured the post would show up at some point. That has happened before when someone edits a post, but since I was actively using the computer, I didn't want to accidentally lose the comments because I clicked off the window or forgot about it. Thanks for linking to the comment so people can put it in context.

robotcars
Community Champion

The difference between my edited post and your reply is 12 minutes. Moderation isn't designed for this frequency. Should we slow down?

Off to make dinner anyway.

James
Community Champion

I think I was aware of this the double firing, but I just want to make sure we're on the same page.

Without moving things into the tray, the only thing I am getting "Do your thing" twice on was the Profile click.

290011_pastedImage_1.png

However, what you are describing sounds like the warning I gave when I posted the code that you would need to check for your stuff otherwise you'll be mutating the thing again. I would use an ID that you could use to check that your code had inserted something to avoid firing the mutation again.

It was part of the second paragraph of that post that got moderated and so I did the PasteBin, so you probably missed it because you had been looking at the PasteBin code. Also, I write so much that it's easy to lose something.

You'll want to add your logic to make sure that your code isn't the ones making the mutations. That could be as simple as another querySelector.

After the other discussions and testing, getElementById would probably be faster than using a querySelector().

awilliams
Community Champion

Hey  @James ‌ and carroll-ccsd‌. I may need to create a ticket for this. I haven't done a lot of code pasting here in Jive. Is the recognizing of URL's new behavior in your experience? By "recognizing" we are referring to the feature that makes a pasted URL a link that can be clicked correct? Can you provide any more details about this I could include in our ticket?

robotcars
Community Champion

Hi Adam,

Thanks for asking, this shouldn't happen. In the context of syntax highlighting, we probably don't care if it's an external link, clickable or not. While the added icon is distracting.

var xhr = function (url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", 'https://reqres.in' + url);
xhr."token operator" >= () => resolve(xhr.response);
xhr. = () => reject(xhr.status);
xhr.send();
})
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
James
Community Champion

Hey awilliams, good to talk again.

The behavior of adding links within code blocks (More >  Syntax Highlighter) is relatively recent, but it seems to be doing it to existing content so I can't show you a picture of the way it used to be. I was using it as a way to not automatically hyperlink links that people should not be clicking on. Jive has always been bad about marking things as links if you paste it into regular text (not a code block), even if you go through and unlink it.

The removal of anything "on" related is also new, but has been going on since soon after InstructureCon (possibly earlier). If you type on-line or on-click [without the dash] or any other words that begin with "on" it is stripped. It's extremely frustrating for an on-line learning management system when you can't use on-line the way most people do. Laura Gibbs was very frustrated by it.

Now we see that it's also doing that in code, though. I don't know if that happened at the same time the on-issue appeared or it happened when the linked codes happened, but it can render the code unusable. I cannot tell if it's a case of stripping it out completely when saving it or stripping it out when displaying it. If it is stripping it out when saving, then there's no way to recover it. In some places, updated as late as Aug 25, it's there: How do I submit an assignment?  I'm going to try typing the on-line without the hyphen here:::  ::: so if it is between the colons, maybe that issue has been fixed, but it was stripping code out as recently as 2 days ago on Sept 2 when Robert tried to use on click (no space) inside his code.

James
Community Champion

Adam, the link thing is within the last two weeks. I found where I was commenting to Sean Nufer about how he could use the Syntax Highlighter to keep it from mangling things. It worked then, but it's linked now.

https://community.canvaslms.com/community/instcon/instructurecon-2018/blog/2018/08/18/embed-magician... 

James
Community Champion

carroll-ccsd, was that a missing on-error I didn't see in your code?  Great example of the problem.

I'm just doing some testing here (hyphen, camelcase, lowercase) to see what gets stripped.

  • on-error
  •  

I think I noticed in direct messages that it didn't strip the on-stuff

robotcars
Community Champion

Yep! Line 5 and 6, on-load, on-error respectively. I missed that just showing an example of the external link.

robotcars
Community Champion

Adam,

I first noticed the ON issue on Aug 10, then Laura noticed it... https://community.canvaslms.com/people/laurakgibbs/blog/2018/08/27/test-blog-post-please-ignore 

robotcars
Community Champion

Yep. I ended up finding that too... all after I posted the snippet, waiting for moderation. I'm actually tinkering with the code now just trying to check out all your suggestions. Very helpful!

James
Community Champion

I read the thing on clean code and here are my "I'm not a professional programmer" thoughts.

I tend to error on the side of validating any input in a function that might be used in multiple places. Because I often hack additional functionality on to existing routines and need an extra parameter included for the new stuff but don't want to go through all the existing code that was already calling it, I check for missing parameters and set defaults. In much of my code, not necessarily even the stuff shared publicly, the first few lines are some code to make sure that necessary values are defined and returning right away if it doesn't have the proper information to function. Some people argue that there should only be one return in a function, but I tend to go returning early rather than deeply nesting if() statements. Having those checks at the top has saved me a few times because my code just won't run if I leave out something or put the wrong thing in there.

If you're DRYing a function and do a document.getElementById() and then do something with it, it's a good idea to check that it's there. In this case, you reasonably suspect that it's there or you would haven't gotten to that point so it may not be necessary. If you are guaranteed that something exists and is properly formed, then the if() statements get in the way of clean code.

If you don't want to check for the existence of stuff, you can throw an error and handle it that way, but must end-users don't want to see errors, they just want the program to work. To the programmer, it's useful to know why it didn't work so you can fix it. In some cases, not having an element there is fatal and you shouldn't do anything. In other cases, it's a minor inconvenience.

Ultimately, I decided I wasn't trying to go for any awards for writing code. I tend to be a perfectionist, so letting go of "my code is not worthy" was a big thing for me. I finally accepted that I was trying to produce things that work so that people can use them and that many were happy to have something, anything, as long as it worked.

Still, there's that side of me that wants it to work properly, not because I relied on something that may only work on my machine because it's slower than most and so the element is there before I try to act on it.

This is a benefit of being the sole programmer -- you don't have to agree upon a common coding style with anyone. I think that, in the long run, the benefits of code review and having someone to bounce ideas off of outweigh that small benefit of getting to do your own thing.

robotcars
Community Champion

Without getting overwhelmed by  @James  trove of detailed comments, I chose a few of the most important bits (from my view) and decided to update the gist for posted above. Along with the above features still being included.

I tackled the following issues:

  1. The most important to me, which started a week of assaults on mutations was an issue with attaching a mutation observer to an On-Click event. Which from what I could tell was adding observers every time the user clicked the tray. Making the tray respond slower and slower. Not sure if anyone else experienced this, I tend to open links from this menu in a new tab. James provided an extremely useful mutation example in #comment-125486, which I have ported into the code quite successfully. This also solved something I have growing concern for, an on-click event preventing keyboard users from activating the tray. I've tested a tab/enter to open the tray, with success.
    His mutation observer beats mine, elegantly, see it beginning at jj_ L169
  2. He introduced a query selector which allows for internationalization, using... 
    #nav-tray-portal a[href="https://community.canvaslms.com/accounts"]
    ...as a selector vs looking for something like an H2 and checking for Admin
  3. James also mentioned my parsing of Canvas ID's as integers, which I was first worried about changing, but the recursion functions didn't care, so I just removed parseInt, and everything worked as expected. I'd like to know if anyone experiences issues with that.

The SKIP DEPTH setting for the user script is on line 307 now.

I will probably continue tinkering with some changes and suggestions and release a new version later this week or early next week. Please test and provide feedback... it's really really useful.

 @James , thanks for your valuable contributions!

James
Community Champion

I'm glad it's coming together. I'm also glad you chose to fix the things that mattered as opposed to everything I ask. It's too easy for me to get distracted by shiny objects.

robotcars
Community Champion

Squirrel!!!

awilliams
Community Champion

James Jones wrote:

Now we see that it's also doing that in code, though. I don't know if that happened at the same time the on-issue appeared or it happened when the linked codes happened

Thanks guys. I was aware of the "on-line" issue and we've had some testing going on there. However, this issue with code blocks with links and apparently that word definitely seems new and that's exactly what I needed to know. I also appreciate the narrowing down of the timeframe and the information that the link behavior seems to be affecting past content. For the record, we are not aware of any past content being affected by the word issue so far. 

robotcars
Community Champion

Losing jQuery in subacctray.append()

jQuery

subacctray.append = function(html) {
     var appendToTray = $(subacctray.where)
     if(!appendToTray.length) return;
     // start fresh
     if ($('#adm-tray-subacctray')) { $('#adm-tray-subacctray').remove() }
     // append html to tray
     appendToTray.closest('li').after(
          $('<li>', {
               'id': 'adm-tray-subacctray',
               // dynamically grab the class set from the closest LI
               // for continuity and maybe future proof some Canvas updates
               'class': $(subacctray.where).closest('li').attr('class'),
               html: html
          })
     )
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

plain/vanilla JS

subacctray.append = (html) => {
     if(!document.querySelector(`${subacctray.where} a[href="/accounts"]`)) return;
     // start fresh
     let already = document.getElementById('adm-tray-subacctray');
     if(already) { already.remove(); }
     let tray_last_li = document.querySelector(`${subacctray.where} ul li:last-child`);
     let subacctray_li = document.createElement('li');
     subacctray_li.id = 'adm-tray-subacctray';
     // dynamically grab the class set from the closest LI
     // for continuity and maybe future proof some Canvas updates
     subacctray_li.className = tray_last_li.getAttribute('class');
     // append html to tray
     tray_last_li.parentNode
          .insertBefore(subacctray_li, tray_last_li.nextSibling)
          .insertAdjacentHTML('beforeend', html);
};‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
robotcars
Community Champion

I've updated the gist for the user script again with some final tweaks for this release. I have to start making time for some other projects. I also removed jQuery from the subacctray.append() function, which is posted on #comment-126215, which isn't visible yet.

I've also deployed the minified cdn version for testing in our production environment. I'll get feedback from our staff this week and then publish all files. Which seems to work nicely after updating with a quick shift+refresh.

awilliams
Community Champion

Hey again carroll-ccsd‌ and  @James ‌. I wanted to let you know that at this time I don't intend to file a support ticket for the hyperlinking of URL formatted text in syntax highlight blocks. We will continue to pursue the vanishing word of course.

My rationale is that we have a number of issues consuming the Lithium and Jive support folks, and ourselves tracking those issues. I don't feel the hyperlinking is a risk and is a marginal user experience issue currently. If things stabilize with the other issues or if you feel I have misjudged the impact of the issue I will reconsider.

James
Community Champion

awilliams

It may not be a security risk, but it was a way to include hyperlinks that you don't want to be active because the highlighting happened in the middle of URLs and made them ugly.  It definitely should not be happening inside of a code block. Furthermore, it is a very loose URL checker and highlights things that aren't valid URLs like the http://*.instructure.com links in the metadata of the user scripts.

Jive is really going downhill and becoming less useful. They shouldn't intentionally break things and then make people have to decide what functionality to ask back for so they use up all their support hours.

Disappointment is an understatement, but I get it. In the future, though, you may want to pick a nicer word than marginal -- like minimal impact or nuisance. No one likes to feel marginalized.

awilliams
Community Champion

I agree on all points. On the use of marginal, it was not intended to apply to any individuals. On a second read, I realize I phrased that poorly. When I said "marginal user experience issue" I was referring to the impact on user experience being marginal or "small, minute, tiny" and "in relating to or sitting at the edge of" the myriad of other issues we are currently experiencing. I definitely did not mean to imply any user is marginal Smiley Happy 

robotcars
Community Champion

Since this past weekend was the release cycle, I took advantage of the settings migration across Production, Test, Beta, and tested this on all versions and the user script. I updated all files in the repository, except the readme so far and added the new user script. I definitely recommend updating this version. The improved mutation observer thanks to  @James , along with the #nav-tray-portal instead of document.body greatly improves performance. While the recovery mode that caches API progress to continue after page reload, and the new loading indicator improve the user experience for users with many sub accounts. 

I already posted the updates and additions in #comment-125205#comment-125596

I'm making this post for the change log at the top, and to let followers see an updated post.

Here are the git commit comments...

ccsd-canvas@7788eabuse of #nav-tray-portal
caching the data in the event the user interrupts the build process
improved the loading indicator by adding a counter of the API pagination
addition of user script version
less jQuery #comment-126215
more  @James ‌ @ L164 & 179 admintray-subaccmenu.js

Because circle's of hell are a theme here, the improved loading indicator...

291398_admin-tray-sam-loading.png

Using or Updating Global Themes

  1. Read the README
  2. Download and Host, or if you are using the RawGit version it's already updated
  3. in Canvas, HOLD SHIFT+REFRESH, clearing the site cache, reloading new files
  4. Download and host the updated CSS file

User Script

There is now a user script version, full package of JavaScript and CSS, ready to use with Tampermonkey

ccsd-canvas/admintray-subaccmenu.user.js

Thanks for the contributions and feedback!

Boekenoogen
Community Contributor

Thanks for the update. I was not able to get it to work the first time, but with the update, it works great!

robotcars
Community Champion

Thanks John! I'm glad it's working for you now.

Did you have issues with the previous release or the most recent User Script beta?

I see your only other reply in this thread was the initial release.

I always welcome feedback, if you are having issues with something I post or share, please let me know.

canvas18
Community Participant

Robert,

Getting around to trying your userscript version of this this morning.

I've updated the include for our domains (at least I think I have...), but I get this and remain in the circle of hell....

https://lms.au.af.edu

https://lms.stag.af.edu

291591_Image 2.png

Thoughts? 

v/r 

 MJH