nharward / Pivotal Story ID Name Prefix

// ==UserScript==
// @name Pivotal Story ID Name Prefix
// @namespace https://openuserjs.org/meta/nharward/pivotal
// @description Adds the story ID in front of the story name like you'd expect
// @author Nathaniel Harward
// @include https://www.pivotaltracker.com/*
// @copyright 2019, Nathaniel Harward
// @license MIT; https://opensource.org/licenses/MIT
// @version 1.0.1
// ==/UserScript==
// ==OpenUserJS==
// @author nharward
// ==/OpenUserJS==

var STORY_ID_PREFIX_CLASS = 'story_id_prefix';

var getAncestorAttributeValue = function (node, attributeName) {
  var attributeValue = null;
  var ancestor = node;
  while (ancestor && !ancestor.hasAttribute(attributeName)) {
    ancestor = ancestor.parentNode;
  }
  if (ancestor) {
    attributeValue = ancestor.getAttribute(attributeName);
  }
  return attributeValue;
};

var hasStoryIDPrefix = function (node) {
  var firstChild = node.firstChild;
  return firstChild &&
    firstChild.nodeType == Node.ELEMENT_NODE &&
    firstChild.nodeName == 'SPAN' &&
    firstChild.getAttribute('class') == STORY_ID_PREFIX_CLASS;
};

var pivotalStoryNameNode = function (node) {
  return node.nodeType == Node.ELEMENT_NODE &&
    node.nodeName == 'SPAN' &&
    node.getAttribute('class') == 'story_name';
};

var pivotalEpicNameNode = function (node) {
  return node.nodeType == Node.ELEMENT_NODE &&
    node.nodeName == 'SPAN' &&
    node.getAttribute('class') == 'tracker_markup' &&
    node.parentNode &&
    node.parentNode.className == 'name epic_name';
};

var addIDToName = function (node) {
  var storyID = getAncestorAttributeValue(node, 'data-id');
  if (storyID) {
    var spanElement = document.createElement('span');
    spanElement.setAttribute('class', STORY_ID_PREFIX_CLASS);
    spanElement.appendChild(document.createTextNode(storyID + ' - '));
    node.insertBefore(spanElement, node.firstChild);
  }
};

var handleNode = function (node) {
  if ((pivotalStoryNameNode(node) || pivotalEpicNameNode(node)) && !hasStoryIDPrefix(node)) {
    addIDToName(node);
  }
  if (node.hasChildNodes()) {
    node.childNodes.forEach(handleNode);
  }
};

var mutationCallback = function (mutationsList, observer) {
  mutationsList.forEach((mutationRecord) => {
    if (mutationRecord.addedNodes) {
      mutationRecord.addedNodes.forEach(handleNode);
    }
  });
}

new MutationObserver(mutationCallback).observe(document.body, {
  childList: true,
  subtree: true
});