@@ -5,15 +5,31 @@ import { SignalEmitter } from '../emitter'
55import { SignalGenerator } from './types'
66
77interface Label {
8- textContent ?: string
8+ textContent : string
9+ id : string
10+ attributes : Record < string , unknown >
911}
12+
13+ const parseFormData = ( data : FormData ) : Record < string , string > => {
14+ return [ ...data ] . reduce ( ( acc , [ key , value ] ) => {
15+ if ( typeof value === 'string' ) {
16+ acc [ key ] = value
17+ }
18+ return acc
19+ } , { } as Record < string , string > )
20+ }
21+
1022const parseLabels = (
1123 labels : NodeListOf < HTMLLabelElement > | null | undefined
1224) : Label [ ] => {
1325 if ( ! labels ) return [ ]
14- return [ ...labels ] . map ( ( label ) => ( {
15- textContent : label . textContent ?? undefined ,
16- } ) )
26+ return [ ...labels ]
27+ . map ( ( label ) => ( {
28+ id : label . id ,
29+ attributes : parseNodeMap ( label . attributes ) ,
30+ textContent : label . textContent ? cleanText ( label . textContent ) : undefined ,
31+ } ) )
32+ . filter ( ( el ) : el is Label => Boolean ( el . textContent ) )
1733}
1834
1935const parseNodeMap = ( nodeMap : NamedNodeMap ) : Record < string , unknown > => {
@@ -31,21 +47,70 @@ export const cleanText = (str: string): string => {
3147 . trim ( ) // Trim leading and trailing spaces
3248}
3349
34- const parseElement = ( el : HTMLElement ) => {
50+ interface ParsedElementBase {
51+ attributes : Record < string , unknown >
52+ classList : string [ ]
53+ id : string
54+ labels ?: Label [ ]
55+ label ?: Label
56+ name : string
57+ nodeName : string
58+ tagName : string
59+ title : string
60+ type : string
61+ value : string
62+ textContent ?: string
63+ innerText ?: string
64+ }
65+
66+ interface ParsedSelectElement extends ParsedElementBase {
67+ selectedOptions : { value : string ; text : string } [ ]
68+ selectedIndex : number
69+ }
70+ interface ParsedInputElement extends ParsedElementBase {
71+ checked : boolean
72+ }
73+ interface ParsedMediaElement extends ParsedElementBase {
74+ currentSrc ?: string
75+ currentTime ?: number
76+ duration : number
77+ ended : boolean
78+ muted : boolean
79+ paused : boolean
80+ playbackRate : number
81+ readyState ?: number
82+ src ?: string
83+ volume ?: number
84+ }
85+
86+ interface ParsedHTMLFormElement extends ParsedElementBase {
87+ formData : Record < string , string >
88+ }
89+
90+ type AnyParsedElement =
91+ | ParsedHTMLFormElement
92+ | ParsedSelectElement
93+ | ParsedInputElement
94+ | ParsedMediaElement
95+ | ParsedElementBase
96+
97+ const parseElement = ( el : HTMLElement ) : AnyParsedElement => {
98+ const labels = parseLabels ( ( el as HTMLInputElement ) . labels )
3599 const base = {
36100 // adding a bunch of fields that are not on _all_ elements, but are on enough that it's useful to have them here.
37101 attributes : parseNodeMap ( el . attributes ) ,
38102 classList : [ ...el . classList ] ,
39103 id : el . id ,
40- labels : parseLabels ( ( el as HTMLInputElement ) . labels ) ,
104+ labels,
105+ label : labels [ 0 ] ,
41106 name : ( el as HTMLInputElement ) . name ,
42107 nodeName : el . nodeName ,
43108 tagName : el . tagName ,
44109 title : el . title ,
45110 type : ( el as HTMLInputElement ) . type ,
46111 value : ( el as HTMLInputElement ) . value ,
47- textContent : el . textContent && cleanText ( el . textContent ) ,
48- innerText : el . innerText && cleanText ( el . innerText ) ,
112+ textContent : ( el . textContent && cleanText ( el . textContent ) ) ?? undefined ,
113+ innerText : ( el . innerText && cleanText ( el . innerText ) ) ?? undefined ,
49114 }
50115
51116 if ( el instanceof HTMLSelectElement ) {
@@ -76,6 +141,11 @@ const parseElement = (el: HTMLElement) => {
76141 src : el . src ,
77142 volume : el . volume ,
78143 }
144+ } else if ( el instanceof HTMLFormElement ) {
145+ return {
146+ ...base ,
147+ formData : parseFormData ( new FormData ( el ) ) ,
148+ }
79149 }
80150 return base
81151}
@@ -111,11 +181,18 @@ export class FormSubmitGenerator implements SignalGenerator {
111181 id = 'form-submit'
112182 register ( emitter : SignalEmitter ) {
113183 const handleSubmit = ( ev : SubmitEvent ) => {
114- const target = ev . submitter !
184+ const target = ev . target as HTMLFormElement | null
185+
186+ if ( ! target ) return
187+
188+ // reference to the form element that the submit event is being fired at
189+ const submitter = ev . submitter
190+ // If the form is submitted via JavaScript using form.submit(), the submitter property will be null because no specific button/input triggered the submission.
115191 emitter . emit (
116192 createInteractionSignal ( {
117193 eventType : 'submit' ,
118- submitter : parseElement ( target ) ,
194+ target : parseElement ( target ) ,
195+ submitter : submitter ? parseElement ( submitter ) : undefined ,
119196 } )
120197 )
121198 }
@@ -128,8 +205,9 @@ export class OnChangeGenerator implements SignalGenerator {
128205 id = 'change'
129206 register ( emitter : SignalEmitter ) {
130207 const handleChange = ( ev : Event ) => {
131- const target = ev . target as HTMLElement
132- if ( target instanceof HTMLInputElement ) {
208+ const target = ev . target as HTMLElement | null
209+ if ( ! target ) return
210+ if ( target && target instanceof HTMLInputElement ) {
133211 if ( target . type === 'password' ) {
134212 logger . debug ( 'Ignoring change event for input' , target )
135213 return
0 commit comments