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

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

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.

(I already have a user style manager, let me install it!)

// ==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
  }
})();