Set Default Due Time(s) using a Script

fisher1
Community Contributor
13
3680

Hello, fellow Canvas people,

 

I've successfully developed a user script that allows you to set a default due date time in the Canvas assignment due date input field. Essentially, it adds a button right beneath the input field where you use a different "date picker" menu and it adds the time you set within the script.

If you are a Canvas Admin, you can add this code to your Javascript file for your institution so that it will work for all of your teachers.

Screenshot_5.png

 

Here's how to use it:

Step 1: Install Tampermonkey Chrome Extension or Install Firefox Chrome Extension

 

Step 2: Click this URL to install the "default time" script with Tampermonkey: https://github.com/creanlutheran/canvas-scripts/raw/main/set-default-due-time-v1.0.user.js (code is updated to include suggestions for improvement by James Jones -- thank you, @James !)

 

Step 3: After installing the script, modify the script to choose the due date time(s) you want! (screenshots below)

  • Click the Tampermonkey Chrome extension button in the toolbar and select "Dashboard", which shows you all of your installed Tampermonkey scripts.
  • Click the pencil icon on the right side of the script you want to modify. At the top of the code, you should see an array of times that you can edit to change the default times.
  • Click File and then click Save. 

Screenshot_1.pngScreenshot_2.pngScreenshot_3.png

Screenshot_4.png

 

Step 4: Test it out!

Go to a Canvas assignment, quiz, or discussion edit page, and the buttons should appear right underneath the normal due date input field. Refresh the page if you don't see it appear.

 

Disclaimer: I am a novice programmer and would appreciate any suggestions/improvements to make this better!

13 Comments
James
Community Champion

Ben ( @fisher1  )

Thanks for this contribution.

It's scary putting your code out there (at least it was the first time I did it). Since you asked for feedback about the script, here are some things that popped out at me with the set-4 code (I just skimmed the code, I didn't install and test it).

You define dateContainer at the top and then check do a check if it's defined. That's the only two places that constant is used, which makes it unnecessary. You hardcoded the content of dateContainer everywhere. I think you mean to go through and use the constant, but then never did.

