@@ -43,47 +43,48 @@ const SBFileUploaderHOC = function (WrappedComponent) {
43
43
constructor ( props ) {
44
44
super ( props ) ;
45
45
bindAll ( this , [
46
+ 'createFileObjects' ,
46
47
'getProjectTitleFromFilename' ,
48
+ 'handleFinishedLoadingUpload' ,
47
49
'handleStartSelectingFileUpload' ,
48
- 'setFileInput' ,
49
50
'handleChange' ,
50
51
'onload' ,
51
- 'resetFileInput '
52
+ 'removeFileObjects '
52
53
] ) ;
53
54
}
54
- componentWillMount ( ) {
55
- this . reader = new FileReader ( ) ;
56
- this . reader . onload = this . onload ;
57
- this . resetFileInput ( ) ;
58
- }
59
55
componentDidUpdate ( prevProps ) {
60
56
if ( this . props . isLoadingUpload && ! prevProps . isLoadingUpload ) {
61
- if ( this . fileToUpload && this . reader ) {
62
- this . reader . readAsArrayBuffer ( this . fileToUpload ) ;
63
- } else {
64
- this . props . cancelFileUpload ( this . props . loadingState ) ;
65
- }
57
+ this . handleFinishedLoadingUpload ( ) ; // cue step 5 below
66
58
}
67
59
}
68
60
componentWillUnmount ( ) {
69
- this . reader = null ;
70
- this . resetFileInput ( ) ;
61
+ this . removeFileObjects ( ) ;
71
62
}
72
- resetFileInput ( ) {
73
- this . fileToUpload = null ;
74
- if ( this . fileInput ) {
75
- this . fileInput . value = null ;
76
- }
63
+ // step 1: this is where the upload process begins
64
+ handleStartSelectingFileUpload ( ) {
65
+ this . createFileObjects ( ) ; // go to step 2
77
66
}
78
- getProjectTitleFromFilename ( fileInputFilename ) {
79
- if ( ! fileInputFilename ) return '' ;
80
- // only parse title with valid scratch project extensions
81
- // (.sb, .sb2, and .sb3)
82
- const matches = fileInputFilename . match ( / ^ ( .* ) \. s b [ 2 3 ] ? $ / ) ;
83
- if ( ! matches ) return '' ;
84
- return matches [ 1 ] . substring ( 0 , 100 ) ; // truncate project title to max 100 chars
67
+ // step 2: create a FileReader and an <input> element, and issue a
68
+ // pseudo-click to it. That will open the file chooser dialog.
69
+ createFileObjects ( ) {
70
+ // redo step 7, in case it got skipped last time and its objects are
71
+ // still in memory
72
+ this . removeFileObjects ( ) ;
73
+ // create fileReader
74
+ this . fileReader = new FileReader ( ) ;
75
+ this . fileReader . onload = this . onload ;
76
+ // create <input> element and add it to DOM
77
+ this . inputElement = document . createElement ( 'input' ) ;
78
+ this . inputElement . accept = '.sb,.sb2,.sb3' ;
79
+ this . inputElement . style = 'display: none;' ;
80
+ this . inputElement . type = 'file' ;
81
+ this . inputElement . onchange = this . handleChange ; // connects to step 3
82
+ document . body . appendChild ( this . inputElement ) ;
83
+ // simulate a click to open file chooser dialog
84
+ this . inputElement . click ( ) ;
85
85
}
86
- // called when user has finished selecting a file to upload
86
+ // step 3: user has picked a file using the file chooser dialog.
87
+ // We don't actually load the file here, we only decide whether to do so.
87
88
handleChange ( e ) {
88
89
const {
89
90
intl,
@@ -92,73 +93,95 @@ const SBFileUploaderHOC = function (WrappedComponent) {
92
93
projectChanged,
93
94
userOwnsProject
94
95
} = this . props ;
95
-
96
96
const thisFileInput = e . target ;
97
97
if ( thisFileInput . files ) { // Don't attempt to load if no file was selected
98
98
this . fileToUpload = thisFileInput . files [ 0 ] ;
99
99
100
100
// If user owns the project, or user has changed the project,
101
- // we must confirm with the user that they really intend to replace it.
102
- // (If they don't own the project and haven't changed it, no need to confirm.)
101
+ // we must confirm with the user that they really intend to
102
+ // replace it. (If they don't own the project and haven't
103
+ // changed it, no need to confirm.)
103
104
let uploadAllowed = true ;
104
105
if ( userOwnsProject || ( projectChanged && isShowingWithoutId ) ) {
105
106
uploadAllowed = confirm ( // eslint-disable-line no-alert
106
107
intl . formatMessage ( sharedMessages . replaceProjectWarning )
107
108
) ;
108
109
}
109
110
if ( uploadAllowed ) {
111
+ // cues step 4
110
112
this . props . requestProjectUpload ( loadingState ) ;
111
113
} else {
112
- this . resetFileInput ( ) ;
114
+ // skips ahead to step 7
115
+ this . removeFileObjects ( ) ;
113
116
}
114
117
this . props . closeFileMenu ( ) ;
115
118
}
116
119
}
117
- // called when file upload raw data is available in the reader
120
+ // step 4 is below, in mapDispatchToProps
121
+
122
+ // step 5: called from componentDidUpdate when project state shows
123
+ // that project data has finished "uploading" into the browser
124
+ handleFinishedLoadingUpload ( ) {
125
+ if ( this . fileToUpload && this . fileReader ) {
126
+ // begin to read data from the file. When finished,
127
+ // cues step 6 using the reader's onload callback
128
+ this . fileReader . readAsArrayBuffer ( this . fileToUpload ) ;
129
+ } else {
130
+ this . props . cancelFileUpload ( this . props . loadingState ) ;
131
+ // skip ahead to step 7
132
+ this . removeFileObjects ( ) ;
133
+ }
134
+ }
135
+ // used in step 6 below
136
+ getProjectTitleFromFilename ( fileInputFilename ) {
137
+ if ( ! fileInputFilename ) return '' ;
138
+ // only parse title with valid scratch project extensions
139
+ // (.sb, .sb2, and .sb3)
140
+ const matches = fileInputFilename . match ( / ^ ( .* ) \. s b [ 2 3 ] ? $ / ) ;
141
+ if ( ! matches ) return '' ;
142
+ return matches [ 1 ] . substring ( 0 , 100 ) ; // truncate project title to max 100 chars
143
+ }
144
+ // step 6: attached as a handler on our FileReader object; called when
145
+ // file upload raw data is available in the reader
118
146
onload ( ) {
119
- if ( this . reader ) {
147
+ if ( this . fileReader ) {
120
148
this . props . onLoadingStarted ( ) ;
121
149
const filename = this . fileToUpload && this . fileToUpload . name ;
122
- this . props . vm . loadProject ( this . reader . result )
150
+ this . props . vm . loadProject ( this . fileReader . result )
123
151
. then ( ( ) => {
124
152
this . props . onLoadingFinished ( this . props . loadingState , true ) ;
125
- // Reset the file input after project is loaded
126
- // This is necessary in case the user wants to reload a project
127
153
if ( filename ) {
128
154
const uploadedProjectTitle = this . getProjectTitleFromFilename ( filename ) ;
129
155
this . props . onUpdateProjectTitle ( uploadedProjectTitle ) ;
130
156
}
131
- this . resetFileInput ( ) ;
132
157
} )
133
158
. catch ( error => {
134
159
log . warn ( error ) ;
135
160
this . props . intl . formatMessage ( messages . loadError ) ; // eslint-disable-line no-alert
136
161
this . props . onLoadingFinished ( this . props . loadingState , false ) ;
137
- // Reset the file input after project is loaded
138
- // This is necessary in case the user wants to reload a project
139
- this . resetFileInput ( ) ;
162
+ } )
163
+ . then ( ( ) => {
164
+ // go back to step 7: whether project loading succeeded
165
+ // or failed, reset file objects
166
+ this . removeFileObjects ( ) ;
140
167
} ) ;
141
168
}
142
169
}
143
- handleStartSelectingFileUpload ( ) {
144
- // open filesystem browsing window
145
- this . fileInput . click ( ) ;
146
- }
147
- setFileInput ( input ) {
148
- this . fileInput = input ;
170
+ // step 7: remove the <input> element from the DOM and clear reader and
171
+ // fileToUpload reference, so those objects can be garbage collected
172
+ removeFileObjects ( ) {
173
+ if ( this . inputElement ) {
174
+ this . inputElement . value = null ;
175
+ document . body . removeChild ( this . inputElement ) ;
176
+ }
177
+ this . inputElement = null ;
178
+ this . fileReader = null ;
179
+ this . fileToUpload = null ;
149
180
}
150
181
render ( ) {
151
- const fileInput = (
152
- < input
153
- accept = ".sb,.sb2,.sb3"
154
- ref = { this . setFileInput }
155
- style = { { display : 'none' } }
156
- type = "file"
157
- onChange = { this . handleChange }
158
- />
159
- ) ;
160
182
const {
161
183
/* eslint-disable no-unused-vars */
184
+ cancelFileUpload,
162
185
closeFileMenu : closeFileMenuProp ,
163
186
isLoadingUpload,
164
187
isShowingWithoutId,
@@ -177,7 +200,6 @@ const SBFileUploaderHOC = function (WrappedComponent) {
177
200
onStartSelectingFileUpload = { this . handleStartSelectingFileUpload }
178
201
{ ...componentProps }
179
202
/>
180
- { fileInput }
181
203
</ React . Fragment >
182
204
) ;
183
205
}
@@ -210,19 +232,25 @@ const SBFileUploaderHOC = function (WrappedComponent) {
210
232
loadingState : loadingState ,
211
233
projectChanged : state . scratchGui . projectChanged ,
212
234
userOwnsProject : ownProps . authorUsername && user &&
213
- ( ownProps . authorUsername === user . username )
214
- // vm: state.scratchGui.vm
235
+ ( ownProps . authorUsername === user . username ) ,
236
+ vm : state . scratchGui . vm // NOTE: double check this belongs here
215
237
} ;
216
238
} ;
217
239
const mapDispatchToProps = ( dispatch , ownProps ) => ( {
218
240
cancelFileUpload : loadingState => dispatch ( onLoadedProject ( loadingState , false , false ) ) ,
219
241
closeFileMenu : ( ) => dispatch ( closeFileMenu ( ) ) ,
242
+ // transition project state from loading to regular, and close
243
+ // loading screen and file menu
220
244
onLoadingFinished : ( loadingState , success ) => {
221
245
dispatch ( onLoadedProject ( loadingState , ownProps . canSave , success ) ) ;
222
246
dispatch ( closeLoadingProject ( ) ) ;
223
247
dispatch ( closeFileMenu ( ) ) ;
224
248
} ,
249
+ // show project loading screen
225
250
onLoadingStarted : ( ) => dispatch ( openLoadingProject ( ) ) ,
251
+ // step 4: transition the project state so we're ready to handle the new
252
+ // project data. When this is done, the project state transition will be
253
+ // noticed by componentDidUpdate()
226
254
requestProjectUpload : loadingState => dispatch ( requestProjectUpload ( loadingState ) )
227
255
} ) ;
228
256
// Allow incoming props to override redux-provided props. Used to mock in tests.
0 commit comments