11/**
22 * Parse XML response from AWS S3 and return the file key.
33 *
4- * @param {string } responseText - XML response form AWS S3.
4+ * @param {string } responseText - XML response from AWS S3.
55 * @return {string } - Key from response.
66 */
77export function getKeyFromResponse ( responseText ) {
@@ -11,33 +11,201 @@ export function getKeyFromResponse(responseText) {
1111
1212/**
1313 * Custom element to upload files to AWS S3.
14+ * Safari-compatible autonomous custom element that acts as a file input.
1415 *
15- * @extends HTMLInputElement
16+ * @extends HTMLElement
1617 */
17- export class S3FileInput extends globalThis . HTMLInputElement {
18+ export class S3FileInput extends globalThis . HTMLElement {
19+ static passThroughAttributes = [ "accept" , "required" , "multiple" , "class" , "style" ]
1820 constructor ( ) {
1921 super ( )
20- this . type = "file"
2122 this . keys = [ ]
2223 this . upload = null
24+ this . _files = [ ]
25+ this . _validationMessage = ""
26+ this . _internals = null
27+
28+ // Try to attach ElementInternals for form participation
29+ try {
30+ this . _internals = this . attachInternals ?. ( )
31+ } catch ( e ) {
32+ // ElementInternals not supported
33+ }
2334 }
2435
2536 connectedCallback ( ) {
26- this . form . addEventListener ( "formdata" , this . fromDataHandler . bind ( this ) )
27- this . form . addEventListener ( "submit" , this . submitHandler . bind ( this ) , { once : true } )
28- this . form . addEventListener ( "upload" , this . uploadHandler . bind ( this ) )
29- this . addEventListener ( "change" , this . changeHandler . bind ( this ) )
37+ // Create a hidden file input for the file picker functionality
38+ this . _fileInput = document . createElement ( "input" )
39+ this . _fileInput . type = "file"
40+
41+ // Sync attributes to hidden input
42+ this . _syncAttributesToHiddenInput ( )
43+
44+ // Listen for file selection on hidden input
45+ this . _fileInput . addEventListener ( "change" , ( ) => {
46+ this . _files = this . _fileInput . files
47+ this . dispatchEvent ( new Event ( "change" , { bubbles : true } ) )
48+ this . changeHandler ( )
49+ } )
50+
51+ // Append elements
52+ this . appendChild ( this . _fileInput )
53+
54+ // Setup form event listeners
55+ this . form ?. addEventListener ( "formdata" , this . fromDataHandler . bind ( this ) )
56+ this . form ?. addEventListener ( "submit" , this . submitHandler . bind ( this ) , {
57+ once : true ,
58+ } )
59+ this . form ?. addEventListener ( "upload" , this . uploadHandler . bind ( this ) )
60+ }
61+
62+ /**
63+ * Sync attributes from custom element to hidden input.
64+ */
65+ _syncAttributesToHiddenInput ( ) {
66+ if ( ! this . _fileInput ) return
67+
68+ S3FileInput . passThroughAttributes . forEach ( ( attr ) => {
69+ if ( this . hasAttribute ( attr ) ) {
70+ this . _fileInput . setAttribute ( attr , this . getAttribute ( attr ) )
71+ } else {
72+ this . _fileInput . removeAttribute ( attr )
73+ }
74+ } )
75+
76+ this . _fileInput . disabled = this . hasAttribute ( "disabled" )
77+ }
78+
79+ /**
80+ * Implement HTMLInputElement-like properties.
81+ */
82+ get files ( ) {
83+ return this . _files
84+ }
85+
86+ get type ( ) {
87+ return "file"
88+ }
89+
90+ get name ( ) {
91+ return this . getAttribute ( "name" ) || ""
92+ }
93+
94+ set name ( value ) {
95+ this . setAttribute ( "name" , value )
96+ }
97+
98+ get value ( ) {
99+ if ( this . _files && this . _files . length > 0 ) {
100+ return this . _files [ 0 ] . name
101+ }
102+ return ""
103+ }
104+
105+ set value ( val ) {
106+ // Setting value on file inputs is restricted for security
107+ if ( val === "" || val === null ) {
108+ this . _files = [ ]
109+ if ( this . _fileInput ) {
110+ this . _fileInput . value = ""
111+ }
112+ }
113+ }
114+
115+ get form ( ) {
116+ return this . _internals ?. form || this . closest ( "form" )
117+ }
118+
119+ get disabled ( ) {
120+ return this . hasAttribute ( "disabled" )
121+ }
122+
123+ set disabled ( value ) {
124+ if ( value ) {
125+ this . setAttribute ( "disabled" , "" )
126+ } else {
127+ this . removeAttribute ( "disabled" )
128+ }
129+ }
130+
131+ get required ( ) {
132+ return this . hasAttribute ( "required" )
133+ }
134+
135+ set required ( value ) {
136+ if ( value ) {
137+ this . setAttribute ( "required" , "" )
138+ } else {
139+ this . removeAttribute ( "required" )
140+ }
141+ }
142+
143+ get validity ( ) {
144+ if ( this . _internals ) {
145+ return this . _internals . validity
146+ }
147+ // Create a basic ValidityState-like object
148+ const isValid = ! this . required || ( this . _files && this . _files . length > 0 )
149+ return {
150+ valid : isValid && ! this . _validationMessage ,
151+ valueMissing : this . required && ( ! this . _files || this . _files . length === 0 ) ,
152+ customError : ! ! this . _validationMessage ,
153+ badInput : false ,
154+ patternMismatch : false ,
155+ rangeOverflow : false ,
156+ rangeUnderflow : false ,
157+ stepMismatch : false ,
158+ tooLong : false ,
159+ tooShort : false ,
160+ typeMismatch : false ,
161+ }
162+ }
163+
164+ get validationMessage ( ) {
165+ return this . _validationMessage
166+ }
167+
168+ setCustomValidity ( message ) {
169+ this . _validationMessage = message || ""
170+ if ( this . _internals && typeof this . _internals . setValidity === "function" ) {
171+ if ( message ) {
172+ this . _internals . setValidity ( { customError : true } , message )
173+ } else {
174+ this . _internals . setValidity ( { } )
175+ }
176+ }
177+ }
178+
179+ reportValidity ( ) {
180+ const validity = this . validity
181+ if ( validity && ! validity . valid ) {
182+ this . dispatchEvent ( new Event ( "invalid" , { bubbles : false , cancelable : true } ) )
183+ return false
184+ }
185+ return true
186+ }
187+
188+ checkValidity ( ) {
189+ return this . validity . valid
190+ }
191+
192+ click ( ) {
193+ if ( this . _fileInput ) {
194+ this . _fileInput . click ( )
195+ }
30196 }
31197
32198 changeHandler ( ) {
33199 this . keys = [ ]
34200 this . upload = null
35201 try {
36- this . form . removeEventListener ( "submit" , this . submitHandler . bind ( this ) )
202+ this . form ? .removeEventListener ( "submit" , this . submitHandler . bind ( this ) )
37203 } catch ( error ) {
38204 console . debug ( error )
39205 }
40- this . form . addEventListener ( "submit" , this . submitHandler . bind ( this ) , { once : true } )
206+ this . form ?. addEventListener ( "submit" , this . submitHandler . bind ( this ) , {
207+ once : true ,
208+ } )
41209 }
42210
43211 /**
@@ -48,15 +216,15 @@ export class S3FileInput extends globalThis.HTMLInputElement {
48216 */
49217 async submitHandler ( event ) {
50218 event . preventDefault ( )
51- this . form . dispatchEvent ( new window . CustomEvent ( "upload" ) )
52- await Promise . all ( this . form . pendingRquests )
53- this . form . requestSubmit ( event . submitter )
219+ this . form ? .dispatchEvent ( new window . CustomEvent ( "upload" ) )
220+ await Promise . all ( this . form ? .pendingRquests )
221+ this . form ? .requestSubmit ( event . submitter )
54222 }
55223
56224 uploadHandler ( ) {
57225 if ( this . files . length && ! this . upload ) {
58226 this . upload = this . uploadFiles ( )
59- this . form . pendingRquests = this . form . pendingRquests || [ ]
227+ this . form . pendingRquests = this . form ? .pendingRquests || [ ]
60228 this . form . pendingRquests . push ( this . upload )
61229 }
62230 }
@@ -99,7 +267,10 @@ export class S3FileInput extends globalThis.HTMLInputElement {
99267 s3Form . append ( "file" , file )
100268 console . debug ( "uploading" , this . dataset . url , file )
101269 try {
102- const response = await fetch ( this . dataset . url , { method : "POST" , body : s3Form } )
270+ const response = await fetch ( this . dataset . url , {
271+ method : "POST" ,
272+ body : s3Form ,
273+ } )
103274 if ( response . status === 201 ) {
104275 this . keys . push ( getKeyFromResponse ( await response . text ( ) ) )
105276 } else {
@@ -108,11 +279,29 @@ export class S3FileInput extends globalThis.HTMLInputElement {
108279 }
109280 } catch ( error ) {
110281 console . error ( error )
111- this . setCustomValidity ( error )
282+ this . setCustomValidity ( String ( error ) )
112283 this . reportValidity ( )
113284 }
114285 }
115286 }
287+
288+ /**
289+ * Called when observed attributes change.
290+ */
291+ static get observedAttributes ( ) {
292+ return this . passThroughAttributes . concat ( [ "name" , "id" ] )
293+ }
294+
295+ attributeChangedCallback ( name , oldValue , newValue ) {
296+ this . _syncAttributesToHiddenInput ( )
297+ }
298+
299+ /**
300+ * Declare this element as a form-associated custom element.
301+ */
302+ static get formAssociated ( ) {
303+ return true
304+ }
116305}
117306
118- globalThis . customElements . define ( "s3-file" , S3FileInput , { extends : "input" } )
307+ globalThis . customElements . define ( "s3-file" , S3FileInput )
0 commit comments