Skip to content

Commit 57d1dbe

Browse files
committed
WIP: Progress on refactoring the locator
1 parent f016bda commit 57d1dbe

8 files changed

Lines changed: 218 additions & 182 deletions

File tree

packages/debugger/src/locator/element-overlay.tsx

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,40 @@
1+
import * as s from 'solid-js'
2+
import * as sweb from 'solid-js/web'
13
import {createElementBounds} from '@solid-primitives/bounds'
24
import {createElementCursor} from '@solid-primitives/cursor'
35
import {createRootPool} from '@solid-primitives/rootless'
4-
import {type Accessor, type Component, createMemo, getOwner, runWithOwner, Show} from 'solid-js'
5-
import {Portal} from 'solid-js/web'
6-
import {type LocatorComponent} from './find-components.ts'
6+
import type {LocatorComponent} from './index.ts'
77

8-
export function createElementsOverlay(selected: Accessor<LocatorComponent[]>) {
9-
const useElementOverlay = createRootPool((component: Accessor<LocatorComponent>, active) => (
8+
export function createElementsOverlay(selected: s.Accessor<LocatorComponent[]>) {
9+
const useElementOverlay = createRootPool((component: s.Accessor<LocatorComponent>, active) => (
1010
<ElementOverlay component={active() ? component() : null} />
1111
))
1212

1313
// wait a second to let the framework mess with the document before attaching the overlay
14-
const owner = getOwner()!
14+
const owner = s.getOwner()!
1515
setTimeout(() => {
16-
runWithOwner(owner, () => (
17-
<Portal useShadow mount={document.documentElement}>
16+
s.runWithOwner(owner, () => (
17+
<sweb.Portal useShadow mount={document.documentElement}>
1818
<div data-darkreader-ignore>{selected().map(useElementOverlay)}</div>
19-
</Portal>
19+
</sweb.Portal>
2020
))
2121
}, 1000)
2222
}
2323

24-
const ElementOverlay: Component<{component: LocatorComponent | null}> = props => {
24+
const ElementOverlay: s.Component<{component: LocatorComponent | null}> = props => {
2525
const element = () => props.component?.element
2626
// set pointer cursor to selected component
2727
createElementCursor(element, 'pointer')
2828
const tag = () => element()?.localName
2929
const name = () => props.component?.name
3030

3131
const bounds = createElementBounds(element)
32-
const left = createMemo<number>(prev => (bounds.left === null ? prev : bounds.left), 0)
33-
const top = createMemo<number>(prev => (bounds.top === null ? prev : bounds.top), 0)
34-
const width = createMemo<number>(prev => (bounds.width === null ? prev : bounds.width), 0)
35-
const height = createMemo<number>(prev => (bounds.height === null ? prev : bounds.height), 0)
36-
const transform = createMemo(() => `translate(${Math.round(left())}px, ${Math.round(top())}px)`)
37-
const placeOnTop = createMemo(() => top() > window.innerHeight / 2)
32+
const left = s.createMemo<number>(prev => (bounds.left === null ? prev : bounds.left), 0)
33+
const top = s.createMemo<number>(prev => (bounds.top === null ? prev : bounds.top), 0)
34+
const width = s.createMemo<number>(prev => (bounds.width === null ? prev : bounds.width), 0)
35+
const height = s.createMemo<number>(prev => (bounds.height === null ? prev : bounds.height), 0)
36+
const transform = s.createMemo(() => `translate(${Math.round(left())}px, ${Math.round(top())}px)`)
37+
const placeOnTop = s.createMemo(() => top() > window.innerHeight / 2)
3838

3939
return (
4040
<>
@@ -48,7 +48,7 @@ const ElementOverlay: Component<{component: LocatorComponent | null}> = props =>
4848
}}
4949
>
5050
<div class="border" />
51-
<Show when={name()}>
51+
<s.Show when={name()}>
5252
<div class={`name-container ${placeOnTop() ? 'top' : 'bottom'}`}>
5353
<div class="name-animated-container">
5454
<div class="name-background"></div>
@@ -60,7 +60,7 @@ const ElementOverlay: Component<{component: LocatorComponent | null}> = props =>
6060
</div>
6161
</div>
6262
</div>
63-
</Show>
63+
</s.Show>
6464
</div>
6565
</>
6666
)

packages/debugger/src/locator/index.ts

Lines changed: 140 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,41 @@ import {defer} from '@solid-primitives/utils'
33
import {makeEventListener} from '@solid-primitives/event-listener'
44
import {createKeyHold} from '@solid-primitives/keyboard'
55
import {scheduleIdle} from '@solid-primitives/scheduled'
6-
import {makeHoverElementListener} from '@solid-devtools/shared/primitives'
76
import {msg, warn} from '@solid-devtools/shared/utils'
87
import * as walker from '../structure/walker.ts'
98
import {ObjectType, getObjectById} from '../main/id.ts'
109
import SolidAPI from '../main/setup.ts'
11-
import {type NodeID, type OutputEmit} from '../main/types.ts'
10+
import {type ElementInterface, type NodeID, type OutputEmit, type SourceLocation} from '../main/types.ts'
1211
import {createElementsOverlay} from './element-overlay.tsx'
13-
import {
14-
type LocatorComponent,
15-
type SourceCodeData,
16-
type SourceLocation,
17-
type TargetIDE,
18-
type TargetURLFunction,
19-
getLocationAttr,
20-
getProjectPath,
21-
getSourceCodeData,
22-
openSourceCode,
23-
} from './find-components.ts'
24-
import {type HighlightElementPayload, type LocatorOptions} from './types.ts'
25-
26-
export {parseLocationString} from './find-components.ts'
27-
28-
export * from './types.ts'
12+
import * as locator from './locator.ts'
13+
import {unwrap_append} from '../main/utils.ts'
14+
15+
export * from './locator.ts'
16+
17+
export type LocatorComponent<TEl extends object> = {
18+
id: NodeID
19+
name: string | undefined
20+
element: TEl
21+
location?: SourceLocation | null
22+
}
23+
24+
function makeHoverElementListener<TEl extends object>(
25+
eli: ElementInterface<TEl>,
26+
onHover: (el: TEl | null) => void,
27+
): void {
28+
let last: TEl | null = null
29+
makeEventListener(window, 'mouseover', e => {
30+
let el = eli.getElementAt(e)
31+
if (el !== last) {
32+
onHover(last = el)
33+
}
34+
})
35+
makeEventListener(document, 'mouseleave', () => {
36+
if (null !== last) {
37+
onHover(last = null)
38+
}
39+
})
40+
}
2941

3042
export function createLocator<TEl extends object>(props: {
3143
locatorEnabled: s.Accessor<boolean>,
@@ -37,55 +49,80 @@ export function createLocator<TEl extends object>(props: {
3749
const [enabledByPressingSignal, setEnabledByPressingSignal] = s.createSignal((): boolean => false)
3850
props.setLocatorEnabledSignal(s.createMemo(() => enabledByPressingSignal()()))
3951

40-
const [hoverTarget, setHoverTarget] = s.createSignal<HTMLElement | null>(null)
41-
const [devtoolsTarget, setDevtoolsTarget] = s.createSignal<HighlightElementPayload>(null)
42-
43-
const [highlightedComponents, setHighlightedComponents] = s.createSignal<LocatorComponent[]>([])
52+
const [hoverTarget, setHoverTarget] = s.createSignal<TEl | null>(null)
53+
const [devtoolsTarget, setDevtoolsTarget] = s.createSignal<locator.HighlightElementPayload>(null)
4454

45-
const calcHighlightedComponents = (
46-
target: HTMLElement | HighlightElementPayload,
47-
): LocatorComponent[] => {
48-
if (!target) return []
55+
const [highlightedComponents, setHighlightedComponents] = s.createSignal<LocatorComponent<TEl>[]>([])
4956

50-
// target is an elementId
51-
if ('type' in target && target.type === 'element') {
52-
const element = getObjectById(target.id, ObjectType.Element)
53-
if (!(element instanceof HTMLElement)) return []
54-
target = element
55-
}
56-
57-
// target is an element
58-
if (target instanceof HTMLElement) {
59-
const comp = walker.findComponent(props.component_registry, target)
60-
if (!comp) return []
61-
return [
62-
{
63-
location: getLocationAttr(target),
64-
element: target,
65-
id: comp.id,
66-
name: comp.name,
67-
},
68-
]
69-
}
70-
71-
// target is a component or an element of a component (in DOM walker mode)
72-
const comp = walker.getComponent(props.component_registry, target.id)
73-
if (!comp) return []
74-
return comp.elements.map(element => ({
75-
element,
57+
function getLocatorComponentFromElement(
58+
el: TEl,
59+
): LocatorComponent<TEl> | null {
60+
let comp = walker.findComponent(props.component_registry, el)
61+
return comp && {
62+
location: props.component_registry.eli.getLocation(el),
63+
element: el,
7664
id: comp.id,
7765
name: comp.name,
78-
}))
66+
}
7967
}
8068

81-
s.createEffect(
82-
defer(
83-
() => hoverTarget() ?? devtoolsTarget(),
84-
scheduleIdle(target =>
85-
setHighlightedComponents(() => calcHighlightedComponents(target)),
86-
),
87-
),
88-
)
69+
const target = s.createMemo(() => {
70+
let hover = hoverTarget()
71+
return hover != null
72+
? {
73+
type: 'hover' as const,
74+
element: hover,
75+
}
76+
: devtoolsTarget()
77+
}, undefined, {
78+
equals: (a, b) => {
79+
if (a === b) return true
80+
if (a == null && b == null) return true
81+
if (a == null || b == null) return false
82+
if (a.type !== b.type) return false
83+
switch (a.type) {
84+
case 'hover': return a.element === (b as any).element
85+
case 'node': return a.id === (b as any).id
86+
case 'element': return a.id === (b as any).id
87+
}
88+
},
89+
})
90+
91+
s.createEffect(defer(target, scheduleIdle(target => {
92+
93+
let locator_components: LocatorComponent<TEl>[] = []
94+
95+
if (target != null) {
96+
switch (target.type) {
97+
case 'hover': {
98+
unwrap_append(locator_components, getLocatorComponentFromElement(target.element))
99+
break
100+
}
101+
case 'element': {
102+
let element = getObjectById(target.id, ObjectType.Element) as TEl | null
103+
if (element != null) {
104+
unwrap_append(locator_components, getLocatorComponentFromElement(element))
105+
}
106+
break
107+
}
108+
case 'node': {
109+
// target is a component or an element of a component (in DOM walker mode)
110+
let comp = walker.getComponent(props.component_registry, target.id)
111+
if (comp != null) {
112+
for (let el of comp.elements) {
113+
locator_components.push({
114+
element: el,
115+
id: comp.id,
116+
name: comp.name,
117+
})
118+
}
119+
}
120+
}
121+
}
122+
}
123+
124+
setHighlightedComponents(locator_components)
125+
})))
89126

90127
createElementsOverlay(highlightedComponents)
91128

@@ -103,39 +140,49 @@ export function createLocator<TEl extends object>(props: {
103140
}
104141
})
105142

106-
let targetIDE: TargetIDE | TargetURLFunction | undefined
143+
let target_ide: locator.TargetIDE | locator.TargetURLFunction | undefined
107144

108145
s.createEffect(() => {
109146
if (!props.locatorEnabled()) return
110147

111148
// set hovered element as target
112-
makeHoverElementListener(el => setHoverTarget(el))
149+
makeHoverElementListener(props.component_registry.eli, el => setHoverTarget(() => el))
113150
s.onCleanup(() => setHoverTarget(null))
114151

115152
// go to selected component source code on click
116-
makeEventListener(
117-
window,
118-
'click',
119-
e => {
120-
const {target} = e
121-
if (!(target instanceof HTMLElement)) return
122-
const highlighted = highlightedComponents()
123-
const comp =
124-
highlighted.find(({element}) => target.contains(element)) ?? highlighted[0]
125-
if (!comp) return
126-
const sourceCodeData =
127-
comp.location && getSourceCodeData(comp.location, comp.element)
128-
129-
// intercept on-page components clicks and send them to the devtools overlay
130-
props.onComponentClick(comp.id, () => {
131-
if (!targetIDE || !sourceCodeData) return
132-
e.preventDefault()
133-
e.stopPropagation()
134-
openSourceCode(targetIDE, sourceCodeData)
135-
})
136-
},
137-
true,
138-
)
153+
makeEventListener(window, 'click', e => {
154+
155+
let el = props.component_registry.eli.getElementAt(e)
156+
if (el == null) {
157+
DEV: {warn("Locator: can't find element at click target (target=%o)", e.target)}
158+
return
159+
}
160+
161+
let comp = getLocatorComponentFromElement(el)
162+
if (comp == null) {
163+
DEV: {warn("Locator: can't find component at click target (target=%o, el=%o)", e.target, el)}
164+
return
165+
}
166+
167+
if (comp.location == null) {
168+
DEV: {warn("Locator: can't find source location for component (comp=%o)", comp)}
169+
return
170+
}
171+
172+
let source_code_data = locator.getSourceCodeData(comp.location, comp.element)
173+
if (source_code_data == null) {
174+
DEV: {warn("Locator: can't find source code data for component (comp=%o)", comp)}
175+
return
176+
}
177+
178+
// intercept on-page components clicks and send them to the devtools overlay
179+
props.onComponentClick(comp.id, () => {
180+
if (target_ide == null) return
181+
e.preventDefault()
182+
e.stopPropagation()
183+
locator.openSourceCode(target_ide, source_code_data)
184+
})
185+
}, true)
139186
})
140187

141188
let locatorUsed = false
@@ -147,11 +194,11 @@ export function createLocator<TEl extends object>(props: {
147194
*
148195
* @param options {@link LocatorOptions} for the locator.
149196
*/
150-
function useLocator(options: LocatorOptions): void {
197+
function useLocator(options: locator.LocatorOptions): void {
151198
s.runWithOwner(owner, () => {
152199
if (locatorUsed) return warn('useLocator can be called only once.')
153200
locatorUsed = true
154-
if (options.targetIDE) targetIDE = options.targetIDE
201+
if (options.targetIDE) target_ide = options.targetIDE
155202
if (options.key !== false) {
156203
const isHoldingKey = createKeyHold(options.key ?? 'Alt', {preventDefault: true})
157204
setEnabledByPressingSignal(() => isHoldingKey)
@@ -167,14 +214,14 @@ export function createLocator<TEl extends object>(props: {
167214

168215
return {
169216
useLocator,
170-
setDevtoolsHighlightTarget(target: HighlightElementPayload) {
217+
setDevtoolsHighlightTarget(target: locator.HighlightElementPayload) {
171218
setDevtoolsTarget(target)
172219
},
173-
openElementSourceCode(location: SourceLocation, element: SourceCodeData['element']) {
174-
if (!targetIDE) return warn('Please set `targetIDE` it in useLocator options.')
175-
const projectPath = getProjectPath()
220+
openElementSourceCode(location: SourceLocation, element: locator.SourceCodeData['element']) {
221+
if (!target_ide) return warn('Please set `targetIDE` it in useLocator options.')
222+
const projectPath = locator.getProjectPath()
176223
if (!projectPath) return warn('projectPath is not set.')
177-
openSourceCode(targetIDE, {
224+
locator.openSourceCode(target_ide, {
178225
...location,
179226
projectPath,
180227
element,

0 commit comments

Comments
 (0)