ghostfill 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-VMCU3BNJ.mjs → chunk-CKZ6PWBH.mjs} +2 -1
- package/dist/chunk-CKZ6PWBH.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +100 -32
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +101 -32
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +1 -1
- package/dist/{types-B3DGbZyx.d.mts → types-BYkjRHCV.d.mts} +1 -1
- package/dist/{types-B3DGbZyx.d.ts → types-BYkjRHCV.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/chunk-VMCU3BNJ.mjs.map +0 -1
|
@@ -264,6 +264,7 @@ async function generateFillData(fields, userPrompt, provider, transport, systemP
|
|
|
264
264
|
|
|
265
265
|
export {
|
|
266
266
|
PROVIDERS,
|
|
267
|
+
findLabel,
|
|
267
268
|
detectFields,
|
|
268
269
|
describeFields,
|
|
269
270
|
SYSTEM_PROMPT,
|
|
@@ -272,4 +273,4 @@ export {
|
|
|
272
273
|
parseFillDataPayload,
|
|
273
274
|
generateFillData
|
|
274
275
|
};
|
|
275
|
-
//# sourceMappingURL=chunk-
|
|
276
|
+
//# sourceMappingURL=chunk-CKZ6PWBH.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/detector.ts","../src/ai.ts"],"sourcesContent":["/** Configuration options for GhostFill */\nexport interface GhostFillOptions {\n /**\n * @deprecated Browser API keys are insecure and ignored.\n * Configure `ai` and keep provider credentials on your backend instead.\n */\n apiKey?: string;\n /** Keyboard shortcut to toggle GhostFill (default: \"Alt+G\") */\n shortcut?: string;\n /** Custom system prompt to prepend */\n systemPrompt?: string;\n /** Secure AI transport configuration */\n ai?: GhostFillAIOptions;\n}\n\nexport type Provider = \"openai\" | \"xai\" | \"moonshot\";\n\nexport const PROVIDERS: Record<Provider, { label: string; model: string; baseURL: string; helpText: string }> = {\n openai: { label: \"OpenAI\", model: \"gpt-4o-mini\", baseURL: \"https://api.openai.com/v1\", helpText: \"Uses gpt-4o-mini — fast & cheap\" },\n xai: { label: \"xAI\", model: \"grok-4-fast\", baseURL: \"https://api.x.ai/v1\", helpText: \"Uses Grok 4 Fast\" },\n moonshot: { label: \"Moonshot\", model: \"kimi-k2\", baseURL: \"https://api.moonshot.ai/v1\", helpText: \"Uses Kimi K2 — fast & cheap\" },\n};\n\n/** Non-secret field metadata sent to an AI backend */\nexport interface GhostFillPromptField {\n index: number;\n type: string;\n name: string;\n label: string;\n options?: string[];\n required: boolean;\n min?: string;\n max?: string;\n pattern?: string;\n}\n\n/** Secure AI request payload for a backend route or callback */\nexport interface GhostFillAIRequest {\n provider: Provider;\n prompt: string;\n systemPrompt?: string;\n fields: GhostFillPromptField[];\n}\n\nexport type GhostFillAIHandler = (\n request: GhostFillAIRequest\n) => Promise<FieldFillData[]>;\n\n/** Secure AI configuration. Requests must go through a backend or custom handler. */\nexport interface GhostFillAIOptions {\n /** Same-origin backend route. Defaults to `/api/ghostfill` when `ai` is enabled. */\n endpoint?: string;\n /** Optional custom transport that forwards requests to a secure backend. */\n requestFillData?: GhostFillAIHandler;\n /** Default provider shown in the UI. */\n provider?: Provider;\n}\n\n/** A saved prompt preset */\nexport interface Preset {\n id: string;\n name: string;\n prompt: string;\n}\n\n/** Persisted settings (localStorage) */\nexport interface GhostFillSettings {\n apiKey: string;\n provider: Provider;\n highlightColor: string;\n theme: \"dark\" | \"light\";\n useAI: boolean;\n presets: Preset[];\n activePresetId: string | null;\n}\n\n/** A detected form field */\nexport interface DetectedField {\n /** The DOM element */\n element: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLElement;\n /** Field type: text, email, number, select, textarea, checkbox, radio, date, etc. */\n type: string;\n /** Field name attribute */\n name: string;\n /** Field label (from <label>, aria-label, or placeholder) */\n label: string;\n /** For <select>, the available options */\n options?: string[];\n /** Whether the field is required */\n required: boolean;\n /** Current value */\n currentValue: string;\n /** Min/max for number/date fields */\n min?: string;\n max?: string;\n /** Pattern attribute */\n pattern?: string;\n}\n\n/** Field data returned by the AI */\nexport interface FieldFillData {\n /** Index matching the DetectedField array */\n index: number;\n /** Value to fill */\n value: string;\n /** For checkboxes: whether to check */\n checked?: boolean;\n}\n\n/** Internal state */\nexport interface GhostFillState {\n active: boolean;\n selecting: boolean;\n selectedBlock: HTMLElement | null;\n fields: DetectedField[];\n}\n","import type { DetectedField, GhostFillPromptField } from \"./types\";\n\nconst INPUT_SELECTORS = [\n \"input:not([type=hidden]):not([type=submit]):not([type=button]):not([type=reset]):not([type=image])\",\n \"textarea\",\n \"select\",\n // Custom dropdown triggers (Headless UI Listbox, Radix, etc.)\n \"button[role=combobox]\",\n \"[role=combobox]:not(input)\",\n \"button[aria-haspopup=listbox]\",\n].join(\", \");\n\n/** Find the label text for a form field */\nexport function findLabel(el: HTMLElement): string {\n // 1. Explicit <label for=\"id\">\n if (el.id) {\n const label = document.querySelector<HTMLLabelElement>(\n `label[for=\"${CSS.escape(el.id)}\"]`\n );\n if (label?.textContent?.trim()) return label.textContent.trim();\n }\n\n // 2. Wrapping <label>\n const parentLabel = el.closest(\"label\");\n if (parentLabel) {\n // Get text content excluding the input itself\n const clone = parentLabel.cloneNode(true) as HTMLElement;\n clone.querySelectorAll(\"input, textarea, select\").forEach((c) => c.remove());\n const text = clone.textContent?.trim();\n if (text) return text;\n }\n\n // 3. aria-label\n const ariaLabel = el.getAttribute(\"aria-label\");\n if (ariaLabel?.trim()) return ariaLabel.trim();\n\n // 4. aria-labelledby\n const labelledBy = el.getAttribute(\"aria-labelledby\");\n if (labelledBy) {\n const parts = labelledBy\n .split(/\\s+/)\n .map((id) => document.getElementById(id)?.textContent?.trim())\n .filter(Boolean);\n if (parts.length) return parts.join(\" \");\n }\n\n // 5. placeholder\n if (\"placeholder\" in el) {\n const ph = (el as HTMLInputElement).placeholder;\n if (ph?.trim()) return ph.trim();\n }\n\n // 6. title\n const title = el.getAttribute(\"title\");\n if (title?.trim()) return title.trim();\n\n // 7. Preceding sibling text (common pattern: <span>Label</span><input>)\n const prev = el.previousElementSibling;\n if (prev && ![\"INPUT\", \"TEXTAREA\", \"SELECT\"].includes(prev.tagName)) {\n const prevText = prev.textContent?.trim();\n if (prevText && prevText.length < 60) return prevText;\n }\n\n // 8. Parent's first text node or heading\n const parent = el.parentElement;\n if (parent) {\n // Look for a heading or label-like element before this input in the parent\n for (const child of Array.from(parent.children)) {\n if (child === el) break;\n if ([\"LABEL\", \"SPAN\", \"P\", \"H1\", \"H2\", \"H3\", \"H4\", \"H5\", \"H6\", \"LEGEND\", \"DIV\"].includes(child.tagName)) {\n const text = child.textContent?.trim();\n if (text && text.length < 60 && !child.querySelector(\"input, textarea, select\")) {\n return text;\n }\n }\n }\n }\n\n // 9. id attribute humanized\n if (el.id) return el.id.replace(/[_\\-]/g, \" \").replace(/([a-z])([A-Z])/g, \"$1 $2\").trim();\n\n // 10. name attribute as fallback\n const name = el.getAttribute(\"name\");\n if (name) return name.replace(/[_\\-[\\]]/g, \" \").replace(/([a-z])([A-Z])/g, \"$1 $2\").trim();\n\n return \"unknown\";\n}\n\n/** Detect all fillable fields within a container element */\nexport function detectFields(container: HTMLElement): DetectedField[] {\n const elements = container.querySelectorAll<\n HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement\n >(INPUT_SELECTORS);\n\n const fields: DetectedField[] = [];\n\n elements.forEach((el) => {\n // Skip disabled fields\n if ((el as HTMLInputElement).disabled) return;\n if ((el as HTMLInputElement).readOnly) return;\n // Skip hidden elements\n if (el.offsetParent === null && (el as HTMLInputElement).type !== \"hidden\") return;\n\n // Custom combobox / listbox (Headless UI, Radix, etc.)\n const isCustomDropdown =\n (el.getAttribute(\"role\") === \"combobox\" && !(el instanceof HTMLInputElement)) ||\n el.getAttribute(\"aria-haspopup\") === \"listbox\";\n if (isCustomDropdown) {\n // Find listbox options: aria-controls, sibling [role=listbox], or Headless UI pattern\n const listboxId = el.getAttribute(\"aria-controls\");\n let listbox: Element | null = listboxId ? document.getElementById(listboxId) : null;\n if (!listbox) {\n // Headless UI puts the listbox as a sibling inside the same relative container\n listbox = el.parentElement?.querySelector(\"[role=listbox]\") || null;\n }\n\n const options: string[] = [];\n if (listbox) {\n listbox.querySelectorAll(\"[role=option]\").forEach((opt) => {\n const text = opt.textContent?.trim();\n if (text) options.push(text);\n });\n }\n\n // Get current display value from button text\n const buttonText = el.textContent?.trim() || \"\";\n // Check if it looks like a placeholder\n const looksLikePlaceholder = buttonText.toLowerCase().startsWith(\"select\") || buttonText === \"\";\n\n const field: DetectedField = {\n element: el as HTMLElement,\n type: \"select\",\n name: el.id || el.getAttribute(\"name\") || \"\",\n label: findLabel(el),\n required: el.getAttribute(\"aria-required\") === \"true\",\n currentValue: looksLikePlaceholder ? \"\" : buttonText,\n options: options.length > 0 ? options : undefined,\n };\n fields.push(field);\n return;\n }\n\n const field: DetectedField = {\n element: el as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement,\n type: el instanceof HTMLSelectElement ? \"select\" : (el as HTMLInputElement).type || \"text\",\n name: (el as HTMLInputElement).name || el.id || \"\",\n label: findLabel(el),\n required: (el as HTMLInputElement).required || el.getAttribute(\"aria-required\") === \"true\",\n currentValue: (el as HTMLInputElement).value,\n };\n\n // Collect select options\n if (el instanceof HTMLSelectElement) {\n field.options = Array.from(el.options)\n .filter((opt) => opt.value && !opt.disabled)\n .map((opt) => opt.textContent?.trim() || opt.value);\n }\n\n // Collect constraints\n if (\"min\" in el && (el as HTMLInputElement).min) {\n field.min = (el as HTMLInputElement).min;\n }\n if (\"max\" in el && (el as HTMLInputElement).max) {\n field.max = (el as HTMLInputElement).max;\n }\n if (\"pattern\" in el && (el as HTMLInputElement).pattern) {\n field.pattern = (el as HTMLInputElement).pattern;\n }\n\n fields.push(field);\n });\n\n return fields;\n}\n\n/** Build a description of fields for the AI prompt */\nexport function describeFields(\n fields: Array<\n Pick<\n DetectedField | GhostFillPromptField,\n \"type\" | \"label\" | \"required\" | \"options\" | \"min\" | \"max\" | \"pattern\"\n >\n >\n): string {\n return fields\n .map((f, i) => {\n let desc = `[${i}] \"${f.label}\" (type: ${f.type}`;\n if (f.required) desc += \", required\";\n if (f.options?.length) desc += `, options: [${f.options.join(\", \")}]`;\n if (f.min) desc += `, min: ${f.min}`;\n if (f.max) desc += `, max: ${f.max}`;\n if (f.pattern) desc += `, pattern: ${f.pattern}`;\n desc += \")\";\n return desc;\n })\n .join(\"\\n\");\n}\n","import { describeFields } from \"./detector\";\nimport type {\n DetectedField,\n FieldFillData,\n GhostFillAIOptions,\n GhostFillAIRequest,\n GhostFillPromptField,\n Provider,\n} from \"./types\";\n\nexport const SYSTEM_PROMPT = `You are a form-filling assistant. Given a list of form fields and an optional user prompt, generate realistic fake data to fill ALL fields.\n\nRules:\n- Return ONLY a JSON object with a \"fields\" array of objects, each with \"index\" and \"value\" keys\n- You MUST fill EVERY field — do not skip any\n- Match the field type (email → valid email, phone → valid phone with country code, date → YYYY-MM-DD, datetime-local → YYYY-MM-DDTHH:MM, etc.)\n- For select/dropdown fields: you MUST pick one of the listed options EXACTLY as written\n- For checkboxes: add a \"checked\" boolean (true or false)\n- For radio buttons: only fill one per group, use the option value\n- Respect min/max constraints and patterns\n- Generate contextually coherent data (same person's name, matching city/state/zip, etc.)\n- If no user prompt is given, infer appropriate data from the field labels\n- Do NOT request or rely on secrets, tokens, passwords, or existing form values\n- Do NOT wrap the JSON in markdown code blocks — return raw JSON only`;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction toFieldFillData(value: unknown): FieldFillData {\n if (!isRecord(value)) {\n throw new Error(\"AI response item is not an object\");\n }\n\n const index = value.index;\n const rawValue = value.value;\n const checked = value.checked;\n\n if (typeof index !== \"number\" || !Number.isInteger(index)) {\n throw new Error(\"AI response item is missing a numeric index\");\n }\n\n if (typeof rawValue !== \"string\") {\n throw new Error(\"AI response item is missing a string value\");\n }\n\n return {\n index,\n value: rawValue,\n checked: typeof checked === \"boolean\" ? checked : undefined,\n };\n}\n\nexport function toPromptFields(fields: DetectedField[]): GhostFillPromptField[] {\n return fields.map((field, index) => ({\n index,\n type: field.type,\n name: field.name,\n label: field.label,\n options: field.options,\n required: field.required,\n min: field.min,\n max: field.max,\n pattern: field.pattern,\n }));\n}\n\nexport function buildFillMessages(\n request: GhostFillAIRequest\n): Array<{ role: \"system\" | \"user\"; content: string }> {\n const fieldDescription = describeFields(request.fields);\n let userContent = `Form fields:\\n${fieldDescription}`;\n\n if (request.prompt) {\n userContent += `\\n\\nUser instructions: ${request.prompt}`;\n } else {\n userContent +=\n \"\\n\\nNo specific instructions — generate realistic, contextually appropriate data for all fields.\";\n }\n\n return [\n {\n role: \"system\",\n content: request.systemPrompt\n ? `${request.systemPrompt}\\n\\n${SYSTEM_PROMPT}`\n : SYSTEM_PROMPT,\n },\n {\n role: \"user\",\n content: userContent,\n },\n ];\n}\n\nexport function parseFillDataPayload(payload: unknown): FieldFillData[] {\n if (typeof payload === \"string\") {\n const jsonMatch =\n payload.match(/```(?:json)?\\s*([\\s\\S]*?)```/) || [null, payload];\n return parseFillDataPayload(JSON.parse(jsonMatch[1]!.trim()));\n }\n\n if (Array.isArray(payload)) {\n return payload.map(toFieldFillData);\n }\n\n if (isRecord(payload)) {\n if (Array.isArray(payload.choices)) {\n const content = (payload.choices[0] as Record<string, unknown> | undefined)\n ?.message as Record<string, unknown> | undefined;\n if (typeof content?.content === \"string\") {\n return parseFillDataPayload(content.content);\n }\n }\n\n const candidate =\n payload.fields ?? payload.data ?? payload.items ?? payload.result;\n if (Array.isArray(candidate)) {\n return candidate.map(toFieldFillData);\n }\n }\n\n throw new Error(\"AI response is not an array of field fills\");\n}\n\nfunction createRequest(\n fields: DetectedField[],\n userPrompt: string,\n provider: Provider,\n systemPrompt?: string\n): GhostFillAIRequest {\n return {\n provider,\n prompt: userPrompt,\n systemPrompt,\n fields: toPromptFields(fields),\n };\n}\n\n/** Call a secure backend or callback to generate fill data. */\nexport async function generateFillData(\n fields: DetectedField[],\n userPrompt: string,\n provider: Provider,\n transport: GhostFillAIOptions,\n systemPrompt?: string\n): Promise<FieldFillData[]> {\n const request = createRequest(fields, userPrompt, provider, systemPrompt);\n\n if (transport.requestFillData) {\n return transport.requestFillData(request);\n }\n\n const endpoint = transport.endpoint ?? \"/api/ghostfill\";\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(request),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`AI API error (${response.status}): ${error}`);\n }\n\n const contentType = response.headers.get(\"content-type\") || \"\";\n const payload = contentType.includes(\"application/json\")\n ? await response.json()\n : await response.text();\n\n return parseFillDataPayload(payload);\n}\n"],"mappings":"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","names":["field"]}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { G as GhostFillAIOptions, P as Provider, a as GhostFillOptions } from './types-
|
|
2
|
-
export { D as DetectedField, F as FieldFillData, b as GhostFillAIRequest, c as GhostFillPromptField, d as PROVIDERS } from './types-
|
|
1
|
+
import { G as GhostFillAIOptions, P as Provider, a as GhostFillOptions } from './types-BYkjRHCV.mjs';
|
|
2
|
+
export { D as DetectedField, F as FieldFillData, b as GhostFillAIRequest, c as GhostFillPromptField, d as PROVIDERS } from './types-BYkjRHCV.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Initialize GhostFill — adds a floating ghost icon to the page.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { G as GhostFillAIOptions, P as Provider, a as GhostFillOptions } from './types-
|
|
2
|
-
export { D as DetectedField, F as FieldFillData, b as GhostFillAIRequest, c as GhostFillPromptField, d as PROVIDERS } from './types-
|
|
1
|
+
import { G as GhostFillAIOptions, P as Provider, a as GhostFillOptions } from './types-BYkjRHCV.js';
|
|
2
|
+
export { D as DetectedField, F as FieldFillData, b as GhostFillAIRequest, c as GhostFillPromptField, d as PROVIDERS } from './types-BYkjRHCV.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Initialize GhostFill — adds a floating ghost icon to the page.
|
package/dist/index.js
CHANGED
|
@@ -214,8 +214,8 @@ function generateForField(field, context) {
|
|
|
214
214
|
return `https://www.${context.company.toLowerCase().replace(/\s+/g, "")}.com`;
|
|
215
215
|
}
|
|
216
216
|
if (field.type === "number" || field.type === "range") {
|
|
217
|
-
const min = field.min ? parseInt(field.min) : 1;
|
|
218
|
-
const max = field.max ? parseInt(field.max) : 100;
|
|
217
|
+
const min = field.min ? parseInt(field.min, 10) || 1 : 1;
|
|
218
|
+
const max = field.max ? parseInt(field.max, 10) || 100 : 100;
|
|
219
219
|
return String(randInt(min, max));
|
|
220
220
|
}
|
|
221
221
|
if (label.includes("first name") || label.includes("firstname")) return context.firstName;
|
|
@@ -275,14 +275,96 @@ function focusBlur(el) {
|
|
|
275
275
|
}
|
|
276
276
|
async function fillCustomSelect(button, value) {
|
|
277
277
|
button.click();
|
|
278
|
-
|
|
278
|
+
let waited = 0;
|
|
279
|
+
const step = 50;
|
|
280
|
+
while (waited < 500) {
|
|
281
|
+
await new Promise((r) => setTimeout(r, step));
|
|
282
|
+
waited += step;
|
|
283
|
+
const lb = button.getAttribute("aria-controls") ? document.getElementById(button.getAttribute("aria-controls")) : button.parentElement?.querySelector("[role=listbox]") || document.querySelector("[role=listbox]");
|
|
284
|
+
if (lb) break;
|
|
285
|
+
const cont = button.closest("[class*='relative']") || button.parentElement;
|
|
286
|
+
if (cont) {
|
|
287
|
+
const divs = cont.querySelectorAll("div");
|
|
288
|
+
for (const d of divs) {
|
|
289
|
+
if (d === button || d.contains(button) || button.contains(d)) continue;
|
|
290
|
+
if (d.classList.toString().includes("absolute") || d.classList.toString().includes("z-50")) {
|
|
291
|
+
waited = 999;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
279
297
|
const listboxId = button.getAttribute("aria-controls");
|
|
280
298
|
let listbox = listboxId ? document.getElementById(listboxId) : null;
|
|
281
299
|
if (!listbox) {
|
|
282
|
-
listbox = button.parentElement?.querySelector("[role=listbox]") ||
|
|
300
|
+
listbox = button.parentElement?.querySelector("[role=listbox]") || null;
|
|
301
|
+
}
|
|
302
|
+
if (!listbox) {
|
|
303
|
+
const all = document.querySelectorAll("[role=listbox]");
|
|
304
|
+
for (const lb of all) {
|
|
305
|
+
if (lb.offsetParent !== null || lb.offsetHeight > 0) {
|
|
306
|
+
listbox = lb;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
let options = listbox ? listbox.querySelectorAll("[role=option]") : [];
|
|
312
|
+
if (!listbox || options.length === 0) {
|
|
313
|
+
let panel = null;
|
|
314
|
+
const container = button.closest("[class*='relative']") || button.parentElement;
|
|
315
|
+
if (container) {
|
|
316
|
+
for (const child of Array.from(container.children)) {
|
|
317
|
+
if (child === button || child.contains(button)) continue;
|
|
318
|
+
if (child.tagName === "LABEL" || child.tagName === "P" || child.tagName === "SPAN") continue;
|
|
319
|
+
const el = child;
|
|
320
|
+
const style = window.getComputedStyle(el);
|
|
321
|
+
if (el.classList.contains("absolute") || el.classList.contains("z-50") || style.position === "absolute" || style.position === "fixed") {
|
|
322
|
+
panel = el;
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (!panel) {
|
|
328
|
+
let sibling = button.nextElementSibling;
|
|
329
|
+
while (sibling) {
|
|
330
|
+
const tag = sibling.tagName;
|
|
331
|
+
if (tag !== "P" && tag !== "LABEL" && tag !== "SPAN" && tag === "DIV") {
|
|
332
|
+
panel = sibling;
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
sibling = sibling.nextElementSibling;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (panel && panel !== button) {
|
|
339
|
+
let contentWait = 0;
|
|
340
|
+
while (contentWait < 1e3) {
|
|
341
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
342
|
+
contentWait += 100;
|
|
343
|
+
const btns = panel.querySelectorAll("button");
|
|
344
|
+
let realBtns = 0;
|
|
345
|
+
for (const b of btns) {
|
|
346
|
+
if (!b.querySelector("input")) realBtns++;
|
|
347
|
+
}
|
|
348
|
+
if (realBtns > 0) break;
|
|
349
|
+
}
|
|
350
|
+
const clickables = panel.querySelectorAll("button, [role=option], [data-value], li");
|
|
351
|
+
const filtered = [];
|
|
352
|
+
for (const c of clickables) {
|
|
353
|
+
const text = c.textContent?.trim() || "";
|
|
354
|
+
if (!text || text === "\xD7" || text === "\u2715") continue;
|
|
355
|
+
if (c.querySelector("input")) continue;
|
|
356
|
+
if (c === button) continue;
|
|
357
|
+
const hasInput = c.querySelector("input[type='text'], input[type='search']");
|
|
358
|
+
if (hasInput) continue;
|
|
359
|
+
filtered.push(c);
|
|
360
|
+
}
|
|
361
|
+
if (filtered.length > 0) {
|
|
362
|
+
options = filtered;
|
|
363
|
+
listbox = panel;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
283
366
|
}
|
|
284
|
-
if (
|
|
285
|
-
const options = listbox.querySelectorAll("[role=option]");
|
|
367
|
+
if (options.length > 0) {
|
|
286
368
|
for (const opt of options) {
|
|
287
369
|
const text = opt.textContent?.trim();
|
|
288
370
|
if (text === value || text?.toLowerCase() === value.toLowerCase()) {
|
|
@@ -301,7 +383,7 @@ async function fillCustomSelect(button, value) {
|
|
|
301
383
|
}
|
|
302
384
|
for (const opt of options) {
|
|
303
385
|
const text = (opt.textContent?.trim() || "").toLowerCase();
|
|
304
|
-
const isPlaceholder = text.startsWith("select") || text === "" || text === "---" || text === "choose" || text.startsWith("choose");
|
|
386
|
+
const isPlaceholder = text.startsWith("select") || text === "" || text === "---" || text === "choose" || text.startsWith("choose") || text.startsWith("search");
|
|
305
387
|
if (!isPlaceholder) {
|
|
306
388
|
opt.click();
|
|
307
389
|
await new Promise((r) => setTimeout(r, 50));
|
|
@@ -354,7 +436,7 @@ async function fillFields(fields, fillData) {
|
|
|
354
436
|
`input[type="radio"][name="${CSS.escape(radio.name)}"]`
|
|
355
437
|
);
|
|
356
438
|
for (const r of group) {
|
|
357
|
-
if (r.value === item.value ||
|
|
439
|
+
if (r.value === item.value || findLabel(r) === item.value) {
|
|
358
440
|
const nativeCheckedSetter = Object.getOwnPropertyDescriptor(
|
|
359
441
|
HTMLInputElement.prototype,
|
|
360
442
|
"checked"
|
|
@@ -423,26 +505,11 @@ async function fillFields(fields, fillData) {
|
|
|
423
505
|
}
|
|
424
506
|
return { filled, errors };
|
|
425
507
|
}
|
|
426
|
-
function findLabel2(el) {
|
|
427
|
-
if (el.id) {
|
|
428
|
-
const label = document.querySelector(
|
|
429
|
-
`label[for="${CSS.escape(el.id)}"]`
|
|
430
|
-
);
|
|
431
|
-
if (label?.textContent?.trim()) return label.textContent.trim();
|
|
432
|
-
}
|
|
433
|
-
const parent = el.closest("label");
|
|
434
|
-
if (parent) {
|
|
435
|
-
const clone = parent.cloneNode(true);
|
|
436
|
-
clone.querySelectorAll("input").forEach((c) => c.remove());
|
|
437
|
-
return clone.textContent?.trim() || "";
|
|
438
|
-
}
|
|
439
|
-
return "";
|
|
440
|
-
}
|
|
441
508
|
|
|
442
509
|
// src/selector.ts
|
|
443
510
|
var currentHighlight = null;
|
|
444
511
|
var overlay = null;
|
|
445
|
-
function
|
|
512
|
+
function createSelectorOverlay(color) {
|
|
446
513
|
const div = document.createElement("div");
|
|
447
514
|
div.id = "ghostfill-selector-overlay";
|
|
448
515
|
const r = parseInt(color.slice(1, 3), 16);
|
|
@@ -461,7 +528,7 @@ function createOverlay(color) {
|
|
|
461
528
|
return div;
|
|
462
529
|
}
|
|
463
530
|
function positionOverlay(el, color) {
|
|
464
|
-
if (!overlay) overlay =
|
|
531
|
+
if (!overlay) overlay = createSelectorOverlay(color);
|
|
465
532
|
const rect = el.getBoundingClientRect();
|
|
466
533
|
Object.assign(overlay.style, {
|
|
467
534
|
top: `${rect.top}px`,
|
|
@@ -533,6 +600,9 @@ function startSelection(onSelect, onCancel, ghostfillRoot, highlightColor = "#63
|
|
|
533
600
|
return cleanup;
|
|
534
601
|
}
|
|
535
602
|
|
|
603
|
+
// package.json
|
|
604
|
+
var version = "0.2.4";
|
|
605
|
+
|
|
536
606
|
// src/overlay.ts
|
|
537
607
|
var STORAGE_KEY = "ghostfill_settings";
|
|
538
608
|
var POS_KEY = "ghostfill_pos";
|
|
@@ -757,7 +827,7 @@ var CSS2 = `
|
|
|
757
827
|
font-size: 13px; font-weight: 600; color: #fff;
|
|
758
828
|
letter-spacing: -0.0094em;
|
|
759
829
|
}
|
|
760
|
-
.gf-pop-header .gf-slash { color:
|
|
830
|
+
.gf-pop-header .gf-slash { color: #6366f1; }
|
|
761
831
|
.gf-pop-header .gf-header-right {
|
|
762
832
|
display: flex; align-items: center; gap: 6px;
|
|
763
833
|
}
|
|
@@ -1022,7 +1092,7 @@ var CSS2 = `
|
|
|
1022
1092
|
line-height: 1.5;
|
|
1023
1093
|
}
|
|
1024
1094
|
`;
|
|
1025
|
-
function
|
|
1095
|
+
function createOverlay(options) {
|
|
1026
1096
|
const aiConfig = options.ai || null;
|
|
1027
1097
|
const saved = loadSettings(aiConfig?.provider || "openai");
|
|
1028
1098
|
if (options.apiKey) {
|
|
@@ -1043,9 +1113,7 @@ function createOverlay2(options) {
|
|
|
1043
1113
|
active: false,
|
|
1044
1114
|
selecting: false,
|
|
1045
1115
|
selectedBlock: null,
|
|
1046
|
-
fields: []
|
|
1047
|
-
overlay: host,
|
|
1048
|
-
shadowRoot: shadow
|
|
1116
|
+
fields: []
|
|
1049
1117
|
};
|
|
1050
1118
|
const bar = document.createElement("div");
|
|
1051
1119
|
bar.className = "gf-bar";
|
|
@@ -1192,7 +1260,7 @@ function createOverlay2(options) {
|
|
|
1192
1260
|
<div class="gf-pop-header">
|
|
1193
1261
|
<h3><span class="gf-slash">/</span>ghostfill</h3>
|
|
1194
1262
|
<div class="gf-header-right">
|
|
1195
|
-
<span class="gf-version">
|
|
1263
|
+
<span class="gf-version">v${version}</span>
|
|
1196
1264
|
<button class="gf-theme-btn" id="gf-s-theme" title="Toggle theme">
|
|
1197
1265
|
${saved.theme === "dark" ? ICONS.sun : ICONS.moon}
|
|
1198
1266
|
</button>
|
|
@@ -2122,7 +2190,7 @@ function init(options = {}) {
|
|
|
2122
2190
|
instance.destroy();
|
|
2123
2191
|
instance = null;
|
|
2124
2192
|
}
|
|
2125
|
-
const { state, destroy } =
|
|
2193
|
+
const { state, destroy } = createOverlay(options);
|
|
2126
2194
|
instance = { destroy };
|
|
2127
2195
|
return { destroy };
|
|
2128
2196
|
}
|