There is a lot of copied code here with just a few changes. That makes it a prime candidate for two functions and something that is iterable. The code would be a lot shorter and easier for people to add additional times. An acronym in programming is DRY (don't repeat yourself).

With template literals, you can greatly simplify your code. Instead of saying 'date_custom_picker1', you can use the backtick `date_custom_picker${i}` and instead of value="'+dueTime12hr_1'" you can put value="${dueTime12hr[i]}".

If you created a function for it and passed in the index, starting time, and display time, then it would be even cleaner.

I would consider including the dayjs() library to allow for parsing and display of dates. Then people could set their own formats and you could specify the times in an array, which would then be the iterable you would need to loop over.

Assuming that you have loaded dayjs as dayjs, then this code could simplify a lot of things.

/**
 * defaultTimes is an array of 24 hour times in HH:MM format
 * timeFormat is the default format for displaying times
 *   see https://day.js.org/docs/en/display/format for more info
 */
const defaultTimes = ['07:55', '09:50', '11:20', '13:15'];
const timeFormat = 'h:mma';

const today = dayjs().format('YYYY-MM-DD');
const displayTimes = defaultTimes.map(t =>
  dayjs(`${today} ${t}`).format(timeFormat)
);

After that, you now have a defaultTimes that is the 24 hour format and displayTimes that is the 12 hour (or whatever the user specifies) format.

Then you could have a loop that iterates over the array and adds your code that you added to a function. Javascript loops start with an index of 0, so if you want your items to start with 1, you'll need to adjust accordingly.

I'm not going to comment on the code itself other than to say you might want to add /edit to the end of the paths on the include lines so that to avoid running it unnecessarily and I personally try to avoid setTimeout() in preference to the event-driven mutationObserver.

fisher1
Community Contributor
Author

Hello, @James ,

 

Thank you so much for your helpful feedback! I implemented all of it except for the mutationObserver (couldn't figure out how to incorporate it, even after searching online, which shows how much of a noob I am).

If you are willing and able, I would greatly appreciate your eyes on the updated script and see if it works on your end.

James
Community Champion

Ben ( @fisher1 )

MutationObservers take a bit of time to understand. When I was teaching myself JavaScript a few years ago for the Canvancements I write, I didn't have years of legacy code to go off of and the Mozilla Developer Network (MDN) recommended using them as opposed to setTimeOut. Since I didn't already know how to use setTimeOut and they deemed it bad, I set out to learn MutationObservers.

All of my testing is done on the assignments page. You would need to test this on the other types of pages as well.

It looks like there is a div#ui-datepicker-div element that lives off the body. It is not there in the HTML that is delivered, so it is added by Canvas. Initially (before you click on any dates), it's empty. When you click on the calendar to open the datepicker, it adds some styling, of which display:block; is the key. When you close it, it goes to display:none;

Complicating things a little is that the div is reused for all of the datepickers, rather than being destroyed and reused. That means that just watching the body for added nodes (childList:true) will allow you to check for the first creation, but it doesn't tell you when it's closed or reopened.

You could use subtree:true, but that would trigger on every change to anywhere in the body -- definitely not what you want as that generates a lot of unnecessary traffic. A better approach is to wait until the div is available and then watch the div itself. That's two mutation observers -- one on the body to find the div and then one on the div to see when it opens and closes.

Similarly, using childList on the div itself will tell you when it's opened, but never when it's closed. I haven't looked at your code enough to tell if you need to do anything when it closes, but if you don't, this might be the simplest way as it fires only once per open. It is a little misleading, but it may not matter. If you click to open the date picker and then click it again without closing it, it registers that it was closed but not that it was reopened. If the content inside isn't being changed, it wouldn't matter.

If you need to know when the date picker is closed, then watching the attributes would catch it being opened and closed. Except there are a couple of handfuls of mutations generated for opening and again for closing. Knowing when the last one is fired becomes an interesting task. You can look for 2 mutations for an open and 3 for a close. 

A safer way is to check for the presence of your code and if it's already there, then do your stuff.

Here's a function that will watch the body for the ui-datepicker-div element to appear and then add a watch to it listening for changes to the children (catches opens but not closes).

  waitForDatePicker();

  function waitForDatePicker(mutations, observer) {
    if (typeof mutations === 'undefined') {
      const obs = new MutationObserver(waitForDatePicker);
      obs.observe(document.body, {childList:true});
    }
    else {
      const el = document.getElementById('ui-datepicker-div');
      if (el) {
        observer.disconnect();
        const obs = new MutationObserver(watchDatePicker);
        obs.observe(el, {childList:true});
      }
    }
  }

 

If you want to watch for style changes, then modify that last observe to be

        obs.observe(el, {attributeFilter : ['style']});

 

Then you have a watchDatePicker() function that executes when the date picker element is modified.

Here's an example block for monitoring the style changes that allows for both opens and closes.

  function watchDatePicker(mutations) {
    const el = document.getElementById('ui-datepicker-div');
    if (mutations.length === 3 && el.style.display === 'none') {
      console.log('DatePicker Closed');
    }
    else if (mutations.length ===2 && el.style.display === 'block') {
      console.log('DatePicker Open');
    }
  }

 

If you go the childList way, then you don't actually care about the mutations or observer (you don't care about observer anyway) and you could shorten it up. Again, this is just for an event that fires when opening the date picker.

  function watchDatePicker() {
    const el = document.getElementById('ui-datepicker-div');
    if (el.style.display === 'block') {
      console.log('DatePicker Open');
    }
  }

 

I've got a console.log() in there just for debugging purposes. You would call the function that handles your code.

Ron_Bowman
Community Champion

@fisher1 

I like the script and use.  Did you ever consider adding that functionality for the until date/time and the available from date?  I guess with those you would have to do a 2x2 output instead of 4 across

sfrantz
Community Explorer

Awesome! Thanks so much for writing this! For those of us who are institutions that have a different Canvas address, could you add these lines to your script? -- Thanks!

// @include https://canvas.*.edu/courses/*/assignments/*/edit
// @include https://canvas.*.edu/courses/*/assignments/new
// @include https://canvas.*.edu/courses/*/assignments/new?quiz_lti
// @include https://canvas.*.edu/courses/*/assignments/*/edit?quiz_lti
// @include https:/canvas.*.edu/courses/*/discussion_topics/new
// @include https://canvas.*.edu/courses/*/discussion_topics/new
// @include https://canvas.*.edu/courses/*/discussion_topics/*/edit
// @include https://canvas.*.edu/courses/*/quizzes/*/edit

Ron_Bowman
Community Champion

@sfrantz -

You can do that yourself in tampermonkey.

Open tampermonkey and got to the dashboard.

Click on the script name and it will come up in an editor.  Add whatever you want to the script and then save it.

The script will then run with your additions(you will have to reload the page).

Edit: I just added the script to tampermonkey, and it looks like you will need to reinstall the script because @fisher1 has put those includes into the script.

Ron

sfrantz
Community Explorer

Yes, I know I can do it and have. It would just be a nice thing to have in this downloadable script so that others don't have to make the change.

fisher1
Community Contributor
Author

@sfrantz 

Thank you for the tip! I've adjusted the script to include the https://canvas.*.edu.com @include URLs.

sfrantz
Community Explorer

Thanks, Ben!  Here's my blog post featuring your script: http://suefrantz.com/2022/04/14/canvas-tampermonkey-script-for-setting-default-due-time/

I also alerted the OIT at UC-Boulder of your script. We'll see if they add it here: https://oit.colorado.edu/services/teaching-learning-applications/canvas/enhancements-integrations/en...s

Ron_Bowman
Community Champion

@sfrantz 

Thanks for the post of your blog.  I think I have come across your blog previously for some other reason.  

By the way, I looked at your blog post for canvas enhancements where you mentioned UC-Boulders page(which I know about).  I know you mention that there are other enhancements from James' post, but I think you really should put his as a link in your post as well.  

 

sfrantz
Community Explorer

Done! "Many of these scripts have been written by James Jones, a Canvas master (visit his GitHub page to access all of his scripts)."

I also gave James kudos in this post and this one. I'm a fan!

Ron_Bowman
Community Champion

@fisher1 -

Jeff I was just using your script for a quiz to check something out, and I noticed an interesting feature/bug(I do not know which).  If I select a time and date, and then if I decide to change to a different time and date, when I click the new time and date the field disappears completely and I have to go through it again.

A better description may be:

1) click on a time and select a date. 
2) Realize that either time or date is incorrect. 
3) click on the new time and date
4) the old date disappears from the field. 
5) I have to select the new time and date again to make it show up.

Is that the way you designed the script, or is this an unexpected behavior

BrianLester
Community Contributor

I tried installing this with tamper monkey. It ran correctly once. On subsequent attempts the buttons don't appear...