Completed
Push — 16.1 ( fbc1d1...f15ff7 )
by Nathan
30:28 queued 16:35
created

  B

Complexity

Conditions 5
Paths 5

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 5
nop 0
dl 0
loc 24
rs 8.5125
c 0
b 0
f 0
1
/**
2
 * EGroupware eTemplate2 - JS Link 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 2011 Nathan Gray
10
 * @version $Id$
11
 */
12
13
/*egw:uses
14
	/vendor/bower-asset/jquery/dist/jquery.js;
15
	/vendor/bower-asset/jquery-ui/jquery-ui.js;
16
	et2_core_inputWidget;
17
	et2_core_valueWidget;
18
19
	// Include menu system for list context menu
20
	egw_action.egw_menu_dhtmlx;
21
*/
22
23
/**
24
 * UI widgets for Egroupware linking system
25
 *
26
 * @augments et2_inputWidget
27
 */
28
var et2_link_to = (function(){ "use strict"; return et2_inputWidget.extend(
29
{
30
	attributes: {
31
		"only_app": {
32
			"name": "Application",
33
			"type": "string",
34
			"default": "",
35
			"description": "Limit to just this one application - hides app selection"
36
		},
37
		"application_list": {
38
			"name": "Application list",
39
			"type": "any",
40
			"default": "",
41
			"description": "Limit to the listed application or applications (comma seperated)"
42
		},
43
		"blur": {
44
			"name": "Placeholder",
45
			"type": "string",
46
			"default": "",
47
			"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.",
48
			translate:true
49
		},
50
 		"no_files": {
51
			"name": "No files",
52
			"type": "boolean",
53
			"default": false,
54
			"description": "Suppress attach-files"
55
		},
56
 		"search_label": {
57
			"name": "Search label",
58
			"type": "string",
59
			"default": "",
60
			"description": "Label to use for search"
61
		},
62
		"link_label": {
63
			"name": "Link label",
64
			"type": "string",
65
			"default": "Link",
66
			"description": "Label for the link button"
67
		},
68
		"value": {
69
			// Could be string or int if application is provided, or an Object
70
			"type": "any"
71
		}
72
	},
73
74
	/**
75
	 * Constructor
76
	 *
77
	 * @memberOf et2_link_to
78
	 */
79
	init: function() {
80
		this._super.apply(this, arguments);
81
82
		this.div = null;
83
84
		this.link_button = null;
85
		this.status_span = null;
86
87
		this.link_entry = null;
88
		this.file_upload = null;
89
90
		if (!this.options.readonly) this.createInputWidget();
91
	},
92
93
	destroy: function() {
94
		this.link_button = null;
95
		this.status_span = null;
96
97
		if(this.link_entry)
98
		{
99
			this.link_entry.destroy();
100
			this.link_entry = null;
101
		}
102
		if(this.file_upload)
103
		{
104
			this.file_upload.destroy();
105
			this.file_upload = null;
106
		}
107
		this.div = null;
108
109
		this._super.apply(this, arguments);
110
	},
111
112
	/**
113
	 * Override to provide proper node for sub widgets to go in
114
	 *
115
	 * @param {Object} _sender
116
	 */
117
	getDOMNode: function(_sender) {
118
		if(_sender == this) {
119
			return this.div[0];
120
		} else if (_sender._type == 'link-entry') {
121
			return this.link_div[0];
122
		} else if (_sender._type == 'file') {
123
			return this.file_div[0];
124
		} else if (_sender._type == 'vfs-select') {
125
			return this.filemanager_button[0];
126
		}
127
	},
128
129
	createInputWidget: function() {
130
		this.div = jQuery(document.createElement("div")).addClass("et2_link_to et2_toolbar");
131
132
		// Need a div for file upload widget
133
		this.file_div = jQuery(document.createElement("div")).css({display:'inline-block'}).appendTo(this.div);
134
135
		// Filemanager link popup
136
		this.filemanager_button = jQuery(document.createElement("div")).css({display:'inline-block'}).appendTo(this.div);
137
138
		// Need a div for link-to widget
139
		this.link_div = jQuery(document.createElement("div"))
140
			.addClass('div_link')
141
			// Leave room for link button
142
			.appendTo(this.div);
143
144
                // One common link button
145
		this.link_button = jQuery(document.createElement("button"))
146
			.text(this.egw().lang(this.options.link_label))
147
			.appendTo(this.div).hide()
148
			.addClass('link')
149
			.click(this, this.createLink);
150
151
                // Span for indicating status
152
		this.status_span = jQuery(document.createElement("span"))
153
			.appendTo(this.div).addClass("status").hide();
154
155
		this.setDOMNode(this.div[0]);
156
	},
157
158
	doLoadingFinished: function() {
159
		this._super.apply(this, arguments);
160
161
		var self = this;
162
		if(this.link_entry && this.vfs_select && this.file_upload)
163
		{
164
			// Already done
165
			return false;
166
		}
167
168
		// Link-to
169
		var link_entry_attrs = {
170
			id: this.id + '_link_entry',
171
			only_app: this.options.only_app,
172
			application_list: this.options.application_list,
173
			blur: this.options.search_label ? this.options.search_label : this.egw().lang('Search...'),
174
			query: function() { self.link_button.hide(); return true;},
175
			select: function() {self.link_button.show(); return true;},
176
			readonly: this.options.readonly
177
		};
178
		this.link_entry = et2_createWidget("link-entry", link_entry_attrs,this);
179
180
		// Filemanager select
181
		var select_attrs = {
182
			button_label: egw.lang('Link'),
183
			button_caption: '',
184
			readonly: this.options.readonly
185
		};
186
		// only set server-side callback, if we have a real application-id (not null or array)
187
		// otherwise it only gives an error on server-side
188
		if (self.options.value && self.options.value.to_id && typeof self.options.value.to_id != 'object') {
189
			select_attrs.method = 'EGroupware\\Api\\Etemplate\\Widget\\Link::link_existing';
190
			select_attrs.method_id = self.options.value.to_app + ':' + self.options.value.to_id;
191
		}
192
		this.vfs_select = et2_createWidget("vfs-select", select_attrs,this);
193
		this.vfs_select.set_readonly(this.options.readonly);
194
		jQuery(this.vfs_select.getDOMNode()).change( function() {
195
			var values = true;
196
			// If entry not yet saved, store for linking on server
197
			if(!self.options.value.to_id || typeof self.options.value.to_id == 'object')
198
			{
199
				values = self.options.value.to_id || {};
200
				var files = self.vfs_select.getValue();
201
				if(typeof files !== 'undefined')
202
				{
203
					for(var i = 0; i < files.length; i++)
204
					{
205
						values['link:'+files[i]] = {
206
							app: 'link',
207
							id: files[i],
208
							type: 'unknown',
209
							icon: 'link',
210
							remark: '',
211
							title: files[i]
212
						};
213
					}
214
				}
215
			}
216
			self._link_result(values);
217
		});
218
219
		// File upload
220
		var file_attrs = {
221
			multiple: true,
222
			id: this.id + '_file',
223
			label: '',
224
			// Make the whole template a drop target
225
			drop_target: this.getInstanceManager().DOMContainer.getAttribute("id"),
226
			readonly: this.options.readonly,
227
228
			// Change to this tab when they drop
229
			onStart: function(event, file_count) {
230
				// Find the tab widget, if there is one
231
				var tabs = self;
232
				do {
233
					tabs = tabs._parent;
234
				} while (tabs != self.getRoot() && tabs._type != 'tabbox');
235
				if(tabs != self.getRoot())
236
				{
237
					// Find the tab index
238
					for(var i = 0; i < tabs.tabData.length; i++)
239
					{
240
						// Find the tab
241
						if(tabs.tabData[i].contentDiv.has(self.div).length)
242
						{
243
							tabs.setActiveTab(i);
244
							break;
245
						}
246
					}
247
				}
248
				return true;
249
			},
250
			onFinish: function(event, file_count) {
251
				event.data = self;
252
				self.filesUploaded(event);
253
254
				// Auto-link uploaded files
255
				self.createLink(event);
256
			}
257
		};
258
259
		this.file_upload = et2_createWidget("file", file_attrs,this);
260
		this.file_upload.set_readonly(this.options.readonly);
261
		return true;
262
	},
263
264
	getValue: function() {
265
		return this.options.value;
266
	},
267
268
	filesUploaded: function(event) {
269
		var self = this;
270
271
		this.link_button.show();
272
	},
273
274
	/**
275
	 * Create a link using the current internal values
276
	 *
277
	 * @param {Object} event
278
	 */
279
	createLink: function(event) {
280
		// Disable link button
281
		event.data.link_button.attr("disabled", true);
282
283
		var values = event.data.options.value;
284
		var self = event.data;
285
286
		var links = [];
287
288
		// Links to other entries
289
		event.data = self.link_entry;
290
		self.link_entry.createLink(event,links);
291
292
		// Files
293
		if(!self.options.no_files)
294
		{
295
			for(var file in self.file_upload.options.value) {
296
297
				links.push({
298
					app: 'file',
299
					id: file,
300
					name: self.file_upload.options.value[file].name,
301
					type: self.file_upload.options.value[file].type,
302
					remark: jQuery("li[file='"+self.file_upload.options.value[file].name.replace(/'/g, '&quot')+"'] > input", self.file_upload.progress)
303
						.filter(function() { return jQuery(this).attr("placeholder") != jQuery(this).val();}).val()
304
				});
305
			}
306
		}
307
		if(links.length == 0)
308
		{
309
			return;
310
		}
311
312
		var request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link",
313
			[values.to_app, values.to_id, links],
314
			self._link_result,
315
			self,
316
			true,
317
			self
318
		);
319
		request.sendRequest();
320
	},
321
322
	/**
323
	 * Sent some links, server has a result
324
	 *
325
	 * @param {Object} success
326
	 */
327
	_link_result: function(success) {
328
		if(success) {
329
			this.link_button.hide().attr("disabled", false);
330
			this.status_span.removeClass("error").addClass("success");
331
			this.status_span.fadeIn().delay(1000).fadeOut();
332
			delete this.options.value.app;
333
			delete this.options.value.id;
334
			for(var file in this.file_upload.options.value) {
335
				delete this.file_upload.options.value[file];
336
			}
337
			this.file_upload.progress.empty();
338
339
			// Server says it's OK, but didn't store - we'll send this again on submit
340
			// This happens if you link to something before it's saved to the DB
341
			if(typeof success == "object")
342
			{
343
				// Save as appropriate in value
344
				if(typeof this.options.value != "object")
345
				{
346
					this.options.value = {};
347
				}
348
				this.options.value.to_id = success;
349
				for(var link in success)
350
				{
351
					// Icon should be in registry
352
					if(typeof success[link].icon == 'undefined')
353
					{
354
						success[link].icon = egw.link_get_registry(success[link].app,'icon');
355
						// No icon, try by mime type - different place for un-saved entries
356
						if(success[link].icon == false && success[link].id.type)
357
						{
358
							// Triggers icon by mime type, not thumbnail or app
359
							success[link].type = success[link].id.type;
360
							success[link].icon = true;
361
						}
362
					}
363
					// Special handling for file - if not existing, we can't ask for title
364
					if(success[link].app == 'file' && typeof success[link].title == 'undefined')
365
					{
366
						success[link].title = success[link].id.name || '';
367
					}
368
				}
369
			}
370
371
			// Look for a link-list with the same ID, refresh it
372
			var self = this;
373
			var list_widget = null;
374
			this.getRoot().iterateOver(
375
				function(widget) {
376
					if(widget.id == self.id) {
377
						list_widget = widget;
378
						if(success === true)
379
						{
380
							widget._get_links();
381
						}
382
					}
383
				},
384
				this, et2_link_list
385
			);
386
387
			// If there's an array of data (entry is not yet saved), updating the list will
388
			// not work, so add them in explicitly.
389
			if(list_widget && success)
390
			{
391
				// Clear list
392
				list_widget.set_value(null);
393
394
				// Add temp links in
395
				for(var link_id in success)
396
				{
397
					var link = success[link_id];
398
					if(typeof link.title == 'undefined')
399
					{
400
						// Callback to server for title
401
						egw.link_title(link.app, link.id, function(title) {
402
							link.title = title;
0 ignored issues
show
Bug introduced by
The variable link is changed as part of the for-each loop for example by success.link_id on line 397. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
403
							list_widget._add_link(link);
404
						});
405
					}
406
					else
407
					{
408
						// Add direct
409
						list_widget._add_link(link);
410
					}
411
				}
412
			}
413
		}
414
		else
415
		{
416
			this.status_span.removeClass("success").addClass("error")
417
				.fadeIn();
418
		}
419
		this.div.trigger('link.et2_link_to',success);
420
	},
421
422
	set_no_files: function(no_files)
423
	{
424
		if(no_files)
425
		{
426
			this.file_div.hide();
427
			this.filemanager_button.hide();
428
		}
429
		else
430
		{
431
			this.file_div.show();
432
			this.filemanager_button.show();
433
		}
434
		this.options.no_files = no_files;
435
	}
436
});}).call(this);
437
et2_register_widget(et2_link_to, ["link-to"]);
438
439
/**
440
 * @augments et2_selectbox
441
 */
