TedCart / Mackey JIRA Kanban formatter

// ==UserScript==
// @namespace    https://openuserjs.org/users/TedCart
// @name         Mackey JIRA Kanban formatter
// @copyright    2020-2021, TedCart (https://openuserjs.org/users/TedCart)
// @version      1.2
// @license      MIT
// @description  Reformat Jira for standup meetings
// @author       You
// @match        https://jira.mackeyllc.com/secure/RapidBoard.jspa*
// @grant        none
// @run-at       document-end
// ==/UserScript==

// ==OpenUserJS==
// @author       TedCart
// ==/OpenUserJS==

// link to source:
// https://openuserjs.org/scripts/TedCart/Mackey_JIRA_Kanban_formatter/source

(function () {
  'use strict';

  const addCustomStyleTag = () => {
    const newStyle = document.createElement("style");
    newStyle.setAttribute('type', 'text/css');
    newStyle.innerHTML = `
    /* DO NOT EDIT BELOW THIS POINT */
    /* BEGIN CUSTOM CSS */
body {
  margin-top: 0;
}

/* Increase main font-size */
.ghx-issue-fields .ghx-summary .ghx-inner {
  font-size: 1.8em;
  max-height: 10em;
}

/* Begin flattening header rows */
.ghx-controls-plan, .ghx-header-compact .ghx-controls-report, .ghx-controls-work {
  min-height: 0px;
  height: 2em;
}

.ghx-rapid-views #gh #ghx-work #ghx-pool-column #ghx-column-headers .ghx-column {
  padding: 0px 8px;
}

.ghx-swimlane-header .ghx-heading {
  margin: 2px 0;
}

.ghx-rapid-views #gh #ghx-work #ghx-pool-column .ghx-swimlane .ghx-swimlane-header {
  top: 25px;
}

/* End   flattening header rows */
.ghx-issue {
  padding: 2px 10px;
}

.ghx-issue .ghx-highlighted-fields,
.ghx-issue .ghx-card-footer {
  margin-top: 2px;
}

.ghx-summary {
  margin-top: -5px;
}

.ghx-header-compact #ghx-operations {
  padding-top: 0px;
}

.ghx-issue .ghx-extra-fields {
  margin-top: 2px;
}

.ghx-issue .ghx-key-link {
  font-size: 16px;
  margin: -5px 0px -6px;
}

/* Increase font-size in tooltip */
.aui-tooltip.tipsy {
  font-size: 1.5em;
  line-height: 1.6em;
}

/* Move user icons further up to the left */
.ghx-issue .ghx-avatar {
  position: absolute;
  right: 2px;
  /* 10px; */
  top: 4px;
  /* 10px; */
}

.ghx-issue.ghx-has-avatar .ghx-issue-fields,
.ghx-issue.ghx-has-corner .ghx-issue-fields {
  padding-right: 30px;
}

/* END CUSTOM CSS */
  `;
    document.head.appendChild(newStyle);
  };

  // import { addCustomModalStyleTag } from "./_styles.js"

  const modalBlockId = "draggable-modal-block";

  const collapsingArrowSvg
    = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" width="17px" height="17px">
      <g fill="none" fill-rule="evenodd">
        <path d="M3.29175 4.793c-.389.392-.389 1.027 0 1.419l2.939 2.965c.218.215.5.322.779.322s.556-.107.769-.322l2.93-2.955c.388-.392.388-1.027 0-1.419-.389-.392-1.018-.392-1.406 0l-2.298 2.317-2.307-2.327c-.194-.195-.449-.293-.703-.293-.255 0-.51.098-.703.293z" fill="#FFFFFF">
        </path>
      </g>
    </svg>`;

  function getMainModalElement () {
    return document.querySelector(`#${modalBlockId}`)
  }

  const maxListHeight
    = Math.max( Math.floor(window.innerHeight / 2)
              , 200
              );

  function createModalBlock () {

    addCustomModalStyleTag();
    const modalBlock = document.createElement('div');
    modalBlock.setAttribute('id', modalBlockId);
    // modalBlock.setAttribute('title', "Click and hold to drag...")

    document.querySelector('body').prepend(modalBlock);

    modalBlock.onmousedown = moveModalContainer;
    modalBlock.ondragstart = () => false;

    // This function came from https://javascript.info/mouse-drag-and-drop
    function moveModalContainer (event) {

      if (event.target.tagName === "INPUT") return
      if (event.target.tagName === "BUTTON") return
      if (event.target.tagName === "SPAN") return
      if (event.target.tagName === "SECTION") return
      if (event.target.tagName === "LABEL") return
      if (event.target.tagName === "SVG") return
      if (event.target.tagName === "G") return
      if (event.target.tagName === "PATH") return
      // if (event.target.tagName === "LI") return

      // const modalBlock = document.querySelector(`#${modalBlockId}`)
      // if (!modalBlock) return

      const top = document.querySelector('html').scrollTop;

      let shiftX = event.clientX - modalBlock.getBoundingClientRect().left;
      let shiftY = event.clientY - modalBlock.getBoundingClientRect().top;
      modalBlock.style.position = 'fixed';
      modalBlock.style.zIndex = 1000;
      document.body.append(modalBlock);

      moveAt(event.pageX, event.pageY);

      // moves the modalBlock at (pageX, pageY) coordinates
      // taking initial shifts into account
      function moveAt(pageX, pageY) {
        modalBlock.style.left = pageX - shiftX + 'px';
        modalBlock.style.top = (pageY - top) - shiftY + 'px';
      }

      function onMouseMove(event) {
        moveAt(event.pageX, event.pageY);
      }

      // move the modalBlock on mousemove
      document.addEventListener('mousemove', onMouseMove);

      // drop the modalBlock, remove unneeded handlers
      modalBlock.onmouseup = function() {
        document.removeEventListener('mousemove', onMouseMove);
        modalBlock.onmouseup = null;
      };

    }; // end moveModalContainer

    // return modalBlock
  }

  function createCollapsibleSectionContainer (sectionTitle, idPrefix) {
    const el = getMainModalElement();
    if (!el) return

    const firstAttempt = el.querySelector(`#${idPrefix}-filter-container`);
    if (firstAttempt) return firstAttempt

    function toggleSectionVisibility (ev) {
      const clickableDiv = ev.target.closest(`#${idPrefix}-filter-container > .section-header`);
      if (!clickableDiv) return;
      ev.preventDefault();
      const containerDiv = document.querySelector(`#${idPrefix}-filter-container`);
      const isOpen = /open/.test(containerDiv.className);

      if (isOpen) {
        containerDiv.classList.remove('open');
        localStorage.setItem(`${idPrefix}IsHidden`, 'true');
      } else {
        containerDiv.classList.add('open');
        localStorage.setItem(`${idPrefix}IsHidden`, 'false');
      }
    } // end toggleSectionVisibility

    // Putting this click listener on the document is (ironically) better for performance
    // https://gomakethings.com/detecting-click-events-on-svgs-with-vanilla-js-event-delegation/
    document.addEventListener('click', toggleSectionVisibility);

    const sectionContainerDiv = document.createElement('section');
    sectionContainerDiv.setAttribute('id',`${idPrefix}-filter-container`);
    if (!JSON.parse(localStorage.getItem(`${idPrefix}IsHidden`) || 'false')) {
      sectionContainerDiv.setAttribute('class','open');
    }

    const sectionHeaderDiv = document.createElement('section');
    sectionHeaderDiv.setAttribute('class','section-header');

    const toggleArrowDiv = document.createElement('section');
    toggleArrowDiv.setAttribute('class','toggle-arrow');
    toggleArrowDiv.innerHTML = collapsingArrowSvg;
    sectionHeaderDiv.append(toggleArrowDiv);

    const sectionHeaderText = document.createElement('span');
    sectionHeaderText.setAttribute('class','modal-section-header');
    sectionHeaderText.innerText = sectionTitle;
    sectionHeaderDiv.append(sectionHeaderText);

    sectionContainerDiv.append(sectionHeaderDiv);
    el.append(sectionContainerDiv);

    return sectionContainerDiv
  } // end createCollapsibleSectionContainer

  function addCustomModalStyleTag () {
    const newStyle = document.createElement("style");
    newStyle.setAttribute('type', 'text/css');
    // classToColor background, color, padding, margin
    newStyle.innerHTML = `
    svg { pointer-events: none; }
    .modal-button {
      text-align: center;
      margin:auto;
      display: block;
      min-width: 7em;
      border: 1px solid #777;
      padding: 3px 5px;
      border-radius: 5px;
    }
    .modal-section-header {
      display: inline-block;
      cursor: pointer;
      font-size: 1.2em;
      user-select: none;
    }
    .hide-button {
      margin-bottom:5px;
      /* background: #DDD; */
      /* color:#222; */
      border: 1px solid #777;
      border-radius: 2px;
      padding: 3px;
    }
    .live-button {
      margin-bottom:5px;
      font-weight: 800;
      font-size: 107%;
      border: 1px solid #777;
      border-radius: 3px;
      padding: 3px;
      color: #EEE;
      background: #2762a6;
    }
    .modal-input-list-button {
      background: #000;
      color: #DDD;
    }
    #draggable-modal-block {
      position: fixed;
      top: 40px;
      left: 10px;
      padding: 15px 5px;
      min-height: 20px;
      min-width: 20px;
      background: #333333BB;
      color: #DDD;
      z-index: 1000;
      border: solid transparent 1px;
      border-radius: 8px;
      transition: 500ms;
      opacity: .2;
    }
    #draggable-modal-block:hover {
      background: #333333BB;
      transition: 0ms;
      opacity: 1;
    }
    .deactivated-filters#draggable-modal-block button,
    .deactivated-filters#draggable-modal-block section {
      display: none;
    }
    .deactivated-filters#draggable-modal-block button:nth-child(1) {
      display: block;
    }
    .input-modal-checkbox {
      display:inline-block;
      margin-left: 25px;
    }

    .modal-input-list label {
      display: inline-block;
      margin: 0 0 0 8px;
      font-size: 1em;
      transition: 250ms;
      user-select: none;
    }
    .modal-input-list label:hover,
    .modal-input-list li:hover label {
      color: #FFF;
      transition: 50ms;
    }
    .modal-input-list li { margin: 0; }

    section.section-header {
      display: flex;
    }

    .toggle-arrow {
      display: flex;
      justify-content: space-around;
      align-items: center;
      height: 25px;
      width:  25px;
      border-radius: 2px;
      margin: 0;
      transition: 300 ms;
      cursor: pointer;
      /* transform: rotate(-90deg); */
      opacity: .8;
    }
    .toggle-arrow:hover {
      opacity: 1;
      background: black;
    }

    .toggle-arrow svg       { transform: rotate(-90deg); transition: 300ms; }
    .open .toggle-arrow svg { transform: rotate(0deg); }

    .modal-input-list {
      opacity: 0;
      display: none;
      visibility: hidden;
      transition: visibility 0s lineaer 0.1s, opacity 0.3s ease;
      padding: 0;
      margin: 0;
      max-height: ${maxListHeight}px;
      overflow-y: auto;
    }
    .open .modal-input-list {
      display: block;
      visibility: visible;
      opacity: 1;
      transition-delay: 0s;
    }
    .modal-input-list li {
      display: block;
    }
  `;
    document.head.appendChild(newStyle);
  }

  function createCheckboxWithLabel (options) {
    /*  options example:
         { id: "modal-checkbox-example-id"
         , label: "Example Checkbox"
         , checked: true }
    */
    options = options || {};
    const modalBlock = getMainModalElement();
    if (!modalBlock) return []

    let newCheckbox;
    if (options && options.id) newCheckbox = modalBlock.querySelector(`#${options.id}`);
    if (newCheckbox) {
      console.log("not creating duplicate input", options.id);
      return [] // don't create duplicates
    }
    newCheckbox = document.createElement('input');
    newCheckbox.setAttribute('type',`checkbox`);
    newCheckbox.setAttribute('class',`input-modal-checkbox`);
    newCheckbox.oninput = function(e) {
      this.setAttribute('value', newCheckbox.checked);
      this.blur();
    };

    for (const key in options) {
      if (key === 'label') continue
      if (key === 'checked') {
        newCheckbox.checked = !!options[key];
      } else {
        newCheckbox.setAttribute(key, options[key]);
      }
    } // end for loop

    const newLabel = document.createElement('label');
    newLabel.innerText = options.label;
    // newLabel.onclick = function () { newCheckbox.checked = !newCheckbox.checked  }
    newLabel.onclick = function () { newCheckbox.click();  };
    return [newCheckbox, newLabel]
  }

  function createNewCheckboxListItem (el, options) {
    const newListItem = document.createElement('li');
    const newCheckboxElements = createCheckboxWithLabel(options);
    if (newCheckboxElements.length > 0) {
      newCheckboxElements.forEach(el => newListItem.append(el));
      el.append(newListItem);
    }
  }

  function createSpan (options) {
    /*  options example:
         { id: "modal-checkbox-example-id"
         , label: "Example Checkbox"
         , checked: true }
    */
    options = options || {};
    const modalBlock = getMainModalElement();
    if (!modalBlock) return

    let newSpan;
    if (options && options.id) newSpan = modalBlock.querySelector(`#${options.id}`);
    if (newSpan) {
      console.log("not creating duplicate span", options.id);
      return // don't create duplicates
    }
    newSpan = document.createElement('span');

    for (const key in options) {
      newSpan.setAttribute(key, options[key]);
    } // end for loop
    return newSpan
  }

  function createNewCheckboxListItemWithCount (el, countOptions, checkboxOptions) {
    const newListItem = document.createElement('li');
    const newCheckboxElements = createCheckboxWithLabel(checkboxOptions);
    if (newCheckboxElements.length > 0) {
      const countElement = createSpan(countOptions);
      if (countElement) newCheckboxElements.unshift(countElement);
      newCheckboxElements.forEach(el => newListItem.append(el));
      el.append(newListItem);
    }
  }

  const selectors
    = { singlePost: '.ghx-avatar > img'
      // singlePost: '.ghx-columns > .ghx-column:nth-child(2) > div'
      , postContentSelector: 'table tr:nth-of-type(2) td:nth-of-type(2)'
      , parentNodeCount: 3
      };

  const noDisplayString = "display:none;";

  const elementSelector
    = { expandCollapseButtonName: '.ghx-swimlane .aui-button[role="button"]'
      , expandCollapseButtonParentNodeCount: 3
      , buttonContainerParentNodeCount: 1
      // I'm not sure which containers are completely re-rendered,
      // which would remove any MutationObserver I create
      // So I've opted to use the very highlevel selection of 'section#content'
      // which contains basically the whole page
      // , mainContainerForAllTickets:  '.ghx-work#ghx-work'
      // , mainContainerForAllTickets:  'section#content'
      , mainContainerForAllTickets:  'div#content'
      };

  const hideButtonAttributes
    = { specialId: "custom-hide-button"
      , hideLabel: "Disable Filters"
      , showLabel: "Enable Filters"
      , hideClass: "hide-button" // This option can invert the active/inactive
      , showClass: "live-button" // behavior compared to other buttons
      , localStorageBooleanLabel: "isHidingPosts"
      , singlePost: "" };

  const extraFieldsButtonAttributes
    = { specialId: "custom-hide-button-extra-fields"
      , hideLabel: "Show Extra Fields"
      , showLabel: "Hide Extra Fields"
      , localStorageBooleanLabel: "hideExtraFields"
      , singlePost: ".ghx-issue-content .ghx-extra-fields" };

  const epicButtonAttributes
    = { specialId: "custom-hide-button-epic"
      , hideLabel: "Show Epics"
      , showLabel: "Hide Epics"
      , localStorageBooleanLabel: "hideEpics"
      , singlePost: ".ghx-highlighted-fields" };

  const footerButtonAttributes
    = { specialId: "custom-hide-button-footer"
      , hideLabel: "Show Card Footers"
      , showLabel: "Hide Card Footers"
      , localStorageBooleanLabel: "hideFooters"
      , singlePost: ".ghx-card-footer > :not(.ghx-avatar)" };

  addCustomStyleTag();
  createModalBlock();
  hidePieces();
  const bod = document.querySelector('body');
  // bod.addEventListener("keydown", elementScrollJump)

  setTimeout(
    () => {
      // console.log('Running the setTimeout function!')
      putMutationObserverOnMainElement(bod, hidePieces);
      hidePieces();
      createButtonsToRemoveKanbanSections();
      createActiveDeveloperCheckboxes();
    }
  , 1500);

  // ===========================================================================
  // Begin support functions
  // Nothing invoked beneath this point
  // ===========================================================================

  function getHideStatus (string) {
    const rawJSON = localStorage.getItem(string || 'isHidingPosts') || 'false';
    return JSON.parse(rawJSON)
  }

  function getNameFromDevIcon (el) {
    const res
      = { visible: "NO_NAME"
        , id: "NO_ID" };
    if (el.dataset && el.dataset.tooltip) {
      res.visible = el.dataset.tooltip.split(':')[1].trim();
      res.id = res.visible.replace(/[^a-zA-Z]+/, '').toLowerCase();
    }
    return res
  }

  function getActiveDevelopers () {
    const rawJSON = localStorage.getItem('activeDevelopers') || '{}';
    const activeDevelopers = JSON.parse(rawJSON);

    const imgElements = document.querySelectorAll(selectors.singlePost);
    if (!imgElements) return activeDevelopers
    for (const key in activeDevelopers) activeDevelopers[key]['present'] = false;

    imgElements.forEach(el => {
      const nameObj = getNameFromDevIcon(el);
      activeDevelopers[nameObj.id] = activeDevelopers[nameObj.id] || {};
      const curDev = activeDevelopers[nameObj.id];
      curDev.label = nameObj.visible;
      curDev.active = curDev.active === undefined ? true : curDev.active;
      curDev.present = true;
    }); // end forEach imgElement

    return activeDevelopers
  }

  function hidePieces (buttonAttributes) {
    // each through all selectors collections
    const mainModalElement = getMainModalElement();
    const hideBadPosts = getHideStatus();
    hideBadPosts
      ? mainModalElement.classList.remove('deactivated-filters')
      : mainModalElement.classList.add('deactivated-filters');
    // console.log(`${hideBadPosts ? 'Hiding' : 'Revealing'} those posts...`)

    const collectionList
      = buttonAttributes
        ? [ buttonAttributes ]
        : [ hideButtonAttributes
          , extraFieldsButtonAttributes
          , epicButtonAttributes
          , footerButtonAttributes ];

    collectionList.forEach(buttonAttributesX => {
      createHideButton(buttonAttributesX);
      if (!buttonAttributesX.singlePost) return
      const postElements = document.querySelectorAll(buttonAttributesX.singlePost);
      if (!postElements) return

      postElements.forEach((el) => {
        let targetEl = el;
        const isHidden = getHideStatus(buttonAttributesX.localStorageBooleanLabel);
        if (isHidden && hideBadPosts) {
          targetEl.style = noDisplayString;
        } else {
          targetEl.style = "";
          // (targetEl.style || '').replace("display:none;", "")
        }
      }); // end postElements forEach

    }); // end forEach collectionList

    updateActiveDevelopers();
    const activeDevelopers = getActiveDevelopers();
    // console.log(JSON.stringify(activeDevelopers, null, 2));

    const cardElements = document.querySelectorAll(selectors.singlePost);
    if (!cardElements) return
    cardElements.forEach(el => {
      let targetEl = el;
      for (let i = 0; i < selectors.parentNodeCount; i++) {targetEl = targetEl.parentNode;}
      const nameObj = getNameFromDevIcon(el);

      const isHidden = !(nameObj && nameObj.id && activeDevelopers && activeDevelopers[nameObj.id] && activeDevelopers[nameObj.id]['active']);
      if (isHidden && hideBadPosts) {
        targetEl.style = noDisplayString;
      } else {
        targetEl.style = "";
        // (targetEl.style || '').replace("display:none;", "")
      }

    }); // end cardElements forEach


  } // end hidePieces function

  function createHideFunction(string) {
    return () => {
      localStorage.setItem((string || 'isHidingPosts'), JSON.stringify(!getHideStatus(string || 'isHidingPosts')));
      hidePieces();
    } // end callback function
  } // end createHideFunction

  function createHideButton (buttonAttributes) {
    buttonAttributes = buttonAttributes || hideButtonAttributes;
    const buttonContainerElement = getMainModalElement();
    if (!buttonContainerElement) {
      console.log('well shit');
      return
    }

    if (!buttonAttributes.specialId) return

    let hideButtonElement = buttonContainerElement.querySelector(`#${buttonAttributes.specialId}`);
    const buttonJustCreated = !hideButtonElement;
    if (!hideButtonElement) {
      console.log("creating hide button...");
      hideButtonElement     = document.createElement('button');
      hideButtonElement.id  = buttonAttributes.specialId;
      hideButtonElement.addEventListener('click', createHideFunction(buttonAttributes.localStorageBooleanLabel));
    }

    hideButtonElement.innerText
      = getHideStatus(buttonAttributes.localStorageBooleanLabel)
        ? buttonAttributes.hideLabel
        : buttonAttributes.showLabel;
    hideButtonElement.className
      = getHideStatus(buttonAttributes.localStorageBooleanLabel)
        ? `modal-button ${buttonAttributes.hideClass || 'hide-button'}`
        : `modal-button ${buttonAttributes.showClass || 'live-button'}`;

    if (buttonJustCreated) buttonContainerElement.append(hideButtonElement);

  } // end createHideButton


  function createButtonsToRemoveKanbanSections () {
    const buttonList
      = document.querySelectorAll(elementSelector.expandCollapseButtonName);

    if (buttonList.length === 0) {
      console.log("Unable to add buttons for Kanban section removal");
      return
    }

    buttonList.forEach(button => {
      let buttonContainerDiv = button;
      for (let i = 0; i < elementSelector.buttonContainerParentNodeCount; i++) {
        buttonContainerDiv = buttonContainerDiv.parentNode;
      }
      let newButton = buttonContainerDiv.querySelector(`#${hideButtonAttributes.specialId}`);
      if (!newButton) {
        const newButton = document.createElement('button');
        newButton.id          = hideButtonAttributes.specialId;
        newButton.innerHTML   = "Hide Section"; // hideButtonAttributes.hideLabel
        let swimlaneContainerDiv = button;
        for (let i = 0; i < elementSelector.expandCollapseButtonParentNodeCount; i++) {
          swimlaneContainerDiv = swimlaneContainerDiv.parentNode;
        }
        newButton.addEventListener('click', createSectionHiderFunction(swimlaneContainerDiv));
        buttonContainerDiv.appendChild(newButton);
      } // end if statement
    });
  }

  function createSectionHiderFunction (sectionDiv) {
    return () => {
      sectionDiv.style = "display: none;";
    }
  }

  function putMutationObserverOnMainElement (el, callback) {
    let mainElementTarget
      = document.querySelector(elementSelector.mainContainerForAllTickets);

    // configuration of the observer:
    const mainElementConfig
      = { attributes: true
        , childList: true
        , subtree: true
        };

    // create an observer instance
    const mainElementObserver
      = new MutationObserver((mutations) => {
          // ignore style attribute changes
          if (mutations.every(m => m.type === 'attributes' && m.attributeName === 'style')) return
          callback();
        }); // end MutationObserver

    // pass in the target node, as well as the observer options
    mainElementObserver.observe(mainElementTarget, mainElementConfig);
  } // end putMutationObserverOnMainElement


  function createActiveDeveloperCheckboxes () {
    const modalBlock = getMainModalElement();
    if (!modalBlock) return
    const activeDevContainerDiv = createCollapsibleSectionContainer("Active Developers", "active-dev");

    let inputListContainer = modalBlock.querySelector('.modal-input-list.developers');
    const inputListIsNew = !inputListContainer;
    if (!inputListContainer) {
      inputListContainer = document.createElement('ul');
    }
    inputListContainer.innerHTML = '';
    inputListContainer.setAttribute('class','modal-input-list developers');
    inputListContainer.onclick="event.stopPropagation()";
    inputListContainer.onmousedown="event.stopPropagation()";

    const activeDevelopers = getActiveDevelopers();
    const sortedKeys = Object.keys(activeDevelopers);
    sortedKeys.sort();
    sortedKeys.forEach(key => {
      // if (!activeDevelopers[key]['present']) return // to hide devs that are in localStorage but not on the current kanban board
      const checkboxOptions
        = { id: `modal-checkbox-${key}`
          , label: activeDevelopers[key]['label']
          , checked: !!activeDevelopers[key]['active'] };
      createNewCheckboxListItem(inputListContainer, checkboxOptions);
    }); // end for each

    if (inputListIsNew) {
      putMutationObserverOnInputList(inputListContainer, hidePieces);
      activeDevContainerDiv.append(inputListContainer);
    }
  } // end createDifficultyCheckboxes

  function putMutationObserverOnInputList (el, callback) {
    let inputEl = el;
    if (!inputEl) return

    // configuration of the observer:
    const inputElConfig
      = { attributes: true
        , characterData: true
        , subtree: true
        };

    // create an observer instance
    const mainElementObserver
      = new MutationObserver(mutations => {callback();}); // end MutationObserver

    // pass in the target node, as well as the observer options
    mainElementObserver.observe(inputEl, inputElConfig);
  } // end createActiveDeveloperCheckboxes

  function updateActiveDevelopers () {
    const listInputElements = document.querySelectorAll('.modal-input-list.developers input[type="checkbox"]');
    if (!listInputElements) return

    const activeDevelopers = getActiveDevelopers();

    listInputElements.forEach(el => {
      const key = el.id.replace("modal-checkbox-", '');
      if (activeDevelopers[key]) {
        activeDevelopers[key]['active'] = !!el.checked;
      }
    });

    localStorage.setItem('activeDevelopers', JSON.stringify(activeDevelopers));
  }

}());