src/lib/form/field-icon-rule.ts

Total Symbols
14
Lines of Code
498
Avg Complexity
7.9
Symbol Types
1

File Relationships

graph LR handleRuleButtonClick["handleRuleButtonClick"] detectSuggestedType["detectSuggestedType"] showRulePopup["showRulePopup"] getRulePopupHTML["getRulePopupHTML"] setupPopupListeners["setupPopupListeners"] updateParamsSection["updateParamsSection"] updatePreview["updatePreview"] positionRulePopup["positionRulePopup"] saveFieldRule["saveFieldRule"] hideRulePopup["hideRulePopup"] handlePopupKeyDown["handlePopupKeyDown"] renderParamFields["renderParamFields"] collectParamsFromUI["collectParamsFromUI"] handleRuleButtonClick -->|calls| detectSuggestedType handleRuleButtonClick -->|calls| showRulePopup showRulePopup -->|calls| getRulePopupHTML showRulePopup -->|calls| setupPopupListeners showRulePopup -->|calls| updateParamsSection showRulePopup -->|calls| updatePreview showRulePopup -->|calls| positionRulePopup setupPopupListeners -->|calls| saveFieldRule setupPopupListeners -->|calls| hideRulePopup setupPopupListeners -->|calls| updatePreview handlePopupKeyDown -->|calls| saveFieldRule handlePopupKeyDown -->|calls| hideRulePopup updateParamsSection -->|calls| renderParamFields updateParamsSection -->|calls| updatePreview updatePreview -->|calls| collectParamsFromUI saveFieldRule -->|calls| collectParamsFromUI saveFieldRule -->|calls| hideRulePopup click handleRuleButtonClick "../symbols/302ac6e6209ebb8e.html" click detectSuggestedType "../symbols/f6cfcac9fed800c4.html" click showRulePopup "../symbols/9a11db3b71d6ccf5.html" click getRulePopupHTML "../symbols/9f97c911ee78d2da.html" click setupPopupListeners "../symbols/ca4c2bf69b7d4b11.html" click updateParamsSection "../symbols/9e0d46bd06f35333.html" click updatePreview "../symbols/dcabab4647818f61.html" click positionRulePopup "../symbols/25cfebd1421f5b74.html" click saveFieldRule "../symbols/ce40bf58e0f8ddd4.html" click hideRulePopup "../symbols/aa799488319c1bed.html" click handlePopupKeyDown "../symbols/9eb9336b629ec1ed.html" click renderParamFields "../symbols/84f004ba7eb960b3.html" click collectParamsFromUI "../symbols/37fd92ff99d24fcf.html"

Architecture violations

View all

  • [warning] max-cyclomatic-complexity: 'detectSuggestedType' has cyclomatic complexity 13 (max 10)
  • [warning] max-cyclomatic-complexity: 'showRulePopup' has cyclomatic complexity 14 (max 10)
  • [warning] max-cyclomatic-complexity: 'renderParamFields' has cyclomatic complexity 15 (max 10)
  • [warning] max-cyclomatic-complexity: 'collectParamsFromUI' has cyclomatic complexity 15 (max 10)
  • [warning] max-cyclomatic-complexity: 'updatePreview' has cyclomatic complexity 12 (max 10)

Symbols by Kind

function 14

All Symbols

Name Kind Visibility Status Lines Signature
handleRuleButtonClick function exported- 36-52 handleRuleButtonClick( target: HTMLElement, onDismiss: () => void, ): : void
detectSuggestedType function - 58-93 detectSuggestedType(target: HTMLElement): : FieldType | undefined
showRulePopup function - 95-164 showRulePopup(anchor: HTMLElement, onDismiss: () => void): : void
setupPopupListeners function - 166-198 setupPopupListeners(): : void
handlePopupKeyDown function - 200-211 handlePopupKeyDown(e: KeyboardEvent): : void
updateParamsSection function - 213-248 updateParamsSection(): : void
renderParamFields function - 250-301 renderParamFields(paramDefs: readonly GeneratorParamDef[]): : string
collectParamsFromUI function - 303-348 collectParamsFromUI(): : GeneratorParams | undefined
updatePreview function - 350-387 updatePreview(): : void
hideRulePopup function exported- 389-394 hideRulePopup(): : void
destroyRulePopup function exported- 396-405 destroyRulePopup(): : void
positionRulePopup function - 407-428 positionRulePopup(anchor: HTMLElement): : void
getRulePopupHTML function - 430-458 getRulePopupHTML(): : string
saveFieldRule function - 460-497 saveFieldRule(): : Promise<void>

