├── 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 |
79 |
80 | Upload photo 81 |
82 |
Tags:
83 |
84 |
    85 |
  • 86 | 93 |
  • 94 |
  • 95 | 102 |
  • 103 |
  • 104 | 111 |
  • 112 |
113 |
114 |
115 |
116 | 121 |
122 |
123 |
124 | 125 |
126 |
127 |
128 | 129 |
130 |
Status:
131 |
132 | 136 | 140 |
141 |
142 |
143 | 148 |
149 |
150 |
151 |
152 | 153 | 154 | 155 |
156 | image preview 157 |
158 |
159 |
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 |
36 |
37 | Upload photo 38 | 39 | | 40 | 41 | | 46 | 47 | 48 | 49 |
50 | image preview 51 |
52 |
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 | --------------------------------------------------------------------------------