cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

Set Default Due Time(s) using a Script

fisher1
Community Contributor
4 3 332

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!

3 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

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.