Full Source

/**
 * Field Icon — rule quick-save popup for field-specific rules
 *
 * Features:
 * - Auto-suggestion: detects field type via HTML attributes + keyword classifier
 * - Live preview: shows generated value (or fixed value) in real-time
 * - Keyboard shortcuts: Enter to save, Escape to cancel
 */

import type { FieldRule, FieldType, FormField, GeneratorParams } from "@/types";
import { RULE_POPUP_ID } from "./field-icon-styles";
import { getUniqueSelector, findLabel, buildSignals } from "./extractors";
import { getFieldTypeOptions } from "@/lib/shared/field-type-catalog";
import { SearchableSelect } from "@/lib/ui/searchable-select";
import { buildGeneratorSelectEntries } from "@/lib/ui/select-builders";
import { generate } from "@/lib/generators";
import { detectBasicType } from "./detectors/html-type-detector";
import { keywordClassifier } from "./detectors/strategies/keyword-classifier";
import {
  getGeneratorKey,
  getGeneratorParamDefs,
  type GeneratorParamDef,
} from "@/types/field-type-definitions";

let rulePopupElement: HTMLElement | null = null;
let genSearchableSelect: SearchableSelect | null = null;
let currentOnDismiss: (() => void) | null = null;
let currentSuggestedType: FieldType | undefined;
let currentRuleField: {
  selector: string;
  label: string;
  name?: string;
  id?: string;
} | null = null;

export function handleRuleButtonClick(
  target: HTMLElement,
  onDismiss: () => void,
): void {
  const selector = getUniqueSelector(target);
  const label =
    findLabel(target) ||
    target.getAttribute("name") ||
    target.getAttribute("id") ||
    "campo";
  const name = (target as HTMLInputElement).name || undefined;
  const id = target.id || undefined;

  currentRuleField = { selector, label, name, id };
  currentSuggestedType = detectSuggestedType(target);
  showRulePopup(target, onDismiss);
}

/**
 * Attempts a fast synchronous detection of the field type using
 * HTML type attributes first, then keyword classifier as fallback.
 */
function detectSuggestedType(target: HTMLElement): FieldType | undefined {
  const el = target as
    | HTMLInputElement
    | HTMLSelectElement
    | HTMLTextAreaElement;
  const isFormEl =
    el instanceof HTMLInputElement ||
    el instanceof HTMLSelectElement ||
    el instanceof HTMLTextAreaElement;
  if (!isFormEl) return undefined;

  const htmlResult = detectBasicType(el);
  if (htmlResult.type !== "unknown" && htmlResult.type !== "text") {
    return htmlResult.type;
  }

  const label = findLabel(target) || undefined;
  const minimalField: Partial<FormField> = {
    element: el,
    name: (el as HTMLInputElement).name || undefined,
    id: el.id || undefined,
    placeholder:
      ("placeholder" in el
        ? (el as HTMLInputElement).placeholder
        : undefined) || undefined,
    label,
  };
  minimalField.contextSignals = buildSignals(minimalField as FormField);

  const kwResult = keywordClassifier.detect(minimalField as FormField);
  if (kwResult?.type && kwResult.type !== "unknown") {
    return kwResult.type;
  }

  return undefined;
}

