Script 1 – Requested / Open – Status Colors

Script 1 (ciblage table fiable) + palette (v6 traverse iframes) + popover modern + 🎨 button

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Script 1 – Requested / Open – Status Colors
// @namespace    http://tampermonkey.net/
// @version      3.5 - 25-12-16
// @description  Script 1 (ciblage table fiable) + palette (v6 traverse iframes) + popover modern + 🎨 button
// @match        https://translations.myelan.net/xtrf/faces/dashboard2/genericBrowseIFrame.seam*
// @match        https://translations.myelan.net/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function () {
  'use strict';

  /* ===================== CONFIG ===================== */

  const TARGET_VIEW_TITLE = "Requested / Open";
  const REQUIRED_HEADERS = ["Job Status", "Deadline"];
  const STATUS_HEADER_TEXT = "Job Status";

  const DEFAULT_STATUS_COLORS = {
    "open": "#e97f7f",
    "offers sent / requested": "#8A8AFF"
  };

  const STORAGE_KEY = "xtrf_requested_open_status_colors_v2";
  const RESCAN_INTERVAL_MS = 1000;

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

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

  function lookupColorForText(text, STATUS_COLORS) {
    if (!text) return null;
    const n = normalize(text);
    if (STATUS_COLORS[n]) return STATUS_COLORS[n];
    for (const k of Object.keys(STATUS_COLORS)) {
      if (normalize(k) === n) return STATUS_COLORS[k];
    }
    return null;
  }

  function loadColorsFromStorage() {
    let STATUS_COLORS = { ...DEFAULT_STATUS_COLORS };
    try {
      const raw = localStorage.getItem(STORAGE_KEY);
      if (!raw) return STATUS_COLORS;
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === "object") STATUS_COLORS = Object.assign(STATUS_COLORS, parsed);
    } catch (e) {
      console.warn("[XTRF] Failed to load colors:", e);
    }
    return STATUS_COLORS;
  }

  function saveColorsToStorage(STATUS_COLORS) {
    try {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(STATUS_COLORS));
    } catch (e) {
      console.warn("[XTRF] Failed to save colors:", e);
    }
  }

  // Traverse all same-origin documents (main doc + iframes)
  function traverseSameOriginDocuments(fn) {
    const seen = new Set();
    function visit(win) {
      if (!win || seen.has(win)) return;
      seen.add(win);

      let doc;
      try { doc = win.document; } catch { return; } // cross-origin

      try { fn(doc, win); } catch (e) { console.warn("[XTRF] traverse fn error", e); }

      const iframes = Array.from(doc.querySelectorAll("iframe"));
      for (const iframe of iframes) {
        try { if (iframe && iframe.contentWindow) visit(iframe.contentWindow); } catch {}
      }
    }
    visit(window);
  }

  /* ===================== HEADING (v6 style) ===================== */

  function findTargetHeadingInDoc(doc) {
    if (!doc) return null;

    const classHeadings = Array.from(doc.querySelectorAll(
      "h2.x-card__header__heading, h1.x-card__header__heading, h3.x-card__header__heading"
    ));
    let found = classHeadings.find(h => normalize(h.textContent) === normalize(TARGET_VIEW_TITLE));
    if (found) return found;

    const cardHeaders = Array.from(doc.querySelectorAll(".x-card__header, .xlt-card__header, .card-header"));
    for (const ch of cardHeaders) {
      if (normalize(ch.textContent) === normalize(TARGET_VIEW_TITLE)) {
        const inner = ch.querySelector(".x-card__header__heading, h1, h2, h3, h4, .header-title");
        return inner || ch;
      }
    }

    const any = Array.from(doc.querySelectorAll("h1,h2,h3,h4,div,span"));
    found = any.find(el => normalize(el.textContent) === normalize(TARGET_VIEW_TITLE));
    return found || null;
  }

  function findWidgetContainerForHeading(heading) {
    if (!heading) return null;
    const selectors = [".xdb-dashboard__widget", ".x-card", ".xlt-card", ".card", ".widget", ".x-card__container", ".panel"];
    for (const sel of selectors) {
      const anc = heading.closest ? heading.closest(sel) : null;
      if (anc) return anc;
    }
    const headerParent = heading.closest ? heading.closest(".x-card__header, .xlt-card__header, .card-header") : null;
    if (headerParent && headerParent.parentElement) return headerParent.parentElement;
    return heading.parentElement || null;
  }

  /* ===================== TABLE LOGIC (Script 1 style) ===================== */

  function extractHeaderTexts(table) {
    if (!table) return null;
    const thead = table.querySelector("thead");
    if (thead) {
      const headerRow = thead.querySelector("tr");
      if (headerRow) return Array.from(headerRow.querySelectorAll("th,td")).map(h => normalize(h.textContent));
    }
    return null;
  }

  function tableHasRequiredHeaders(table) {
    const headers = extractHeaderTexts(table);
    if (!headers || !headers.length) return false;
    return REQUIRED_HEADERS.every(req => headers.some(h => h.includes(normalize(req))));
  }

  function getColumnIndex(table, headerText) {
    const headerRow = table.querySelector("thead tr");
    if (!headerRow) return -1;
    const headers = Array.from(headerRow.querySelectorAll("th, td"));
    for (let i = 0; i < headers.length; i++) {
      if (normalize(headers[i].textContent).includes(normalize(headerText))) return i;
    }
    return -1;
  }

  function resetCellStyles(cell) {
    if (!cell) return;
    try {
      cell.style.removeProperty("background-color");
      cell.style.removeProperty("color");
      cell.style.removeProperty("font-weight");
      cell.style.removeProperty("text-align");
      if (cell.firstElementChild) {
        cell.firstElementChild.style.removeProperty("background-color");
        cell.firstElementChild.style.removeProperty("color");
        cell.firstElementChild.style.removeProperty("font-weight");
      }
    } catch {}
  }

  function styleCell(cell, color) {
    if (!cell) return;
    try {
      cell.style.setProperty("background-color", color, "important");
      cell.style.setProperty("color", "white", "important");
      cell.style.setProperty("font-weight", "700", "important");
      cell.style.setProperty("text-align", "center", "important");
      const child = cell.firstElementChild;
      if (child) {
        child.style.setProperty("background-color", color, "important");
        child.style.setProperty("color", "white", "important");
        child.style.setProperty("font-weight", "700", "important");
      }
    } catch {}
  }

  function findTableForWidget(doc, heading) {
    const container = findWidgetContainerForHeading(heading);
    if (container) {
      const tables = Array.from(container.querySelectorAll("table"));
      const match = tables.find(tableHasRequiredHeaders);
      if (match) return match;
    }
    const allTables = Array.from(doc.querySelectorAll("table"));
    return allTables.find(tableHasRequiredHeaders) || null;
  }

  function applyColorsInDoc(doc, STATUS_COLORS) {
    const heading = findTargetHeadingInDoc(doc);

    let table = null;
    if (heading) table = findTableForWidget(doc, heading);
    if (!table) {
      const allTables = Array.from(doc.querySelectorAll("table"));
      table = allTables.find(tableHasRequiredHeaders) || null;
    }
    if (!table) return;

    const statusCol = getColumnIndex(table, STATUS_HEADER_TEXT);
    if (statusCol < 0) return;

    const rows = table.querySelectorAll("tbody tr");
    if (!rows.length) return;

    rows.forEach(row => {
      const tds = row.querySelectorAll("td");
      if (!tds.length) return;
      const cell = tds[statusCol];
      if (!cell) return;

      const text = normalize(cell.innerText || cell.textContent);
      resetCellStyles(cell);

      const color = lookupColorForText(text, STATUS_COLORS);
      if (color) styleCell(cell, color);
    });
  }

  /* ===================== PALETTE UI (pretty) ===================== */

  let popoverMap = new WeakMap();

  function closePopoverForDoc(doc) {
    const pop = popoverMap.get(doc);
    if (!pop) return;
    try { pop._cleanup?.(); } catch {}
    try { pop.remove(); } catch {}
    popoverMap.delete(doc);
  }

  function showPopoverForDoc(doc, anchorEl, STATUS_COLORS) {
    if (!doc || !anchorEl) return;

    const existing = popoverMap.get(doc);
    if (existing) { closePopoverForDoc(doc); return; }

    const pop = doc.createElement("div");
    pop.className = "xtrf-status-popover";
    Object.assign(pop.style, {
      position: "absolute",
      zIndex: 99999,
      minWidth: "260px",
      padding: "12px 12px 10px",
      background: "rgba(255,255,255,0.92)",
      backdropFilter: "blur(10px)",
      WebkitBackdropFilter: "blur(10px)",
      border: "1px solid rgba(0,0,0,0.10)",
      borderRadius: "14px",
      boxShadow: "0 16px 40px rgba(0,0,0,0.14)",
      fontFamily: "system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial",
      fontSize: "13px",
      color: "#1f2328"
    });

    const header = doc.createElement("div");
    header.textContent = "Status colors";
    Object.assign(header.style, {
      fontWeight: "700",
      fontSize: "13px",
      marginBottom: "10px",
      letterSpacing: "0.2px"
    });

    const sub = doc.createElement("div");
    sub.textContent = "Click a color to change it";
    Object.assign(sub.style, {
      marginTop: "-6px",
      marginBottom: "10px",
      fontSize: "12px",
      color: "rgba(31,35,40,0.65)"
    });

    const form = doc.createElement("div");
    Object.assign(form.style, {
      display: "flex",
      flexDirection: "column",
      gap: "8px"
    });

    Object.keys(STATUS_COLORS).forEach(statusKey => {
      const row = doc.createElement("label");
      Object.assign(row.style, {
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
        gap: "10px",
        padding: "8px 10px",
        borderRadius: "12px",
        border: "1px solid rgba(0,0,0,0.06)",
        background: "rgba(255,255,255,0.65)"
      });

      const nameSpan = doc.createElement("span");
      nameSpan.textContent = statusKey;
      nameSpan.style.flex = "1";

      const input = doc.createElement("input");
      input.type = "color";
      input.value = STATUS_COLORS[statusKey] || "#000000";
      input.dataset.statusKey = statusKey;
      Object.assign(input.style, {
        width: "44px",
        height: "30px",
        border: "none",
        background: "transparent",
        padding: "0",
        cursor: "pointer"
      });

      row.appendChild(nameSpan);
      row.appendChild(input);
      form.appendChild(row);
    });

    const buttons = doc.createElement("div");
    Object.assign(buttons.style, {
      display: "flex",
      justifyContent: "flex-end",
      gap: "8px",
      marginTop: "10px"
    });

    const cancelBtn = doc.createElement("button");
    cancelBtn.type = "button";
    cancelBtn.textContent = "Cancel";
    Object.assign(cancelBtn.style, {
      padding: "8px 12px",
      background: "rgba(0,0,0,0.06)",
      border: "1px solid rgba(0,0,0,0.08)",
      borderRadius: "12px",
      cursor: "pointer"
    });

    const saveBtn = doc.createElement("button");
    saveBtn.type = "button";
    saveBtn.textContent = "Save";
    Object.assign(saveBtn.style, {
      padding: "8px 12px",
      background: "#0b6cff",
      color: "#fff",
      border: "1px solid rgba(0,0,0,0.08)",
      borderRadius: "12px",
      cursor: "pointer",
      boxShadow: "0 8px 18px rgba(11,108,255,0.22)"
    });

    buttons.appendChild(cancelBtn);
    buttons.appendChild(saveBtn);

    pop.appendChild(header);
    pop.appendChild(sub);
    pop.appendChild(form);
    pop.appendChild(buttons);

    try { doc.body.appendChild(pop); } catch { return; }

    // position near the anchor
    const rect = anchorEl.getBoundingClientRect();
    const popRect = pop.getBoundingClientRect();
    let top = rect.bottom + 8;
    let left = rect.left - (popRect.width / 2) + (rect.width / 2);

    const docWidth = doc.documentElement.clientWidth;
    if (left + popRect.width > docWidth - 8) left = docWidth - popRect.width - 8;
    if (left < 8) left = 8;

    const win = doc.defaultView || window;
    pop.style.top = `${top + win.scrollY}px`;
    pop.style.left = `${left + win.scrollX}px`;

    cancelBtn.addEventListener("click", () => closePopoverForDoc(doc));
    saveBtn.addEventListener("click", () => {
      const inputs = pop.querySelectorAll('input[type="color"]');
      inputs.forEach(inp => {
        const key = inp.dataset.statusKey;
        if (key) STATUS_COLORS[key] = inp.value;
      });
      saveColorsToStorage(STATUS_COLORS);
      traverseSameOriginDocuments((d) => applyColorsInDoc(d, STATUS_COLORS));
      closePopoverForDoc(doc);
    });

    const outsideHandler = (ev) => {
      if (!pop.contains(ev.target) && ev.target !== anchorEl && !anchorEl.contains(ev.target)) closePopoverForDoc(doc);
    };
    const escHandler = (ev) => { if (ev.key === "Escape") closePopoverForDoc(doc); };

    setTimeout(() => document.addEventListener("mousedown", outsideHandler), 0);
    document.addEventListener("keydown", escHandler);
    pop._cleanup = () => {
      document.removeEventListener("mousedown", outsideHandler);
      document.removeEventListener("keydown", escHandler);
    };

    popoverMap.set(doc, pop);
  }

  function ensureSettingsIconInDoc(doc, STATUS_COLORS) {
    const heading = findTargetHeadingInDoc(doc);
    if (!heading) return false;

    const headerContainer = heading.closest
      ? heading.closest(".x-card__header, .xlt-card__header, .card-header")
      : (heading.parentElement || doc.body);

    let actionsArea = headerContainer
      ? headerContainer.querySelector(".x-card__header__actions, .header-actions, .card-actions")
      : null;

    if (!actionsArea) {
      actionsArea = headerContainer.querySelector(".xtrf-status-actions");
      if (!actionsArea) {
        actionsArea = doc.createElement("div");
        actionsArea.className = "xtrf-status-actions";
        actionsArea.style.display = "inline-flex";
        actionsArea.style.alignItems = "center";
        actionsArea.style.marginLeft = "8px";
        actionsArea.style.verticalAlign = "middle";
        heading.insertAdjacentElement("afterend", actionsArea);
      }
    }

    if (actionsArea.querySelector(".xtrf-status-settings-icon")) return true;

    const iconBtn = doc.createElement("button");
    iconBtn.type = "button";
    iconBtn.className = "xtrf-status-settings-icon";
    iconBtn.title = "Change status colors";
    Object.assign(iconBtn.style, {
      border: "none",
      background: "transparent",
      cursor: "pointer",
      padding: "0",
      marginLeft: "6px",
      display: "inline-flex",
      alignItems: "center",
      justifyContent: "center",
      height: "22px",
      width: "22px",
      borderRadius: "999px",
      lineHeight: "1",
      transition: "transform 120ms ease, background 120ms ease"
    });

    // 🎨 emoji icon
    iconBtn.textContent = "🎨";
    iconBtn.setAttribute("aria-label", "Change status colors");

    iconBtn.addEventListener("mouseenter", () => {
      iconBtn.style.background = "rgba(0,0,0,0.06)";
      iconBtn.style.transform = "scale(1.06)";
    });
    iconBtn.addEventListener("mouseleave", () => {
      iconBtn.style.background = "transparent";
      iconBtn.style.transform = "scale(1)";
    });

    actionsArea.appendChild(iconBtn);

    iconBtn.addEventListener("click", (ev) => {
      ev.preventDefault();
      ev.stopPropagation();
      showPopoverForDoc(doc, iconBtn, STATUS_COLORS);
    });

    return true;
  }

  /* ===================== RUN ===================== */

  function run() {
    const STATUS_COLORS = loadColorsFromStorage();

    traverseSameOriginDocuments((doc) => {
      try { ensureSettingsIconInDoc(doc, STATUS_COLORS); } catch {}
      try { applyColorsInDoc(doc, STATUS_COLORS); } catch {}
    });
  }

  run();
  setInterval(run, RESCAN_INTERVAL_MS);

  try {
    const observer = new MutationObserver(() => run());
    if (document.body) observer.observe(document.body, { childList: true, subtree: true, characterData: true });
  } catch {}
})();