glass-easel-devtools-agent 0.9.0

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/src/overlay.ts ADDED
@@ -0,0 +1,207 @@
1
+ import * as glassEasel from 'glass-easel'
2
+
3
+ // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
4
+ const wxml = require('./overlay.wxml') as Record<string, unknown>
5
+
6
+ export const enum OverlayState {
7
+ None = 0,
8
+ NodeSelect,
9
+ Highlight,
10
+ }
11
+
12
+ const space = new glassEasel.ComponentSpace()
13
+
14
+ type ComponentExport = {
15
+ startNodeSelect(): void
16
+ endNodeSelect(): void
17
+ }
18
+
19
+ export const overlayCompDef = space
20
+ .define('glass-easel-devtools-agent')
21
+ .template(wxml)
22
+ .data(() => ({
23
+ state: OverlayState.None,
24
+ selectMoveDetecting: false,
25
+ highlightRect: null as null | { left: number; top: number; width: number; height: number },
26
+ }))
27
+ .init((ctx) => {
28
+ const { self, data, setData, method, listener } = ctx
29
+
30
+ // set host styles
31
+ self.setNodeStyle(`
32
+ display: block;
33
+ position: absolute;
34
+ left: 0;
35
+ top: 0;
36
+ width: 0;
37
+ height: 0;
38
+ background: red;
39
+ z-index: 2147483647;
40
+ `)
41
+
42
+ // selection
43
+ let nodeSelectUpdateCallback:
44
+ | null
45
+ | ((elem: glassEasel.Element | null, isFinal: boolean) => void) = null
46
+ const startNodeSelect = method(
47
+ (cb: (elem: glassEasel.Element | null, isFinal: boolean) => void) => {
48
+ if (data.state !== OverlayState.None && data.state !== OverlayState.Highlight) {
49
+ return false
50
+ }
51
+ setData({ state: OverlayState.NodeSelect })
52
+ nodeSelectUpdateCallback = cb
53
+ return true
54
+ },
55
+ )
56
+ const endNodeSelect = method(() => {
57
+ if (data.state !== OverlayState.NodeSelect) {
58
+ return false
59
+ }
60
+ setData({ state: OverlayState.None, highlightRect: null })
61
+ nodeSelectUpdateCallback = null
62
+ return true
63
+ })
64
+ const nodeSelectMove = listener<{ clientX: number; clientY: number }>(({ detail }) => {
65
+ if (data.selectMoveDetecting) return
66
+ const x = detail.clientX
67
+ const y = detail.clientY
68
+ const ctx = self.getBackendContext()
69
+ if (!ctx) {
70
+ setData({ highlightRect: null })
71
+ return
72
+ }
73
+ setData({ selectMoveDetecting: true })
74
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
75
+ elementFromPointInContext(ctx, x, y)
76
+ .then(async (elem) => {
77
+ setData({ selectMoveDetecting: false })
78
+ if (!elem) {
79
+ setData({ highlightRect: null })
80
+ return undefined
81
+ }
82
+ const { left, top, width, height } = await getBoundingClientRectInContext(elem)
83
+ if (data.state === OverlayState.NodeSelect) {
84
+ setData({ highlightRect: { left, top, width, height } })
85
+ }
86
+ nodeSelectUpdateCallback?.(elem, false)
87
+ return undefined
88
+ })
89
+ .catch(() => {
90
+ setData({ selectMoveDetecting: false, highlightRect: null })
91
+ })
92
+ })
93
+ const nodeSelectDone = listener<{ x: number; y: number }>(({ detail }) => {
94
+ const cb = nodeSelectUpdateCallback
95
+ endNodeSelect()
96
+ const { x, y } = detail
97
+ const ctx = self.getBackendContext()
98
+ if (!ctx) {
99
+ setData({ highlightRect: null })
100
+ return
101
+ }
102
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises, promise/catch-or-return
103
+ elementFromPointInContext(ctx, x, y).then(async (elem) => {
104
+ // eslint-disable-next-line promise/no-callback-in-promise
105
+ cb?.(elem, true)
106
+ return undefined
107
+ })
108
+ })
109
+
110
+ // highlight
111
+ const highlight = method(async (node: glassEasel.Node | null) => {
112
+ if (node && node.asElement()) {
113
+ const elem = node.asElement()!
114
+ const { left, top, width, height } = await getBoundingClientRectInContext(elem)
115
+ if (data.state !== OverlayState.None) {
116
+ return false
117
+ }
118
+ setData({ state: OverlayState.Highlight, highlightRect: { left, top, width, height } })
119
+ } else {
120
+ if (data.state !== OverlayState.Highlight) {
121
+ return false
122
+ }
123
+ setData({ state: OverlayState.None, highlightRect: null })
124
+ }
125
+ return true
126
+ })
127
+
128
+ return { startNodeSelect, endNodeSelect, nodeSelectMove, nodeSelectDone, highlight }
129
+ })
130
+ .registerComponent()
131
+
132
+ const elementFromPointInContext = (
133
+ context: glassEasel.GeneralBackendContext,
134
+ x: number,
135
+ y: number,
136
+ ) =>
137
+ new Promise<glassEasel.Element | null>((resolve) => {
138
+ if (!context?.elementFromPoint) {
139
+ resolve(null)
140
+ return
141
+ }
142
+ // eslint-disable-next-line
143
+ context.elementFromPoint(x, y, (elem) => {
144
+ resolve(elem)
145
+ })
146
+ })
147
+
148
+ const getBoundingClientRectInContext = (elem: glassEasel.Element) =>
149
+ new Promise<{ left: number; top: number; width: number; height: number }>((resolve) => {
150
+ elem.getBoundingClientRect((rect) => {
151
+ resolve(rect)
152
+ })
153
+ })
154
+
155
+ export class OverlayManager {
156
+ private backendContexts: WeakMap<
157
+ glassEasel.GeneralBackendContext,
158
+ glassEasel.Component<any, any, ComponentExport>
159
+ > = new WeakMap()
160
+
161
+ get(ctx: glassEasel.GeneralBackendContext): glassEasel.Component<any, any, ComponentExport> {
162
+ const comp = this.backendContexts.get(ctx)
163
+ if (comp) return comp
164
+
165
+ // create the component
166
+ const component = space.createComponentByUrl(
167
+ 'glass-easel-devtools-agent',
168
+ 'glass-easel-devtools-agent',
169
+ null,
170
+ ctx,
171
+ ) as glassEasel.Component<any, any, ComponentExport>
172
+ this.backendContexts.set(ctx, component)
173
+
174
+ // insert into backend
175
+ let parentElement: glassEasel.GeneralBackendElement
176
+ let placeholder: glassEasel.GeneralBackendElement
177
+ if (ctx.mode === glassEasel.BackendMode.Composed) {
178
+ parentElement = ctx.getRootNode()
179
+ placeholder = ctx.createElement('glass-easel-devtools-agent', 'glass-easel-devtools-agent')
180
+ parentElement.appendChild(placeholder)
181
+ } else if (ctx.mode === glassEasel.BackendMode.Domlike) {
182
+ parentElement = ctx.getRootNode()
183
+ placeholder = ctx.document.createElement('glass-easel-devtools-agent')
184
+ parentElement.appendChild(placeholder)
185
+ } else if (ctx.mode === glassEasel.BackendMode.Shadow) {
186
+ const sr = ctx.getRootNode()
187
+ parentElement = sr
188
+ if (!sr) throw new Error('the host element should be inside of a shadow tree')
189
+ placeholder = sr.createElement('glass-easel-devtools-agent', 'glass-easel-devtools-agent')
190
+ sr.appendChild(placeholder)
191
+ } else {
192
+ throw new Error('unrecognized host backend mode')
193
+ }
194
+ glassEasel.Element.replaceDocumentElement(component, parentElement, placeholder)
195
+ if (
196
+ ctx.mode === glassEasel.BackendMode.Composed ||
197
+ ctx.mode === glassEasel.BackendMode.Shadow
198
+ ) {
199
+ const p = parentElement as glassEasel.composedBackend.Element | glassEasel.backend.Element
200
+ p.release()
201
+ const elem = placeholder as glassEasel.composedBackend.Element | glassEasel.backend.Element
202
+ elem.release()
203
+ }
204
+
205
+ return component
206
+ }
207
+ }
@@ -0,0 +1,40 @@
1
+ <!-- node select -->
2
+ <div
3
+ wx:if="{{ state === 1 }}"
4
+ style="
5
+ position: absolute;
6
+ left: 0;
7
+ top: 0;
8
+ width: 100vw;
9
+ height: 100vh;
10
+ overflow: hidden;
11
+ {{ selectMoveDetecting ? 'transform: translateY(-200vh)' : '' }}
12
+ "
13
+ bind:mousemove="nodeSelectMove"
14
+ bind:tap="nodeSelectDone"
15
+ >
16
+ <div
17
+ wx:if="{{ highlightRect }}"
18
+ style="
19
+ position: absolute;
20
+ left: {{ highlightRect.left }}px;
21
+ top: {{ highlightRect.top }}px;
22
+ width: {{ highlightRect.width }}px;
23
+ height: {{ highlightRect.height }}px;
24
+ background: rgba(0, 128, 192, 0.25);
25
+ "
26
+ />
27
+ </div>
28
+
29
+ <!-- temporary highlight -->
30
+ <div
31
+ wx:if="{{ state === 2 && highlightRect }}"
32
+ style="
33
+ position: absolute;
34
+ left: {{ highlightRect.left }}px;
35
+ top: {{ highlightRect.top }}px;
36
+ width: {{ highlightRect.width }}px;
37
+ height: {{ highlightRect.height }}px;
38
+ background: rgba(0, 128, 192, 0.25);
39
+ "
40
+ />
@@ -0,0 +1,222 @@
1
+ import type { Protocol } from 'devtools-protocol'
2
+ import type { EventDetail, NodeId, RequestResponse } from './index'
3
+
4
+ export type AgentEventKind = {
5
+ fontsUpdated: FontsUpdated
6
+ mediaQueryResultChanged: MediaQueryResultChanged
7
+ styleSheetAdded: StyleSheetAdded
8
+ styleSheetChanged: StyleSheetChanged
9
+ styleSheetRemoved: StyleSheetRemoved
10
+ }
11
+
12
+ export type AgentRequestKind = {
13
+ getComputedStyleForNode: GetComputedStyleForNode
14
+ getInlineStylesForNode: GetInlineStylesForNode
15
+ getMatchedStylesForNode: GetMatchedStylesForNode
16
+ addGlassEaselStyleSheetRule: AddGlassEaselStyleSheetRule
17
+ getGlassEaselStyleSheetIndexForNewRules: GetGlassEaselStyleSheetIndexForNewRules
18
+ resetGlassEaselStyleSheetRule: ResetGlassEaselStyleSheetRule
19
+ modifyGlassEaselStyleSheetRuleSelector: ModifyGlassEaselStyleSheetRuleSelector
20
+ addGlassEaselStyleSheetProperty: AddGlassEaselStyleSheetProperty
21
+ setGlassEaselStyleSheetPropertyDisabled: SetGlassEaselStyleSheetPropertyDisabled
22
+ removeGlassEaselStyleSheetProperty: RemoveGlassEaselStyleSheetProperty
23
+ replaceGlassEaselStyleSheetProperty: ReplaceGlassEaselStyleSheetProperty
24
+ replaceGlassEaselStyleSheetAllProperties: ReplaceGlassEaselStyleSheetAllProperties
25
+ replaceGlassEaselStyleSheetInlineStyle: ReplaceGlassEaselStyleSheetInlineStyle
26
+ }
27
+
28
+ export type StyleSheetId = string
29
+
30
+ export type CSSNameValue = { name: string; value: string }
31
+
32
+ export type CSSProperty = CSSNameValue & {
33
+ /** `!important` status */
34
+ important?: boolean
35
+ /** the original style text */
36
+ text?: string
37
+ /** parse success or not */
38
+ parseOk?: boolean
39
+ /** disabled or not */
40
+ disabled?: boolean
41
+ }
42
+
43
+ export type CSSStyle = {
44
+ styleSheetId?: StyleSheetId
45
+ cssProperties: CSSProperty[]
46
+ cssText?: string
47
+ }
48
+
49
+ export type CSSRule = {
50
+ styleSheetId?: StyleSheetId
51
+ selectorList: { selectors: { text: string }[]; text: string }
52
+ style: CSSStyle
53
+ media?: { styleSheetId?: StyleSheetId; text: string }[]
54
+ inactive?: boolean
55
+ }
56
+
57
+ export type CSSMatchedRule = {
58
+ rule: CSSRule
59
+ }
60
+
61
+ /**
62
+ * Get computed style of a node.
63
+ */
64
+ export interface GetComputedStyleForNode extends RequestResponse {
65
+ request: { nodeId: NodeId }
66
+ response: { computedStyle: CSSNameValue[] }
67
+ cdpRequestResponse: [
68
+ Protocol.CSS.GetComputedStyleForNodeRequest,
69
+ Protocol.CSS.GetComputedStyleForNodeResponse,
70
+ ]
71
+ }
72
+
73
+ /**
74
+ * Get inline styles of a node.
75
+ */
76
+ export interface GetInlineStylesForNode extends RequestResponse {
77
+ request: { nodeId: NodeId }
78
+ response: { inlineStyle: CSSStyle }
79
+ cdpRequestResponse: [
80
+ Protocol.CSS.GetInlineStylesForNodeRequest,
81
+ Protocol.CSS.GetInlineStylesForNodeResponse,
82
+ ]
83
+ }
84
+
85
+ /**
86
+ * Get matched styles of a node.
87
+ */
88
+ export interface GetMatchedStylesForNode extends RequestResponse {
89
+ request: { nodeId: NodeId }
90
+ response: {
91
+ inlineStyle: CSSStyle
92
+ matchedCSSRules: CSSMatchedRule[]
93
+ inherited: { inlineStyle?: CSSStyle; matchedCSSRules: CSSMatchedRule[] }[]
94
+ crossOriginFailing?: boolean
95
+ }
96
+ cdpRequestResponse: [
97
+ Protocol.CSS.GetMatchedStylesForNodeRequest,
98
+ Protocol.CSS.GetMatchedStylesForNodeResponse,
99
+ ]
100
+ }
101
+
102
+ /**
103
+ * Add a new CSS rule.
104
+ */
105
+ export interface AddGlassEaselStyleSheetRule extends RequestResponse {
106
+ request: { mediaQueryText: string; selector: string }
107
+ }
108
+
109
+ /**
110
+ * Get the style sheet for temporary rules.
111
+ */
112
+ export interface GetGlassEaselStyleSheetIndexForNewRules extends RequestResponse {
113
+ request: Record<string, never>
114
+ response: { styleSheetId: StyleSheetId }
115
+ }
116
+
117
+ /**
118
+ * Clear a CSS rule.
119
+ */
120
+ export interface ResetGlassEaselStyleSheetRule extends RequestResponse {
121
+ request: { styleSheetId: StyleSheetId; ruleIndex: number }
122
+ }
123
+
124
+ /**
125
+ * Modify the CSS rule selector.
126
+ */
127
+ export interface ModifyGlassEaselStyleSheetRuleSelector extends RequestResponse {
128
+ request: { styleSheetId: StyleSheetId; ruleIndex: number; selector: string }
129
+ }
130
+
131
+ /**
132
+ * Add a new CSS property.
133
+ */
134
+ export interface AddGlassEaselStyleSheetProperty extends RequestResponse {
135
+ request: { styleSheetId: StyleSheetId; ruleIndex: number; styleText: string }
136
+ }
137
+
138
+ /**
139
+ * Set the disabled status of a new CSS property.
140
+ */
141
+ export interface SetGlassEaselStyleSheetPropertyDisabled extends RequestResponse {
142
+ request: {
143
+ styleSheetId: StyleSheetId
144
+ ruleIndex: number
145
+ propertyIndex: number
146
+ disabled: boolean
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Remove a CSS property.
152
+ */
153
+ export interface RemoveGlassEaselStyleSheetProperty extends RequestResponse {
154
+ request: {
155
+ styleSheetId: StyleSheetId
156
+ ruleIndex: number
157
+ propertyIndex: number
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Replace a CSS property.
163
+ */
164
+ export interface ReplaceGlassEaselStyleSheetProperty extends RequestResponse {
165
+ request: {
166
+ styleSheetId: StyleSheetId
167
+ ruleIndex: number
168
+ propertyIndex: number
169
+ styleText: string
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Replace all CSS properties.
175
+ */
176
+ export interface ReplaceGlassEaselStyleSheetAllProperties extends RequestResponse {
177
+ request: {
178
+ styleSheetId: StyleSheetId
179
+ ruleIndex: number
180
+ styleText: string
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Replace inline style for a node.
186
+ */
187
+ export interface ReplaceGlassEaselStyleSheetInlineStyle extends RequestResponse {
188
+ request: {
189
+ nodeId: NodeId
190
+ styleText: string
191
+ }
192
+ }
193
+
194
+ export interface FontsUpdated extends EventDetail {
195
+ detail: Record<string, never>
196
+ cdpEventDetail: unknown
197
+ }
198
+
199
+ export interface MediaQueryResultChanged extends EventDetail {
200
+ detail: Record<string, never>
201
+ cdpEventDetail: unknown
202
+ }
203
+
204
+ export interface StyleSheetAdded extends EventDetail {
205
+ detail: {
206
+ header: {
207
+ styleSheetId: StyleSheetId
208
+ sourceURL?: string
209
+ }
210
+ }
211
+ cdpEventDetail: Protocol.CSS.StyleSheetAddedEvent
212
+ }
213
+
214
+ export interface StyleSheetChanged extends EventDetail {
215
+ detail: { styleSheetId: string }
216
+ cdpEventDetail: Protocol.CSS.StyleSheetChangedEvent
217
+ }
218
+
219
+ export interface StyleSheetRemoved extends EventDetail {
220
+ detail: { styleSheetId: string }
221
+ cdpEventDetail: Protocol.CSS.StyleSheetRemovedEvent
222
+ }