442
var et2_link_apps = (function(){ "use strict"; return et2_selectbox.extend(
443
{
444
	attributes: {
445
		"only_app": {
446
			"name": "Application",
447
			"type": "string",
448
			"default": "",
449
			"description": "Limit to just this one application - hides app selection"
450
		},
451
		"application_list": {
452
			"name": "Application list",
453
			"type": "any",
454
			"default": "",
455
			"description": "Limit to the listed application or applications (comma seperated)"
456
		}
457
	},
458
459
	/**
460
	 * Constructor
461
	 *
462
	 * @memberOf et2_link_apps
463
	 */
464
	init: function() {
465
		this._super.apply(this, arguments);
466
467
		if (this.options.select_options != null)
468
		{
469
			// Preset to last application
470
			if(!this.options.value)
471
			{
472
				this.set_value(egw.preference('link_app', this.egw().getAppName()));
473
			}
474
			// Register to update preference
475
			var self = this;
476
			this.input.bind("click",function() {
477
				if (typeof self.options.value != 'undefined') var appname = self.options.value.to_app;
478
				egw.set_preference(appname || self.egw().getAppName(),'link_app',self.getValue());
479
			});
480
		}
481
	},
482
483
	/**
484
	 * We get some minor speedups by overriding parent searching and directly setting select options
485
	 *
486
	 * @param {Array} _attrs an array of attributes
487
	 */
488
	transformAttributes: function(_attrs) {
489
		var select_options = {};
490
491
		// Limit to one app
492
		if(_attrs.only_app) {
493
			select_options[_attrs.only_app] = this.egw().lang(_attrs.only_app);
494
		} else if (_attrs.application_list) {
495
			select_options = _attrs.application_list;
496
		} else {
497
			select_options = egw.link_app_list('query');
498
			if(typeof select_options['addressbook-email'] !== 'undefined')
499
			{
500
				delete select_options['addressbook-email'];
501
			}
502
		}
503
		_attrs.select_options = select_options;
504
		this._super.apply(this, arguments);
505
	}
506
});}).call(this);
507
et2_register_widget(et2_link_apps, ["link-apps"]);
508
509
/**
510
 * @augments et2_inputWidget
511
 */
