title: Chrome Extension- Text Inputs and onhover events are not responsive on External Display
body: <p>I'm developing a Chrome Extension to quickly access JIRA tickets. The table with the text elements "randomly" become very slow to respond to input/page events such as <code>on hover</code>, <code>focus</code>, and <code>text input</code>, sometimes taking SEVERAL seconds at least to respond.</p>
  
  <p>I'd rather not release this without understanding what is going on and the scope of the impact. I really just want to understand what's going on. </p>
  
  <p>This ONLY happens when I'm on an external display (MacBook Pro 13-inch, 2017, 8GB through a USB-C hub). </p>
  
  <p>I've noticed:
  1) The same code on  more powerful machines work fine using the same peripherals
  2) Everything runs fine when I drag the browser window into my laptop display
  3) This happens seemingly randomly depending upon the input's position on the popup window. Changing the top and bottom margin size can often help either resolve the problem or move it to another row. </p>
  
  <p>I created a function to change the top and bottom margin size depending upon the number of elements but that only mitigates the issue and not fully resolves it.</p>
  
  <p>FWIW, It doesn't look like there's a terrible amount of memory usage and the system detects the inputs as it notices the key events. The text fields with issues do not have a larger CPU / Memory footprint than those that do not.</p>
  
  <p><strong>popup.html</strong></p>
  
  <pre><code>&lt;!DOCTYPE html&gt;
  &lt;html lang="en" dir="ltr"&gt;
    &lt;head&gt;
      &lt;link href="https://fonts.googleapis.com/css?family=IBM+Plex+Mono:400,400i|IBM+Plex+Sans+Condensed:400,400i|IBM+Plex+Sans:100,100i,400,400i,700,700i|IBM+Plex+Serif:400,400i" rel="stylesheet"&gt;
      &lt;link rel="stylesheet" href="popup-style.css"&gt;
      &lt;style type="text/css"&gt; &lt;/style&gt;
      &lt;meta charset="utf-8"&gt;
      &lt;title&gt;Quick Ticket For JIRA&lt;/title&gt;
    &lt;/head&gt;
    &lt;body&gt;
      &lt;div class="NewJIRAForm"&gt;
        &lt;div class="header"&gt;
          &lt;h1&gt;QUICK TICKET FOR JIRA&lt;/h1&gt;
          &lt;div class="QuickJIRAcontrols"&gt;
            &lt;input type="button" id='optionsButton' value="Settings" class="controlButtons"&gt;
            &lt;input type="button" id='toggleAddNewButton' value="Add New" class="controlButtons"&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="addNewForm" id="addNewForm"&gt;
          &lt;div class="addFirstRowForm"&gt;
            &lt;input type="text" name="newJIRABoardName" placeholder="Name of your New JIRA board" class="newJIRABoardNameInputField"  id="newJIRABoardName"&gt;
            &lt;input type="text" name="newJIRABoardKey" placeholder="Project Key" class="newJIRABoardKeyInputField" id="newJIRAProjectKey" &gt;
          &lt;/div&gt;
          &lt;div class="addSecondRowForm"&gt;
            &lt;input type="text" name="newJIRABaseURL" placeholder="Base URL of your JIRA board" class="newJIRABaseURLField" id='newJIRABaseURL'&gt;
            &lt;input type="button" id='addNewButton' value="Add"&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div id='CurrentQuickJIRAs' class="CurrentQuickJIRAs"&gt;
      &lt;/div&gt;
    &lt;/body&gt;
    &lt;script src="popup.js"&gt;&lt;/script&gt;
  &lt;/html&gt;
  </code></pre>
  
  <p><strong>popup.js</strong></p>
  
  <pre><code>document.addEventListener('DOMContentLoaded', documentEvents  , false);
  
  
  // using local array to avoice async issues;
  var localQuickJiraArray = [];
  var addNewQuickJIRAIsShown;
  
  var currentNewJIRABoardNameTextInput;
  var currentNewJIRABoardURLTextInput;
  var currentNewJIRABoardProjectKeyTextInput;
  
  
  
  function documentEvents() {
  
    document.getElementById('addNewButton').addEventListener('click',
      function() { addNewQuickJIRA(document.getElementById('newJIRABaseURL') , document.getElementById('newJIRABoardName'), document.getElementById('newJIRAProjectKey'));
    });
  
    document.getElementById('toggleAddNewButton').addEventListener('click',
      function() { toggleAddNewQuickJIRA();
    });
  
    document.getElementById('optionsButton').addEventListener('click',
      function() { chrome.tabs.create({'url': "/options.html" } );
    });
  
    document.getElementById("newJIRABoardName").addEventListener("keyup", saveNewFormData);
    document.getElementById("newJIRAProjectKey").addEventListener("keyup", saveNewFormData);
    document.getElementById("newJIRABaseURL").addEventListener("keyup", saveNewFormData);
  
    //sync local array of QuickJIRAs with Chrome Storage
  
    chrome.storage.sync.get(function(data) {
      if (Object.keys(data).length &gt; 0 &amp;&amp; data.QuickJIRAs ) {
        localQuickJiraArray = data.QuickJIRAs;
      }
      chrome.storage.sync.set(data, function() {
        drawCurrentQuickJIRATable();
        // logStorage();
      });
    });
  }
  
  function saveNewFormData() {
  
    currentNewJIRABoardNameTextInput = document.getElementById("newJIRABoardName").value;
    currentNewJIRABoardURLTextInput = document.getElementById("newJIRABaseURL").value;
    currentNewJIRABoardProjectKeyTextInput = document.getElementById("newJIRAProjectKey").value;
  
    if ( currentNewJIRABoardNameTextInput == "" &amp;&amp; currentNewJIRABoardURLTextInput == "" &amp;&amp; currentNewJIRABoardProjectKeyTextInput == "" ) {
      isDataToPersist = false;
    } else {
      isDataToPersist = true;
    }
  
    chrome.runtime.sendMessage({
      currentNewJIRABoardNameTextInput: currentNewJIRABoardNameTextInput,
      currentNewJIRABoardURLTextInput: currentNewJIRABoardURLTextInput,
      currentNewJIRABoardProjectKeyTextInput: currentNewJIRABoardProjectKeyTextInput,
      isDataToPersist: isDataToPersist
    },
      function(response) {
        if (response == "saved") {
        } else {
          throw new Error("Error Persisting data while adding Quick Ticket");
        }
  
      }
    );
  }
  
  function drawCurrentQuickJIRATable() {
  
    chrome.storage.sync.get(function(data){
      // logStorage();
      if (Object.keys(data).length&gt; 0 &amp;&amp; data.QuickJIRAs != undefined &amp;&amp; data.QuickJIRAs.length &gt; 0) {
  
        // Clear current jiras
        document.getElementById('CurrentQuickJIRAs').innerHTML = '' ;
  
        //Draw New Table
        //**could improve by just adding the last one and not clearing
        for (var i = 0; i &lt; data.QuickJIRAs.length; i++) {
          var JIRABoardID = data.QuickJIRAs[i].JIRABoardName;
          var JIRABoardBaseURL = data.QuickJIRAs[i].JIRABaseURL;
          document.getElementById('CurrentQuickJIRAs').innerHTML += '&lt;div class="quickJIRARow"&gt;' +
          '&lt;a class="goNewLink" href="' + data.QuickJIRAs[i].JIRABaseURL + "/" + data.QuickJIRAs[i].JIRAProjectKey + ' "&gt; ' + data.QuickJIRAs[i].JIRABoardName + '&lt;/a&gt;' +
          '&lt;input type="text" placeholder="Ticket Number" id="inputFieldBoardID_' + i + '" ' + ' class="goToInput"&gt;' +
          '&lt;div class="quickJIRARowButtons"&gt;' +
          '&lt;button type="button" id="goNewButton_' + i + '" ' + ' class="goNewButton"&gt;New Tab&lt;/button&gt;' +
          '&lt;button type="button" id="goCurrentButton_' + i + '" ' + ' class="goCurrentButton"&gt;Current Tab&lt;/button&gt;' +
          '&lt;button type="button" id="removeButton_' + i + '" ' + ' class="removeButton"&gt;Remove&lt;/button&gt;' +
          '&lt;/div&gt;'
          '&lt;/div&gt;' ;
        }
  
        //add click function to link;
        var jiraBoardLinks = document.getElementsByTagName("a");
        for (var i = 0; i &lt; jiraBoardLinks.length; i++) {
            (function () {
                var link = jiraBoardLinks[i];
                var url = link.href;
                link.onclick = function () {
                    chrome.tabs.create({active: true, url: url});
                };
            })();
        }
  
        // Set Go New Button Click Event
        var goButtonArray = document.getElementsByClassName('goNewButton');
        for (var x = 0; x &lt; goButtonArray.length; x++) {
          var elementID = "goNewButton_" + String(x);
          document.getElementById(elementID).addEventListener('click', function () {
            var newTab = true;
            navigateToQuickJIRA(this, newTab);
          });
        }
  
        // Set Go Current Button Click Event
        var goButtonArray = document.getElementsByClassName('goCurrentButton');
        for (var x = 0; x &lt; goButtonArray.length; x++) {
          var elementID = "goCurrentButton_" + String(x);
          document.getElementById(elementID).addEventListener('click', function () {
            var newTab = false;
            navigateToQuickJIRA(this, newTab);
          });
        }
  
        // Set Go Remove Button Click Event
        var goButtonArray = document.getElementsByClassName('removeButton');
        for (var x = 0; x &lt; goButtonArray.length; x++) {
          var elementID = "removeButton_" + String(x);
          document.getElementById(elementID).addEventListener('click', function () {
            removeJIRA(this);
          });
        }
  
        // Set Go To JIRA in current Tab on Return and
        var goToInputArray = document.getElementsByClassName('goToInput');
        for (var x = 0; x &lt; goToInputArray.length; x++) {
          var elementID = "inputFieldBoardID_" + String(x);
          document.getElementById(elementID).addEventListener("keyup", function (event) {
            if (event.key === "Enter") {
              var newTab = false;
              navigateToQuickJIRA(this, newTab);
              this.value = "";
            }
          });
        }
  
        //To-Do: Add Options to Keep Add Window open
        toggleAddNewQuickJIRA(false);
      } else {
        document.getElementById('CurrentQuickJIRAs').innerHTML = '' ;
        toggleAddNewQuickJIRA(true);
      }
      checkIfExistingAddNewFields()
    });
  
  }
  
  function handleInputFromGoToInput(textfield) {
  
  }
  
  //Check to see if user is mid adding a new JIRA
  function checkIfExistingAddNewFields() {
    // Send message to background to get current input values if user was mid input and closed the popup
    chrome.runtime.sendMessage({
      checkingForData : true
    },
      function(response) {
        if (response.currentNewJIRABoardNameTextInput != undefined &amp;&amp; response.currentNewJIRABoardURLTextInput != undefined &amp;&amp; response.currentNewJIRABoardProjectKeyTextInput != undefined) {
          document.getElementById("newJIRABoardName").value = response.currentNewJIRABoardNameTextInput;
          document.getElementById("newJIRABaseURL").value = response.currentNewJIRABoardURLTextInput;
          document.getElementById("newJIRAProjectKey").value = response.currentNewJIRABoardProjectKeyTextInput;
          toggleAddNewQuickJIRA(true);
        }
      }
    );
  }
  
  
  function navigateToQuickJIRA (button, newTab) {
    // Get the value of button Id to use to map to coresponding textfield
    var idValue = String(button.id).split("_").pop();
    var goToInputId = "inputFieldBoardID_" + idValue;
  
    var goToInputIdValue = document.getElementById(goToInputId).value;
    //Get base URL value from Storage
    // ** Change to local Array
    var projectKey = localQuickJiraArray[idValue].JIRAProjectKey;
  
    var goToInputIdValue= validateTicketNumberInput(goToInputIdValue, projectKey);
  
    chrome.storage.sync.get(function(data){
        if(chrome.runtime.lastError) {
            console.error(chrome.runtime.lastError);
        } else {
            console.log(data);
            var jiraBoardID = data.QuickJIRAs[idValue].JIRABaseURL;
            var jiraURL = jiraBoardID + "/"+ goToInputIdValue;
            //Need function to avoid race-condition, could probably use callback in the future
            goToInputIdValue.value = "";
            gotoNewURL(jiraURL, newTab);
        }
    });
  }
  
  function validateTicketNumberInput(goToInputIdValue, projectKey){
    if (goToInputIdValue.length &gt; projectKey.length) {
      var inputToCheckForProjectKey = goToInputIdValue.substr(0, projectKey.length+1);
    }
  
    //Check if input is just numbers - if yes, add the project key to the front and dash - it's valid
    //Check if input is contains valid project key in the right location with a dash - if yes - it's valid
    //else - not a valid input - throw error
  
    if (/^\d+$/.test(goToInputIdValue)) {
      var sanitizedInput = projectKey + "-" + goToInputIdValue;
      return sanitizedInput;
    } else if (inputToCheckForProjectKey !== 'undefined' &amp;&amp; inputToCheckForProjectKey == projectKey + "-") {
      return goToInputIdValue;
    } else {
      alert("It looks like your ticket number isn't valid for this Project. Please enter a valid ticket number.");
      throw new Error("Invald Ticket Number Entered");
    }
  }
  
  function gotoNewURL(jiraURL, newTab) {
  
    if (newTab == false) {
      chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
        var tab = tabs[0];
        chrome.tabs.update(tab.id, { url : jiraURL });
      });
    } else {
      chrome.tabs.create({ url : jiraURL });
    }
  
  }
  
  function removeJIRA(button) {
  
    var doesWantToDelete = confirm("Do you really want to Delete that link?");
  
    if (doesWantToDelete == true ) {
      var idValue = String(button.id).split("_").pop();
  
      //Get base URL value from Storage
      chrome.storage.sync.get(function(data){
          if(chrome.runtime.lastError) {
              console.error(chrome.runtime.lastError);
          } else {
              data.QuickJIRAs.splice(idValue, 1);
              localQuickJiraArray.splice(idValue, 1);
          }
          chrome.storage.sync.set(data, function() {
            //Race condition - use callback in the future
            drawCurrentQuickJIRATable();
          });
      });
    }
  
  }
  
  function addNewQuickJIRA(baseURLInput , boardNameInput, projectKeyInput) {
  
    if (baseURLInput.value == "" || boardNameInput.value == "" || projectKeyInput.value == "") {
      alert("Looks like some fields are missing. Please fill in all fields before adding a new Quick JIRA");
      throw new Error("missing fields upon Quick JIRA submission");
    }
  
    var baseURL = baseURLInput.value.replace(/\/$/, '');
    baseURL = validateBaseURL(baseURL);
    var boardName = validateBoardName(boardNameInput.value.trim());
    var projectKey = validateProjectKey(projectKeyInput.value.trim());
    var doesExist = checkIfInputDataDoesExist(baseURL, boardName, projectKey);
  
    if (doesExist == false) {
      chrome.storage.sync.get(function(data) {
        var objectToAdd = {'JIRABaseURL' : baseURL , 'JIRABoardName' : boardName , 'JIRAProjectKey' : projectKey};
        if (Object.keys(data).length &gt; 0 &amp;&amp; data.QuickJIRAs ) {
          data.QuickJIRAs.push(objectToAdd);
          localQuickJiraArray.push(objectToAdd);
  
        } else {
          data.QuickJIRAs = [objectToAdd];
          localQuickJiraArray = [objectToAdd];
        }
  
        baseURLInput.value = ""
        boardNameInput.value = ""
        projectKeyInput.value = ""
        saveNewFormData();
        chrome.storage.sync.set(data, function() {
          drawCurrentQuickJIRATable();
          // logStorage();
        });
      });
    }
  
  }
  
  function checkIfInputDataDoesExist(baseURL, boardName, projectKey) {
  
    if (localQuickJiraArray.length != 0) {
      //check for existing baseURL and board name; set doesExist to true if one does
      //Removed checking for Base URL's since it's possible there are multiple projects on the same URLs
  
      for (var i = 0; i &lt; localQuickJiraArray.length; i++) {
        var existingJiraBoardID = localQuickJiraArray[i].JIRABoardName;
        var existingJiraBoardBaseURL = localQuickJiraArray[i].JIRABaseURL;
        var existingJiraProjectKey = localQuickJiraArray[i].JIRAProjectKey;
        if (boardName == existingJiraBoardID) {
          alert("Looks like you already a QuickJIRA with that Name. \n \n Please try adding a your Quick JIRA with a new name");
          return Boolean(true);
        } else if (projectKey == existingJiraProjectKey) {
          alert("Looks like you already a QuickJIRA with that Project Key. \n \n Please try adding a your Quick JIRA with a newJIRAProjectKey");
          return Boolean(true);
        }else {
          return Boolean(false);
        }
      }
    } else {
      return Boolean(false);
    }
  
  }
  
  function validateBaseURL(baseURLInput) {
    var regexQuery = "^(https?://)?(www\\.)?([-a-z0-9]{1,63}\\.)*?[a-z0-9][-a-z0-9]{0,61}[a-z0-9]\\.[a-z]{2,6}(/[-\\w@\\+\\.~#\\?&amp;/=%]*)?$";
    var regexTest = new RegExp(regexQuery, 'i');
    var isValidURL = regexTest.test(baseURLInput);
  
    if (isValidURL == true) {
      var arrayOfMatches = baseURLInput.match(/.*\/(.*)$/);
      if (arrayOfMatches.length &lt; 2 || arrayOfMatches[1].toLowerCase() != "browse") {
        var confirmURL = confirm("Are you sure you want to Add this URL? \n \n Quick Ticket works best when the last part of the URL is `/browse` !");
        if (confirmURL == true) {
          return baseURLInput;
        } else {
          throw new Error("User did not want to add this URL");
        }
      } else {
        return baseURLInput;
      }
  
    } else {
      alert("That doesn't look like valid JIRA URL. \n \n Please enter a valid URL \n \n URLs should not contain Project Keys");
      throw new Error("invalid JIRA entered");
    }
  
  }
  
  function validateBoardName(boardNameInput) {
    if (boardNameInput.length &lt; 50) {
      var regexQuery = "^[A-Za-z0-9 _]*[A-Za-z0-9][A-Za-z0-9 _]*$";
      var regexTest = new RegExp(regexQuery, 'i');
      var isValidBoardNameInput = regexTest.test(boardNameInput);
  
      if (isValidBoardNameInput == true) {
        return boardNameInput;
      } else {
        alert("That doesn't look like valid Board Name. \n \n Project Keys may only contain letters and numbers");
        throw new Error("invalid Board Name Add Attempt");
      }
    } else {
      alert("That doesn't look like valid Board Name. \n \n Please enter a Board Name less than 50 characters");
      throw new Error("invalid Board Name Add Attempt");
    }
  
  }
  
  function validateProjectKey(projectKeyInput) {
    if (projectKeyInput.length &lt; 50) {
      var regexQuery = "^[A-Z][A-Z_0-9]+";
      var regexTest = new RegExp(regexQuery);
      var isValidProjectKeyInput = regexTest.test(projectKeyInput);
      if (isValidProjectKeyInput == true) {
        return projectKeyInput;
      } else {
        alert("That doesn't look like valid Project Key. \n \n Project Keys may only contain Uppercase letters, numbers, and '_' ");
        throw new Error("invalid Project Key Add Attempt");
      }
    } else {
      alert("That doesn't look like valid Project Key. \n \n Please enter a Project Key less than 50 characters");
      throw new Error("invalid Project Key Add Attempt");
    }
  
  }
  
  function toggleAddNewQuickJIRA(shouldDisplay) {
    var newFormDiv = document.getElementById("addNewForm");
    var newFormButton = document.getElementById("toggleAddNewButton");
    if (shouldDisplay == false) {
      newFormDiv.style.display = "none";
      addNewQuickJIRAIsShown = false;
      newFormButton.value="Add New"
    } else if (shouldDisplay == true) {
      newFormDiv.style.display = "block";
      addNewQuickJIRAIsShown = true;
      newFormButton.value="Hide"
    } else {
      if (newFormDiv.style.display == "block") {
        newFormDiv.style.display = "none";
        addNewQuickJIRAIsShown = false;
        newFormButton.value="Add New"
      } else {
        newFormDiv.style.display = "block";
        addNewQuickJIRAIsShown = true;
        newFormButton.value="Hide"
      }
    }
  }
  
  
  // Log Storage for debugging
  function logStorage() {
      if(chrome.storage) {
          chrome.storage.local.get(function(data){
              console.log("chrome.storage.local:");
              if(chrome.runtime.lastError) {
                  console.error(chrome.runtime.lastError);
              } else {
                  console.log(data);
              }
              chrome.storage.sync.get(function(data){
                  console.log("chrome.storage.sync:");
                  if(chrome.runtime.lastError) {
                      console.error(chrome.runtime.lastError);
                  } else {
                      console.log(data);
                  }
              });
          });
      } else {
          console.warn("chrome.storage is not accessible, check permissions");
      }
  }
  </code></pre>
  
  <p><strong>popup-css</strong></p>
  
  <pre><code>h1 {
    margin: 0px;
    font-size: 12pt;
  }
  
  body {
    width: 600px;
    margin: 0px;
    z-index: 1;
  }
  
  .header {
    display: inline-block;
    min-width: 570px;
  }
  
  .header h1{
    margin: 0;
    display: inline-block;
    float: left;
  }
  
  .header .QuickJIRAcontrols {
    float: right;
    width: 275px;
    margin-right: -5px;
    text-align: center;
  }
  
  #toggleAddNewButton, #optionsButton {
    display: inline-block;
    margin: 0;
    padding: 0;
    height: 19px;
    width: 130px;
  }
  #optionsButton {
    float: left;
    color: black;
  }
  
  #optionsButton:hover {
    background-color: #7B7B7B;
    color: #FFFFFF;
  }
  
  #toggleAddNewButton {
    float: right;
  }
  
  .addNewForm {
    display: block;
  }
  .NewJIRAForm {
    min-width: 100%;
    background-color: #D8D8D8;
    padding: 15px;
    padding-top: 20px;
  }
  
  .NewJIRAForm input[type=text] {
    width: 375px;
  }
  
  .NewJIRAForm input[type=button] {
    padding: 0px;
    width: 170px;
    margin-left: 15px;
    color: #7ED321;
  }
  
  .NewJIRAForm input[type=button]:hover {
    color: #FFFFFF;
    background-color: #7ED321;
  }
  
  input[type=button]:focus, button:focus, input[type=text]:focus {
    outline: none;
  }
  #newJIRAProjectKey {
    padding-left: 10px;
    width: 160px;
    margin-left: 15px;
  }
  
  input[type=button], button {
    height: 38px;
    border-radius: 6px;
    border-style: none;
    font-size: 12px;
    font-weight: bold;
  }
  
  .newJIRABoardNameInputField, .newJIRABaseURLField, .newJIRABoardKeyInputField {
    height: 36px;
    border-radius: 6px;
    border-style: none;
    padding-left: 10px;
  }
  
  .goToInput {
    height: 36px;
    border-radius: 6px;
    padding-left: 10px;
    width: 130px;
    border-style: solid;
    margin-right: 15px;
  }
  
  .addFirstRowForm, .addSecondRowForm {
    margin-top: 15px;
  }
  
  .goNewButton, .goCurrentButton {
    margin-right: 15px;
    background-color: #7ED321;
    color: white;
    width: 90px;
  }
  
  .goNewButton:hover, .goCurrentButton:hover {
    background-color: #417505;
  }
  
  .removeButton {
    background-color: #D0021B;
    color: white;
    width: 65px;
  }
  
  .removeButton:hover {
    background-color: #961424;
  }
  
  .removeButton:focus, .goNewButton:focus, .goCurrentButton:focus {
    outline: 0;
  }
  
  .quickJIRARow {
    margin-left: 15px;
    margin-top: 7.5px;
    z-index: 2;
    display: inline-block;
  }
  
  .CurrentQuickJIRAs .quickJIRARow:last-child {
    margin-bottom: 7.5px;
  }
  
  .quickJIRARowButtons {
    display: inline-block;
  }
  
  .CurrentQuickJIRAs {
    max-height:255px;
    overflow-y:scroll;
  }
  
  .quickJIRARow a {
    text-decoration: none;
    color: #000;
    text-align: right;
    display: inline-block;
    white-space: nowrap;
    width: 130px;
    margin-right: 10px;
    font-weight: bold;
    font-size: 12px;
    z-index: 2;
    overflow: hidden;
    text-overflow: ellipsis;
    vertical-align: middle;
    position: relative;
  }
  
  .quickJIRARow a:hover {
    color: blue;
  }
  </code></pre>
  
  <p>Link to repo of (current issue reproable in master): <a href="https://github.com/bzellman/quickJira" rel="nofollow noreferrer">https://github.com/bzellman/quickJira</a></p>