优化选择器操作逻辑,通用模式代码单独封装
This commit is contained in:
+67
-32
@@ -4,22 +4,21 @@
|
||||
* 检测方式:
|
||||
* 1. UI组件库类名匹配(快速路径)
|
||||
* 2. 提示文字检测("请选择"等)
|
||||
* 3. 主动点击检测:点击 input 及其单子标签父级链,对比点击前后 DOM 变化判断是否有弹出层
|
||||
* 3. 主动点击检测:点击 input 及其单子标签父级链,对比点击前后全页面 DOM 可见性变化判断是否有弹出层
|
||||
*
|
||||
* 【注意】方式3 的核心机制:
|
||||
* - 点击前记录全页面所有可见元素快照(offsetHeight > 0 && offsetWidth > 0)
|
||||
* - 点击后对比哪些元素是新变为可见的,以此判断弹出层出现
|
||||
* - 检测到弹出层后【不关闭】,保存弹出层 DOM 引用到 field.pickerDropdownElement
|
||||
* - fillPickerField 步骤0 直接在已有弹出层内搜索选项,避免二次点击导致 toggle 关闭
|
||||
* - 禁止使用固定的 CSS 类名选择器(如 POPUP_SELECTORS)来检测弹出层,必须走纯 DOM 差异对比
|
||||
* - 这和 fillPickerField 步骤1/3 的 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"
|
||||
@@ -70,32 +69,33 @@ function matchUILibTrigger(field: MatchedFormField): UILibPickerConfig | null {
|
||||
return null
|
||||
}
|
||||
|
||||
/** 记录当前页面所有可见弹出层和 body 直接子元素 */
|
||||
function snapshotDOM(): { bodyChildren: Set<Element>; visiblePopups: Set<Element> } {
|
||||
/**
|
||||
* 记录当前 body 直接子元素 + 全页面可见元素快照(用于点击前后 DOM 差异对比)
|
||||
* 【注意】不使用任何固定 CSS 选择器,记录全页面所有可见元素,保证通用性
|
||||
* offsetHeight/offsetWidth > 0 即视为可见,不设更高阈值(避免漏判小尺寸弹出层)
|
||||
*/
|
||||
function snapshotDOM(): { bodyChildren: Set<Element>; visibleEls: 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 }
|
||||
// 记录所有可见的元素(offsetHeight > 0 且 offsetWidth > 0)
|
||||
const visibleEls = new Set<Element>()
|
||||
document.querySelectorAll("*").forEach((el) => {
|
||||
const htmlEl = el as HTMLElement
|
||||
if (htmlEl.offsetHeight > 0 && htmlEl.offsetWidth > 0) visibleEls.add(el)
|
||||
})
|
||||
return { bodyChildren, visibleEls }
|
||||
}
|
||||
|
||||
/** 对比 DOM 快照,检测是否有新增弹出层 */
|
||||
/** 对比 DOM 快照,检测是否有新出现的可见元素(弹出层) */
|
||||
function hasNewPopup(before: ReturnType<typeof snapshotDOM>): boolean {
|
||||
// 检查 body 下新增的直接子元素
|
||||
// 检查 body 下新增的直接子元素(offsetHeight > 0 才算有效弹出层)
|
||||
for (const child of Array.from(document.body.children)) {
|
||||
if (!before.bodyChildren.has(child) && (child as HTMLElement).offsetHeight > 10) return true
|
||||
if (!before.bodyChildren.has(child) && (child as HTMLElement).offsetHeight > 0) 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
|
||||
}
|
||||
// 检查新变为可见的元素(点击前不可见,点击后可见)
|
||||
const allEls = document.querySelectorAll("*")
|
||||
for (const el of Array.from(allEls)) {
|
||||
const htmlEl = el as HTMLElement
|
||||
if (htmlEl.offsetHeight > 0 && htmlEl.offsetWidth > 0 && !before.visibleEls.has(el)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -114,7 +114,13 @@ async function dismissPopup() {
|
||||
|
||||
/**
|
||||
* 主动点击检测:点击 input 及其单子标签父级链,对比 DOM 变化判断是否有弹出层
|
||||
* 如果检测到弹出层,关闭后返回 true
|
||||
* 如果检测到弹出层,【不关闭弹出层】,将弹出层 DOM 引用保存到 field.pickerDropdownElement,返回 true
|
||||
* 点击选择后弹出层会自行关闭,无需手动 dismiss
|
||||
*
|
||||
* 【注意】此处不可调用 dismissPopup 关闭弹出层!原因:
|
||||
* - fillPickerField 步骤0 需要直接在已有弹出层内搜索选项
|
||||
* - 如果这里关闭了弹出层,步骤2 再点击会触发 toggle 导致弹出层反而不展开
|
||||
* - Phoenix 等组件的弹出层用 Escape/blur/body.click 关闭不可靠,会引发状态不一致
|
||||
*/
|
||||
async function detectByClick(field: MatchedFormField): Promise<boolean> {
|
||||
if (!field.inputElement) return false
|
||||
@@ -140,7 +146,12 @@ async function detectByClick(field: MatchedFormField): Promise<boolean> {
|
||||
|
||||
if (hasNewPopup(before)) {
|
||||
console.log(`OfferPie: [${field.key}] 点击 ${target.tagName}.${(target.className || "").toString().split(" ")[0]} 后检测到弹出层`)
|
||||
await dismissPopup()
|
||||
// 【注意】不关闭弹出层,保存弹出层 DOM 引用供 fillPickerField 步骤0 使用
|
||||
const popupEl = findNewPopupElement(before)
|
||||
if (popupEl) {
|
||||
field.pickerDropdownElement = popupEl
|
||||
console.log(`OfferPie: [${field.key}] 保存弹出层引用: ${popupEl.tagName}.${(popupEl.className || "").toString().split(" ")[0]} offsetHeight=${popupEl.offsetHeight}`)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -150,6 +161,30 @@ async function detectByClick(field: MatchedFormField): Promise<boolean> {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 DOM 快照对比中找到新出现的弹出层元素
|
||||
* 逻辑和 fillPickerField 步骤3 一致:找点击后新变为可见的最大元素
|
||||
*/
|
||||
function findNewPopupElement(before: ReturnType<typeof snapshotDOM>): HTMLElement | null {
|
||||
// 优先:body 下新增的、有实际高度的直接子元素
|
||||
for (const child of Array.from(document.body.children)) {
|
||||
if (!before.bodyChildren.has(child) && (child as HTMLElement).offsetHeight > 0) {
|
||||
return child as HTMLElement
|
||||
}
|
||||
}
|
||||
// 其次:全页面找新变为可见的元素,取面积最大的那个
|
||||
let best: HTMLElement | null = null
|
||||
let bestArea = 0
|
||||
document.querySelectorAll("*").forEach((el) => {
|
||||
const htmlEl = el as HTMLElement
|
||||
if (htmlEl.offsetHeight > 0 && htmlEl.offsetWidth > 0 && !before.visibleEls.has(el)) {
|
||||
const area = htmlEl.offsetHeight * htmlEl.offsetWidth
|
||||
if (area > bestArea) { best = htmlEl; bestArea = area }
|
||||
}
|
||||
})
|
||||
return best
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测单个字段是否为数据选择器类型
|
||||
* 优先级:1.UI组件库类名 → 2.提示文字 → 3.主动点击检测DOM变化
|
||||
|
||||
Reference in New Issue
Block a user