diff --git a/content/modules/parsing.js b/content/modules/parsing.js index 8caff99..d1603c2 100644 --- a/content/modules/parsing.js +++ b/content/modules/parsing.js @@ -1,120 +1,194 @@ -// Parsing functions for tickets, tables, and other elements - +/** + * Parsing functions for tickets, tables, and other elements + * + * This module contains functions for parsing and enhancing various TDX (TeamDynamix) UI elements: + * + * Functions: + * - updateHeading(mutation): Updates report/heading elements with highlighting and embeds + * - Applies queue-based highlighting to headings + * - Handles JSON embed data to replace headings with iframes + * + * - parseTicket(mutation): Main function for parsing ticket/task pages + * - Enhances calendar inputs with modern datetime-local inputs and quick date modifiers + * - Updates browser tab title with ticket title + * - Adds queue tag to ticket header with color coding + * - Handles button links for updates and tasks + * - Adjusts feed colors for dark mode compatibility + * + * - parseOtherElements(): Handles miscellaneous UI enhancements + * - Injects styles into shadow DOM elements (search bars, close buttons) + * - Applies custom desktop layout configurations + * - Enables sticky column positioning + * + * - parseTable(element): Parses table structures and extracts data + * - Extracts table headers and row data + * - Creates structured item objects for each row + * - Calls parseItem() for each row to apply enhancements + * + * - parseItem(item): Applies enhancements and styling to individual table row items + * - Colors queue names with configured colors + * - Converts dates to relative time format (e.g., "3 hours ago") + * - Formats detailed dates (due dates, start dates) with calendar format + * - Highlights modified dates based on age + * - Detects and highlights user replies + * - Highlights internal user modifications + * - Applies person highlighting + * - Applies title highlighting + * - Configures links to open in new tabs + */ + +/** + * Updates heading elements with highlighting and embed functionality + * @param {HTMLElement} mutation - The DOM element containing headings to update + */ function updateHeading(mutation) { + // Find all heading elements in the mutation let headings = mutation.querySelectorAll(".tdx-control-bar__title") for (const heading of headings) { - //color the header if queue name is found - let headingTxt = heading.innerText || heading.textContent - - handleHighlight("report", headingTxt, heading) + // Get heading text and apply queue-based highlighting + let headingText = heading.innerText || heading.textContent + handleHighlight("report", headingText, heading) - //try embedding - if (headingTxt.startsWith("{")) { + // Check if heading contains JSON embed data (starts with "{") + // If so, replace the heading with an iframe embed + if (headingText.startsWith("{")) { try { - let embedData = JSON.parse(headingTxt) - - let title = document.createElement("h4") - title.classList = heading.classList - title.innerText = embedData.name + let embedData = JSON.parse(headingText) + + // Create new title element with embed name + let titleElement = document.createElement("h4") + titleElement.classList = heading.classList + titleElement.innerText = embedData.name + + // Find the module body and create iframe embed let moduleBody = heading.parentNode.parentNode.parentNode.querySelector(".tdx-widget") - let embed = document.createElement("iframe") - embed.classList = "customEmbed" - embed.src = embedData.url - heading.replaceWith(title) - moduleBody.replaceWith(embed) + let embedIframe = document.createElement("iframe") + embedIframe.classList = "customEmbed" + embedIframe.src = embedData.url + + // Replace heading with title and module body with iframe + heading.replaceWith(titleElement) + moduleBody.replaceWith(embedIframe) } catch { + // Silently fail if JSON parsing fails } } } } +/** + * Main function for parsing and enhancing ticket/task pages + * @param {HTMLElement|null} mutation - Optional DOM element to parse (if null, parses entire document) + */ function parseTicket(mutation = null) { - let qBlock = document.querySelector("#ctlAttribute2285") - //ticket or task + // Find queue block element (contains queue name) + let queueBlock = document.querySelector("#ctlAttribute2285") + + // Find ticket or task header element let header = document.getElementById("thTicket_spnTitle") || document.querySelector("#upTaskHeader>div>h1") - //find calendar inputs when editing tickets/tasks - let calendars = document.querySelectorAll("#txtStartDate, #txtEndDate") //tasks for now, will add more as found - if (calendars && calendars.length > 0) { + // Find calendar inputs when editing tickets/tasks + // Currently handles start and end dates for tasks + let calendarInputs = document.querySelectorAll("#txtStartDate, #txtEndDate") + if (calendarInputs && calendarInputs.length > 0) { let originalFormat = "M/D/YYYY h:mm A"; let newFormat = "YYYY-MM-DDTHH:mm"; - [...calendars].forEach(calendar => { - let date = window.moment(calendar.value, originalFormat) - - let newCal = document.createElement("input") - newCal.id = `${calendar.id}-new` - newCal.classList = calendar.classList - newCal.type = "datetime-local" - - //date modifier shortcuts - let calBox = document.createElement("div") - calBox.classList = ["calBox"] - let calTxt = document.createElement("span") - if (date && date.isValid()) { - calTxt.textContent = date.format('dddd') + + [...calendarInputs].forEach(calendarInput => { + // Parse existing date value + let dateMoment = window.moment(calendarInput.value, originalFormat) + + // Create new modern datetime-local input + let newCalendarInput = document.createElement("input") + newCalendarInput.id = `${calendarInput.id}-new` + newCalendarInput.classList = calendarInput.classList + newCalendarInput.type = "datetime-local" + + // Create container for date modifier buttons + let calendarButtonContainer = document.createElement("div") + calendarButtonContainer.classList = ["calBox"] + + // Create text element to display day of week and relative time + let calendarText = document.createElement("span") + if (dateMoment && dateMoment.isValid()) { + calendarText.textContent = dateMoment.format('dddd') } + /** + * Updates both the old TDX calendar input and new datetime-local input + * Also updates the calendar text display + */ function updateDate() { - if (!date || !date.isValid()) { + if (!dateMoment || !dateMoment.isValid()) { return } - //update old tdx calendar - let parsedFormat = date.format(originalFormat) - calendar.value = parsedFormat - - //update new calendar - let iso = date.format(newFormat) - newCal.value = iso - if (calTxt) { - calTxt.textContent = date.calendar() + + // Update old TDX calendar input with original format + let parsedFormat = dateMoment.format(originalFormat) + calendarInput.value = parsedFormat + + // Update new datetime-local calendar input with ISO format + let isoFormat = dateMoment.format(newFormat) + newCalendarInput.value = isoFormat + + // Update calendar text with relative time (e.g., "Today at 2:30 PM") + if (calendarText) { + calendarText.textContent = dateMoment.calendar() } } - //convert to original format - newCal.addEventListener("input", event => { + // Listen for changes to the new datetime-local input + // Convert back to original format when user changes the date + newCalendarInput.addEventListener("input", event => { let parsedValue = window.moment(event.target.value, newFormat) if (parsedValue && parsedValue.isValid()) { - date = parsedValue + dateMoment = parsedValue updateDate() } }) - //modify date - function btnModify(btn) { - if (!date || !date.isValid()) { + /** + * Modifies the date based on button configuration + * Handles business day logic (skips weekends, adjusts for after-hours) + * @param {Object} buttonConfig - Button configuration with 'add' or 'set' properties + */ + function modifyDate(buttonConfig) { + if (!dateMoment || !dateMoment.isValid()) { return } - if ('add' in btn) { - date.add(btn.add) - } else if ('set' in btn) { - date.set(btn.set) + // Apply date modification (add time or set specific time) + if ('add' in buttonConfig) { + dateMoment.add(buttonConfig.add) + } else if ('set' in buttonConfig) { + dateMoment.set(buttonConfig.set) } - //check business days - let day = date.isoWeekday() - let hour = date.hour() - let min = date.minute() + // Check if date falls on weekend or after business hours + let dayOfWeek = dateMoment.isoWeekday() // 1=Monday, 7=Sunday + let hour = dateMoment.hour() + let minute = dateMoment.minute() - //if Friday after 5pm - if (day >= 5 && (hour > 17 || (hour === 17 && min > 0))) { - date.set({ + // If Friday after 5pm, move to next Monday at 8am + if (dayOfWeek >= 5 && (hour > 17 || (hour === 17 && minute > 0))) { + dateMoment.set({ h: 8, - d: date.date() + 7, + d: dateMoment.date() + 7, m: 0 }) - } else if (day > 5) { - //if sat/sun - date.set({ - d: date.date() + 7 + } else if (dayOfWeek > 5) { + // If Saturday or Sunday, move to next Monday + dateMoment.set({ + d: dateMoment.date() + 7 }) } updateDate() } - let buttons = [ + // Define quick date modifier buttons + let dateModifierButtons = [ { name: "+1 day", add: { days: 1 } }, { name: "+1 week", add: { weeks: 1 } }, { name: "+1 month", add: { months: 1 } }, @@ -122,28 +196,31 @@ function parseTicket(mutation = null) { { name: "5PM", set: { h: 17, m: 0 } } ] - for (const btn of buttons) { - let button = document.createElement("button") - button.classList = "btn btn-calendar" - button.innerText = btn.name - button.addEventListener("click", (event) => { + // Create and attach date modifier buttons + for (const buttonConfig of dateModifierButtons) { + let buttonElement = document.createElement("button") + buttonElement.classList = "btn btn-calendar" + buttonElement.innerText = buttonConfig.name + buttonElement.addEventListener("click", (event) => { event.preventDefault() - btnModify(btn) + modifyDate(buttonConfig) }) - calBox.appendChild(button) + calendarButtonContainer.appendChild(buttonElement) } + // Initialize the date display updateDate() - //old input still needs to exist for proper format conversion - calendar.style.display = "none" - calendar.parentElement.append(newCal) - calendar.parentElement.append(calTxt) - calendar.parentElement.append(calBox) + // Hide old calendar input (still needed for format conversion) + // Append new elements to the parent + calendarInput.style.display = "none" + calendarInput.parentElement.append(newCalendarInput) + calendarInput.parentElement.append(calendarText) + calendarInput.parentElement.append(calendarButtonContainer) }); } - //change tab title + // Update browser tab title with ticket title if (header) { let ticketTitle = header.childNodes[0].textContent document.title = ticketTitle @@ -151,55 +228,58 @@ function parseTicket(mutation = null) { return } - //add queue tag to ticket title - if (qBlock && header && !document.querySelector(".qBox")) { - let qElement = qBlock.querySelector("span.wrap-text") - if (!qElement) { + // Add queue tag to ticket title with color coding + // Only add if queue block exists, header exists, and queue box doesn't already exist + if (queueBlock && header && !document.querySelector(".qBox")) { + let queueElement = queueBlock.querySelector("span.wrap-text") + if (!queueElement) { return } - let qTxt = (qElement.innerText || qElement.textContent || "").trim() + let queueText = (queueElement.innerText || queueElement.textContent || "").trim() - if (!qTxt) { + if (!queueText) { return } - let newBox = document.createElement("span") - newBox.textContent = qTxt - newBox.classList.add("qBox") - - let qTxtLower = qTxt.toLowerCase() - for (const qK of Object.keys(colorsByQueue)) { - let q = colorsByQueue[qK] - if (qTxtLower === qK) { - newBox.style.color = q.txt - newBox.style.backgroundColor = q.bg + // Create queue tag element + let queueTagBox = document.createElement("span") + queueTagBox.textContent = queueText + queueTagBox.classList.add("qBox") + + // Apply color coding based on queue name + let queueTextLower = queueText.toLowerCase() + for (const queueKey of Object.keys(colorsByQueue)) { + let queueColorConfig = colorsByQueue[queueKey] + if (queueTextLower === queueKey) { + queueTagBox.style.color = queueColorConfig.txt + queueTagBox.style.backgroundColor = queueColorConfig.bg break } } - header.appendChild(newBox) + header.appendChild(queueTagBox) } - //handle button links - updates/tasks for now - //update buttons + // Handle button links - configure to open in new tabs + // Update ticket buttons [...document.querySelectorAll("#btnUpdateTicket, #divUpdateFromActions")].forEach(button => { handleLink("Update", button) }); - //task button + // Add task button [...document.querySelectorAll("#liAddTicketTask>a")].forEach(button => { handleLink("TicketTaskNew", button) }); - //task template button + // Add task template button [...document.querySelectorAll("#divAddTaskTemplate>a")].forEach(button => { handleLink("TicketAddTaskTemplate", button) }); - //color the feed if darkmode + // Adjust feed colors for dark mode compatibility + // Find feed items either from mutation or document var feedItems - if (mutation) { feedItems = mutation } else { @@ -208,90 +288,95 @@ function parseTicket(mutation = null) { if (feedItems) { for (const feedItem of feedItems) { + // Find all elements with inline color styles [...feedItem.querySelectorAll("*[style*='color']")].forEach(feedElement => { - let color = window.tinycolor(feedElement.style.color) - let brightness = color.getBrightness() - - //if color is too dark - if (brightness < 100 && colorScheme == "darkMode") { - var newColor - var isGrey = true - let rgb = color.toRgb() - - if (rgb.r == rgb.b && rgb.r == rgb.g) { - isGrey = true - } else { - isGrey = false - } - - //if color is shade of grey - if (isGrey) { - let flippedColor = window.tinycolor(invertHex(color.spin(180).toHex())) - newColor = flippedColor + let elementColor = window.tinycolor(feedElement.style.color) + let colorBrightness = elementColor.getBrightness() + + // If color is too dark for dark mode, lighten it + if (colorBrightness < 100 && colorScheme == "darkMode") { + var adjustedColor + let rgb = elementColor.toRgb() + + // Check if color is a shade of grey (all RGB values are equal) + let isGreyColor = (rgb.r == rgb.b && rgb.r == rgb.g) + + // Handle grey colors differently - invert and spin hue + if (isGreyColor) { + let invertedColor = window.tinycolor(invertHex(elementColor.spin(180).toHex())) + adjustedColor = invertedColor } else { - let threshold = 50 - let diff = (threshold - brightness) + brightness - newColor = color.brighten(diff) + // For colored text, brighten it + let brightnessThreshold = 50 + let brightnessDifference = (brightnessThreshold - colorBrightness) + colorBrightness + adjustedColor = elementColor.brighten(brightnessDifference) } - feedElement.style.color = newColor.toHexString() + feedElement.style.color = adjustedColor.toHexString() } }); } } } +/** + * Handles miscellaneous UI enhancements for various TDX elements + * - Injects styles into shadow DOM elements + * - Applies custom desktop layouts + * - Enables sticky column positioning + */ function parseOtherElements() { - //inject styles into search bar + // Inject styles into search bar shadow DOM elements [...document.querySelectorAll("tdx-search-bar, .js_sidePanelX")].forEach(searchBar => { - let shadow = searchBar.shadowRoot - if (shadow) { - injectOtherStyles(shadow); - - //check nested shadows - [...shadow.querySelectorAll("tdx-close-x")].forEach(nested => { - let nestedShadow = nested.shadowRoot - if (nestedShadow) { - injectOtherStyles(nestedShadow) + let shadowRoot = searchBar.shadowRoot + if (shadowRoot) { + injectOtherStyles(shadowRoot); + + // Check for nested shadow DOM elements (e.g., close buttons) + [...shadowRoot.querySelectorAll("tdx-close-x")].forEach(nestedElement => { + let nestedShadowRoot = nestedElement.shadowRoot + if (nestedShadowRoot) { + injectOtherStyles(nestedShadowRoot) } }) } }) + // Apply custom desktop layout configuration let desktopLayout = settings('get', 'layout') - let desktop = document.querySelector("#divContent") - if (desktop && desktopLayout) { - let col1 = desktop.querySelector("#Column1") - let col2 = desktop.querySelector("#Column2") - let col3 = desktop.querySelector("#Column3") - - //100% / 33-66% - if (desktopLayout === "1_100-66-33" && col2 && col3) { - //define width classes - let col2WidthClass = "col-md-8" - let col3WidthClass = "col-md-4" - - //resize column 2 - for (const cls of col2.classList) { - if (cls.startsWith("col-md")) { - col2.classList.remove(cls) - col2.classList.add(col2WidthClass) + let desktopContainer = document.querySelector("#divContent") + if (desktopContainer && desktopLayout) { + let column1 = desktopContainer.querySelector("#Column1") + let column2 = desktopContainer.querySelector("#Column2") + let column3 = desktopContainer.querySelector("#Column3") + + // Apply 100% / 66-33% layout (column 2 takes 66%, column 3 takes 33%) + if (desktopLayout === "1_100-66-33" && column2 && column3) { + // Define width classes for Bootstrap grid + let column2WidthClass = "col-md-8" // 66% width + let column3WidthClass = "col-md-4" // 33% width + + // Resize column 2: remove existing col-md class and add new one + for (const className of column2.classList) { + if (className.startsWith("col-md")) { + column2.classList.remove(className) + column2.classList.add(column2WidthClass) break } } - //resize column 3 - for (const cls of col3.classList) { - if (cls.startsWith("col-md")) { - col3.classList.remove(cls) - col3.classList.add(col3WidthClass) + // Resize column 3: remove existing col-md class and add new one + for (const className of column3.classList) { + if (className.startsWith("col-md")) { + column3.classList.remove(className) + column3.classList.add(column3WidthClass) break } } } } - //apply sticky columns + // Apply sticky column positioning if enabled let stickyColumns = settings('get', 'stickyColumns') if (stickyColumns) { [...document.querySelectorAll(".tdx-dashboard__column")].forEach(column => { @@ -302,6 +387,11 @@ function parseOtherElements() { } } +/** + * Parses a table element and extracts structured data from rows + * Creates item objects for each row and applies enhancements via parseItem() + * @param {HTMLElement} element - The container element containing the table + */ function parseTable(element) { let table = element.querySelector("table") let items = []; @@ -309,72 +399,84 @@ function parseTable(element) { if (table) { let headers = []; - //get headers of the tables + // Extract table headers from