@@ -43,47 +43,48 @@ const SBFileUploaderHOC = function (WrappedComponent) {
4343 constructor ( props ) {
4444 super ( props ) ;
4545 bindAll ( this , [
46+ 'createFileObjects' ,
4647 'getProjectTitleFromFilename' ,
48+ 'handleFinishedLoadingUpload' ,
4749 'handleStartSelectingFileUpload' ,
48- 'setFileInput' ,
4950 'handleChange' ,
5051 'onload' ,
51- 'resetFileInput '
52+ 'removeFileObjects '
5253 ] ) ;
5354 }
54- componentWillMount ( ) {
55- this . reader = new FileReader ( ) ;
56- this . reader . onload = this . onload ;
57- this . resetFileInput ( ) ;
58- }
5955 componentDidUpdate ( prevProps ) {
6056 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
6658 }
6759 }
6860 componentWillUnmount ( ) {
69- this . reader = null ;
70- this . resetFileInput ( ) ;
61+ this . removeFileObjects ( ) ;
7162 }
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
7766 }
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 ( ) ;
8585 }
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.
8788 handleChange ( e ) {
8889 const {
8990 intl,
@@ -92,73 +93,95 @@ const SBFileUploaderHOC = function (WrappedComponent) {
9293 projectChanged,
9394 userOwnsProject
9495 } = this . props ;
95-
9696 const thisFileInput = e . target ;
9797 if ( thisFileInput . files ) { // Don't attempt to load if no file was selected
9898 this . fileToUpload = thisFileInput . files [ 0 ] ;
9999
100100 // 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.)
103104 let uploadAllowed = true ;
104105 if ( userOwnsProject || ( projectChanged && isShowingWithoutId ) ) {
105106 uploadAllowed = confirm ( // eslint-disable-line no-alert
106107 intl . formatMessage ( sharedMessages . replaceProjectWarning )
107108 ) ;
108109 }
109110 if ( uploadAllowed ) {
111+ // cues step 4
110112 this . props . requestProjectUpload ( loadingState ) ;
111113 } else {
112- this . resetFileInput ( ) ;
114+ // skips ahead to step 7
115+ this . removeFileObjects ( ) ;
113116 }
114117 this . props . closeFileMenu ( ) ;
115118 }
116119 }
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
118146 onload ( ) {
119- if ( this . reader ) {
147+ if ( this . fileReader ) {
120148 this . props . onLoadingStarted ( ) ;
121149 const filename = this . fileToUpload && this . fileToUpload . name ;
122- this . props . vm . loadProject ( this . reader . result )
150+ this . props . vm . loadProject ( this . fileReader . result )
123151 . then ( ( ) => {
124152 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
127153 if ( filename ) {
128154 const uploadedProjectTitle = this . getProjectTitleFromFilename ( filename ) ;
129155 this . props . onUpdateProjectTitle ( uploadedProjectTitle ) ;
130156 }
131- this . resetFileInput ( ) ;
132157 } )
133158 . catch ( error => {
134159 log . warn ( error ) ;
135160 this . props . intl . formatMessage ( messages . loadError ) ; // eslint-disable-line no-alert
136161 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 ( ) ;
140167 } ) ;
141168 }
142169 }
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 ;
149180 }
150181 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- ) ;
160182 const {
161183 /* eslint-disable no-unused-vars */
184+ cancelFileUpload,
162185 closeFileMenu : closeFileMenuProp ,
163186 isLoadingUpload,
164187 isShowingWithoutId,
@@ -177,7 +200,6 @@ const SBFileUploaderHOC = function (WrappedComponent) {
177200 onStartSelectingFileUpload = { this . handleStartSelectingFileUpload }
178201 { ...componentProps }
179202 />
180- { fileInput }
181203 </ React . Fragment >
182204 ) ;
183205 }
@@ -210,19 +232,25 @@ const SBFileUploaderHOC = function (WrappedComponent) {
210232 loadingState : loadingState ,
211233 projectChanged : state . scratchGui . projectChanged ,
212234 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
215237 } ;
216238 } ;
217239 const mapDispatchToProps = ( dispatch , ownProps ) => ( {
218240 cancelFileUpload : loadingState => dispatch ( onLoadedProject ( loadingState , false , false ) ) ,
219241 closeFileMenu : ( ) => dispatch ( closeFileMenu ( ) ) ,
242+ // transition project state from loading to regular, and close
243+ // loading screen and file menu
220244 onLoadingFinished : ( loadingState , success ) => {
221245 dispatch ( onLoadedProject ( loadingState , ownProps . canSave , success ) ) ;
222246 dispatch ( closeLoadingProject ( ) ) ;
223247 dispatch ( closeFileMenu ( ) ) ;
224248 } ,
249+ // show project loading screen
225250 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()
226254 requestProjectUpload : loadingState => dispatch ( requestProjectUpload ( loadingState ) )
227255 } ) ;
228256 // Allow incoming props to override redux-provided props. Used to mock in tests.
0 commit comments