Word Count Feature in SpeedGrader (Feedback and Testing Support Requested)

JamesSekcienski
Community Coach
Community Coach

Hello everyone,

This is my first time learning about MutationObservers and I saw there was a feature request for a word count in SpeedGrader.  So for a weekend project, I decided to give it an initial attempt.  I know the code can be cleaned up more, but this is an initial prototype.  Please let me know your thoughts about the code and if you notice any errors.

Since I didn't find a way currently to access the iFrame content when a document preview is loaded, I would especially appreciate any feedback if someone knows a way to get access.  Unfortunately, the preview is loaded from a CanvasDocs domain so my code isn't permitted to access that.

Current Design:

  • There is a textarea added to the bottom of the right side panel in SpeedGrader.  When text is input into it, it will show a word count below it.
    • This can be used to copy and paste text from document previews into it without needing to download the document.
    • Current limitations:
      • I tested with a Word document preview, but I'm not sure how accurate it is with a PDF since someone mentioned they had issues with word counts in PDF documents before.
      • I have it subtract 4 if it detects the keywords "Page" and "of" at the beginning to remove the words for "Page 1 of 4" if you do a Ctrl + a in the document preview to copy the text.  Unfortunately, there isn't anything built in to remove text from page headers or footers so that would contribute to the word count.  *If you remove the words from the textarea the word count will update.
  • I also added a MutationObserver to add event listeners to the iFrame when it loads from an internal domain.  This allows me to add word counts directly in text submission previews and discussion boards.
    • The text submission preview currently will show the word count at the bottom and continues to update when switching to past submissions and when switching the text view.
    • The discussion board preview currently will show the word count below each discussion board post.  However, if you switch to the full discussion board view, it currently doesn't show the word counts anymore
  • *I have only completed some initial testing using Google Chrome so far

Here is the JavaScript code I used:

 

 

$(document).ready(function() {
  if (/^\/courses\/[0-9]+\/gradebook\/speed_grader/.test(window.location.pathname)) {
    addWordCountToRightSideOfSpeedGrader();

    let internalFrameHolder = document.getElementById("iframe_holder");

    const internalFrameHolderObserver = new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        if(mutation.addedNodes) {
          mutation.addedNodes.forEach(node => {
            if(node.nodeName === "IFRAME") {
              node.addEventListener("load", addWordCountToIFrameContent);
              node.addEventListener("change", addWordCountToIFrameContent);
            }
          });
        }
      });
    });
  
    internalFrameHolderObserver.observe(internalFrameHolder, {
      childList: true
    });
  }
});

 

 

 

 

 

function countWordsInTextArea() {
  let textareaElem = document.getElementById("word-count-textarea");
  let resultElem = document.getElementById("word-count-total");
  let count = 0;
  let words = (textareaElem.value.split("\n").join(" ").split(" ")).filter(word => word != ""); 
  if(words.length > 4 && words[0] == "Page" && words[2] == "of") { 
    count = words.length - 4; 
  } 
  else { 
    count =  words.length; 
  }

  resultElem.innerHTML = count;
}

 

 

 

 

 

function addWordCountToRightSideOfSpeedGrader() {
  const wordCountDiv = document.createElement("div");
  wordCountDiv.id = "word-count-div";
  wordCountDiv.classList.add("content_box");

  const wordCountHeading = document.createElement("h2");
  const wordCountHeadingText = document.createTextNode("Word Count Input");
  wordCountHeading.appendChild(wordCountHeadingText);
  wordCountDiv.appendChild(wordCountHeading);

  const wordCountTextArea = document.createElement("textarea");
  wordCountTextArea.id = "word-count-textarea";
  wordCountTextArea.placeholder = "Add text here to count the words"
  wordCountTextArea.style.overflowY = "hidden";
  wordCountTextArea.style.height = "5rem";
  wordCountTextArea.style.width = "95%";
  wordCountDiv.appendChild(wordCountTextArea);

  const wordCountResults = document.createElement("p");
  const wordCountText = document.createTextNode("Word Count: ");
  wordCountResults.appendChild(wordCountText);
  const wordCountTotal = document.createElement("span");
  wordCountTotal.id = "word-count-total";
  const wordCountValue = document.createTextNode("0");
  wordCountTotal.appendChild(wordCountValue);
  wordCountResults.appendChild(wordCountTotal);
  wordCountDiv.appendChild(wordCountResults);

  document.getElementById("rightside_inner").appendChild(wordCountDiv);
  
  wordCountTextArea.addEventListener("input", countWordsInTextArea);
}

 

 

 

 

 

function addWordCountToIFrameContent() {
  let internalFrame = document.getElementById("speedgrader_iframe");
  if (internalFrame && /\/courses\/[0-9]+\/assignments\/[0-9]+\/submissions/.test(internalFrame.src)) {
    let posts = internalFrame.contentWindow.document.getElementsByClassName("message_html");
    if (posts.length > 0) {
      let postsArray = Array.from(posts);
      postsArray = postsArray.map(post => post.defaultValue);
      postsArray = postsArray.map(post => post.replace(/<.+?>/g, ""));
      postsArray = postsArray.map(post => post.replaceAll("&nbsp;", " "));
      postsArray = postsArray.map(post => post.replaceAll("\n", " ").trim());
      postsArray = postsArray.map(post => post.split(" "));
      postsArray = postsArray.map(post => post.filter(word => word != "" && word != " "));
      postsArray = postsArray.map(post => post.length);
      
      for (let i = 0; i < postsArray.length; i++) {
        let count = postsArray[i];
        if (count > 0) {
          let countDiv = document.createElement("div");
          countDiv.style.borderStyle = "solid";
          let countPost = document.createTextNode("Word Count: " + count);
          countDiv.appendChild(countPost);

          posts[i].parentNode.appendChild(countDiv);
        }
      }
    }
    else {
      let textSubmission = internalFrame.contentWindow.document.getElementById("submission_preview");
      if (textSubmission) {
        let text = textSubmission.innerHTML;
        text = text.replace(/<span class="screenreader-only".+?<\/span>/g, "");
        text = text.replace(/<.+?>/g, "");
        text = text.replaceAll("&nbsp;", " ");
        text = text.replaceAll("\n", " ").trim();
        
        let count = text.split(" ").filter(word => word != "" && word != " ").length;
        let countDiv = document.createElement("div");
        countDiv.style.borderStyle = "solid";
        let countPost = document.createTextNode("Word Count: " + count);
        countDiv.appendChild(countPost);
        textSubmission.parentNode.appendChild(countDiv);
      }
    }
  }
}