Issues (4868)

api/js/etemplate/expose.js (2 issues)

1
/**
2
 * EGroupware eTemplate2 - JS object implementing expose view of media and a gallery view
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 Hadi Nategh <hn[at]stylite.de>
9
 * @copyright Stylite AG
10
 * @version $Id$
11
 */
12
13
/*egw:uses
14
	/vendor/bower-asset/jquery/dist/jquery.js;
15
	/api/js/jquery/blueimp/js/blueimp-gallery.min.js;
16
*/
17
18
/**
19
 * Interface all exposed widget must support in order to getMedia for the blueimp Gallery.
20
 */
21
var et2_IExposable = new Interface(
22
{
23
	/**
24
	 * get media an array of media objects to pass to blueimp Gallery
25
	 * @param {array} _attrs
26
	 */
27
	getMedia: function(_attrs) {}
28
});
29
30
/**
31
 * This function extends the given widget with blueimp gallery plugin
32
 *
33
 * @param {type} widget
34
 * @returns {widget}
35
 */
36
function expose (widget)
37
{
38
	"use strict";
39
40
	// Common expose functions
41
	var THUMBNAIL_MAX = 100;
42
43
	// Minimum data to qualify as an image and not cause errors
44
	var IMAGE_DEFAULT = {
45
		title: egw.lang('loading'),
46
		href: '',
47
		type: 'image/png',
48
		thumbnail: '',
49
		loading: true
50
	};
51
52
	// For filtering to only show things we can handle
53
	var mime_regex = new RegExp(/(video\/(mp4|ogg|webm))|(image\/:*(?!tif|x-xcf|pdf))/);
54
55
	// open office document mime type currently supported by webodf editor
56
	var mime_odf_regex = new RegExp(/application\/vnd\.oasis\.opendocument\.text/);
57
58
	// IE only supports video/mp4 mime type
59
	if (navigator.userAgent.match(/(MSIE|Trident)/)) mime_regex.compile(/(video\/mp4)|(image\/:*(?!tif|x-xcf|pdf))/);
60
61
	// Only one gallery
62
	var gallery = null;
63
64
	/**
65
	 * See if the current widget is in a nextmatch, as this allows us to display
66
	 * thumbnails underneath
67
	 *
68
	 * @param {et2_IExposable} widget
69
	 * @returns {et2_nextmatch | null}
70
	 */
71
	var find_nextmatch = function(widget)
72
	{
73
		var current = widget;
74
		var nextmatch = null;
75
		while(nextmatch == null && current)
76
		{
77
			current = current.getParent();
78
			if(typeof current !='undefined' && current.instanceOf(et2_nextmatch))
79
			{
80
				nextmatch = current;
81
			}
82
		}
83
		// No nextmatch, or nextmatch not quite ready
84
		// At the moment only filemanger nm would work
85
		// as gallery, thus we disable other nestmatches
86
		// to build up gallery but filemanager
87
		if(nextmatch == null || nextmatch.controller == null || !nextmatch.dom_id.match(/filemanager/,'ig')) return null;
88
89
		return nextmatch;
90
	};
91
92
	/**
93
	 * Read images out of the data for the nextmatch
94
	 *
95
	 * @param {et2_nextmatch} nm
96
	 * @param {Object[]} images
97
	 * @param {number} start_at
98
	 * @returns {undefined}
99
	 */
100
	var read_from_nextmatch = function(nm, images, start_at)
101
	{
102
		if(!start_at) start_at = 0;
103
		var image_index = start_at;
104
		var stop = Math.max.apply(null,Object.keys(nm.controller._indexMap));
105
106
		for(var i = start_at; i <= stop; i++)
107
		{
108
			if(!nm.controller._indexMap[i] || !nm.controller._indexMap[i].uid)
109
			{
110
				// Returning instead of using IMAGE_DEFAULT means we stop as
111
				// soon as a hole is found, instead of getting everything that is
112
				// available.  The gallery can't fill in the holes.
113
				images[image_index++] = IMAGE_DEFAULT;
114
				continue;
115
			}
116
			var uid = nm.controller._indexMap[i].uid;
117
			if(!uid) continue;
118
			var data = egw.dataGetUIDdata(uid);
119
			if(data && data.data && data.data.mime && mime_regex.test(data.data.mime))
120
			{
121
				var media = this.getMedia(data.data);
122
				images[image_index++] = jQuery.extend({}, data.data, media[0]);
123
			}
124
		}
125
	};
126
127
	/**
128
	 * Set a particular index/image in the gallery instead of just appending
129
	 * it to the end
130
	 *
131
	 * @param {integer} index
132
	 * @param {Object} image
133
	 * @returns {undefined}
134
	 */
135
	var set_slide = function(index, image)
136
	{
137
		var active = (index == gallery.index);
138
139
		// Pad with blanks until length is right
140
		while(index > gallery.getNumber())
141
		{
142
			gallery.add([jQuery.extend({}, IMAGE_DEFAULT)]);
143
		}
144
145
		// Don't bother with adding a default, we just did that
146
		if(image.loading)
147
		{
148
			//Add load class if it's really a slide with error
149
			if (gallery.slidesContainer.find('[data-index="'+index+'"]').hasClass(gallery.options.slideErrorClass))
150
				jQuery(gallery.slides[index])
151
					.addClass(gallery.options.slideLoadingClass)
152
					.removeClass(gallery.options.slideErrorClass);
153
			return;
154
		}
155
		// Remove the loading class if the slide is loaded
156
		else
157
		{
0 ignored issues
show
Comprehensibility introduced by
else is not necessary here since all if branches return, consider removing it to reduce nesting and make code more readable.
Loading history...
158
			jQuery(gallery.slides[index]).removeClass(gallery.options.slideLoadingClass);
159
		}
160
161
		// Just use add to let gallery create everything it needs
162
		var new_index = gallery.num;
163
		gallery.add([image]);
164
165
		// Move it to where we want it.
166
		// Gallery uses arrays and indexes and has several internal variables
167
		// that need to be updated.
168
		//
169
		// list
170
		gallery.list[index] = gallery.list[new_index];
171
		gallery.list.splice(new_index,1);
172
173
		// indicators & slides
174
		var dom_nodes = ['indicators','slides'];
175
		for(var i in dom_nodes)
176
		{
177
			var var_name = dom_nodes[i];
178
			// Remove old one from DOM
179
			jQuery(gallery[var_name][index]).remove();
180
			// Move new one into it's place in gallery
181
			gallery[var_name][index] = gallery[var_name][new_index];
182
			// Move into place in DOM
183
			var node = jQuery(gallery[var_name][index]);
184
			node.attr('data-index', index)
185
				.insertAfter(jQuery("[data-index='"+(index-1)+"']",node.parent()));
186
			if(active) node.addClass(gallery.options.activeIndicatorClass);
187
			gallery[var_name].splice(new_index,1);
188
		}
189
		if(active)
190
		{
191
			gallery.activeIndicator = jQuery(gallery.indicators[index]);
192
		}
193
194
		// positions
195
		gallery.positions[index] = active ? 0 : (index > gallery.index ? gallery.slideWidth : -gallery.slideWidth);
196
		gallery.positions.splice(new_index,1);
197
198
		// elements - removing will allow to re-do the slide
199
		if(gallery.elements[index])
200
		{
201
			delete gallery.elements[index];
202
			gallery.loadElement(index);
203
		}
204
205
		// Remove the one we just added
206
		gallery.num -= 1;
207
	};
208
209
	return widget.extend([et2_IExposable],{
210
211
			/**
212
			 * Initialize the expose media gallery
213
			 */
214
			init: function()
215
			{
216
				this._super.apply(this, arguments);
217
				this.mime_regexp = mime_regex;
218
				this.mime_odf_regex = mime_odf_regex;
219
				var self=this;
220
				this.expose_options = {
221
					// The Id, element or querySelector of the gallery widget:
222
					container: '#blueimp-gallery',
223
					// The tag name, Id, element or querySelector of the slides container:
224
					slidesContainer: 'div',
225
					// The tag name, Id, element or querySelector of the title element:
226
					titleElement: 'h3',
227
					// The class to add when the gallery is visible:
228
					displayClass: 'blueimp-gallery-display',
229
					// The class to add when the gallery controls are visible:
230
					controlsClass: 'blueimp-gallery-controls',
231
					// The class to add when the gallery only displays one element:
232
					singleClass: 'blueimp-gallery-single',
233
					// The class to add when the left edge has been reached:
234
					leftEdgeClass: 'blueimp-gallery-left',
235
					// The class to add when the right edge has been reached:
236
					rightEdgeClass: 'blueimp-gallery-right',
237
					// The class to add when the automatic slideshow is active:
238
					playingClass: 'blueimp-gallery-playing',
239
					// The class for all slides:
240
					slideClass: 'slide',
241
					// The slide class for loading elements:
242
					slideLoadingClass: '',
243
					// The slide class for elements that failed to load:
244
					slideErrorClass: 'slide-error',
245
					// The class for the content element loaded into each slide:
246
					slideContentClass: 'slide-content',
247
					// The class for the "toggle" control:
248
					toggleClass: 'toggle',
249
					// The class for the "prev" control:
250
					prevClass: 'prev',
251
					// The class for the "next" control:
252
					nextClass: 'next',
253
					// The class for the "close" control:
254
					closeClass: 'close',
255
					// The class for the "play-pause" toggle control:
256
					playPauseClass: 'play-pause',
257
					// The class to add for fullscreen button option
258
					fullscreenClass:'fullscreen',
259
					// The list object property (or data attribute) with the object type:
260
					typeProperty: 'type',
261
					// The list object property (or data attribute) with the object title:
262
					titleProperty: 'title',
263
					// The list object property (or data attribute) with the object URL:
264
					urlProperty: 'href',
265
					// The gallery listens for transitionend events before triggering the
266
					// opened and closed events, unless the following option is set to false:
267
					displayTransition: true,
268
					// Defines if the gallery slides are cleared from the gallery modal,
269
					// or reused for the next gallery initialization:
270
					clearSlides: true,
271
					// Defines if images should be stretched to fill the available space,
272
					// while maintaining their aspect ratio (will only be enabled for browsers
273
					// supporting background-size="contain", which excludes IE < 9).
274
					// Set to "cover", to make images cover all available space (requires
275
					// support for background-size="cover", which excludes IE < 9):
276
					stretchImages: true,
277
					// Toggle the controls on pressing the Return key:
278
					toggleControlsOnReturn: true,
279
					// Toggle the automatic slideshow interval on pressing the Space key:
280
					toggleSlideshowOnSpace: true,
281
					// Navigate the gallery by pressing left and right on the keyboard:
282
					enableKeyboardNavigation: true,
283
					// Close the gallery on pressing the ESC key:
284
					closeOnEscape: true,
285
					// Close the gallery when clicking on an empty slide area:
286
					closeOnSlideClick: false,
287
					// Close the gallery by swiping up or down:
288
					closeOnSwipeUpOrDown: true,
289
					// Emulate touch events on mouse-pointer devices such as desktop browsers:
290
					emulateTouchEvents: true,
291
					// Stop touch events from bubbling up to ancestor elements of the Gallery:
292
					stopTouchEventsPropagation: false,
293
					// Hide the page scrollbars:
294
					hidePageScrollbars: true,
295
					// Stops any touches on the container from scrolling the page:
296
					disableScroll: true,
297
					// Carousel mode (shortcut for carousel specific options):
298
					carousel: true,
299
					// Allow continuous navigation, moving from last to first
300
					// and from first to last slide:
301
					continuous: false,
302
					// Remove elements outside of the preload range from the DOM:
303
					unloadElements: true,
304
					// Start with the automatic slideshow:
305
					startSlideshow: false,
306
					// Delay in milliseconds between slides for the automatic slideshow:
307
					slideshowInterval: 3000,
308
					// The starting index as integer.
309
					// Can also be an object of the given list,
310
					// or an equal object with the same url property:
311
					index: 0,
312
					// The number of elements to load around the current index:
313
					preloadRange: 2,
314
					// The transition speed between slide changes in milliseconds:
315
					transitionSpeed: 400,
316
					//Hide controls when the slideshow is playing
317
					hideControlsOnSlideshow: true,
318
					//Request fullscreen on slide show
319
					toggleFullscreenOnSlideShow:true,
320
					// The transition speed for automatic slide changes, set to an integer
321
					// greater 0 to override the default transition speed:
322
					slideshowTransitionSpeed: undefined,
323
					// The tag name, Id, element or querySelector of the indicator container:
324
					indicatorContainer: 'ol',
325
					// The class for the active indicator:
326
					activeIndicatorClass: 'active',
327
					// The list object property (or data attribute) with the thumbnail URL,
328
					// used as alternative to a thumbnail child element:
329
					thumbnailProperty: 'thumbnail',
330
					// Defines if the gallery indicators should display a thumbnail:
331
					thumbnailIndicators: true,
332
					//thumbnail with image tag
333
					thumbnailWithImgTag: true,
334
					// Callback function executed when the Gallery is initialized.
335
					// Is called with the gallery instance as "this" object:
336
					onopen: jQuery.proxy(this.expose_onopen,this),
337
					// Callback function executed when the Gallery has been initialized
338
					// and the initialization transition has been completed.
339
					// Is called with the gallery instance as "this" object:
340
					onopened: jQuery.proxy(this.expose_onopened,this),
341
					// Callback function executed on slide change.
342
					// Is called with the gallery instance as "this" object and the
343
					// current index and slide as arguments:
344
					onslide: function(index, slide) {
345
						// Call our onslide method, and include gallery as an attribute
346
						self.expose_onslide.apply(self, [this, index,slide]);
347
					},
348
					// Callback function executed after the slide change transition.
349
					// Is called with the gallery instance as "this" object and the
350
					// current index and slide as arguments:
351
					onslideend: function(index, slide) {
352
						// Call our onslide method, and include gallery as an attribute
353
						self.expose_onslideend.apply(self, [this, index,slide]);
354
					},
355
					//// Callback function executed on slide content load.
356
					// Is called with the gallery instance as "this" object and the
357
					// slide index and slide element as arguments:
358
					onslidecomplete: function(index, slide) {
359
						// Call our onslide method, and include gallery as an attribute
360
						self.expose_onslidecomplete.apply(self, [this, index,slide]);
361
					},
362
					//// Callback function executed when the Gallery is about to be closed.
363
					// Is called with the gallery instance as "this" object:
364
					onclose:jQuery.proxy(this.expose_onclose,this),
365
					// Callback function executed when the Gallery has been closed
366
					// and the closing transition has been completed.
367
					// Is called with the gallery instance as "this" object:
368
					onclosed: jQuery.proxy(this.expose_onclosed,this)
369
				};
370
				var $body = jQuery('body');
371
				if ($body.find('#blueimp-gallery').length == 0)
372
				{
373
					// Gallery Main DIV container
374
					var $expose_node = jQuery(document.createElement('div')).attr({id:"blueimp-gallery", class:"blueimp-gallery"});
375
					// Create Gallery DOM NODE
376
					$expose_node.append('<div class="slides"></div><h3 class="title"></h3><a class="prev">‹</a><a class="next">›</a><a title="'+ egw().lang('Close') + '" class="close">×</a><a title="'+ egw().lang('Play/Pause') + '"  class="play-pause"></a><a title="'+ egw().lang('Fullscreen') + '" class="fullscreen"></a><a title="'+ egw().lang('Save') +'" class="download"></a><ol class="indicator"></ol>');
377
					// Append the gallery Node to DOM
378
					$body.append($expose_node);
379
				}
380
381
			},
382
383
			set_value:function (_value)
384
			{
385
				if (typeof this._super == 'undefined') return;
386
387
				this._super.apply(this,arguments);
388
				// Do not run set value of expose if expose_view is not set
389
				// it causes a wired error on nested image widgets which
390
				// seems the expose is not its child widget
391
				if (!this.options.expose_view )
392
				{
393
					return;
394
				}
395
396
				var fe = egw_get_file_editor_prefered_mimes();
397
				var self=this;
398
				// If the media type is not supported do not bind the click handler
399
				if (!_value || typeof _value.mime != 'string' || (!_value.mime.match(mime_regex,'ig')
400
						&& (!fe || fe.mime && !fe.mime[_value.mime])) || typeof _value.download_url == 'undefined')
401
				{
402
					return;
403
				}
404
				if (typeof this.options.expose_view != 'undefined' && this.options.expose_view )
405
				{
406
					jQuery(this.node).on('click', function(event){
407
						// Do not trigger expose view if one of the operator keys are held
408
						if (!event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey)
409
						{
410
							if (_value.mime.match(mime_regex,'ig'))
411
							{
412
								self._init_blueimp_gallery(event, _value);
413
							}
414
							else if(fe && fe.mime && fe.edit && fe.mime[_value.mime])
415
							{
416
								egw.open_link(egw.link('/index.php', {
417
									menuaction: fe.edit.menuaction,
418
									path: _value.path,
419
									cd: 'no'	// needed to not reload framework in sharing
420
								}), '', fe.edit_popup);
421
							}
422
						}
423
						event.stopImmediatePropagation();
424
					}).addClass('et2_clickable');
425
				}
426
			},
427
428
			_init_blueimp_gallery: function (event, _value)
429
			{
430
				var mediaContent = [];
431
				var nm = find_nextmatch(this);
432
				var current_index = 0;
433
				if(nm && !this._is_target_indepth(nm,event.target))
434
				{
435
					// Get the row that was clicked, find its index in the list
436
					var current_entry = nm.controller.getRowByNode(event.target);
437
438
					// But before it goes, we'll pull everything we can
439
					read_from_nextmatch.call(this, nm, mediaContent);
440
					// find current_entry in array and set it's array-index
441
					for(var i=0; i < mediaContent.length; i++)
442
					{
443
						if ('filemanager::'+mediaContent[i].path == current_entry.uid)
444
						{
445
							current_index = i;
446
							break;
447
						}
448
					}
449
450
					// This will trigger nm to refresh and get just the ones we can handle
451
					// but it might take a while, so do it later - make sure our current
452
					// one is loaded first.
453
					window.setTimeout(function() {
454
						nm.applyFilters({col_filter: {mime: '/'+mime_regex.source+'/'}});
455
					},1);
456
				}
457
				else
458
				{
459
					mediaContent = this.getMedia(_value);
460
					// Do not show thumbnail indicator on single expose view
461
					this.expose_options.thumbnailIndicators = false;
462
				}
463
				this.expose_options.index = current_index;
464
				gallery = blueimp.Gallery(mediaContent, this.expose_options);
465
			},
466
467
			/**
468
			 * Check if clicked target from nm is in depth
469
			 *
470
			 *  @param nm nextmatch widget
471
			 *  @param target selected target dom node
472
			 *
473
			 *  @return {boolean} returns false if target is not in depth otherwise True
474
			 */
475
			_is_target_indepth: function (nm, target){
476
				var res = false;
477
				if (nm)
478
				{
479
					if (!target)
480
					{
481
						var target = this.getDOMNode();
482
					}
483
					var entry = nm.controller.getRowByNode(target);
484
					if (entry && entry.controller.getDepth()>0)
485
					{
486
						res = true;
487
					}
488
				}
489
				return res;
490
			},
491
			expose_onopen: function (event){},
492
			expose_onopened: function (event){
493
				// Check to see if we're in a nextmatch, do magic
494
				var nm = find_nextmatch(this);
495
				var self=this;
496
				if(nm)
497
				{
498
					// Add scrolling to the indicator list
499
					var total_count = nm.controller._grid.getTotalCount();
500
					if(total_count >= gallery.num)
501
					{
502
						var $indicator = gallery.container.find('.indicator');
503
						$indicator.off()
504
							.addClass('paginating')
505
							.swipe(function(event, direction, distance) {
506
								if(direction == jQuery.fn.swipe.directions.LEFT)
507
								{
508
									distance *= -1;
509
								}
510
								else if(direction == jQuery.fn.swipe.directions.RIGHT)
511
								{
0 ignored issues
show
Comprehensibility Documentation Best Practice introduced by
This code block is empty. Consider removing it or adding a comment to explain.
Loading history...
512
									// OK.
513
								}
514
								else
515
								{
516
									return;
517
								}
518
								jQuery(this).css('left',min(0,parseInt(jQuery(this).css('left'))-(distance*30))+'px');
519
							});
520
							// Bind the mousewheel handler for FF (DOMMousewheel), and other browsers (mousewheel)
521
							$indicator.bind('mousewheel DOMMousewheel',function(event, _delta) {
522
								var delta = _delta || event.originalEvent.wheelDelta / 120;
523
								if(delta > 0 && parseInt(jQuery(this).css('left')) > gallery.container.width() / 2)	return;
524
525
								//Reload next pictures into the gallery by scrolling on thumbnails
526
								if (delta<0 && jQuery(this).width() + parseInt(jQuery(this).css('left')) < gallery.container.width())
527
								{
528
									var nextIndex = gallery.indicatorContainer.find('[title="loading"]')[0];
529
									if (nextIndex) self.expose_onslideend(gallery,nextIndex.dataset.index -1);
530
									return;
531
								}
532
								// Move it about 5 indicators
533
								jQuery(this).css('left',parseInt(jQuery(this).css('left'))-(-delta*gallery.activeIndicator.width()*5)+'px');
534
535
								event.preventDefault();
536
							});
537
					}
538
				}
539
			},
540
			/**
541
			 * Trigger on slide left/right
542
			 * @param {Gallery} gallery
543
			 * @param {integer} index
544
			 * @param {DOMNode} slide
545
			 */
546
			expose_onslide: function (gallery, index, slide){
547
				if (typeof this._super == 'undefined') return;
548
				// First let parent try
549
				this._super.apply(this, arguments);
550
				var nm = find_nextmatch(this);
551
				if(nm)
552
				{
553
					// See if we need to move the indicator
554
					var indicator = gallery.container.find('.indicator');
555
					var current = jQuery('.active',indicator).position();
556
557
					if(current)
558
					{
559
						indicator.animate({left: (gallery.container.width() / 2)-current.left},10);
560
					}
561
				}
562
			},
563
			expose_onslideend: function (gallery, index, slide){
564
				// Check to see if we're in a nextmatch, do magic
565
				var nm = find_nextmatch(this);
566
				if(nm)
567
				{
568
					// Check to see if we're near the end, or maybe some pagination
569
					// would be good.
570
					var total_count = nm.controller._grid.getTotalCount();
571
572
					// Already at the end, don't bother
573
					if(index == total_count -1 || index == 0) return;
574
575
					// Try to determine direction from state of next & previous slides
576
					var direction = 1;
577
					for(var i in gallery.elements)
578
					{
579
						// Loading or error
580
						if(gallery.elements[i] == 1 || gallery.elements[i] == 3 || gallery.list[i].loading)
581
						{
582
							direction = i >= index ? 1 : -1;
583
							break;
584
						}
585
					}
586
587
					if(!gallery.list[index+direction] || gallery.list[index+direction].loading ||
588
						total_count > gallery.getNumber() && index + ET2_DATAVIEW_STEPSIZE > gallery.getNumber())
589
					{
590
						// This will get the next batch of rows
591
						var start = Math.max(0, direction > 0 ? index : index - ET2_DATAVIEW_STEPSIZE);
592
						var end = Math.min(total_count - 1, start + ET2_DATAVIEW_STEPSIZE);
593
						nm.controller._gridCallback(start, end);
594
						var images = [];
595
						read_from_nextmatch.call(this, nm, images, start);
596
597
						// Gallery always adds to the end, causing problems with pagination
598
						for(var i in images)
599
						{
600
							//if(i == index || i < gallery.num) continue;
601
							set_slide(i, images[i]);
602
							//gallery.add([images[i]]);
603
						}
604
					}
605
				}
606
			},
607
			expose_onslidecomplete:function (gallery, index, slide){},
608
			expose_onclose: function(event){
609
				// Check to see if we're in a nextmatch, remove magic
610
				var nm = find_nextmatch(this);
611
				if(nm && !this._is_target_indepth(nm))
612
				{
613
					// Remove scrolling from thumbnails
614
					gallery.container.find('.indicator')
615
						.removeClass('paginating')
616
						.off('mousewheel')
617
						.off('swipe');
618
619
					// Remove applied mime filter
620
					nm.applyFilters({col_filter: {mime: ''}});
621
				}
622
			},
623
			expose_onclosed: function (event){},
624
	});
625
}
626