src/lib/form/detectors/native-input-config.ts

Total Symbols
4
Lines of Code
140
Avg Complexity
6.3
Avg Coverage
100.0%

Architecture violations

View all

  • [warning] max-cyclomatic-complexity: 'buildNativeField' has cyclomatic complexity 21 (max 10)

Symbols by Kind

function 3
type 1

All Symbols

Name Kind Visibility Status Lines Signature
NativeElement type exported- 20-23 type NativeElement
isVisible function exported- 59-62 isVisible(el: NativeElement): : boolean
isNotCustomSelect function exported- 65-67 isNotCustomSelect(el: NativeElement): : boolean
buildNativeField function exported- 75-139 buildNativeField(element: NativeElement): : FormField

Full Source

/**
 * Native Input Configuration
 *
 * Shared selectors, filters and the field builder used by collectNativeFields()
 * in classifiers.ts (Steps 1–3: collect → filter → extract).
 *
 * Keeping these separate from classifiers.ts avoids polluting the classifier
 * registry with DOM-querying concerns.
 */

import type { FormField } from "@/types";
import {
  getUniqueSelector,
  findLabelWithStrategy,
  buildSignals,
} from "../extractors";

// ── Types ─────────────────────────────────────────────────────────────────────

export type NativeElement =
  | HTMLInputElement
  | HTMLSelectElement
  | HTMLTextAreaElement;

// ── Step 1: Selectors ─────────────────────────────────────────────────────────

/** CSS selector that targets all native form controls (excluding hidden/submit/etc). */
export const INPUT_SELECTOR = [
  'input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="image"]):not([type="reset"]):not([type="file"]):not([disabled])',
  "select:not([disabled])",
  "textarea:not([disabled])",
].join(", ");

/**
 * Ancestor selector for custom component wrappers (Ant Design, MUI, React Select, etc.).
 * Native <input>/<select>/<textarea> inside these wrappers are handled by adapters,
 * so the native detector must skip them to avoid duplicates.
 */
export const CUSTOM_SELECT_ANCESTOR = [
  // Ant Design
  ".ant-select",
  ".ant-picker",
  ".ant-input-number",
  ".ant-input-affix-wrapper",
  ".ant-mentions",
  ".ant-checkbox-group",
  ".ant-radio-group",
  ".ant-transfer",
  // React Select
  "[class*='react-select']",
  // Material UI
  ".MuiSelect-root",
  "[class*='MuiAutocomplete']",
].join(", ");

// ── Step 2: Filters ───────────────────────────────────────────────────────────

/** Skips zero-size elements (hidden / collapsed). */
export function isVisible(el: NativeElement): boolean {
  const rect = el.getBoundingClientRect();
  return rect.width > 0 || rect.height > 0;
}

/** Skips elements that are children of custom-select components. */
export function isNotCustomSelect(el: NativeElement): boolean {
  return !el.closest(CUSTOM_SELECT_ANCESTOR);
}

// ── Step 3: Builder ───────────────────────────────────────────────────────────

/**
 * Builds a bare FormField from a native DOM element.
 * Extracts selector, label, DOM metadata and context signals — no classification yet.
 */
export function buildNativeField(element: NativeElement): FormField {
  const labelResult = findLabelWithStrategy(element);

  // ── DOM metadata ────────────────────────────────────────────────────────────
  const inputType =
    element instanceof HTMLInputElement ? element.type : undefined;
  const pattern =
    element instanceof HTMLInputElement && element.pattern
      ? element.pattern
      : undefined;
  const maxLength =
    (element instanceof HTMLInputElement ||
      element instanceof HTMLTextAreaElement) &&
    element.maxLength > 0
      ? element.maxLength
      : undefined;
  const minLength =
    (element instanceof HTMLInputElement ||
      element instanceof HTMLTextAreaElement) &&
    element.minLength > 0
      ? element.minLength
      : undefined;

  // ── Select options ──────────────────────────────────────────────────────────
  const options =
    element instanceof HTMLSelectElement
      ? Array.from(element.options)
          .filter((o) => o.value !== "")
          .map((o) => ({ value: o.value, text: o.text.trim() }))
      : undefined;

  // ── Checkbox / Radio ────────────────────────────────────────────────────────
  const isCheckable =
    element instanceof HTMLInputElement &&
    (element.type === "checkbox" || element.type === "radio");
  const checkboxValue = isCheckable
    ? (element as HTMLInputElement).value || undefined
    : undefined;
  const checkboxChecked = isCheckable
    ? (element as HTMLInputElement).checked
    : undefined;

  const field: FormField = {
    element,
    selector: getUniqueSelector(element),
    category: "unknown",
    fieldType: "unknown",
    label: labelResult?.text,
    name: element.name || undefined,
    id: element.id || undefined,
    placeholder:
      ("placeholder" in element ? element.placeholder : undefined) || undefined,
    autocomplete: element.autocomplete || undefined,
    inputType,
    required: element.required,
    pattern,
    maxLength,
    minLength,
    options,
    checkboxValue,
    checkboxChecked,
  };
  field.contextSignals = buildSignals(field);
  return field;
}