function showRulePopup(anchor: HTMLElement, onDismiss: () => void): void {
  currentOnDismiss = onDismiss;

  if (!rulePopupElement) {
    rulePopupElement = document.createElement("div");
    rulePopupElement.id = RULE_POPUP_ID;
    rulePopupElement.innerHTML = getRulePopupHTML();
    document.body.appendChild(rulePopupElement);
    setupPopupListeners();
  }

  if (!genSearchableSelect) {
    const wrap = rulePopupElement.querySelector<HTMLElement>(
      "#fa-rp-generator-wrap",
    );
    if (wrap) {
      genSearchableSelect = new SearchableSelect({
        entries: buildGeneratorSelectEntries(),
        value: "auto",
        placeholder: "Pesquisar tipo…",
      });
      genSearchableSelect.mount(wrap);
      genSearchableSelect.on("change", () => {
        updateParamsSection();
        updatePreview();
      });
    }
  }

  const nameEl =
    rulePopupElement.querySelector<HTMLElement>("#fa-rp-field-name");
  if (nameEl) nameEl.textContent = currentRuleField?.label || "";

  const fixedInput =
    rulePopupElement.querySelector<HTMLInputElement>("#fa-rp-fixed");
  if (fixedInput) fixedInput.value = "";

  const suggestionEl =
    rulePopupElement.querySelector<HTMLElement>("#fa-rp-suggestion");
  const suggestionTypeEl = rulePopupElement.querySelector<HTMLElement>(
    "#fa-rp-suggestion-type",
  );
  const saveBtn =
    rulePopupElement.querySelector<HTMLButtonElement>("#fa-rp-save");

  if (saveBtn) {
    saveBtn.textContent = "💾 Salvar";
    saveBtn.disabled = false;
  }

  genSearchableSelect?.setValue(currentSuggestedType ?? "auto");

  if (suggestionEl && suggestionTypeEl) {
    if (currentSuggestedType) {
      const label =
        getFieldTypeOptions().find((o) => o.value === currentSuggestedType)
          ?.label ?? currentSuggestedType;
      suggestionTypeEl.textContent = label;
      suggestionEl.style.display = "flex";
    } else {
      suggestionEl.style.display = "none";
    }
  }

  updateParamsSection();
  updatePreview();
  positionRulePopup(anchor);
  rulePopupElement.style.display = "block";
  fixedInput?.focus();
}

function setupPopupListeners(): void {
  if (!rulePopupElement) return;

  rulePopupElement
    .querySelector("#fa-rp-save")!
    .addEventListener("mousedown", (e) => {
      e.preventDefault();
      void saveFieldRule();
    });

  rulePopupElement
    .querySelector("#fa-rp-cancel")!
    .addEventListener("mousedown", (e) => {
      e.preventDefault();
      hideRulePopup();
      currentOnDismiss?.();
    });

  rulePopupElement
    .querySelector("#fa-rp-fixed")
    ?.addEventListener("input", () => {
      updatePreview();
    });

  rulePopupElement
    .querySelector("#fa-rp-preview-refresh")
    ?.addEventListener("mousedown", (e) => {
      e.preventDefault();
      updatePreview();
    });

  document.addEventListener("keydown", handlePopupKeyDown);
}

function handlePopupKeyDown(e: KeyboardEvent): void {
  if (!rulePopupElement || rulePopupElement.style.display !== "block") return;

  if (e.key === "Enter") {
    e.preventDefault();
    void saveFieldRule();
  } else if (e.key === "Escape") {
    e.preventDefault();
    hideRulePopup();
    currentOnDismiss?.();
  }
}

function updateParamsSection(): void {
  if (!rulePopupElement) return;
  const container =
    rulePopupElement.querySelector<HTMLElement>("#fa-rp-params");
  if (!container) return;

  const selectedType = genSearchableSelect?.getValue() ?? "auto";

  if (
    selectedType === "auto" ||
    selectedType === "ai" ||
    selectedType === "tensorflow"
  ) {
    container.style.display = "none";
    container.innerHTML = "";
    return;
  }

  const generatorKey = getGeneratorKey(selectedType as FieldType);
  const paramDefs = generatorKey ? getGeneratorParamDefs(generatorKey) : [];

  if (paramDefs.length === 0) {
    container.style.display = "none";
    container.innerHTML = "";
    return;
  }

  container.innerHTML = renderParamFields(paramDefs);
  container.style.display = "block";

  // Listen for param changes to update preview
  container.querySelectorAll("input, select").forEach((el) => {
    el.addEventListener("input", () => updatePreview());
    el.addEventListener("change", () => updatePreview());
  });
}

