Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useSearchParams } from 'react-router'
|
||||
|
||||
interface DeepLinkHighlightOptions {
|
||||
param: string
|
||||
ready: (target: string) => boolean
|
||||
elementId: (target: string) => string
|
||||
onResolve?: (target: string) => void
|
||||
block?: ScrollLogicalPosition
|
||||
}
|
||||
|
||||
// react-router's useSearchParams throws with no router context. Inside Settings
|
||||
// (every original caller) there always is one, so behavior is unchanged; when a
|
||||
// consumer is embedded OUTSIDE the router (e.g. McpTab in a plugin dialog) there
|
||||
// is none, and this degrades to an inert [empty params, no-op setter] instead of
|
||||
// crashing. Router presence is stable for a mounted instance's lifetime, so the
|
||||
// try/catch never changes the hook count between renders (rules-of-hooks safe).
|
||||
function useOptionalSearchParams(): ReturnType<typeof useSearchParams> {
|
||||
try {
|
||||
return useSearchParams()
|
||||
} catch {
|
||||
return [new URLSearchParams(), () => undefined]
|
||||
}
|
||||
}
|
||||
|
||||
// Deep-link from the command palette (?<param>=<id>): once the target row is
|
||||
// renderable, scroll it into view and flash it, then drop the param so it
|
||||
// doesn't re-fire. Returns the pending target (null once consumed) so callers
|
||||
// can force the row open before it mounts.
|
||||
export function useDeepLinkHighlight({
|
||||
param,
|
||||
ready,
|
||||
elementId,
|
||||
onResolve,
|
||||
block = 'center'
|
||||
}: DeepLinkHighlightOptions): null | string {
|
||||
const [searchParams, setSearchParams] = useOptionalSearchParams()
|
||||
const target = searchParams.get(param)
|
||||
|
||||
useEffect(() => {
|
||||
if (!target || !ready(target)) {
|
||||
return
|
||||
}
|
||||
|
||||
onResolve?.(target)
|
||||
|
||||
let cancelled = false
|
||||
let timer = 0
|
||||
|
||||
// onResolve may flip view state that mounts the row a few frames later, so
|
||||
// poll briefly for it and only drop the param AFTER a successful scroll —
|
||||
// deleting up front would lose the deep link when the target mounts late.
|
||||
let attempts = 0
|
||||
|
||||
const attempt = () => {
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
const element = document.getElementById(elementId(target))
|
||||
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block })
|
||||
|
||||
if (!element.hasAttribute('tabindex')) {
|
||||
element.tabIndex = -1
|
||||
}
|
||||
|
||||
element.focus({ preventScroll: true })
|
||||
element.classList.add('setting-field-highlight')
|
||||
window.setTimeout(() => element.classList.remove('setting-field-highlight'), 1600)
|
||||
|
||||
setSearchParams(
|
||||
previous => {
|
||||
const next = new URLSearchParams(previous)
|
||||
next.delete(param)
|
||||
|
||||
return next
|
||||
},
|
||||
{ replace: true }
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (attempts++ < 20) {
|
||||
timer = window.setTimeout(attempt, 80)
|
||||
}
|
||||
}
|
||||
|
||||
timer = window.setTimeout(attempt, 80)
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
window.clearTimeout(timer)
|
||||
}
|
||||
}, [block, elementId, onResolve, param, ready, setSearchParams, target])
|
||||
|
||||
return target
|
||||
}
|
||||
Reference in New Issue
Block a user