512
var et2_link_entry = (function(){ "use strict"; return et2_inputWidget.extend(
513
{
514
	attributes: {
515
		"value": {
516
			"type": "any",
517
			"default": {}
518
		},
519
		"only_app": {
520
			"name": "Application",
521
			"type": "string",
522
			"default": "",
523
			"description": "Limit to just this one application - hides app selection"
524
		},
525
		"application_list": {
526
			"name": "Application list",
527
			"type": "any",
528
			"default": "",
529
			"description": "Limit to the listed applications (comma seperated)"
530
		},
531
		"blur": {
532
			"name": "Placeholder",
533
			"type": "string",
534
			"default": et2_no_init,
535
			"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.",
536
			translate:true
537
		},
538
		"query": {
539
			"name": "Query callback",
540
			"type": "js",
541
			"default": et2_no_init,
542
			"description": "Callback before query to server.  It will be passed the request & et2_link_entry objects.  Must return true, or false to abort query."
543
		},
544
		"select": {
545
			"name": "Select callback",
546
			"type": "js",
547
			"default": et2_no_init,
548
			"description": "Callback when user selects an option.  Must return true, or false to abort normal action."
549
		}
550
	},
551
552
	legacyOptions: ["only_app", "application_list"],
553
	search_timeout: 500, //ms after change to send query
554
	minimum_characters: 4, // Don't send query unless there's at least this many chars
555
556
	/**
557
	 * Constructor
558
	 *
559
	 * @memberOf et2_link_entry
560
	 */
561
	init: function() {
562
		this._super.apply(this, arguments);
563
564
		this.search = null;
565
		this.clear = null;
566
		this.app_select = null;
567
		this._oldValue = {
568
			id: null,
569
			app: this.options.value && this.options.value.app ? this.options.value.app : this.options.only_app
570
		};
571
572
		if(typeof this.options.value == 'undefined' || this.options.value == null)
573
		{
574
			this.options.value = {};
575
		}
576
		this.cache = {};
577
		this.request = null;
578
579
		this.createInputWidget();
580
	},
581
582
	destroy: function() {
583
		this._super.apply(this, arguments);
584
585
		this.div = null;
586
		if(this.search.data("ui-autocomplete"))
587
		{
588
			this.search.autocomplete("destroy");
589
		}
590
		this.search = null;
591
		this.clear = null;
592
		this.app_select = null;
593
		this.request = null;
594
	},
595
596
	createInputWidget: function() {
597
		var self = this;
598
		this.div = jQuery(document.createElement("div")).addClass("et2_link_entry");
599
600
		// Application selection
601
		this.app_select = jQuery(document.createElement("select")).appendTo(this.div)
602
			.change(function(e) {
603
				// Clear cache when app changes
604
				self.cache = {};
605
606
				// Update preference with new value
607
				egw.set_preference(self.options.value.to_app || self.egw().getAppName(),'link_app',self.app_select.val());
608
609
				if(typeof self.options.value != 'object') self.options.value = {};
610
				self.options.value.app = self.app_select.val();
611
			});
612
		var opt_count = 0;
613
		for(var key in this.options.select_options) {
614
			opt_count++;
615
			var option = jQuery(document.createElement("option"))
616
				.attr("value", key)
617
				.text(this.options.select_options[key]);
618
			option.appendTo(this.app_select);
619
		}
620
		if(this.options.only_app)
621
		{
622
			this.app_select.val(this.options.only_app);
623
			this.app_select.hide();
624
			this.div.addClass("no_app");
625
		}
626
		else
627
		{
628
			// Now that options are in, set to last used app
629
			this.app_select.val(this.options.value.app||'');
630
		}
631
632
		// Search input
633
		this.search = jQuery(document.createElement("input"))
634
			// .attr("type", "search") // Fake it for all browsers below
635
			.focus(function(){if(!self.options.only_app) {
636
				// Adjust width, leave room for app select & link button
637
				self.div.removeClass("no_app");self.app_select.show();
638
			}})
639
			.appendTo(this.div);
640
641
		this.set_blur(this.options.blur ? this.options.blur : this.egw().lang("search"), this.search);
642
643
		// Autocomplete
644
		this.search.autocomplete({
645
			source: function(request, response) {
646
				return self.query(request, response);
647
			},
648
			select: function(event, item) {
649
				event.data = self;
650
				// Correct changed value from server
651
				item.item.value = item.item.value.trim();
652
				self.select(event,item);
653
				return false;
654
			},
655
			focus: function(event, item) {
656
				event.stopPropagation();
657
				self.search.val(item.item.label);
658
				return false;
659
			},
660
			minLength: self.minimum_characters,
661
			delay: self.search_timeout,
662
			disabled: self.options.disabled,
663
			appendTo: self.div
664
		});
665
666
		// Custom display (colors)
667
		this.search.data("uiAutocomplete")._renderItem = function(ul, item) {
668
			var li = jQuery(document.createElement('li'))
669
				.data("item.autocomplete", item);
670
			var extra = {};
671
672
			// Extra stuff
673
			if(typeof item.label == 'object') {
674
				extra = item.label;
675
				item.label = extra.label ? extra.label : extra;
676
				if(extra['style.backgroundColor'] || extra.color)
677
				{
678
					li.css({'border-left': '5px solid ' + (extra.color ? extra.color : extra['style.backgroundColor'])});
679
				}
680
				// Careful with this, some browsers may have trouble loading all at once, which can slow display
681
				if(extra.icon)
682
				{
683
					var img = self.egw().image(extra.icon);
684
					if(img)
685
					{
686
						jQuery(document.createElement("img"))
687
							.attr("src", img)
688
							.css("float", "right")
689
							.appendTo(li);
690
					}
691
				}
692
			}
693
694
			// Normal stuff
695
			li.append(jQuery( "<a></a>" ).text( item.label ))
696
				.appendTo(ul);
697
			window.setTimeout(function(){ul.css('max-width', jQuery('.et2_container').width()-ul.offset().left)}, 300);
698
			return li;
699
		};
700
701
		// Bind to enter key to start search early
702
		this.search.keydown(function(e) {
703
			var keycode = (e.keyCode ? e.keyCode : e.which);
704
			if(keycode == '13' && !self.processing)
705
			{
706
				self.search.autocomplete("option","minLength", 0);
707
				self.search.autocomplete("search");
708
				self.search.autocomplete("option","minLength", self.minimum_characters);
709
				return false;
710
			}
711
		});
712
713
		// Clear / last button
714
		this.clear = jQuery(document.createElement("span"))
715
			.addClass("ui-icon ui-icon-close")
716
			.click(function(e){
717
				if (!self.search) return;	// only gives an error, we should never get into that situation
718
				// No way to tell if the results is open, so if they click the button while open, it clears
719
				if(self.last_search && self.last_search != self.search.val())
720
				{
721
					// Repeat last search (should be cached)
722
					self.search.val(self.last_search);
723
					self.last_search = "";
724
					self.search.autocomplete("search");
725
				}
726
				else
727
				{
728
					// Clear
729
					self.search.autocomplete("close");
730
					self.set_value(null);
731
					self.search.val("");
732
					// call trigger, after finishing this handler, not in the middle of it
733
					window.setTimeout(function()
734
					{
735
						self.search.trigger("change");
736
					}, 0);
737
				}
738
				self.search.focus();
739
			})
740
			.appendTo(this.div)
741
			.hide();
742
743
		this.setDOMNode(this.div[0]);
744
	},
745
746
	getDOMNode: function() {
747
		return this.div ? this.div[0] : null;
748
	},
749
750
	transformAttributes: function(_attrs) {
751
		this._super.apply(this, arguments);
752
753
754
		_attrs["select_options"] = {};
755
		if(_attrs["application_list"])
756
		{
757
			var apps = (typeof _attrs["application_list"] == "string") ? et2_csvSplit(_attrs["application_list"], null, ","): _attrs["application_list"];
758
			for(var i = 0; i < apps.length; i++)
759
			{
760
				_attrs["select_options"][apps[i]] = this.egw().lang(apps[i]);
761
			}
762
		}
763
		else
764
		{
765
			_attrs["select_options"] = this.egw().link_app_list('query');
766
			if (typeof _attrs["select_options"]["addressbook-email"] != 'undefined') delete _attrs["select_options"]["addressbook-email"];
767
		}
768
769
		// Check whether the options entry was found, if not read it from the
770
		// content array.
771
		if (_attrs["select_options"] == null)
772
		{
773
			_attrs["select_options"] = this.getArrayMgr('content')
774
				.getEntry("options-" + this.id);
775
		}
776
777
		// Default to an empty object
778
		if (_attrs["select_options"] == null)
779
		{
780
			_attrs["select_options"] = {};
781
		}
782
	},
783
784
	doLoadingFinished: function() {
785
		if(typeof this.options.value == 'object' && !this.options.value.app)
786
		{
787
			this.options.value.app = egw.preference('link_app',this.options.value.to_app || this.egw().getAppName());
788
			// If there's no value set for app, then take the first one from the selectbox
789
			if (typeof this.options.value.app == 'undefined' || !this.options.value.app)
790
			{
791
				this.options.value.app = Object.keys(this.options.select_options)[0];
792
			}
793
			this.app_select.val(this.options.value.app);
794
		}
795
		return this._super.apply(this,arguments);
796
	},
797
798
	getValue: function() {
799
		var value = this.options && this.options.only_app ? this.options.value.id : this.options? this.options.value: null;
800
		if(this.options && !this.options.only_app && this.search)
801
		{
802
			value.search = this.search.val();
803
		}
804
		return value;
805
	},
806
807
	set_value: function(_value) {
808
		if(typeof _value == 'string' || typeof _value == 'number')
809
		{
810
			if(typeof _value == 'string' && _value.indexOf(",") > 0) _value = _value.replace(",",":");
811
			if(typeof _value == 'string' && _value.indexOf(":") >= 0)
812
			{
813
				var split = _value.split(":");
814
815
				_value = {
816
					app: split.shift(),
817
					id: split.length == 1 ? split[0] : split
818
				};
819
			}
820
			else if(_value && this.options.only_app)
821
			{
822
				_value = {
823
					app: this.options.only_app,
824
					id: _value
825
				};
826
			}
827
		}
828
		this._oldValue = this.options.value;
829
		if(!_value || _value.length == 0 || _value == null || jQuery.isEmptyObject(_value))
830
		{
831
			this.search.val("");
832
			this.clear.hide();
833
			this.options.value = _value = {'id':null};
834
		}
835
		if(!_value.app) _value.app = this.options.only_app || this.app_select.val();
836
837
		if(_value.id) {
838
			// Remove specific display and revert to CSS file
839
			// show() would use inline, should be inline-block
840
			this.clear.css('display','');
841
		} else {
842
			this.clear.hide();
843
			return;
844
		}
845
		if(typeof _value != 'object' || (!_value.app && !_value.id))
846
		{
847
			console.warn("Bad value for link widget.  Need an object with keys 'app', 'id', and optionally 'title'", _value);
848
			return;
849
		}
850
		if(!_value.title) {
851
			var title = this.egw().link_title(_value.app, _value.id);
852
			if(title != null) {
853
				_value.title = title;
854
			}
855
			else
856
			{
857
				// Title will be fetched from server and then set
858
				var title = this.egw().link_title(_value.app, _value.id, function(title) {
859
					this.search.removeClass("loading").val(title+"");
860
					// Remove specific display and revert to CSS file
861
					// show() would use inline, should be inline-block
862
					this.clear.css('display','');
863
				}, this);
864
				this.search.addClass("loading");
865
			}
866
		}
867
		if(_value.title)
868
		{
869
			this.search.val(_value.title+"");
870
		}
871
		this.options.value = _value;
872
873
		jQuery("option[value='"+_value.app+"']",this.app_select).prop("selected",true);
874
		this.app_select.hide();
875
		this.div.addClass("no_app");
876
	},
877
878
	set_blur: function(_value, input) {
879
880
		if(typeof input == 'undefined') input = this.search;
881
882
		if(_value) {
883
			input.attr("placeholder", _value);	// HTML5
884
			if(!input[0].placeholder) {
885
				// Not HTML5
886
				if(input.val() == "") input.val(_value);
887
				input.focus(input,function(e) {
888
					var placeholder = _value;
889
					if(e.data.val() == placeholder) e.data.val("");
890
				}).blur(input, function(e) {
891
					var placeholder = _value;
892
					if(e.data.val() == "") e.data.val(placeholder);
893
				});
894
				if(input.val() == "") input.val(_value);
895
			}
896
		} else {
897
			this.search.removeAttr("placeholder");
898
		}
899
	},
900
901
	/**
902
	 * Set the query callback
903
	 *
904
	 * @param {function} f
905
	 */
906
	set_query: function(f)
907
	{
908
		this.options.query = f;
909
	},
910
911
	/**
912
	 * Set the select callback
913
	 *
914
	 * @param {function} f
915
	 */
916
	set_select: function(f)
917
	{
918
		this.options.select = f;
919
	},
920
921
	/**
922
	 * Ask server for entries matching selected app/type and filtered by search string
923
	 *
924
	 * @param {Object} request
925
	 * @param {Object} response
926
	 */
927
	query: function(request, response) {
928
		// If there is a pending request, abort it
929
		if(this.request)
930
		{
931
			this.request.abort();
932
			this.request = null;
933
		}
934
935
		// Remember last search
936
		this.last_search = this.search.val();
937
938
		// Allow hook / tie in
939
		if(this.options.query && typeof this.options.query == 'function')
940
		{
941
			if(!this.options.query(request, this)) return false;
942
		}
943
944
		if((typeof request.no_cache == 'undefined' && !request.no_cache) && request.term in this.cache) {
945
			return response(this.cache[request.term]);
946
		}
947
948
		// Remember callback
949
		this.response = response;
950
951
		this.search.addClass("loading");
952
		// Remove specific display and revert to CSS file
953
		// show() would use inline, should be inline-block
954
		this.clear.css('display','');
955
		this.request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_search",
956
			[this.app_select.val(), '', request.term, request.options],
957
			this._results,
958
			this,true,this
959
		).sendRequest();
960
	},
961
962
	/**
963
	 * User selected a value
964
	 *
965
	 * @param {Object} event
966
	 * @param {Object} selected
967
	 *
968
	 */
969
	select: function(event, selected) {
970
		if(selected.item.value !== null && typeof selected.item.value == "string")
971
		{
972
			// Correct changed value from server
973
			selected.item.value = selected.item.value.trim();
974
		}
975
		if(this.options.select && typeof this.options.select == 'function')
976
		{
977
			if(!this.options.select(event, selected)) return false;
978
		}
979
		if(typeof event.data.options.value != 'object' || event.data.options.value == null)
980
		{
981
			event.data.options.value = {};
982
		}
983
		event.data.options.value.id = selected.item.value;
984
985
		// Set a processing flag to filter some events
986
		event.data.processing = true;
987
988
		// Remove specific display and revert to CSS file
989
		// show() would use inline, should be inline-block
990
		this.clear.css('display','');
991
		event.data.search.val(selected.item.label);
992
993
		// Fire change event
994
		this.search.change();
995
996
		// Turn off processing flag when done
997
		window.setTimeout(jQuery.proxy(function() {delete this.processing;},event.data));
998
	},
999
1000
	/**
1001
	 * Server found some results
1002
	 *
1003
	 * @param {Array} data
1004
	 */
1005
	_results: function(data) {
1006
		if(this.request)
1007
		{
1008
			this.request = null;
1009
		}
1010
		this.search.removeClass("loading");
1011
		var result = [];
1012
		for(var id in data) {
1013
			result.push({"value": id, "label":data[id]});
1014
		}
1015
		this.cache[this.search.val()] = result;
1016
		this.response(result);
1017
	},
1018
1019
	/**
1020
	 * Create a link using the current internal values
1021
	 *
1022
	 * @param {Object} event
1023
	 * @param {Object} _links
1024
	 */
1025
	createLink: function(event, _links) {
1026
1027
		var values = event.data.options.value;
1028
		var self = event.data;
1029
		var links = [];
1030
1031
		if(typeof _links == 'undefined')
1032
		{
1033
			links = [];
1034
		}
1035
		else
1036
		{
1037
			links = _links;
1038
		}
1039
1040
		// Links to other entries
1041
		if(values.id) {
1042
			links.push({
1043
				app: values.app,
1044
				id: values.id
1045
			});
1046
			self.search.val("");
1047
		}
1048
1049
		// If a link array was passed in, don't make the ajax call
1050
		if(typeof _links == 'undefined')
1051
		{
1052
			var request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link",
1053
				[values.to_app, values.to_id, links],
1054
				self._link_result,
1055
				this,
1056
				true
1057
			);
1058
			request.sendRequest();
1059
		}
1060
	},
1061
1062
	/**
1063
	 * Sent some links, server has a result
1064
	 *
1065
	 * @param {Object} success
1066
	 *
1067
	 */
1068
	_link_result: function(success) {
1069
		if(success) {
1070
			this.link_button.hide().attr("disabled", false);
1071
			this.status_span.fadeIn().delay(1000).fadeOut();
1072
			delete this.options.value.app;
1073
			delete this.options.value.id;
1074
		}
1075
	}
1076
});}).call(this);
1077
et2_register_widget(et2_link_entry, ["link-entry"]);
1078
1079
/**
1080
 * UI widget for a single (read-only) link
1081
 *
1082
 * @augments et2_valueWidget
1083
 */
