Skip to content

Commit efed4d3

Browse files
committed
file input is dynamically added to DOM, not rendered
1 parent f2291d9 commit efed4d3

File tree

1 file changed

+86
-58
lines changed

1 file changed

+86
-58
lines changed

src/lib/sb-file-uploader-hoc.jsx

Lines changed: 86 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -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(/^(.*)\.sb[23]?$/);
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(/^(.*)\.sb[23]?$/);
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

Comments
 (0)