@@ -14,7 +14,10 @@ type Placement =
14
14
| "left-start"
15
15
| "left-end" ;
16
16
17
- type StyleFunction = ( element : Element ) => Partial < CSSStyleDeclaration > ;
17
+ type StyleFunction = (
18
+ element : Element ,
19
+ index : number
20
+ ) => Partial < CSSStyleDeclaration > ;
18
21
type StyleObject = Partial < CSSStyleDeclaration > ;
19
22
20
23
interface MarkOptions {
@@ -67,6 +70,14 @@ interface MarkOptions {
67
70
* @default false
68
71
*/
69
72
viewPortOnly ?: boolean ;
73
+ /**
74
+ * Additional class to apply to the mark elements.
75
+ */
76
+ markClass ?: string ;
77
+ /**
78
+ * Additional class to apply to the bounding box elements.
79
+ */
80
+ boundingBoxClass ?: string ;
70
81
}
71
82
72
83
interface MarkedElement {
@@ -77,134 +88,150 @@ interface MarkedElement {
77
88
78
89
let cleanupFns : ( ( ) => void ) [ ] = [ ] ;
79
90
80
- async function mark (
81
- options : MarkOptions = { }
82
- ) : Promise < Record < string , MarkedElement > > {
83
- const {
84
- selector = "button, input, a, select, textarea" ,
85
- getLabel = ( _ , index ) => index . toString ( ) ,
86
- markAttribute = "data-mark-label" ,
87
- markPlacement = "top-start" ,
88
- markStyle = {
89
- backgroundColor : "red" ,
90
- color : "white" ,
91
- padding : "2px 4px" ,
92
- fontSize : "12px" ,
93
- fontWeight : "bold" ,
94
- } ,
95
- boundingBoxStyle = {
96
- outline : "2px dashed red" ,
97
- backgroundColor : "transparent" ,
98
- } ,
99
- showBoundingBoxes = true ,
100
- containerElement = document . body ,
101
- viewPortOnly = false ,
102
- } = options ;
103
-
104
- const elements = Array . from (
105
- containerElement . querySelectorAll ( selector )
106
- ) . filter (
107
- ( el ) => ! viewPortOnly || el . getBoundingClientRect ( ) . top < window . innerHeight
108
- ) ;
91
+ function mark ( options : MarkOptions = { } ) : Record < string , MarkedElement > {
92
+ try {
93
+ const {
94
+ selector = "button, input, a, select, textarea" ,
95
+ getLabel = ( _ , index ) => index . toString ( ) ,
96
+ markAttribute = "data-mark-label" ,
97
+ markPlacement = "top-start" ,
98
+ markStyle = {
99
+ backgroundColor : "red" ,
100
+ color : "white" ,
101
+ padding : "2px 4px" ,
102
+ fontSize : "12px" ,
103
+ fontWeight : "bold" ,
104
+ } ,
105
+ boundingBoxStyle = {
106
+ outline : "2px dashed red" ,
107
+ backgroundColor : "transparent" ,
108
+ } ,
109
+ showBoundingBoxes = true ,
110
+ containerElement = document . body ,
111
+ viewPortOnly = false ,
112
+ markClass = "" ,
113
+ boundingBoxClass = "" ,
114
+ } = options ;
115
+
116
+ const isInViewport = ( el : Element ) => {
117
+ const rect = el . getBoundingClientRect ( ) ;
118
+ return rect . top < window . innerHeight && rect . bottom >= 0 ;
119
+ } ;
120
+
121
+ const elements = Array . from (
122
+ containerElement . querySelectorAll ( selector )
123
+ ) . filter ( ( el ) => ! viewPortOnly || isInViewport ( el ) ) ;
109
124
110
- const markedElements : Record < string , MarkedElement > = { } ;
125
+ const markedElements : Record < string , MarkedElement > = { } ;
126
+ const fragment = document . createDocumentFragment ( ) ;
111
127
112
- await Promise . all (
113
- elements . map ( async ( element , index ) => {
128
+ elements . forEach ( ( element , index ) => {
114
129
const label = getLabel ( element , index ) ;
115
- const markElement = createMark ( element , markStyle , label , markPlacement ) ;
130
+ const markElement = createMark (
131
+ element ,
132
+ index ,
133
+ markStyle ,
134
+ label ,
135
+ markPlacement ,
136
+ markClass
137
+ ) ;
138
+ fragment . appendChild ( markElement ) ;
116
139
117
140
const boundingBoxElement = showBoundingBoxes
118
- ? createBoundingBox ( element , boundingBoxStyle , label )
141
+ ? createBoundingBox ( element , boundingBoxStyle , label , boundingBoxClass )
119
142
: undefined ;
143
+ if ( boundingBoxElement ) {
144
+ fragment . appendChild ( boundingBoxElement ) ;
145
+ }
120
146
121
147
markedElements [ label ] = { element, markElement, boundingBoxElement } ;
122
148
element . setAttribute ( markAttribute , label ) ;
123
- } )
124
- ) ;
149
+ } ) ;
125
150
126
- document . documentElement . setAttribute ( "data-marked" , "true" ) ;
127
- return markedElements ;
151
+ document . body . appendChild ( fragment ) ;
152
+ document . documentElement . setAttribute ( "data-marked" , "true" ) ;
153
+ return markedElements ;
154
+ } catch ( error ) {
155
+ console . error ( "Error in mark function:" , error ) ;
156
+ throw error ;
157
+ }
128
158
}
129
159
130
160
function createMark (
131
161
element : Element ,
162
+ index : number ,
132
163
style : StyleObject | StyleFunction ,
133
164
label : string ,
134
- markPlacement : Placement = "top-start"
165
+ markPlacement : Placement = "top-start" ,
166
+ markClass : string
135
167
) : HTMLElement {
136
168
const markElement = document . createElement ( "div" ) ;
137
- markElement . className = " webmarker" ;
169
+ markElement . className = ` webmarker ${ markClass } ` . trim ( ) ;
138
170
markElement . id = `webmarker-${ label } ` ;
139
171
markElement . textContent = label ;
140
- document . body . appendChild ( markElement ) ;
141
- positionMark ( markElement , element , markPlacement ) ;
172
+ markElement . setAttribute ( "aria-hidden" , "true" ) ;
173
+ positionElement ( markElement , element , markPlacement , ( x , y ) => {
174
+ Object . assign ( markElement . style , {
175
+ left : `${ x } px` ,
176
+ top : `${ y } px` ,
177
+ } ) ;
178
+ } ) ;
142
179
applyStyle (
143
180
markElement ,
144
181
{
145
182
zIndex : "999999999" ,
146
183
position : "absolute" ,
147
184
pointerEvents : "none" ,
148
185
} ,
149
- typeof style === "function" ? style ( element ) : style
186
+ typeof style === "function" ? style ( element , index ) : style
150
187
) ;
151
188
return markElement ;
152
189
}
153
190
154
191
function createBoundingBox (
155
192
element : Element ,
156
193
style : StyleObject | StyleFunction ,
157
- label : string
194
+ label : string ,
195
+ boundingBoxClass : string
158
196
) : HTMLElement {
159
197
const boundingBoxElement = document . createElement ( "div" ) ;
160
- boundingBoxElement . className = "webmarker-bounding-box" ;
198
+ boundingBoxElement . className =
199
+ `webmarker-bounding-box ${ boundingBoxClass } ` . trim ( ) ;
161
200
boundingBoxElement . id = `webmarker-bounding-box-${ label } ` ;
162
- document . body . appendChild ( boundingBoxElement ) ;
163
- positionBoundingBox ( boundingBoxElement , element ) ;
201
+ boundingBoxElement . setAttribute ( "aria-hidden" , "true" ) ;
202
+ positionElement ( boundingBoxElement , element , "top-start" , ( x , y ) => {
203
+ const { width, height } = element . getBoundingClientRect ( ) ;
204
+ Object . assign ( boundingBoxElement . style , {
205
+ left : `${ x } px` ,
206
+ top : `${ y } px` ,
207
+ width : `${ width } px` ,
208
+ height : `${ height } px` ,
209
+ } ) ;
210
+ } ) ;
164
211
applyStyle (
165
212
boundingBoxElement ,
166
213
{
167
214
zIndex : "999999999" ,
168
215
position : "absolute" ,
169
216
pointerEvents : "none" ,
170
217
} ,
171
- typeof style === "function" ? style ( element ) : style
218
+ typeof style === "function" ? style ( element , parseInt ( label ) ) : style
172
219
) ;
173
220
return boundingBoxElement ;
174
221
}
175
222
176
- function positionMark (
177
- markElement : HTMLElement ,
178
- element : Element ,
179
- markPlacement : Placement
223
+ function positionElement (
224
+ target : HTMLElement ,
225
+ anchor : Element ,
226
+ placement : Placement ,
227
+ updateCallback : ( x : number , y : number ) => void
180
228
) {
181
- async function updatePosition ( ) {
182
- const { x, y } = await computePosition ( element , markElement , {
183
- placement : markPlacement ,
184
- } ) ;
185
- Object . assign ( markElement . style , {
186
- left : `${ x } px` ,
187
- top : `${ y } px` ,
229
+ function updatePosition ( ) {
230
+ computePosition ( anchor , target , { placement } ) . then ( ( { x, y } ) => {
231
+ updateCallback ( x , y ) ;
188
232
} ) ;
189
233
}
190
-
191
- cleanupFns . push ( autoUpdate ( element , markElement , updatePosition ) ) ;
192
- }
193
-
194
- async function positionBoundingBox ( boundingBox : HTMLElement , element : Element ) {
195
- const { width, height } = element . getBoundingClientRect ( ) ;
196
- async function updatePosition ( ) {
197
- const { x, y } = await computePosition ( element , boundingBox , {
198
- placement : "top-start" ,
199
- } ) ;
200
- Object . assign ( boundingBox . style , {
201
- left : `${ x } px` ,
202
- top : `${ y + height } px` ,
203
- width : `${ width } px` ,
204
- height : `${ height } px` ,
205
- } ) ;
206
- }
207
- cleanupFns . push ( autoUpdate ( element , boundingBox , updatePosition ) ) ;
234
+ cleanupFns . push ( autoUpdate ( anchor , target , updatePosition ) ) ;
208
235
}
209
236
210
237
function applyStyle (
@@ -216,6 +243,15 @@ function applyStyle(
216
243
}
217
244
218
245
function unmark ( ) : void {
246
+ const markAttribute = document
247
+ . querySelector ( "[data-mark-label]" )
248
+ ?. getAttribute ( "data-mark-label" )
249
+ ? "data-mark-label"
250
+ : "data-webmarker-label" ;
251
+
252
+ document . querySelectorAll ( `[${ markAttribute } ]` ) . forEach ( ( el ) => {
253
+ el . removeAttribute ( markAttribute ) ;
254
+ } ) ;
219
255
document
220
256
. querySelectorAll ( ".webmarker, .webmarker-bounding-box" )
221
257
. forEach ( ( el ) => el . remove ( ) ) ;
0 commit comments