1084
var	et2_link = (function(){ "use strict"; return et2_valueWidget.extend([et2_IDetachedDOM],
1085
{
1086
	attributes: {
1087
		"only_app": {
1088
			"name": "Application",
1089
			"type": "string",
1090
			"default": "",
1091
			"description": "Use the given application, so you can pass just the ID for value"
1092
		},
1093
		"value": {
1094
			description: "Array with keys app, id, and optionally title",
1095
			type: "any"
1096
		},
1097
		"needed": {
1098
			"ignore": true
1099
		}
1100
	},
1101
	legacyOptions: ["only_app"],
1102
1103
	/**
1104
	 * Constructor
1105
	 *
1106
	 * @memberOf et2_link
1107
	 */
1108
	init: function() {
1109
		this._super.apply(this, arguments);
1110
1111
		this.label_span = jQuery(document.createElement("label"))
1112
			.addClass("et2_label");
1113
		this.link = jQuery(document.createElement("span"))
1114
			.addClass("et2_link")
1115
			.appendTo(this.label_span);
1116
1117
		if(this.options['class']) this.label_span.addClass(this.options['class']);
1118
		this.setDOMNode(this.label_span[0]);
1119
	},
1120
	destroy: function() {
1121
		if(this.link) this.link.unbind();
1122
		this.link = null;
1123
		this._super.apply(this, arguments);
1124
	},
1125
	set_label: function(label) {
1126
		// Remove current label
1127
		this.label_span.contents()
1128
			.filter(function(){ return this.nodeType == 3; }).remove();
1129
1130
		var parts = et2_csvSplit(label, 2, "%s");
1131
		this.label_span.prepend(parts[0]);
1132
		this.label_span.append(parts[1]);
1133
		this.label = label;
1134
1135
		// add class if label is empty
1136
		this.label_span.toggleClass('et2_label_empty', !label || !parts[0]);
1137
	},
1138
	set_value: function(_value) {
1139
		if(typeof _value != 'object' && _value && !this.options.only_app)
1140
		{
1141
			if(_value.indexOf(':') >= 0)
1142
			{
1143
				var app = _value.split(':',1);
1144
				var id = _value.substr(app[0].length+1);
1145
				_value = {'app': app[0], 'id': id};
1146
			}
1147
			else
1148
			{
1149
				console.warn("Bad value for link widget.  Need an object with keys 'app', 'id', and optionally 'title'", _value);
1150
				return;
1151
			}
1152
		}
1153
		// Application set, just passed ID
1154
		else if (typeof _value != "object")
1155
		{
1156
			_value = {
1157
				app:	this.options.only_app,
1158
				id:	_value
1159
			};
1160
		}
1161
		if(!_value || jQuery.isEmptyObject(_value)) {
1162
			this.link.text("").unbind();
1163
			return;
1164
		}
1165
		var self = this;
1166
		this.link.unbind();
1167
		if(_value.id && _value.app)
1168
		{
1169
			this.link.addClass("et2_link");
1170
			this.link.click( function(e){
1171
				self.egw().open(_value, "", "view",null,_value.app,_value.app);
1172
				e.stopImmediatePropagation();
1173
			});
1174
		}
1175
		else
1176
		{
1177
			this.link.removeClass("et2_link");
1178
		}
1179
		if(!_value.title) {
1180
			var self = this;
1181
			var node = this.link[0];
1182
			if(_value.app && _value.id)
1183
			{
1184
				var title = this.egw().link_title(_value.app, _value.id, function(title) {self.set_title(node, title);}, this);
1185
				if(title != null) {
1186
					_value.title = title;
1187
				}
1188
				else
1189
				{
1190
					// Title will be fetched from server and then set
1191
					return;
1192
				}
1193
			}
1194
			else
1195
			{
1196
				_value.title = "";
1197
			}
1198
		}
1199
		this.set_title(this.link, _value.title);
1200
	},
1201
1202
	/**
1203
	 * Sets the text to be displayed.
1204
	 * Used as a callback, so node is provided to make sure we get the right one
1205
	 *
1206
	 * @param {Object} node
1207
	 * @param {String} _value description
1208
	 */
1209
	set_title: function(node, _value) {
1210
		if(_value === false || _value === null) _value = "";
1211
		jQuery(node).text(_value+"");
1212
	},
1213
1214
	/**
1215
	 * Creates a list of attributes which can be set when working in the
1216
	 * "detached" mode. The result is stored in the _attrs array which is provided
1217
	 * by the calling code.
1218
	 *
1219
	 * @param {Array} _attrs an array of attributes
1220
	 */
1221
	getDetachedAttributes: function(_attrs) {
1222
		_attrs.push("label","value");
1223
	},
1224
1225
	/**
1226
	 * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be
1227
	 * passed to the "setDetachedAttributes" function in the same order.
1228
	 */
1229
	getDetachedNodes: function() {
1230
		return [this.node, this.link[0]];
1231
	},
1232
1233
	/**
1234
	 * Sets the given associative attribute->value array and applies the
1235
	 * attributes to the given DOM-Node.
1236
	 *
1237
	 * @param _nodes is an array of nodes which have to be in the same order as
1238
	 *      the nodes returned by "getDetachedNodes"
1239
	 * @param _values is an associative array which contains a subset of attributes
1240
	 *      returned by the "getDetachedAttributes" function and sets them to the
1241
	 *      given values.
1242
	 */
1243
	setDetachedAttributes: function(_nodes, _values) {
1244
		this.node = _nodes[0];
1245
		this.label_span = jQuery(_nodes[0]);
1246
		this.link = jQuery(_nodes[1]);
1247
		if(typeof _values["id"] !== "undefined") this.set_id(_values['id']);
1248
		if(typeof _values["label"] !== "undefined") this.set_label(_values['label']);
1249
		if(typeof _values["value"] !== "undefined") this.set_value(_values["value"]);
1250
	}
1251
1252
});}).call(this);
1253
et2_register_widget(et2_link, ["link", "link-entry_ro"]);
1254
1255
/**
1256
 * UI widget for one or more links, comma separated
1257
 *
1258
 * @augments et2_valueWidget
1259
 */
