初始化
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* 选择器识别模块
|
||||
* 负责:检测表单字段是否为选择器类型(下拉、日期、级联等)
|
||||
* 检测方式:
|
||||
* 1. UI组件库类名匹配(快速路径)
|
||||
* 2. 提示文字检测("请选择"等)
|
||||
* 3. 主动点击检测:点击 input 及其单子标签父级链,对比点击前后 DOM 变化判断是否有弹出层
|
||||
*/
|
||||
|
||||
import type { MatchedFormField, UILibPickerConfig } from "./types"
|
||||
import { UI_LIB_PICKER_CONFIGS } from "./constants"
|
||||
import { delay } from "./autofill"
|
||||
|
||||
/** 弹出层相关的 CSS 选择器 */
|
||||
const POPUP_SELECTORS = [
|
||||
'[class*="dropdown"]', '[class*="popup"]', '[class*="popper"]',
|
||||
'[class*="picker-panel"]', '[class*="overlay"]', '[class*="popover"]',
|
||||
'[class*="select-dropdown"]', '[class*="cascader"]',
|
||||
'[class*="menu"][class*="content"]',
|
||||
'[role="listbox"]', '[role="menu"]',
|
||||
]
|
||||
|
||||
/** 通过提示文字判断是否为选择器 */
|
||||
function hasPickerHintText(field: MatchedFormField, lang: "zh" | "en"): boolean {
|
||||
const hints = lang === "zh"
|
||||
? ["请选择", "请输入并选择", "点击选择"]
|
||||
: ["Please select", "Choose", "Click to select", "Pick a"]
|
||||
|
||||
if (field.inputElement) {
|
||||
const placeholder = field.inputElement.getAttribute("placeholder") || ""
|
||||
if (hints.some((h) => placeholder.includes(h))) return true
|
||||
if (field.inputElement.hasAttribute("readonly")) {
|
||||
let parent: Element | null = field.inputElement.parentElement
|
||||
for (let i = 0; i < 3 && parent; i++) {
|
||||
const cls = parent.className || ""
|
||||
if (typeof cls === "string" && (
|
||||
cls.includes("select") || cls.includes("picker") || cls.includes("date") ||
|
||||
cls.includes("cascader") || cls.includes("dropdown") || cls.includes("calendar")
|
||||
)) return true
|
||||
parent = parent.parentElement
|
||||
}
|
||||
}
|
||||
// 检查紧邻兄弟元素的文字
|
||||
for (const sib of [field.inputElement.nextElementSibling, field.inputElement.previousElementSibling]) {
|
||||
if (sib) {
|
||||
const text = sib.textContent?.trim() || ""
|
||||
if (text.length < 30 && hints.some((h) => text.includes(h))) return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** 检查 input 附近是否有已知 UI 组件库的选择器触发区域类名 */
|
||||
function matchUILibTrigger(field: MatchedFormField): UILibPickerConfig | null {
|
||||
const classSet = new Set<string>()
|
||||
let el: Element | null = field.inputElement || field.labelElement
|
||||
for (let i = 0; i < 5 && el; i++) {
|
||||
if (el.className && typeof el.className === "string") {
|
||||
el.className.trim().split(/\s+/).forEach((c) => classSet.add(c))
|
||||
}
|
||||
el = el.parentElement
|
||||
}
|
||||
if (field.buttonElement?.className && typeof field.buttonElement.className === "string") {
|
||||
field.buttonElement.className.trim().split(/\s+/).forEach((c) => classSet.add(c))
|
||||
}
|
||||
for (const lib of UI_LIB_PICKER_CONFIGS) {
|
||||
if (lib.triggerClasses.some((cls) => classSet.has(cls))) return lib
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** 记录当前页面所有可见弹出层和 body 直接子元素 */
|
||||
function snapshotDOM(): { bodyChildren: Set<Element>; visiblePopups: Set<Element> } {
|
||||
const bodyChildren = new Set(Array.from(document.body.children))
|
||||
const visiblePopups = new Set<Element>()
|
||||
for (const sel of POPUP_SELECTORS) {
|
||||
document.querySelectorAll(sel).forEach((el) => {
|
||||
const htmlEl = el as HTMLElement
|
||||
if (htmlEl.offsetHeight > 0 && htmlEl.offsetWidth > 0) visiblePopups.add(el)
|
||||
})
|
||||
}
|
||||
return { bodyChildren, visiblePopups }
|
||||
}
|
||||
|
||||
/** 对比 DOM 快照,检测是否有新增弹出层 */
|
||||
function hasNewPopup(before: ReturnType<typeof snapshotDOM>): boolean {
|
||||
// 检查 body 下新增的直接子元素
|
||||
for (const child of Array.from(document.body.children)) {
|
||||
if (!before.bodyChildren.has(child) && (child as HTMLElement).offsetHeight > 10) return true
|
||||
}
|
||||
// 检查新变为可见的弹出层
|
||||
for (const sel of POPUP_SELECTORS) {
|
||||
const els = document.querySelectorAll(sel)
|
||||
for (const el of Array.from(els)) {
|
||||
const htmlEl = el as HTMLElement
|
||||
if (htmlEl.offsetHeight > 10 && htmlEl.offsetWidth > 0 && !before.visiblePopups.has(el)) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** 关闭可能打开的弹出层 */
|
||||
async function dismissPopup() {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { key: "Escape", code: "Escape", keyCode: 27, bubbles: true })
|
||||
)
|
||||
document.activeElement.blur()
|
||||
}
|
||||
document.body.click()
|
||||
await delay(200)
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动点击检测:点击 input 及其单子标签父级链,对比 DOM 变化判断是否有弹出层
|
||||
* 如果检测到弹出层,关闭后返回 true
|
||||
*/
|
||||
async function detectByClick(field: MatchedFormField): Promise<boolean> {
|
||||
if (!field.inputElement) return false
|
||||
|
||||
// 收集要点击的元素:input 本身 + 单子标签父级链(最多5层)
|
||||
const clickTargets: HTMLElement[] = [field.inputElement as HTMLElement]
|
||||
let current: HTMLElement | null = field.inputElement as HTMLElement
|
||||
for (let i = 0; i < 5 && current?.parentElement; i++) {
|
||||
const parentEl = current.parentElement
|
||||
if (parentEl.children.length <= 2) {
|
||||
clickTargets.push(parentEl)
|
||||
current = parentEl
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个尝试点击,检测 DOM 变化
|
||||
for (const target of clickTargets) {
|
||||
const before = snapshotDOM()
|
||||
target.click()
|
||||
await delay(200)
|
||||
|
||||
if (hasNewPopup(before)) {
|
||||
console.log(`OfferPie: [${field.key}] 点击 ${target.tagName}.${(target.className || "").toString().split(" ")[0]} 后检测到弹出层`)
|
||||
await dismissPopup()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 没检测到弹出层,确保关闭焦点状态
|
||||
await dismissPopup()
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测单个字段是否为数据选择器类型
|
||||
* 优先级:1.UI组件库类名 → 2.提示文字 → 3.主动点击检测DOM变化
|
||||
*/
|
||||
export async function detectPickerField(field: MatchedFormField, lang: "zh" | "en"): Promise<void> {
|
||||
if (field.inputType === "radio") return
|
||||
|
||||
// 方式1:UI 组件库类名匹配
|
||||
const matchedLib = matchUILibTrigger(field)
|
||||
if (matchedLib) {
|
||||
console.log(`OfferPie: [${field.key}] 匹配到 UI 组件库: ${matchedLib.libName}`)
|
||||
field.isPicker = true
|
||||
field.inputType = "picker"
|
||||
field.pickerDropdownSelector = matchedLib.optionItemClasses.map((cls) => "." + CSS.escape(cls)).join(", ")
|
||||
return
|
||||
}
|
||||
|
||||
// 方式2:提示文字检测
|
||||
if (hasPickerHintText(field, lang)) {
|
||||
console.log(`OfferPie: [${field.key}] 有选择器提示文字,标记为 picker`)
|
||||
field.isPicker = true
|
||||
field.inputType = "picker"
|
||||
return
|
||||
}
|
||||
|
||||
// 方式3:主动点击检测 DOM 变化
|
||||
const detected = await detectByClick(field)
|
||||
if (detected) {
|
||||
console.log(`OfferPie: [${field.key}] 通过点击检测到弹出层,标记为 picker`)
|
||||
field.isPicker = true
|
||||
field.inputType = "picker"
|
||||
return
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user