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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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
  }
})();