Script 3 – Empty widget congratulations

Replaces the "No items to display." empty-state text with a custom message + light green highlight for selected dashboard widgets

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         Script 3 – Empty widget congratulations
// @namespace    http://tampermonkey.net/
// @version      2.1 - 25-12-16
// @description  Replaces the "No items to display." empty-state text with a custom message + light green highlight for selected dashboard widgets
// @match        https://translations.myelan.net/*
// @grant        none
// @run-at       document-end
// @noframes
// ==/UserScript==

(function () {
  'use strict';

  /* =========================================================
     DEPLOYMENT CONFIG
     =========================================================
     - Add/remove widgets in WIDGETS below.
     - "title" must match the widget header text in XTRF.
     - "message" is what will replace the default empty-state text.
  */

  const CHECK_INTERVAL_MS = 700;

  /** Widgets to process (widget header -> replacement empty message) */
  const WIDGETS = [
    { title: "Requests pending", message: "No pending requests 🤖" },
    { title: "Projects due today", message: "Congrats, all done for today! 🎉" },
    { title: "Projects ready for finalization", message: "Good job, all done... For now! 🌱" },
    { title: "Projects still to be started", message: "You're on a rrrrrroll! 🐯" },
    { title: "Requested / Open", message: "It's empty in here... 🍃" },
  ];

  /* =========================================================
     HELPERS
     ========================================================= */

  function normalize(text) {
    return (text || "")
      .replace(/\u00A0/g, " ")
      .replace(/\s+/g, " ")
      .trim()
      .toLowerCase();
  }

  /**
   * Find the dashboard widget "card" by its title.
   * Supports multiple common XTRF container classes.
   */
  function findWidgetCardByTitle(title) {
    const wanted = normalize(title);

    const cards = Array.from(document.querySelectorAll(
      ".x-card, .xlt-card, .xdb-dashboard__widget, .card, .panel"
    ));

    for (const card of cards) {
      const heading =
        card.querySelector(".x-card__header__heading, .xlt-card__header__heading, h1, h2, h3, h4") ||
        card.querySelector(".x-card__header, .xlt-card__header, .card-header");

      if (!heading) continue;

      const txt = normalize(heading.textContent);
      if (txt === wanted || txt.includes(wanted)) return card;
    }

    return null;
  }

  /**
   * Apply a light green "success" style to the empty-state cell/element.
   * Prefer styling the parent <td> when available.
   */
  function applyGreenStyle(targetEl) {
    if (!targetEl) return;

    const td = targetEl.closest ? targetEl.closest("td") : null;
    const el = td || targetEl;

    el.style.setProperty("background", "rgba(46, 204, 113, 0.12)", "important");
    el.style.setProperty("color", "#1b6b3a", "important");
    el.style.setProperty("font-weight", "650", "important");
    el.style.setProperty("text-align", "center", "important");
    el.style.setProperty("padding", "22px 12px", "important");
    el.style.setProperty("border-radius", "12px", "important");
  }

  /**
   * Replace the default empty-state message within a given root (widget DOM or iframe DOM).
   * We search broadly because XTRF sometimes wraps the message in spans/divs.
   *
   * Returns true if an empty-state message was found (and styled).
   */
  function replaceNoItemsMessage(root, newText) {
    if (!root) return false;

    const candidates = Array.from(root.querySelectorAll("td, div, span, p"));
    let changed = false;

    for (const el of candidates) {
      const t = normalize(el.textContent);
      if (!t) continue;

      if (t.includes("no items to display")) {
        // Avoid rewriting repeatedly if XTRF re-renders
        if (normalize(el.textContent) !== normalize(newText)) {
          el.textContent = newText;
        }

        applyGreenStyle(el);
        changed = true;
      }
    }

    return changed;
  }

  /**
   * Process a single widget:
   * 1) Try to replace the empty-state message directly in the widget card.
   * 2) If not found, try inside the widget's iframe (Smart View pattern).
   */
  function processWidget(widget) {
    const card = findWidgetCardByTitle(widget.title);
    if (!card) return;

    // Case 1: table/message is directly inside the widget card
    if (replaceNoItemsMessage(card, widget.message)) return;

    // Case 2: Smart View content is inside an iframe
    const iframe = card.querySelector("iframe");
    if (!iframe) return;

    let iframeDoc = null;
    try {
      iframeDoc = iframe.contentDocument || iframe.contentWindow?.document || null;
    } catch {
      // If the iframe is cross-origin, we cannot access it (unlikely in this setup)
      return;
    }

    if (!iframeDoc || !iframeDoc.body) return;

    replaceNoItemsMessage(iframeDoc, widget.message);
  }

  /* =========================================================
     RUN LOOP (XTRF re-renders often, so we keep it resilient)
     ========================================================= */

  function run() {
    for (const w of WIDGETS) processWidget(w);
  }

  run();
  setInterval(run, CHECK_INTERVAL_MS);

  // Extra resilience: handle DOM changes triggered by XTRF (Angular/JSF re-render)
  try {
    const obs = new MutationObserver(() => run());
    if (document.body) {
      obs.observe(document.body, { childList: true, subtree: true, characterData: true });
    }
  } catch {
    // Ignore observer failures
  }
})();