1260
var et2_link_string = (function(){ "use strict"; return expose(et2_valueWidget.extend([et2_IDetachedDOM],
1261
{
1262
	attributes: {
1263
		"application": {
1264
			"name": "Application",
1265
			"type": "string",
1266
			"default": "",
1267
			"description": "Use the given application, so you can pass just the ID for value"
1268
		},
1269
		"value": {
1270
			"description": "Either an array of link information (see egw_link::link()) or array with keys to_app and to_id",
1271
			"type": "any"
1272
		},
1273
		"only_app": {
1274
			"name": "Application filter",
1275
			"type": "string",
1276
			"default": "",
1277
			"description": "Appname, eg. 'projectmananager' to list only linked projects"
1278
		},
1279
		"link_type": {
1280
			"name": "Type filter",
1281
			"type": "string",
1282
			"default":"",
1283
			"description": "Sub-type key to list only entries of that type"
1284
		},
1285
		"expose_view":{
1286
			name: "Expose view",
1287
			type: "boolean",
1288
			default: true,
1289
			description: "Clicking on description with href value would popup an expose view, and will show content referenced by href."
1290
		}
1291
	},
1292
1293
	/**
1294
	 * Constructor
1295
	 *
1296
	 * @memberOf et2_link_string
1297
	 */
1298
	init: function() {
1299
		this._super.apply(this, arguments);
1300
1301
		this.list = jQuery(document.createElement("ul"))
1302
			.addClass("et2_link_string");
1303
1304
		if(this.options['class']) this.list.addClass(this.options['class']);
1305
		this.setDOMNode(this.list[0]);
1306
	},
1307
1308
	destroy: function() {
1309
		this._super.apply(this, arguments);
1310
		if (this.node != null) {
1311
			this.node.children().unbind();
1312
		}
1313
	},
1314
1315
	set_value: function(_value) {
1316
		// Get data
1317
		if(!_value || _value == null)
1318
		{
1319
			this.list.empty();
1320
			return;
1321
		}
1322
		if(typeof _value == "string" && _value.indexOf(',') > 0)
1323
		{
1324
			_value = _value.split(',');
1325
		}
1326
		if(!_value.to_app && typeof _value == "object" && this.options.application)
1327
		{
1328
			_value.to_app = this.options.application;
1329
		}
1330
1331
		if(typeof _value == 'object' && _value.to_app && _value.to_id)
1332
		{
1333
			this.value = _value;
1334
			this._get_links();
1335
			return;
1336
		}
1337
		this.list.empty();
1338
		if(typeof _value == 'object' && _value.length > 0) {
1339
			// Have full info
1340
			// Don't store new value, just update display
1341
1342
1343
			// Make new links
1344
			for(var i = 0; i < _value.length; i++)
1345
			{
1346
				if(!this.options.only_app || this.options.only_app && _value[i].app == this.options.only_app)
1347
				{
1348
					this._add_link(_value[i].id ? _value[i] : {id:_value[i], app: _value.to_app});
1349
				}
1350
			}
1351
		}
1352
		else if(this.options.application)
1353
		{
1354
			this._add_link({id:_value, app: this.options.application});
1355
		}
1356
	},
1357
1358
	_get_links: function() {
1359
		var _value = this.value;
1360
		// Just IDs - get from server
1361
		if(this.options.only_app)
1362
		{
1363
			_value.only_app = this.options.only_app;
1364
		}
1365
		this.egw().jsonq('EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_list', [_value], this.set_value, this);
1366
		return;
0 ignored issues
show
Unused Code introduced by
This return has no effect and can be removed.
Loading history...
1367
	},
1368
	/**
1369
	 * Function to get media content to feed the expose
1370
	 * @param {type} _value
1371
	 * @returns {Array|Array.getMedia.mediaContent}
1372
	 */
1373
	getMedia: function (_value)
1374
	{
1375
		var base_url = egw.webserverUrl.match(/^\//,'ig')?egw(window).window.location.origin + egw.webserverUrl : egw.webserverUrl;
1376
		var mediaContent = [];
1377
		if (_value && typeof _value.type !='undefined' && _value.type.match(/video\//,'ig'))
1378
		{
1379
			mediaContent = [{
1380
					title: _value.id,
1381
					type: _value.type,
1382
					poster:'', // TODO: Should be changed by correct video thumbnail later
1383
					href: base_url + egw().mime_open(_value),
1384
					download_href: base_url + egw().mime_open(_value) + '?download'
1385
				}];
1386
		}
1387
		else if(_value)
1388
		{
1389
			mediaContent = [{
1390
				title: _value.id,
1391
				href: base_url + egw().mime_open(_value).url,
1392
				download_href: base_url + egw().mime_open(_value).url + '?download',
1393
				type: _value.type
1394
			}];
1395
		}
1396
		if (mediaContent[0].href && mediaContent[0].href.match(/\/webdav.php/,'ig')) mediaContent[0]["download_href"] = mediaContent[0].href + '?download';
1397
		return mediaContent;
1398
	},
1399
	_add_link: function(_link_data) {
1400
		var self = this;
1401
		var link = jQuery(document.createElement("li"))
1402
			.appendTo(this.list)
1403
			.addClass("et2_link loading")
1404
			.click( function(e){
1405
				if (self.options.expose_view && typeof _link_data.type !='undefined'
1406
					&& _link_data.type.match(self.mime_regexp,'ig'))
1407
				{
1408
					self._init_blueimp_gallery(e, _link_data);
1409
				}
1410
				else
1411
				{
1412
					self.egw().open(_link_data, "", "view",null,_link_data.app,_link_data.app);
1413
				}
1414
				e.stopImmediatePropagation();
1415
			});
1416
1417
		if(_link_data.title) link.text(_link_data.title);
1418
1419
		// Now that link is created, get title from server & update
1420
		if(!_link_data.title) {
1421
			this.egw().link_title(_link_data.app, _link_data.id, function(title) {
1422
				if (title)
1423
					this.removeClass("loading").text(title);
1424
				else
1425
					this.remove();	// no rights or not found
1426
			}, link);
1427
		}
1428
	},
1429
1430
	/**
1431
	 * Creates a list of attributes which can be set when working in the
1432
	 * "detached" mode. The result is stored in the _attrs array which is provided
1433
	 * by the calling code.
1434
	 *
1435
	 * @param {Array} _attrs an array of attributes
1436
	 */
1437
	getDetachedAttributes: function(_attrs) {
1438
		// Create the label container if it didn't exist yet
1439
		if (this._labelContainer == null)
1440
		{
1441
			this._labelContainer = jQuery(document.createElement("label"))
1442
					.addClass("et2_label");
1443
			this.getSurroundings().insertDOMNode(this._labelContainer[0]);
1444
			this.getSurroundings().update();
1445
		}
1446
		_attrs.push("value","label");
1447
	},
1448
1449
	/**
1450
	 * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be
1451
	 * passed to the "setDetachedAttributes" function in the same order.
1452
	 */
1453
	getDetachedNodes: function() {
1454
		// Create the label container if it didn't exist yet
1455
		if (this._labelContainer == null)
1456
		{
1457
			this._labelContainer = jQuery(document.createElement("label"))
1458
				.addClass("et2_label");
1459
			this.getSurroundings().insertDOMNode(this._labelContainer[0]);
1460
		}
1461
		return [this.list[0], this._labelContainer[0]];
1462
	},
1463
1464
	/**
1465
	 * Sets the given associative attribute->value array and applies the
1466
	 * attributes to the given DOM-Node.
1467
	 *
1468
	 * @param _nodes is an array of nodes which have to be in the same order as
1469
	 *      the nodes returned by "getDetachedNodes"
1470
	 * @param _values is an associative array which contains a subset of attributes
1471
	 *      returned by the "getDetachedAttributes" function and sets them to the
1472
	 *      given values.
1473
	 */
1474
	setDetachedAttributes: function(_nodes, _values) {
1475
		this.list = jQuery(_nodes[0]);
1476
1477
		this.set_value(_values["value"]);
1478
1479
		// Special detached, to prevent DOM node modification of the normal method
1480
		this._labelContainer = _nodes.length > 1 ? jQuery(_nodes[1]) : null;
1481
		if(_values['label'])
1482
		{
1483
			this.set_label(_values['label']);
1484
		}
1485
		else if (this._labelContainer)
1486
		{
1487
			this._labelContainer.contents().not(this.list).remove();
1488
		}
1489
	}
1490
}));}).call(this);
1491
et2_register_widget(et2_link_string, ["link-string"]);
1492
1493
/**
1494
 * UI widget for one or more links in a list (table)
1495
 *
1496
 * @augments et2_link_string
1497
 */
1498
var et2_link_list = (function(){ "use strict"; return et2_link_string.extend(
1499
{
1500
	attributes: {
1501
		"show_deleted": {
1502
			"name": "Show deleted",
1503
			"type": "boolean",
1504
			"default": false,
1505
			"description": "Show links that are marked as deleted, being held for purge"
1506
		},
1507
		"onchange": {
1508
			"name": "onchange",
1509
			"type": "js",
1510
			"default": et2_no_init,
1511
			"description": "JS code which is executed when the links change."
1512
		},
1513
		readonly: {
1514
			name: "readonly",
1515
			type: "boolean",
1516
			"default": false,
1517
			description: "Does NOT allow user to enter data, just displays existing data"
1518
		}
1519
	},
1520
1521
	/**
1522
	 * Constructor
1523
	 *
1524
	 * @memberOf et2_link_list
1525
	 */
1526
	init: function() {
1527
		this._super.apply(this, arguments);
1528
1529
		this.list = jQuery(document.createElement("table"))
1530
			.addClass("et2_link_list");
1531
		if(this.options['class']) this.list.addClass(this.options['class']);
1532
		this.setDOMNode(this.list[0]);
1533
1534
		// Set up context menu
1535
		var self = this;
1536
		this.context = new egwMenu();
1537
		this.context.addItem("comment", this.egw().lang("Comment"), "", function() {
1538
			var link_id = typeof self.context.data.link_id == 'number' ? self.context.data.link_id : self.context.data.link_id.replace(/[:\.]/g,'_');
1539
1540
			et2_dialog.show_prompt(
1541
				function(button, comment) {
1542
					if(button != et2_dialog.OK_BUTTON) return;
1543
					var remark = jQuery('#link_'+(self.context.data.dom_id ? self.context.data.dom_id : link_id), self.list).children('.remark');
1544
					if(isNaN(self.context.data.link_id))	// new entry, not yet stored
1545
					{
1546
						remark.text(comment);
1547
						// Look for a link-to with the same ID, refresh it
1548
						if(self.context.data.link_id)
1549
						{
1550
							var _widget = link_id.widget || null;
1551
							self.getRoot().iterateOver(
1552
								function(widget) {
1553
									if(widget.id == self.id) {
1554
										_widget = widget;
1555
									}
1556
								},
1557
								self, et2_link_to
1558
							);
1559
							var value = _widget != null ? _widget.getValue() : false;
1560
							if(_widget && value && value.to_id)
1561
							{
1562
								value.to_id[self.context.data.link_id].remark = comment;
1563
							}
1564
						}
1565
						return;
1566
					}
1567
					remark.addClass("loading");
1568
					var request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_comment",
1569
						[link_id, comment],
1570
						function() {
1571
							if(remark)
1572
							{
1573
								// Append "" to make sure it's a string, not undefined
1574
								remark.removeClass("loading").text(comment+"");
1575
								// Update internal data
1576
								self.context.data.remark = comment+"";
1577
							}
1578
						},
1579
						this,true
1580
					).sendRequest();
1581
				},
1582
				'',self.egw().lang("Comment"),self.context.data.remark||''
1583
			);
1584
1585
		});
1586
		this.context.addItem("file_info", this.egw().lang("File information"), this.egw().image("edit"), function(menu_item) {
1587
			var link_data = self.context.data;
1588
			if(link_data.app == 'file')
1589
			{
1590
				// File info is always the same
1591
				var url = '/apps/'+link_data.app2+'/'+link_data.id2+'/'+decodeURIComponent(link_data.id);
1592
				if(typeof url == 'string' && url.indexOf('webdav.php'))
1593
				{
1594
					// URL is url to file in webdav, so get rid of that part
1595
					url = url.replace('/webdav.php', '');
1596
				}
1597
				else if (typeof url == 'object' && url.path)
1598
				{
1599
					url = url.path;
1600
				}
1601
				self.egw().open(url, "filemanager", "edit");
1602
			}
1603
		});
1604
		this.context.addItem("-", "-");
1605
		this.context.addItem("save", this.egw().lang("Save as"), this.egw().image('save'), function(menu_item) {
1606
			var link_data = self.context.data;
1607
			// Download file
1608
			if(link_data.download_url)
1609
			{
1610
				var url = link_data.download_url;
1611
				if (url[0] == '/') url = egw.link(url);
1612
1613
				var a = document.createElement('a');
1614
				if(typeof a.download == "undefined")
1615
				{
1616
					window.location = url+"?download";
1617
					return false;
1618
				}
1619
1620
				// Multiple file download for those that support it
1621
				a = jQuery(a)
1622
					.prop('href', url)
1623
					.prop('download', link_data.title || "")
1624
					.appendTo(self.getInstanceManager().DOMContainer);
1625
1626
				var evt = document.createEvent('MouseEvent');
1627
				evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
1628
				a[0].dispatchEvent(evt);
1629
				a.remove();
1630
				return false;
1631
			}
1632
1633
			self.egw().open(link_data, "", "view",'download',link_data.target ? link_data.target : link_data.app,link_data.app);
1634
		});
1635
		this.context.addItem("zip", this.egw().lang("Save as Zip"), this.egw().image('save_zip'), function(menu_item) {
1636
			// Highlight files for nice UI indicating what will be in the zip.
1637
			// Files have negative IDs.
1638
			jQuery('[id^="link_-"]',this.list).effect('highlight',{},2000);
1639
1640
			// Download ZIP
1641
			window.location = self.egw().link('/index.php',{
1642
				menuaction: 'EGroupware\\Api\\Etemplate\\Widget\\Link::download_zip',
1643
				app: self.value.to_app,
1644
				id: self.value.to_id
1645
			});
1646
		});
1647
		this.context.addItem("-", "-");
1648
		this.context.addItem("delete", this.egw().lang("Delete link"), this.egw().image("delete"), function(menu_item) {
1649
			var link_id = isNaN(self.context.data.link_id) ? self.context.data : self.context.data.link_id;
1650
			var row = jQuery('#link_'+(self.context.data.dom_id ? self.context.data.dom_id : self.context.data.link_id), self.list);
1651
			et2_dialog.show_dialog(
1652
				function(button) { if(button == et2_dialog.YES_BUTTON) self._delete_link(link_id,row);},
1653
				egw.lang('Delete link?')
1654
			);
1655
		});
1656
1657
		// Native DnD - Doesn't play nice with jQueryUI Sortable
1658
		// Tell jQuery to include this property
1659
		jQuery.event.props.push('dataTransfer');
1660
1661
	},
1662
1663
	destroy: function() {
1664
1665
		this._super.apply(this, arguments);
1666
		if(this.context)
1667
		{
1668
			this.context.clear();
1669
			delete this.context;
1670
		}
1671
	},
1672
1673
	set_value: function(_value)
1674
	{
1675
		this.list.empty();
1676
		// Handle server passed a list of links that aren't ready yet
1677
		if(_value && typeof _value == "object")
1678
		{
1679
			var list = [];
1680
			if(_value.to_id && typeof _value.to_id == "object")
1681
			{
1682
				list = _value.to_id;
1683
			}
1684
			else if (_value.length)
1685
			{
1686
				list = _value;
1687
			}
1688
			if(list.length > 0)
1689
			{
1690
				for(var id in list)
1691
				{
1692
					var link = list[id];
1693
					if(link.app)
1694
					{
1695
						// Temp IDs can cause problems since the ID includes the file name or :
1696
						if(link.link_id && typeof link.link_id != 'number')
1697
						{
1698
							link.dom_id = 'temp_'+egw.uid();
1699
						}
1700
						// Icon should be in registry
1701
						if(!link.icon)
1702
						{
1703
							link.icon = egw.link_get_registry(link.app,'icon');
1704
							// No icon, try by mime type - different place for un-saved entries
1705
							if(link.icon == false && link.id.type)
1706
							{
1707
								// Triggers icon by mime type, not thumbnail or app
1708
								link.type = link.id.type;
1709
								link.icon = true;
1710
							}
1711
						}
1712
						// Special handling for file - if not existing, we can't ask for title
1713
						if(typeof link.id =='object' && !link.title)
1714
						{
1715
							link.title = link.id.name || '';
1716
						}
1717
						this._add_link(link);
1718
					}
1719
				}
1720
			}
1721
			else
1722
			{
1723
				this._super.apply(this,arguments);
1724
			}
1725
		}
1726
	},
1727
1728
	_add_link: function(_link_data) {
1729
		var row = jQuery(document.createElement("tr"))
1730
			.attr("id", "link_"+(_link_data.dom_id ? _link_data.dom_id : (typeof _link_data.link_id == "string" ? _link_data.link_id.replace(/[:\.]/g,'_'):_link_data.link_id ||_link_data.id)))
1731
			.attr("draggable", _link_data.app == 'file' ? "true" : "")
1732
			.appendTo(this.list);
1733
		if(!_link_data.link_id)
1734
		{
1735
			for(var k in _link_data)
1736
			{
1737
				row[0].dataset[k] = _link_data[k];
1738
			}
1739
		}
1740
1741
		// Icon
1742
		var icon = jQuery(document.createElement("td"))
1743
			.appendTo(row)
1744
			.addClass("icon");
1745
		if(_link_data.icon)
1746
		{
1747
			var icon_widget = et2_createWidget("image");
1748
			var src = '';
1749
			// Creat a mime widget if the link has type
1750
			if(_link_data.type)
1751
			{
1752
				// VFS - file
1753
				var vfs_widget = et2_createWidget('vfs-mime');
1754
				vfs_widget.set_value({
1755
					download_url:_link_data.download_url,
1756
					name:_link_data.title,
1757
					mime:_link_data.type,
1758
					path:_link_data.icon
1759
				});
1760
				icon.append(vfs_widget.getDOMNode());
1761
			}
1762
			else
1763
			{
1764
				src = this.egw().image(_link_data.icon);
1765
				if(src)	icon_widget.set_src(src);
1766
				icon.append(icon_widget.getDOMNode());
1767
			}
1768
		}
1769
1770
		var columns = ['title','remark'];
1771
1772
		var self = this;
1773
		for(var i = 0; i < columns.length; i++) {
1774
			var $td = jQuery(document.createElement("td"))
1775
				.appendTo(row)
1776
				.addClass(columns[i])
1777
				.text(_link_data[columns[i]] ? _link_data[columns[i]]+"" : "");
1778
1779
			var dirs = _link_data[columns[i]] ? _link_data[columns[i]].split('/') : [];
1780
			if(columns[i] == 'title' && _link_data.type && dirs.length > 1)
1781
			{
1782
				this._format_vfs($td, dirs, _link_data);
1783
			}
1784
			//Bind the click handler if there is download_url
1785
			if (_link_data && (typeof _link_data.download_url != 'undefined' || _link_data.app !='egw-data'))
1786
			{
1787
				$td.click( function(){
1788
					// Check if the link entry is mime with media type, in order to open it in expose view
1789
					if (typeof _link_data.type != 'undefined' && _link_data.type.match(self.mime_regexp,'ig'))
1790
					{
1791
						var $vfs_img_node = jQuery(this).parent().find('.vfsMimeIcon');
1792
						if ($vfs_img_node.length > 0) $vfs_img_node.click();
1793
					}
1794
					else
1795
					{
1796
						self.egw().open(_link_data, "", "view",null,_link_data.target ? _link_data.target : _link_data.app,_link_data.app);
1797
					}
1798
				});
1799
			}
1800
		}
1801
1802
		if (typeof _link_data.title == 'undefined')
1803
		{
1804
			// Title will be fetched from server and then set
1805
			jQuery('td.title',row).addClass("loading");
1806
			var title = this.egw().link_title(_link_data.app, _link_data.id, function(title) {
1807
				jQuery('td.title',this).removeClass("loading").text(title+"");
1808
			}, row);
1809
		}
1810
		// Date
1811
		/*
1812
		var date_row = jQuery(document.createElement("td"))
1813
			.appendTo(row);
1814
		if(_link_data.lastmod)
1815
		{
1816
			var date_widget = et2_createWidget("date-since");
1817
			date_widget.set_value(_link_data.lastmod);
1818
			date_row.append(date_widget.getDOMNode());
1819
		}
1820
		*/
1821
1822
		// Delete
1823
		// build delete button if the link is not readonly
1824
		if (!this.options.readonly)
1825
		{
1826
			var delete_button = jQuery(document.createElement("td"))
1827
				.appendTo(row);
1828
			jQuery("<div />")
1829
				.appendTo(delete_button)
1830
				// We don't use ui-icon because it assigns a bg image
1831
				.addClass("delete icon")
1832
				.bind( 'click', function() {
1833
					et2_dialog.show_dialog(
1834
						function(button) {
1835
							if(button == et2_dialog.YES_BUTTON)
1836
							{
1837
								self._delete_link(
1838
									self.value && typeof self.value.to_id != 'object' && _link_data.link_id ? _link_data.link_id:_link_data,
1839
									row
1840
								);
1841
							}
1842
						},
1843
						egw.lang('Delete link?')
1844
					);
1845
				});
1846
		}
1847
		// Context menu
1848
		row.bind("contextmenu", function(e) {
1849
			// Comment only available if link_id is there and not readonly
1850
			self.context.getItem("comment").set_enabled(typeof _link_data.link_id != 'undefined' && !self.options.readonly);
1851
			// File info only available for existing files
1852
			self.context.getItem("file_info").set_enabled(typeof _link_data.id != 'object' && _link_data.app == 'file');
1853
			self.context.getItem("save").set_enabled(typeof _link_data.id != 'object' && _link_data.app == 'file');
1854
			// Zip download only offered if there are at least 2 files
1855
			self.context.getItem("zip").set_enabled(jQuery('[id^="link_-"]',this.list).length >= 2);
1856
			// Show delete item only if the widget is not readonly
1857
			self.context.getItem("delete").set_enabled(!self.options.readonly);
1858
1859
			self.context.data = _link_data;
1860
			self.context.showAt(e.pageX, e.pageY, true);
1861
			e.preventDefault();
1862
		});
1863
1864
1865
		// Drag - adapted from egw_action_dragdrop, sidestepping action system
1866
		// so all linked files get it
1867
		// // Unfortunately, dragging files is currently only supported by Chrome
1868
		if(navigator && navigator.userAgent.indexOf('Chrome') >= 0)
1869
		{
1870
			row.on("dragstart", _link_data, function(event) {
1871
				if(event.dataTransfer == null) {
1872
					return;
1873
				}
1874
				var data = event.data || {};
1875
				if(data && data.type && data.download_url)
1876
				{
1877
					event.dataTransfer.dropEffect="copy";
1878
					event.dataTransfer.effectAllowed="copy";
1879
1880
					var url = data.download_url;
1881
1882
					// NEED an absolute URL
1883
					if (url[0] == '/') url = egw.link(url);
1884
					// egw.link adds the webserver, but that might not be an absolute URL - try again
1885
					if (url[0] == '/') url = window.location.origin+url;
1886
1887
					// Unfortunately, dragging files is currently only supported by Chrome
1888
					if(navigator && navigator.userAgent.indexOf('Chrome'))
1889
					{
1890
						event.dataTransfer.setData("DownloadURL", data.type+':'+data.title+':'+url);
1891
					}
1892
1893
					// Include URL as a fallback
1894
					event.dataTransfer.setData("text/uri-list", url);
1895
				}
1896
1897
				if(event.dataTransfer.types.length == 0)
1898
				{
1899
					// No file data? Abort: drag does nothing
1900
					event.preventDefault();
1901
					return;
1902
				}
1903
				//event.dataTransfer.setDragImage(event.delegate.target,0,0);
1904
				var div = jQuery(document.createElement("div"))
1905
					.attr('id', 'drag_helper')
1906
					.css({
1907
						position: 'absolute',
1908
						top: '0px',
1909
						left: '0px',
1910
						width: '300px'
1911
					});
1912
				div.append(event.target.cloneNode(true));
1913
1914
				self.list.append(div);
1915
1916
				event.dataTransfer.setDragImage(div.get(0),0,0);
1917
			})
1918
			.on('drag', function() {
1919
				jQuery('#drag_helper',self.list).remove();
1920
			});
1921
		}
1922
	},
1923
	_delete_link: function(link_id, row) {
1924
		if(row)
1925
		{
1926
			var delete_button = jQuery('.delete',row);
1927
			delete_button.removeClass("delete").addClass("loading");
1928
			row.off();
1929
		}
1930
		if(this.onchange)
1931
		{
1932
			this.onchange(this,link_id,row);
1933
		}
1934
		if(typeof link_id != "object")
1935
		{
1936
			egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_delete", [link_id],
1937
				function(data) { if(data) {row.slideUp(row.remove);}}
1938
			).sendRequest();
1939
		}
1940
		else if (row)
1941
		{
1942
			// No link ID means a link on an unsaved entry.
1943
			// Just remove the row, but need to adjust the link_to value also
1944
			row.slideUp(row.remove);
1945
1946
			// Look for a link-to with the same ID, refresh it
1947
			if(link_id.link_id)
1948
			{
1949
				var self = this;
1950
				var _widget = link_id.widget || null;
1951
				this.getRoot().iterateOver(
1952
					function(widget) {
1953
						if(widget.id == self.id) {
1954
							_widget = widget;
1955
						}
1956
					},
1957
					this, et2_link_to
1958
				);
1959
				var value = _widget != null ? _widget.getValue() : false;
1960
				if(_widget && value && value.to_id)
1961
				{
1962
					delete value.to_id[link_id.link_id];
1963
					_widget.set_value(value);
1964
				}
1965
			}
1966
		}
1967
	},
1968
1969
	/**
1970
	 * When the link is to a VFS file, we do some special formatting.
1971
	 *
1972
	 * Instead of listing the full path, we use
1973
	 *	Path: - filename
1974
	 * When multiple files from the same directory are linked, we exclude
1975
	 * the directory name from all but the first link to that directory
1976
	 *
1977
	 * @param {JQuery} $td Current table data cell for the title
1978
	 * @param {String[]} dirs List of directories in the linked file's path
1979
	 * @param {String[]} _link_data Data for the egw_link
1980
	 * @returns {undefined}
1981
	 */
1982
	_format_vfs: function($td, dirs, _link_data)
1983
	{
1984
		// Keep it here for matching next row
1985
		$td.attr('data-title', _link_data['title']);
1986
1987
		// VFS link - check for same dir as above, and hide dir
1988
		var reformat = false;
1989
		var span_size = 0.3;
1990
		var prev = jQuery('td.title',$td.parent().prev('tr'));
1991
		if(prev.length === 1)
1992
		{
1993
			var prev_dirs = (prev.attr('data-title') || '').split('/');
1994
			if(prev_dirs.length > 1 && prev_dirs.length == dirs.length)
1995
			{
1996
				for(var i = 0; i < dirs.length; i++)
1997
				{
1998
					// Current is same as prev, blank it
1999
					if(dirs[i] === prev_dirs[i])
2000
					{
2001
						reformat = true;
2002
						span_size += dirs[i].length+1;
2003
						dirs[i] = '';
2004
					}
2005
					else
2006
					{
2007
						break;
2008
					}
2009
				}
2010
			}
2011
		}
2012
		if(reformat)
2013
		{
2014
			$td.html('<span style="display: inline-block; width:'+span_size+'ex;"></span> - '+dirs.join(''));
2015
		}
2016
		else
2017
		{
2018
			// Different format for directory
2019
			var filename = dirs.pop();
2020
			span_size += dirs.join('/').length+1;
2021
			$td.html('<span style="display: inline-block; width:'+span_size+'ex;">'+dirs.join('/')+':</span> - ' + filename);
2022
		}
2023
	}
2024
});}).call(this);
2025
et2_register_widget(et2_link_list, ["link-list"]);
2026
2027
2028
/**
2029
 * UI widget for one or more links in a list (table)
2030
 *
2031
 * @augments et2_inputWidget
2032
 */
2033
var et2_link_add = (function(){ "use strict"; return et2_inputWidget.extend(
2034
{
2035
	attributes: {
2036
		"value": {
2037
			"description": "Either an array of link information (see egw_link::link()) or array with keys to_app and to_id",
2038
			"type": "any"
2039
		},
2040
		"application": {
2041
			"name": "Application",
2042
			"type": "string",
2043
			"default": "",
2044
			"description": "Limit to the listed application or applications (comma seperated)"
2045
		}
2046
	},
2047
	/**
2048
	 * Constructor
2049
	 *
2050
	 * @memberOf et2_link_add
2051
	 */
2052
	init: function() {
2053
		this._super.apply(this, arguments);
2054
2055
		this.span = jQuery(document.createElement("span"))
2056
				.text(this.egw().lang("Add new"))
2057
				.addClass('et2_link_add_span');
2058
		this.div = jQuery(document.createElement("div")).append(this.span);
2059
		this.setDOMNode(this.div[0]);
2060
	},
2061
	doLoadingFinished: function() {
2062
		this._super.apply(this, arguments);
2063
		if(this.app_select && this.button)
2064
		{
2065
			// Already done
2066
			return false;
2067
		}
2068
		this.app_select = et2_createWidget("link-apps", jQuery.extend({},this.options,{
2069
			'id': this.options.id + 'app',
2070
			value: this.options.application ? this.options.application : this.options.value && this.options.value.add_app ? this.options.value.add_app : null,
2071
			application_list: this.options.application ? this.options.application : null
2072
		}) ,this);
2073
		this.div.append(this.app_select.getDOMNode());
2074
		this.button = et2_createWidget("button", {id:this.options.id+"_add",label: this.egw().lang("add")}, this);
2075
		this.button.set_label(this.egw().lang("add"));
2076
		var self = this;
2077
		this.button.click = function() {
2078
			self.egw().open(self.options.value.to_app + ":" + self.options.value.to_id, self.app_select.get_value(), 'add');
2079
		};
2080
		this.div.append(this.button.getDOMNode());
2081
2082
		return true;
2083
	},
2084
	/**
2085
	 * Should be handled client side.
2086
	 * Return null to avoid overwriting other link values, in case designer used the same ID for multiple widgets
2087
	 */
2088
	getValue: function() {
2089
		return null;
2090
	}
2091
});}).call(this);
2092
et2_register_widget(et2_link_add, ["link-add"]);
2093