Completed
Push — 16.1 ( 1e4888...9df24e )
by Ralf
12:06
created

➔ et2_inputWidget.extend.init   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 4
nop 0
dl 0
loc 49
rs 9.2258
c 0
b 0
f 0
1
/**
2
 * EGroupware eTemplate2 - JS Number object
3
 *
4
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
5
 * @package etemplate
6
 * @subpackage api
7
 * @link http://www.egroupware.org
8
 * @author Nathan Gray
9
 * @copyright Nathan Gray 2011
10
 * @version $Id$
11
 */
12
13
/*egw:uses
14
	et2_core_inputWidget;
15
	phpgwapi.Resumable.resumable;
16
*/
17
18
/**
19
 * Class which implements file upload
20
 *
21
 * @augments et2_inputWidget
22
 */
23
var et2_file = (function(){ "use strict"; return et2_inputWidget.extend(
24
{
25
	attributes: {
26
		"multiple": {
27
			"name": "Multiple files",
28
			"type": "boolean",
29
			"default": false,
30
			"description": "Allow the user to select more than one file to upload at a time.  Subject to browser support."
31
		},
32
		"max_file_size": {
33
			"name": "Maximum file size",
34
			"type": "integer",
35
			"default":0,
36
			"description": "Largest file accepted, in bytes.  Subject to server limitations.  8MB = 8388608"
37
		},
38
		"mime": {
39
			"name": "Allowed file types",
40
			"type": "string",
41
			"default": et2_no_init,
42
			"description": "Mime type (eg: image/png) or regex (eg: /^text\//i) for allowed file types"
43
		},
44
		"blur": {
45
			"name": "Placeholder",
46
			"type": "string",
47
			"default": "",
48
			"description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text."
49
		},
50
		"progress": {
51
			"name": "Progress node",
52
			"type": "string",
53
			"default": et2_no_init,
54
			"description": "The ID of an alternate node (div) to display progress and results.  The Node is fetched with et2 getWidgetById so you MUST use the id assigned in XET-File (it may not be available at creation time, so we (re)check on createStatus time)"
55
		},
56
		"onStart": {
57
			"name": "Start event handler",
58
			"type": "any",
59
			"default": et2_no_init,
60
			"description": "A (js) function called when an upload starts.  Return true to continue with upload, false to cancel."
61
		},
62
		"onFinish": {
63
			"name": "Finish event handler",
64
			"type": "any",
65
			"default": et2_no_init,
66
			"description": "A (js) function called when all files to be uploaded are finished."
67
		},
68
		drop_target: {
69
			"name": "Optional, additional drop target for HTML5 uploads",
70
			"type": "string",
71
			"default": et2_no_init,
72
			"description": "The ID of an additional drop target for HTML5 drag-n-drop file uploads"
73
		},
74
		label: {
75
			"name": "Label of file upload",
76
			"type": "string",
77
			"default": "Choose file...",
78
			"description": "String caption to be displayed on file upload span"
79
		},
80
		progress_dropdownlist: {
81
			"name": "List on files in progress like dropdown",
82
			"type": "boolean",
83
			"default": false,
84
			"description": "Style list of files in uploading progress like dropdown list with a total upload progress indicator"
85
		},
86
		onFinishOne: {
87
			"name": "Finish event handler for each one",
88
			"type": "any",
89
			"default": et2_no_init,
90
			"description": "A (js) function called when a file to be uploaded is finished."
91
		},
92
		accept: {
93
			"name": "Acceptable extensions",
94
			"type": "string",
95
			"default": '',
96
			"description": "Define types of files that the server accepts. Multiple types can be seperated by comma and the default is to accept everything."
97
		}
98
	},
99
100
	asyncOptions: {},
101
102
	/**
103
	 * Constructor
104
	 *
105
	 * @memberOf et2_file
106
	 */
107
	init: function() {
108
		this._super.apply(this, arguments);
109
110
		this.node = null;
111
		this.input = null;
112
		this.progress = null;
113
		this.span = null;
114
115
		if(!this.options.id) {
116
			console.warn("File widget needs an ID.  Used 'file_widget'.");
117
			this.options.id = "file_widget";
118
		}
119
120
		// Legacy - id ending in [] means multiple
121
		if(this.options.id.substr(-2) == "[]")
122
		{
123
			this.options.multiple = true;
124
		}
125
126
		// Set up the URL to have the request ID & the widget ID
127
		var instance = this.getInstanceManager();
128
129
		var self = this;
130
131
		this.asyncOptions = jQuery.extend({
132
			// Callbacks
133
			onStart: function(event, file_count) {
134
				return self.onStart(event, file_count);
135
			},
136
			onFinish: function(event, file_count) {
137
				self.onFinish.apply(self, [event, file_count])
138
			},
139
			onStartOne: function(event, file_name, index, file_count) {
140
141
			},
142
			onFinishOne: function(event, response, name, number, total) { return self.finishUpload(event,response,name,number,total);},
143
			onProgress: function(event, progress, name, number, total) { return self.onProgress(event,progress,name,number,total);},
144
			onError: function(event, name, error) { return self.onError(event,name,error);},
145
			beforeSend: function(form) { return self.beforeSend(form);},
146
147
148
			target: egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\File::ajax_upload"),
149
			query: function(file) {return self.beforeSend(file);},
150
			// Disable checking for already uploaded chunks
151
			testChunks: false
152
		},this.asyncOptions);
153
		this.asyncOptions.fieldName = this.options.id;
154
		this.createInputWidget();
155
	},
156
157
	destroy: function() {
158
		this._super.apply(this, arguments);
159
		this.set_drop_target(null);
160
		this.node = null;
161
		this.input = null;
162
		this.span = null;
163
		this.progress = null;
164
	},
165
166
	createInputWidget: function() {
167
		this.node = jQuery(document.createElement("div")).addClass("et2_file");
168
		this.span = jQuery(document.createElement("span"))
169
			.addClass('et2_file_span et2_button')
170
			.appendTo (this.node);
171
		if (this.options.label != '') this.span.addClass('et2_button_text');
172
		var span = this.span;
173
		this.input = jQuery(document.createElement("input"))
174
			.attr("type", "file").attr("placeholder", this.options.blur)
175
			.addClass ("et2_file_upload")
176
			.appendTo(this.node)
177
			.hover(function(e){
178
				jQuery(span)
179
					.toggleClass('et2_file_spanHover');
180
			})
181
			.on({
182
				mousedown:function (e){
183
					jQuery(span).addClass('et2_file_spanActive');
184
				},
185
				mouseup:function (e){
186
					jQuery(span).removeClass('et2_file_spanActive');
187
				}
188
			});
189
		if (this.options.accept) this.input.attr('accept', this.options.accept);
190
		var self = this;
191
		// trigger native input upload file
192
		if (!this.options.readonly) this.span.click(function(){self.input.click()});
193
		// Check for File interface, should fall back to normal form submit if missing
194
		if(typeof File != "undefined" && typeof (new XMLHttpRequest()).upload != "undefined")
195
		{
196
			this.resumable = new Resumable(this.asyncOptions);
197
			this.resumable.assignBrowse(this.input);
198
			this.resumable.on('fileAdded', jQuery.proxy(this._fileAdded, this));
199
			this.resumable.on('fileProgress', jQuery.proxy(this._fileProgress, this));
200
			this.resumable.on('fileSuccess', jQuery.proxy(this.finishUpload, this));
201
			this.resumable.on('complete', jQuery.proxy(this.onFinish, this));
202
		}
203
		else
204
		{
205
			// This may be a problem submitting via ajax
206
		}
207
		if(this.options.progress)
208
		{
209
			var widget = this.getRoot().getWidgetById(this.options.progress);
210
			if(widget)
211
			{
212
				//may be not available at createInputWidget time
213
				this.progress = jQuery(widget.getDOMNode());
214
			}
215
		}
216
		if(!this.progress)
217
		{
218
			this.progress = jQuery(document.createElement("div")).appendTo(this.node);
219
		}
220
		this.progress.addClass("progress");
221
222
		if(this.options.multiple)
223
		{
224
			this.input.attr("multiple","multiple");
225
		}
226
227
		this.setDOMNode(this.node[0]);
228
	},
229
230
	/**
231
	 * Set a widget or DOM node as a HTML5 file drop target
232
	 *
233
	 * @param String new_target widget ID or DOM node ID to be used as a new target
0 ignored issues
show
Documentation introduced by
The parameter String does not exist. Did you maybe forget to remove this comment?
Loading history...
234
	 */
235
	set_drop_target: function(new_target)
236
	{
237
		// Cancel old drop target
238
		if(this.options.drop_target)
239
		{
240
			var widget = this.getRoot().getWidgetById(this.options.drop_target);
241
			var drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target);
242
			if(drop_target)
243
			{
244
				this.resumable.unAssignDrop(drop_target);
245
			}
246
		}
247
248
		this.options.drop_target = new_target;
249
250
		if(!this.options.drop_target) return;
251
252
		// Set up new drop target
253
		var widget = this.getRoot().getWidgetById(this.options.drop_target);
254
		var drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target);
255
		if(drop_target)
256
		{
257
			this.resumable.assignDrop([drop_target]);
258
		}
259
		else
260
		{
261
			this.egw().debug("warn", "Did not find file drop target %s", this.options.drop_target);
262
		}
263
264
	},
265
	attachToDOM: function() {
266
		this._super.apply(this, arguments);
267
		// Override parent's change, file widget will fire change when finished uploading
268
		this.input.unbind("change.et2_inputWidget");
269
	},
270
	getValue: function() {
271
		var value = this.options.value ? this.options.value : this.input.val();
272
		return value;
273
	},
274
275
	/**
276
	 * Set the value of the file widget.
277
	 *
278
	 * If you pass a FileList or list of files, it will trigger the async upload
279
	 *
280
	 * @param {FileList|File[]|false} value List of files to be uploaded, or false to reset.
281
	 * @param {Event} event Most browsers require the user to initiate file transfers in some way.
282
	 *	Pass the event in, if you have it.
283
	 */
284
	set_value: function(value, event) {
285
		if(!value || typeof value == "undefined")
286
		{
287
			value = {};
288
		}
289
		if(jQuery.isEmptyObject(value))
290
		{
291
			this.options.value = {};
292
			if (this.resumable.progress() == 1) this.progress.empty();
293
294
			// Reset the HTML element
295
			this.input.wrap('<form>').closest('form').get(0).reset();
296
			this.input.unwrap();
297
298
			return;
299
		}
300
301
		if(typeof value == 'object' && value.length && typeof value[0] == 'object' && value[0].name)
302
		{
303
			try
304
			{
305
				this.input[0].files = value;
306
			}
307
			catch (e)
308
			{
309
				var self = this;
310
				var args = arguments;
311
				jQuery.each(value, function(i,file) {self.resumable.addFile(this,event);});
312
			}
313
		}
314
	},
315
316
	/**
317
	 * Set the value for label
318
	 * The label is used as caption for span tag which customize the HTML file upload styling
319
	 *
320
	 * @param {string} value text value of label
321
	 */
322
	set_label: function (value)
323
	{
324
		if (this.span != null && value != null)
325
		{
326
			this.span.text(value);
327
		}
328
	},
329
330
	getInputNode: function() {
331
		if (typeof this.input == 'undefined') return false;
332
		return this.input[0];
333
	},
334
335
336
	set_mime: function(mime) {
337
		if(!mime)
338
		{
339
			this.options.mime = null;
340
		}
341
		if(mime.indexOf("/") != 0)
342
		{
343
			// Lower case it now, if it's not a regex
344
			this.options.mime = mime.toLowerCase();
345
		}
346
		else
347
		{
348
			// Convert into a js regex
349
			var parts = mime.substr(1).match(/(.*)\/([igm]?)$/);
350
			this.options.mime = new RegExp(parts[1],parts.length > 2 ? parts[2] : "");
351
		}
352
	},
353
354
	set_multiple: function(_multiple) {
355
		this.options.multiple = _multiple;
356
		if(_multiple)
357
		{
358
			return this.input.attr("multiple", "multiple");
359
		}
360
		return this.input.removeAttr("multiple");
361
	},
362
	/**
363
	 * Check to see if the provided file's mimetype matches
364
	 *
365
	 * @param f File object
366
	 * @return boolean
367
	 */
368
	checkMime: function(f) {
369
		// If missing, let the server handle it
370
		if(!this.options.mime || !f.type) return true;
371
372
		var is_preg = (typeof this.options.mime == "object");
373
		if(!is_preg && f.type.toLowerCase() == this.options.mime || is_preg && this.options.mime.test(f.type))
374
		{
375
			return true;
376
		}
377
378
		// Not right mime
379
		return false;
380
	},
381
382
	_fileAdded: function(file,event) {
383
		// Manual additions have no event
384
		if(typeof event == 'undefined')
385
		{
386
			event = {};
387
		}
388
		// Trigger start of uploading, calls callback
389
		if(!this.resumable.isUploading())
390
		{
391
			if (!(this.onStart(event,this.resumable.files.length))) return;
392
		}
393
394
		// Here 'this' is the input
395
		if(this.checkMime(file.file))
396
		{
397
			if(this.createStatus(event,file))
398
			{
399
				// Actually start uploading
400
				this.resumable.upload();
401
			}
402
		}
403
		else
404
		{
405
			// Wrong mime type - show in the list of files
406
			return this.createStatus(
407
				this.egw().lang("File is of wrong type (%1 != %2)!", file.file.type, this.options.mime),
408
				file
409
			);
410
		}
411
412
	},
413
414
	/**
415
	 * Add in the request id
416
	 */
417
	beforeSend: function(form) {
418
		var instance = this.getInstanceManager();
419
420
		return {
421
			request_id: instance.etemplate_exec_id,
422
			widget_id: this.id
423
		};
424
	},
425
426
	/**
427
	 * Disables submit buttons while uploading
428
	 */
429
	onStart: function(event, file_count) {
430
		// Hide any previous errors
431
		this.hideMessage();
432
433
		// Disable buttons
434
		this.disabled_buttons = jQuery("input[type='submit'], button")
435
				.not("[disabled]")
436
				.attr("disabled", true)
437
				.addClass('et2_button_ro')
438
				.removeClass('et2_clickable')
439
				.css('cursor', 'default');
440
441
		event.data = this;
442
443
		//Add dropdown_progress
444
		if (this.options.progress_dropdownlist)
445
		{
446
			this._build_progressDropDownList();
447
		}
448
449
		// Callback
450
		if(this.options.onStart) return et2_call(this.options.onStart, event, file_count);
451
		return true;
452
	},
453
454
	/**
455
	 * Re-enables submit buttons when done
456
	 */
457
	onFinish: function() {
458
		this.disabled_buttons.attr("disabled", false).css('cursor','pointer').removeClass('et2_button_ro');
459
460
		var file_count = this.resumable.files.length;
461
462
		// Remove files from list
463
		while(this.resumable.files.length > 0)
464
		{
465
			this.resumable.removeFile(this.resumable.files[this.resumable.files.length -1]);
466
		}
467
468
		var event = jQuery.Event('upload');
469
470
		event.data = this;
471
472
		var result = false;
473
474
		//Remove progress_dropDown_fileList class and unbind the click handler from body
475
		if (this.options.progress_dropdownlist)
476
		{
477
			this.progress.removeClass("progress_dropDown_fileList");
478
			jQuery(this.node).find('span').removeClass('totalProgress_loader');
479
			jQuery('body').off('click');
480
		}
481
482
		if(this.options.onFinish && !jQuery.isEmptyObject(this.getValue()))
483
		{
484
			result =  et2_call(this.options.onFinish, event, file_count);
485
		}
486
		else
487
		{
488
			result = (file_count == 0 || !jQuery.isEmptyObject(this.getValue()));
489
		}
490
		if(result)
491
		{
492
			// Fire legacy change action when done
493
			this.change(this.input);
494
		}
495
	},
496
497
	/**
498
	 * Build up dropdown progress with total count indicator
499
	 *
500
	 * @todo Implement totalProgress bar instead of ajax-loader, in order to show how much percent of uploading is completed
501
	 */
502
	_build_progressDropDownList: function ()
503
	{
504
		this.progress.addClass("progress_dropDown_fileList");
505
506
		//Add uploading indicator and bind hover handler on it
507
		jQuery(this.node).find('span').addClass('totalProgress_loader');
508
509
		jQuery(this.node).find('span.et2_file_span').hover(function(){
510
					jQuery('.progress_dropDown_fileList').show();
511
		});
512
		//Bind click handler to dismiss the dropdown while uploading
513
		jQuery('body').on('click', function(event){
514
			if (event.target.className != 'remove')
515
			{
516
				jQuery('.progress_dropDown_fileList').hide();
517
			}
518
		});
519
520
	},
521
522
	/**
523
	 * Creates the elements used for displaying the file, and it's upload status, and
524
	 * attaches them to the DOM
525
	 *
526
	 * @param _event Either the event, or an error message
527
	 */
528
	createStatus: function(_event, file) {
529
		var error = (typeof _event == "object" ? "" : _event);
530
531
		if(this.options.max_file_size && file.size > this.options.max_file_size) {
532
			error = this.egw().lang("File too large.  Maximum %1", et2_vfsSize.prototype.human_size(this.options.max_file_size));
533
		}
534
535
		if(this.options.progress)
536
		{
537
			var widget = this.getRoot().getWidgetById(this.options.progress);
538
			if(widget)
539
			{
540
				this.progress = jQuery(widget.getDOMNode());
541
				this.progress.addClass("progress");
542
			}
543
		}
544
		if(this.progress)
545
		{
546
			var fileName = file.fileName || 'file';
547
			var status = jQuery("<li data-file='"+fileName+"'>"+fileName
548
					+"<div class='remove'/><span class='progressBar'><p/></span></li>")
549
				.appendTo(this.progress);
550
			jQuery("div.remove",status).on('click', file, jQuery.proxy(this.cancel,this));
551
			if(error != "")
552
			{
553
				status.addClass("message ui-state-error");
554
				status.append("<div>"+error+"</diff>");
555
				jQuery(".progressBar",status).css("display", "none");
556
			}
557
		}
558
		return error == "";
559
	},
560
561
	_fileProgress: function(file) {
562
		if(this.progress)
563
		{
564
			jQuery("li[data-file='"+file.fileName+"'] > span.progressBar > p").css("width", Math.ceil(file.progress()*100)+"%");
565
566
		}
567
		return true;
568
	},
569
570
	onError: function(event, name, error) {
571
		console.warn(event,name,error);
572
	},
573
574
	/**
575
	 * A file upload is finished, update the UI
576
	 */
577
	finishUpload: function(file, response) {
578
		var name = file.fileName || 'file';
579
580
		if(typeof response == 'string') response = jQuery.parseJSON(response);
581
		if(response.response[0] && typeof response.response[0].data.length == 'undefined') {
582
			if(typeof this.options.value != 'object') this.options.value = {};
583
			for(var key in response.response[0].data) {
584
				if(typeof response.response[0].data[key] == "string")
585
				{
586
					// Message from server - probably error
587
					jQuery("[data-file='"+name+"']",this.progress)
588
						.addClass("error")
589
						.css("display", "block")
590
						.text(response.response[0].data[key]);
591
				}
592
				else
593
				{
594
					this.options.value[key] = response.response[0].data[key];
595
					// If not multiple, we already destroyed the status, so re-create it
596
					if(!this.options.multiple)
597
					{
598
						this.createStatus({}, file);
599
					}
600
					if(this.progress)
601
					{
602
						jQuery("[data-file='"+name+"']",this.progress).addClass("message success");
603
					}
604
				}
605
			}
606
		}
607
		else if (this.progress)
608
		{
609
			jQuery("[data-file='"+name+"']",this.progress)
610
				.addClass("ui-state-error")
611
				.css("display", "block")
612
				.text(this.egw().lang("Server error"));
613
		}
614
		var event = jQuery.Event('upload');
615
616
		event.data = this;
617
618
		// Callback
619
		if(this.options.onFinishOne)
620
		{
621
			return et2_call(this.options.onFinishOne,event,response,name);
622
		}
623
		return true;
624
	},
625
626
	/**
627
	 * Remove a file from the list of values
628
	 *
629
	 * @param {File|string} File object, or file name, to remove
0 ignored issues
show
Documentation introduced by
The parameter File does not exist. Did you maybe forget to remove this comment?
Loading history...
630
	 */
631
	remove_file: function(file)
632
	{
633
		//console.info(filename);
634
		if(typeof file == 'string')
635
		{
636
			file = {fileName: file};
637
		}
638
		for(var key in this.options.value)
639
		{
640
			if(this.options.value[key].name == file.fileName)
641
			{
642
				delete this.options.value[key];
643
				jQuery('[data-file="'+file.fileName+'"]',this.node).remove();
644
				return;
645
			}
646
		}
647
		if(file.isComplete && !file.isComplete() && file.cancel) file.cancel();
648
	},
649
650
	/**
651
	 * Cancel a file - event callback
652
	 */
653
	cancel: function(e)
654
	{
655
		e.preventDefault();
656
		// Look for file name in list
657
		var target = jQuery(e.target).parents("li");
658
659
		this.remove_file(e.data);
660
661
		// In case it didn't make it to the list (error)
662
		target.remove();
663
		jQuery(e.target).remove();
664
	},
665
666
	/**
667
	 * Set readonly
668
	 *
669
	 * @param {boolean} _ro boolean readonly state, true means readonly
670
	 */
671
	set_readonly: function(_ro)
672
	{
673
		if (typeof _ro != "undefined")
674
		{
675
			this.options.readonly = _ro;
676
			this.span.toggleClass('et2_file_ro',_ro);
677
			if (this.options.readonly)
678
			{
679
				this.span.unbind('click');
680
			}
681
			else
682
			{
683
				var self = this;
684
				this.span.off().bind('click',function(){self.input.click()});
685
			}
686
		}
687
	}
688
});}).call(this);
689
690
et2_register_widget(et2_file, ["file"]);
691
692