-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathautoload.js
415 lines (359 loc) · 12.6 KB
/
autoload.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
/**
* If the browser supports it, find all input[type=file] and attch event
* listners to automatically load the selected file(s) using FileReader(s).
*
* Since the above is run only once when the DOM is ready, it won't pick up any
* dynamically added input elements. For those situations this adds new static
* method `FileReader.auto` which takes an input element and enables the above.
* It is safe to call `FileReader.auto` multiple times on the same element, as
* it remembers if it already has been called with that element.
*/
if (typeof FileReader === "function") (function() {
"use strict"
// --- Begin Definitions ---
var HIDDEN_FIELD_SYMBOL = Symbol("@@autoFileReader")
/**
* AutoFileReader represents the infrastructure to automatically read files
* using native FileReader objects.
*
* @constructor
* @param {HTMLInputElement} input element to attach to
* @param {String = FileReader.format} format to read as
*/
function AutoFileReader(input, format) {
var maxSize = input.getAttribute("data-max-size")
this.target = input
this.format = "readAs" + (format || FileReader.format)
if (maxSize) {
this.maxSize = _parseSize.apply(void 0, maxSize.split(" "))
} else {
this.maxSize = _parseSize(10, "MiB")
}
Object.defineProperty(input, HIDDEN_FIELD_SYMBOL, {
enumerable: false,
configurable: false,
writable: false,
value: this
})
// Enable automatic reading when this input changes
input.addEventListener("change", this.onChange.bind(this), false)
// Enable Drag'N'Drop on all of this inputs labels
this.addDragNDropListeners()
}
/**
* Parse and calculate the provided file size with the provided multipler.
*
* The provided multipler must be one of "B", "KB", "KiB", "MB", "MiB", "GB"
* or "GiB". The return value is the file size in plain bytes.
*
* @param {Number|String} size in the provided format
* @param {String = "B"} multipler for the provided size
*
* @return {Number} of bytes for the provided size and multipler
*/
function _parseSize(size, multipler) {
size = parseInt(size, 10)
multipler = _parseSize.multiplers[multipler] || _parseSize.multiplers.B
// Ensure size is not `NaN`
if (size !== size) {
throw new TypeError("max-size is not a number")
}
return parseInt(size, 10) * multipler
}
_parseSize.multiplers = {
1000: ["B", "KB", "MB", "GB"],
1024: ["B", "KiB", "MiB", "GiB"]
}
// Turns the above object into a lookup table for multiplers.
_parseSize.multiplers = Object.keys(_parseSize.multiplers)
.reduce(function calculate(result, base) {
var power, names = _parseSize.multiplers[base]
for (power = 0; power < names.length; ++power) {
result[names[power]] = Math.pow(base, power)
}
return result
}, {})
/**
* Formats the provided size, in bytes, to be human readable.
*
* Essentially the reverse of _parseSize.
*
* @param {Number} size in bytes
* @return {[Number, String]} [size, multipler]
*/
function _humanSize(size) {
var i, name
for (i = 0; i < _humanSize.multiplers.length; ++i) {
name = _humanSize.multiplers[i]
if (_parseSize.multiplers[name] < size) {
break
}
}
// Basic rounding, will have errors but that's fine for this function
return [Math.round(100 * size / _parseSize.multiplers[name]) / 100, name]
}
_humanSize.multiplers = ["GiB", "MiB", "KiB", "B"]
Object.defineProperty(AutoFileReader.prototype, "labels", {
enumerable: true,
configurable: false,
get: function getLabels() {
var parent,
input = this.target,
labels = input.labels
// Find the labels for this input in the DOM
if (labels == null) {
if (input.id) {
labels = Array.prototype.concat.apply(
[], document.querySelectorAll("label[for=" + input.id + "]")
)
} else {
labels = []
}
for (parent = input; parent != null; parent = parent.parentElement) {
if (parent instanceof HTMLLabelElement) {
if (labels.indexOf(parent) === -1) {
labels.push(parent)
}
break
}
}
}
return Array.prototype.concat.apply([], labels)
}
})
/**
* Define a lazily evaluated memonized property on the provided object with
* the provided key using the provided property descriptor.
*
* It has the same parameters as `Object.defineProperty`.
*
* @param {Object} object
* @param {String} key
* @param {Object} descriptor
*/
function _defineLazyProperty(object, key, descriptor) {
var getter = descriptor.get,
writable = descriptor.writeable || false
delete descriptor.writeable
descriptor.get = function init() {
delete descriptor.get
delete descriptor.set
descriptor.value = getter.call(this)
descriptor.writeable = writable
Object.defineProperty(this, key, descriptor)
return descriptor.value
}
Object.defineProperty(object, key, descriptor)
}
/**
* Adds the drag-n-drop event listeners to the input element's labels.
*/
AutoFileReader.prototype.addDragNDropListeners = function addDragNDropListeners() {
for (var i = 0; i < this.labels.length; ++i) {
this.labels[i].addEventListener("dragenter", enableDragAndDrop, false)
this.labels[i].addEventListener("dragover", enableDragAndDrop, false)
this.labels[i].addEventListener("drop", this.onDrop.bind(this), false)
}
}
/**
* Creates and returns a dispatcher function bound to the input element
* this is attached to.
*
* @param {File} file to set as relatedTarget on the dispatached event
* @return {dispatchEventAsync} dispatcher function
*/
AutoFileReader.prototype.dispatcher = function AutoFileReader$dispatcher(file) {
var dispatchEvent = this.target.dispatchEvent.bind(this.target)
/**
* @callback dispatchEventAsync
* @param {Event} event to dispatch asyncronously
*/
return function dispatchEventAsync(event) {
event.relatedTarget = file
// Dispatch asyncronous to avoid "The event is already being dispatched"
setTimeout(dispatchEvent, 0, event)
}
}
/**
* Read the provided file using a FileReader and pipe the readers events to
* the input element this is attached to.
*
* @param {File} file to read
*/
AutoFileReader.prototype.read = function AutoFileReader$read(file) {
var dispatchEvent = this.dispatcher(file)
if (file.size < this.maxSize) {
file.reader = new FileReader()
// Pipe events from the Filereader to this input element, but use the
// "on<event>" attributes to avoid memory leaks.
for (name in file.reader) {
if (name.slice(0, 2) === "on") {
file.reader[name] = dispatchEvent
}
}
// Start reading
file.reader[this.format](file)
} else {
console.warn("File exceeds max-size:",
_humanSize(file.size), ">", _humanSize(this.maxSize))
// Create a dummy reader to work around read only properties.
file.reader = Object.create(FileReader.prototype, {
readyState: { value: FileReader.done },
result: { value: null },
error: { value: new RangeError("File exceeds max-size") }
})
file.reader.error.max = this.maxSize
// Bind event handling to this input element
file.reader.dispatchEvent = dispatchEvent
file.reader.addEventListener = this.target.addEventListener.bind(this.target)
file.reader.removeEventListener = this.target.removeEventListener.bind(this.target)
_dispatchCustomEvent.call(file.reader, "error", file.reader.error)
}
}
/**
* Create and dispatch a CustomEvent with the provided name and details to
* the EventTarget bound to this function.
*
* @this {EventTarget} to dispatch the event to
* @param {String} name of the event
* @param {*} details for the event
*/
function _dispatchCustomEvent(name, details) {
var event = document.createEvent("CustomEvent")
event.initCustomEvent(name, false, false, details)
this.dispatchEvent(event)
}
/**
* Callback for _change_ events from the input element this is attached to.
*
* @callback onChange
* @param {Event} event
*/
AutoFileReader.prototype.onChange = function AutoFileReader$onChange(event) {
if (event.target.disabled) return
this.processFiles(event.target.files)
}
/**
* Callback for _drop_ events from the input element this is attached to.
*
* @callback onDrop
* @param {Event} event
*/
AutoFileReader.prototype.onDrop = function AutoFileReader$onDrop(event) {
event.preventDefault()
if (event.target.control.disabled) return
event.target.control.files = event.dataTransfer.files
this.processFiles(event.dataTransfer.files)
}
/**
* Read all the provided files and fire the corresponding events.
*
* @param {FileList} files to start reading
*/
AutoFileReader.prototype.processFiles = function AutoFileReader$processFiles(files) {
var i, dispatchEvent = _dispatchCustomEvent.bind(this.target)
// Fail fast, since this is most likely a programmer error
if (typeof FileReader.prototype[this.format] !== "function") {
throw new TypeError('FileReader cannot read as "'+ this.format +'"')
}
// Start reading all the files
for (i = 0; i < files.length; ++i) {
this.read(files[i])
}
dispatchEvent("loadstart-all", files)
return Promise.all(Array.prototype.map.call(files, _fileToPromise))
.then(function allLoaded() {
dispatchEvent("load-all", files)
dispatchEvent("loadend-all", files)
}, function failure(reason) {
dispatchEvent("loadend-all", files)
return Promise.reject(reason)
})
}
/**
* Listen on the provided files' readers' events and return a Promise that
* acts accordingly.
*
* @param {File} file whose reader will listened on
* @return {Promise} for the completion of the reading
*/
function _fileToPromise(file) {
var name,
events = Object.create(null),
promise = new Promise(function resolver(resolve, reject) {
events.abort = reject
events.error = reject
events.load = resolve
})
for (name in events) {
file.reader.addEventListener(name, events[name], false)
}
function removeAllListeners() {
for (var name in events) {
file.reader.removeEventListener(name, events[name], false)
}
}
// Make sure to always remove all event listners.
return promise.then(
removeAllListeners,
function failure(reason) {
removeAllListeners()
return Promise.reject(reason)
})
}
/**
* To trigger Drag'n'Drop behaviour on an element, it has to call
* `event.preventDefault` on `dragenter`/`dragover`
*
* @param {Event} event
*/
function enableDragAndDrop(event) {
event.stopPropagation()
event.preventDefault()
}
// --- End Definitions ---
if (typeof ErrorStackParser === "object") {
_defineLazyProperty(Error.prototype, "__stackFrame", {
get: function getStackFrame() {
return ErrorStackParser.parse(this)[0]
}
})
Object.defineProperties(Error.prototype, [
"fileName", "lineNumber", "columnNumber"
].filter(Object.prototype.hasOwnProperty.bind(Error.prototype))
.reduce(function toGetter(descriptors, name) {
descriptors[name] = { get: function getter() {
return this.__stackFrame[name]
}}
return descriptors
}, {}))
}
/**
* Attach the needed infrastructure for automatic reading on the provided
* input element in the provided format.
*
* This is safe to call multiple times on the same input element. It will
* remember if it already has been called.
*
* @param {HTMLInputElement} input element to attach to
* @param {String} format to read the file as
* @return {AutoFileReader}
*/
FileReader.auto = function FileReader_auto(input, format) {
if (!(input instanceof HTMLInputElement)) {
throw new TypeError(String(input) + " is not a HTMLInputElement")
}
return input[HIDDEN_FIELD_SYMBOL] || new AutoFileReader(input, format)
}
if (typeof FileReader.format === "undefined") {
FileReader.format = "DataURL"
}
// Run automatically when the DOM is ready
document.addEventListener("DOMContentLoaded", function autoAttach() {
document.removeEventListener("DOMContentLoaded", autoAttach)
var i, inputs = document.querySelectorAll("input[type=file]")
for (i = 0; i < inputs.length; ++i) {
FileReader.auto(inputs[i])
}
})
})()