├── README
├── complex
├── index.html
├── upload.php
└── uploader.js
└── simple
├── index.html
├── upload.php
└── uploader.js
/README:
--------------------------------------------------------------------------------
1 | WHAT'S THIS
2 | -------------------------------------------------------------------------------
3 | The code available in the /complex and /simple directories is POC that real
4 | Ajax file upload is possible... but only in FF3 for the moment. The code had
5 | initially accompanied a tutorial that is published at the moment at
6 | http://igstan.ro/posts/2009-01-11-ajax-file-upload-with-pure-javascript.html.
7 |
8 | SOME DETAILS
9 | -------------------------------------------------------------------------------
10 | The code in /simple concentrates on getting ONLY file data to the server, while
11 | the code in /complex tries to serialize a full blown form with every type of
12 | component that I could imagine and then send it to the server.
13 |
14 | LICENSE
15 | -------------------------------------------------------------------------------
16 | What you'll find in the source files is licensed under the new BSD license, so
17 | feel free to use it for whatever you wish. I know of somebody that used it to
18 | built a Mozilla Ubiquity command.
19 |
20 | Enjoy it!
21 |
--------------------------------------------------------------------------------
/complex/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
53 |
54 |
75 |
76 |
77 |
78 |
160 |
161 |
162 |
163 |
--------------------------------------------------------------------------------
/complex/upload.php:
--------------------------------------------------------------------------------
1 | 0) {
101 | var files = Array.prototype.slice.call(element.files, 0);
102 |
103 | return files.map(function(file, index, allFiles) {
104 | return {
105 | isFile : true,
106 | name : element.name,
107 | value : file.getAsBinary(),
108 | fileName : file.fileName
109 | };
110 | });
111 | }
112 | throw new Uploader.InvalidField;
113 | default:
114 | throw new Uploader.InvalidField;
115 | }
116 | },
117 |
118 | /**
119 | * @param HTMLSelectElement element
120 | * @param Number index
121 | * @param Array[HTMLInputElement]
122 | * @return Array
123 | */
124 | _selectField : function(element, index, elements) {
125 | var fields = [];
126 |
127 | if (element.multiple === true) {
128 | var options = element.getElementsByTagName("option");
129 | options = Array.prototype.slice.call(element.options, 0);
130 |
131 | options.forEach(function(option, index, all) {
132 | if (option.selected && option.disabled !== true) {
133 | fields.push({
134 | isFile : false,
135 | name : element.name,
136 | value : option.value,
137 | fileName : null
138 | });
139 | }
140 | });
141 | } else {
142 | fields.push({
143 | isFile : false,
144 | name : element.name,
145 | value : element.value,
146 | fileName : null
147 | });
148 | }
149 |
150 | return fields;
151 | },
152 |
153 | /**
154 | * @param element HTMLElement
155 | * @return Boolean
156 | */
157 | _filter : function(element) {
158 | return element.nodeName.toUpperCase() !== "fieldset"
159 | &&
160 | element.disabled !== true;
161 | },
162 |
163 | /**
164 | * @return Array
165 | */
166 | get elements() {
167 | var fields = [];
168 |
169 | // Transform an HTMLCollection into an Array, so that we can use
170 | // functional style array methods
171 | var elements = Array.prototype.slice.call(this.form.elements, 0);
172 |
173 | // keep only the elements that we're interested in
174 | elements.filter(this._filter, this)
175 | // Process form values for each of the elements it has
176 | .forEach(function(element, index, elements) {
177 | var type = element.nodeName.toLowerCase();
178 | var method = "_" + type + "Field";
179 |
180 | if (type !== "fieldset") {
181 | try {
182 | fields = fields.concat(this[method](element, index, elements));
183 | } catch (e if e instanceof Uploader.InvalidField) {}
184 | }
185 | }, this);
186 |
187 | return fields;
188 | },
189 |
190 | /**
191 | * @return String
192 | */
193 | generateBoundary : function() {
194 | return "AJAX-----------------------" + (new Date).getTime();
195 | },
196 |
197 | /**
198 | * @param Array elements
199 | * @param String boundary
200 | * @return String
201 | */
202 | buildMessage : function(elements, boundary) {
203 | var CRLF = "\r\n";
204 | var parts = [];
205 |
206 | elements.forEach(function(element, index, all) {
207 | var part = "";
208 |
209 | if (element.isFile) {
210 | /*
211 | * Content-Disposition header contains name of the field used
212 | * to upload the file and also the name of the file as it was
213 | * on the user's computer.
214 | */
215 | part += 'Content-Disposition: form-data; ';
216 | part += 'name="' + element.name + '"; ';
217 | part += 'filename="'+ element.fileName + '"' + CRLF;
218 |
219 | /*
220 | * Content-Type header contains the mime-type of the file to
221 | * send. Although we could build a map of mime-types that match
222 | * certain file extensions, we'll take the easy approach and
223 | * send a general binary header: application/octet-stream.
224 | */
225 | part += "Content-Type: application/octet-stream" + CRLF + CRLF;
226 |
227 | /*
228 | * File contents read as binary data, obviously
229 | */
230 | part += element.value + CRLF;
231 | } else {
232 | /*
233 | * In case of non-files fields, Content-Disposition contains
234 | * only the name of the field holding the data.
235 | */
236 | part += 'Content-Disposition: form-data; ';
237 | part += 'name="' + element.name + '"' + CRLF + CRLF;
238 |
239 | /*
240 | * Field value
241 | */
242 | part += element.value + CRLF;
243 | }
244 |
245 | parts.push(part);
246 | });
247 |
248 | var request = "--" + boundary + CRLF;
249 | request+= parts.join("--" + boundary + CRLF);
250 | request+= "--" + boundary + "--" + CRLF;
251 |
252 | return request;
253 | },
254 |
255 | /**
256 | * @return null
257 | */
258 | send : function() {
259 | var xhr = new XMLHttpRequest;
260 |
261 | xhr.open("POST", this.form.action, true);
262 | xhr.onreadystatechange = function() {
263 | if (xhr.readyState === 4) {
264 | alert(xhr.responseText);
265 | }
266 | };
267 |
268 | var boundary = this.generateBoundary();
269 | var contentType = "multipart/form-data; boundary=" + boundary;
270 | xhr.setRequestHeader("Content-Type", contentType);
271 |
272 | for (var header in this.headers) {
273 | xhr.setRequestHeader(header, headers[header]);
274 | }
275 |
276 | // finally send the request as binary data
277 | xhr.sendAsBinary(this.buildMessage(this.elements, boundary));
278 | }
279 | };
280 |
--------------------------------------------------------------------------------
/simple/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
31 |
32 |
33 |
34 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/simple/upload.php:
--------------------------------------------------------------------------------
1 | 0) {
79 | var fieldName = element.name;
80 | var fileName = element.files[0].fileName;
81 |
82 | /*
83 | * Content-Disposition header contains name of the field used
84 | * to upload the file and also the name of the file as it was
85 | * on the user's computer.
86 | */
87 | part += 'Content-Disposition: form-data; ';
88 | part += 'name="' + fieldName + '"; ';
89 | part += 'filename="'+ fileName + '"' + CRLF;
90 |
91 | /*
92 | * Content-Type header contains the mime-type of the file to
93 | * send. Although we could build a map of mime-types that match
94 | * certain file extensions, we'll take the easy approach and
95 | * send a general binary header: application/octet-stream.
96 | */
97 | part += "Content-Type: application/octet-stream" + CRLF + CRLF;
98 |
99 | /*
100 | * File contents read as binary data, obviously
101 | */
102 | part += element.files[0].getAsBinary() + CRLF;
103 | } else {
104 | /*
105 | * In case of non-files fields, Content-Disposition contains
106 | * only the name of the field holding the data.
107 | */
108 | part += 'Content-Disposition: form-data; ';
109 | part += 'name="' + element.name + '"' + CRLF + CRLF;
110 |
111 | /*
112 | * Field value
113 | */
114 | part += element.value + CRLF;
115 | }
116 |
117 | parts.push(part);
118 | });
119 |
120 | var request = "--" + boundary + CRLF;
121 | request+= parts.join("--" + boundary + CRLF);
122 | request+= "--" + boundary + "--" + CRLF;
123 |
124 | return request;
125 | },
126 |
127 | /**
128 | * @return null
129 | */
130 | send : function() {
131 | var boundary = this.generateBoundary();
132 | var xhr = new XMLHttpRequest;
133 |
134 | xhr.open("POST", this.form.action, true);
135 | xhr.onreadystatechange = function() {
136 | if (xhr.readyState === 4) {
137 | alert(xhr.responseText);
138 | }
139 | };
140 | var contentType = "multipart/form-data; boundary=" + boundary;
141 | xhr.setRequestHeader("Content-Type", contentType);
142 |
143 | for (var header in this.headers) {
144 | xhr.setRequestHeader(header, headers[header]);
145 | }
146 |
147 | // finally send the request as binary data
148 | xhr.sendAsBinary(this.buildMessage(this.elements, boundary));
149 | }
150 | };
151 |
--------------------------------------------------------------------------------