function renderParamFields(paramDefs: readonly GeneratorParamDef[]): string {
  const fields = paramDefs
    .map((def) => {
      const label = chrome.i18n?.getMessage(def.labelKey) ?? def.labelKey;
      if (def.type === "select" && def.selectOptions) {
        const options = def.selectOptions
          .map((opt) => {
            const optLabel =
              chrome.i18n?.getMessage(opt.labelKey) ?? opt.labelKey;
            const selected = opt.value === def.defaultValue ? "selected" : "";
            return `<option value="${opt.value}" ${selected}>${optLabel}</option>`;
          })
          .join("");
        return `
          <div class="fa-rp-param-field">
            <label class="fa-rp-param-label">${label}</label>
            <select data-param-key="${def.key}" class="fa-rp-input fa-rp-param-input">${options}</select>
          </div>`;
      }
      if (def.type === "boolean") {
        const checked = def.defaultValue ? "checked" : "";
        return `
          <label class="fa-rp-param-toggle">
            <input type="checkbox" data-param-key="${def.key}" ${checked} />
            <span>${label}</span>
          </label>`;
      }
      if (def.type === "text") {
        const placeholder = def.placeholder
          ? (chrome.i18n?.getMessage(def.placeholder) ?? def.placeholder)
          : "";
        return `
          <div class="fa-rp-param-field">
            <label class="fa-rp-param-label">${label}</label>
            <input type="text" data-param-key="${def.key}" value="${def.defaultValue}" placeholder="${placeholder}" class="fa-rp-input fa-rp-param-input" />
          </div>`;
      }
      const min = def.min != null ? `min="${def.min}"` : "";
      const max = def.max != null ? `max="${def.max}"` : "";
      const step = def.step != null ? `step="${def.step}"` : "";
      return `
        <div class="fa-rp-param-field">
          <label class="fa-rp-param-label">${label}</label>
          <input type="number" data-param-key="${def.key}" value="${def.defaultValue}" ${min} ${max} ${step} class="fa-rp-input fa-rp-param-input" />
        </div>`;
    })
    .join("");

  const title =
    chrome.i18n?.getMessage("paramSectionTitle") ?? "Parâmetros do Gerador";
  return `<div class="fa-rp-param-title">${title}</div>${fields}`;
}

function collectParamsFromUI(): GeneratorParams | undefined {
  if (!rulePopupElement) return undefined;
  const container =
    rulePopupElement.querySelector<HTMLElement>("#fa-rp-params");
  if (!container || container.style.display === "none") return undefined;

  const inputs = container.querySelectorAll<HTMLInputElement>(
    "input[data-param-key]",
  );
  const selects = container.querySelectorAll<HTMLSelectElement>(
    "select[data-param-key]",
  );
  if (inputs.length === 0 && selects.length === 0) return undefined;

  const params: Record<string, unknown> = {};
  let hasAny = false;

  inputs.forEach((input) => {
    const key = input.dataset.paramKey!;
    if (input.type === "checkbox") {
      params[key] = input.checked;
      hasAny = true;
    } else if (input.type === "number") {
      const val = parseFloat(input.value);
      if (!isNaN(val)) {
        params[key] = val;
        hasAny = true;
      }
    } else if (input.type === "text") {
      if (input.value !== "") {
        params[key] = input.value;
        hasAny = true;
      }
    }
  });

  selects.forEach((select) => {
    const key = select.dataset.paramKey!;
    if (select.value) {
      params[key] = select.value;
      hasAny = true;
    }
  });

  return hasAny ? (params as GeneratorParams) : undefined;
}

function updatePreview(): void {
  if (!rulePopupElement) return;

  const fixedInput =
    rulePopupElement.querySelector<HTMLInputElement>("#fa-rp-fixed");
  const previewValueEl = rulePopupElement.querySelector<HTMLElement>(
    "#fa-rp-preview-value",
  );
  const refreshBtn = rulePopupElement.querySelector<HTMLElement>(
    "#fa-rp-preview-refresh",
  );

  if (!fixedInput || !previewValueEl) return;

  const fixedVal = fixedInput.value.trim();

  if (fixedVal) {
    previewValueEl.textContent = fixedVal;
    previewValueEl.className = "fa-rp-preview-fixed";
    if (refreshBtn) refreshBtn.style.display = "none";
  } else {
    const selectedType = genSearchableSelect?.getValue() ?? "auto";
    const typeToGenerate: FieldType =
      selectedType === "auto"
        ? (currentSuggestedType ?? "text")
        : (selectedType as FieldType);

    try {
      const overrideParams = collectParamsFromUI();
      previewValueEl.textContent = generate(typeToGenerate, overrideParams);
    } catch {
      previewValueEl.textContent = "—";
    }

    previewValueEl.className = "fa-rp-preview-generated";
    if (refreshBtn) refreshBtn.style.display = "flex";
  }
}

export function hideRulePopup(): void {
  if (rulePopupElement) {
    rulePopupElement.style.display = "none";
    currentRuleField = null;
  }
}

export function destroyRulePopup(): void {
  document.removeEventListener("keydown", handlePopupKeyDown);
  genSearchableSelect?.destroy();
  genSearchableSelect = null;
  rulePopupElement?.remove();
  rulePopupElement = null;
  currentRuleField = null;
  currentOnDismiss = null;
  currentSuggestedType = undefined;
}

function positionRulePopup(anchor: HTMLElement): void {
  if (!rulePopupElement) return;

  const rect = anchor.getBoundingClientRect();
  const popupWidth = 280;
  const popupHeight = 280;

  let top = rect.bottom + window.scrollY + 4;
  let left = rect.left + window.scrollX;

  if (left + popupWidth > window.innerWidth + window.scrollX - 8) {
    left = window.innerWidth + window.scrollX - popupWidth - 8;
  }
  left = Math.max(window.scrollX + 8, left);

  if (top + popupHeight > window.innerHeight + window.scrollY) {
    top = rect.top + window.scrollY - popupHeight - 4;
  }

  rulePopupElement.style.top = `${top}px`;
  rulePopupElement.style.left = `${left}px`;
}

function getRulePopupHTML(): string {
  return `
    <div class="fa-rp-header">📌 Regra — <span id="fa-rp-field-name"></span></div>
    <div class="fa-rp-body">
      <div class="fa-rp-suggestion" id="fa-rp-suggestion" style="display:none">
        ✨ Sugerido: <span id="fa-rp-suggestion-type"></span>
      </div>
      <div class="fa-rp-group">
        <label class="fa-rp-label">Valor fixo</label>
        <input type="text" id="fa-rp-fixed" class="fa-rp-input" placeholder="Deixe vazio para usar gerador" />
      </div>
      <div class="fa-rp-group">
        <label class="fa-rp-label">Gerador automático</label>
        <div id="fa-rp-generator-wrap"></div>
      </div>
      <div id="fa-rp-params" class="fa-rp-params" style="display:none"></div>
      <div class="fa-rp-preview">
        <span class="fa-rp-preview-label">Preview</span>
        <span id="fa-rp-preview-value" class="fa-rp-preview-generated">—</span>
        <button id="fa-rp-preview-refresh" type="button" title="Gerar novo valor" style="display:none">↻</button>
      </div>
      <div class="fa-rp-actions">
        <button id="fa-rp-save" class="fa-rp-btn-primary" type="button">💾 Salvar</button>
        <button id="fa-rp-cancel" class="fa-rp-btn-cancel" type="button">Cancelar</button>
      </div>
      <div class="fa-rp-hint">Enter para salvar · Esc para cancelar</div>
    </div>
  `;
}

async function saveFieldRule(): Promise<void> {
  if (!currentRuleField) return;

  const fixedInput =
    rulePopupElement?.querySelector<HTMLInputElement>("#fa-rp-fixed");
  const genSelect =
    rulePopupElement?.querySelector<HTMLSelectElement>("#fa-rp-generator");
  const fixedValue = fixedInput?.value.trim() || undefined;
  const generator = (genSearchableSelect?.getValue() ||
    "auto") as FieldRule["generator"];

  const rule: FieldRule = {
    id: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
    urlPattern: `${window.location.origin}${window.location.pathname}*`,
    fieldSelector: currentRuleField.selector,
    fieldName: currentRuleField.name || currentRuleField.id || undefined,
    fieldType: currentSuggestedType ?? "unknown",
    fixedValue,
    generator: fixedValue ? "auto" : generator,
    generatorParams: fixedValue ? undefined : collectParamsFromUI(),
    priority: 10,
    createdAt: Date.now(),
    updatedAt: Date.now(),
  };

  await chrome.runtime.sendMessage({ type: "SAVE_RULE", payload: rule });

  const saveBtn =
    rulePopupElement?.querySelector<HTMLButtonElement>("#fa-rp-save");
  if (saveBtn) {
    saveBtn.textContent = "✓ Salvo!";
    saveBtn.disabled = true;
    setTimeout(() => {
      hideRulePopup();
      currentOnDismiss?.();
    }, 800);
  }
}