xref: /plugin/popupviewer/script.js (revision a67a834b0bbc9088a4ef640bbeb666dc4da3d605)
1(function($){
2
3	var popupviewer = function() {
4	};
5
6	/* singleton */
7	var instance = null;
8	$.popupviewer = function() {
9		return instance || (instance = new popupviewer());
10	};
11
12	// Static functions
13	(function(_){
14
15		var viewer = null;
16		var content = null;
17		var additionalContent = null;
18		var BASE_URL = DOKU_BASE + 'lib/exe/ajax.php';
19		var viewerIsFixed = false;
20		var next = null;
21		var previous = null;
22		var internal = {};
23
24		_.popupImageStack = null;
25
26		internal.log = function(message) {
27			console.log(message);
28		};
29
30		_.showViewer = function() {
31
32			if ( viewer == null ) {
33
34				viewer = $('<div id="popupviewer"/>').click(_.hideViewer).appendTo('body');
35				content = $('<div class="content"/>').click(function(e){e.stopPropagation()});
36				content.current = $();
37
38				additionalContent = $('<div class="additionalContent dokuwiki"/>');
39				viewerIsFixed = viewer.css('position');
40
41				$('<div class="controls"/>').
42					append(content).
43					append(additionalContent).
44					append(previous = $('<a class="previous"/>').click({'direction': -1}, _.skipImageInDirection)).
45					append(next = $('<a class="next"/>').click({'direction': 1}, _.skipImageInDirection)).
46					append($('<a class="close"/>').addClass('visible').click(_.hideViewer)).
47					appendTo(viewer);
48
49				$(document).keydown(internal.globalKeyHandler);
50
51			}
52
53			content.empty();
54			additionalContent.empty();
55			$('body').css('overflow', 'hidden');
56			viewer.show();
57			return _;
58		};
59
60		_.hideViewer = function(e, finalFunction) {
61			if ( viewer != null ) {
62				$('body').css('overflow', 'auto');
63
64				additionalContent.animate({
65					opacity: 0,
66					height: 0
67				});
68
69				content.animate({
70					width : 208,
71					height : 13,
72				}).parent('.controls').animate({
73					top : '50%',
74					left : '50%',
75					'margin-left' : -104
76				}).parent('#popupviewer').animate({
77					opacity: finalFunction ? 1 : 0
78				}, function(){
79					viewer.hide();
80
81					content.empty();
82					additionalContent.empty();
83					content.current = null;
84
85					additionalContent.css({
86						opacity: 1,
87						height: ''
88					});
89
90					content.css({
91						width : '',
92						height : '',
93					}).parent('.controls').css({
94						top : '',
95						left : '',
96						'margin-left' : ''
97					}).parent('#popupviewer').css({
98						opacity : 1
99					});
100
101					if ( typeof finalFunction == 'function' ) {
102						finalFunction(e);
103					}
104				});
105			}
106
107			return _;
108		};
109
110		internal.globalKeyHandler = function(e) {
111
112			if ( !viewer.is(":visible") ) return;
113
114			switch(e.keyCode) {
115				case 39: // Right
116					e.stopPropagation();
117					next.click();
118					break;
119				case 37: // Left
120					e.stopPropagation();
121					previous.click();
122					break;
123				case 27: // Escape
124					e.stopPropagation();
125					_.hideViewer();
126					break;
127			}
128		};
129
130		_.presentViewerWithContent = function(e, popupData) {
131
132			popupData = popupData || this.popupData || e.target.popupData; // Either as param or from object
133
134			/*
135				popupData = {
136					isImage: boolean,
137					call: ajax_handler,
138					src: URL,
139					id: alternate_wiki_page,
140					width: width_of_window,
141					height: height_of_window
142				}
143			*/
144
145			if ( !popupData ) { return; }
146
147			e && e.preventDefault();
148			_.showViewer();
149
150			content.current = $(this);
151
152			internal.log(popupData);
153
154			if ( popupData.isImage ) {
155
156				// Load image routine
157				internal.log("loading an image");
158				popupData.call = popupData.call || '_popup_load_image_meta';
159				$(new Image()).attr('src', popupData.src || this.href).waitForImages(function(){
160
161					var image = $(this);
162
163					var wrapper = $('<div/>').load(BASE_URL, popupData, function() {
164
165						// Force size for the moment
166						content.css({
167							width: content.width(),
168							height: content.height(),
169							overflow: 'hidden'
170						})
171
172						content.append(image);
173						content.popupData = jQuery.extend(true, {}, popupData);
174
175						additionalContent.html(wrapper.html());
176						_.resizePopup(popupData.width, popupData.height, additionalContent.innerHeight(), image);
177					});
178				});
179
180			} else {
181
182				popupData.call = popupData.call || '_popup_load_file';
183				popupData.src = popupData.src || BASE_URL;
184				var wrapper = $('<div/>').load(popupData.src, popupData, function(response, status, xhr) {
185
186					var success = function(node)
187					{
188						node.find('div.dokuwiki,body').first().waitForImages({
189							finished: function() {
190
191							// Force size for the moment
192							content.css({
193								width: content.width(),
194								height: content.height()
195							})
196
197							content.html(this);
198
199							// Check for Javascript to execute
200							var script = "";
201							node.find('popupscript').
202							each(function() {
203								script += $.parseJSON((this.innerHTML || this.innerText));
204							})
205
206							var newContext = "jQuery.noConflict(); containerContext = this; ___ = function( selector, context ){return new jQuery.fn.init(selector,context||containerContext);}; ___.fn = ___.prototype = jQuery.fn;jQuery.extend( ___, jQuery );jQuery = ___;\n"
207
208							if ( script.length > 0 ) {
209								var randomID = Math.ceil(Math.random()*1000000);
210								content.attr('id', randomID);
211
212								var newContext = "jQuery.noConflict(); containerContext = this; ___ = function( selector, context ){return new jQuery.fn.init(selector,context||containerContext);}; ___.fn = ___.prototype = jQuery.fn;jQuery.extend( ___, jQuery );jQuery = ___;\n"
213
214								try{
215									$.globalEval("try{\n(function(){\n"+newContext+script+"\n}).call(jQuery('div#"+randomID+"').get(0));\n}catch(e){}\n//");
216								} catch (e) {
217									internal.log("Exception!");
218									internal.log(e);
219								}
220							}
221
222							_.resizePopup(popupData.width, popupData.height, null, content, true);
223
224						}, waitForAll: true});
225					}
226
227
228					if ( status == "error") {
229						// Go for an iframe
230						var finished = false;
231						var iframe = null;
232
233						var messageFunction = function(event) {
234
235							finished = true;
236							var data = event.data || event.originalEvent.data;
237							// If this message does not come with what we want, discard it.
238							if ((typeof data).toLowerCase() == "string" || !data.message
239									|| data.message != 'frameContent') {
240								alert("Could not load page via popupviewer. The page responded with a wrong message.");
241								return;
242							}
243
244							iframe.remove();
245
246							// Clear the window Event after we are done!
247							$(window).unbind("message", messageFunction);
248
249							success($(data.body));
250						};
251
252						var iframe = $('<iframe/>').load(function(){
253
254							var frame = this;
255							if ( frame.contentWindow.postMessage ) {
256
257								// Register the Message Event for PostMessage receival
258								$(window).bind("message", messageFunction);
259
260								// Send a message
261								var message = "getFrameContent";
262								frame.contentWindow.postMessage(message, "*");
263							}
264
265						}).hide().attr('src', internal.getCurrentLocation() ).appendTo('body');
266
267						window.setTimeout(function() {
268							if (!finished) {
269								iframe.remove();
270								alert("Could not load page via popupviewer. The page is not available.");
271							}
272						}, 30000);
273
274					} else {
275						success(wrapper)
276					}
277
278				});
279			}
280		};
281
282		/* has to be called via popupscript in page if needed. */
283		_.propagateClickHandler = function(node) {
284			node.find('a[href],form[action]').
285			each(function(){
286				// Replace all event handler
287
288				var element = $(this);
289
290				urlpart = element.attr('href') || element.attr('action') || "";
291				if ( urlpart.match(new RegExp("^#.*?$")) ) {
292					// Scroll to anchor
293					element.click(function(){
294						content.get(0).scrollTop( urlpart == '#' ? 0 : $(urlpart).offset().top);
295					});
296				}
297
298				if ( this.getAttribute('popupviewerdata') ) {
299					this.popupData = $.parseJSON(this.getAttribute('popupviewerdata'));
300					this.removeAttribute('popupviewerdata');
301				} else {
302					this.popupData = jQuery.extend(true, {}, popupData);
303					this.popupData.src = urlpart;
304					delete(this.popupData.id); // or it will always load this file.
305				}
306
307				$(this).bind('click', function(e){
308					e.stopPropagation(); e.preventDefault();
309					_.hideViewer(e, _.presentViewerWithContent);
310				});
311			});
312		};
313
314		internal.getCurrentLocation = function() {
315			return content.current.attr('href') || content.current.attr('src') || content.current.attr('action');
316		};
317
318		internal.optimalSize = function(offsetElement) {
319
320			var prevWidth = content.width();
321			var prevHeight = content.height();
322
323			offsetElement.css({width:'', height: ''});
324
325			width = offsetElement.width();
326			height = offsetElement.height();
327
328			// Reset to previous size so the whole thing will animate from the middle
329			offsetElement.css({width:prevWidth, height: prevHeight});
330
331			return {width: width, height: height};
332		}
333
334		_.resizePopup = function(width, height, additionalHeight, offsetElement, isPageContent) {
335
336			if ( offsetElement && !width && !height) {
337				var optimalSize = internal.optimalSize(offsetElement);
338				width = optimalSize.width;
339				height = optimalSize.height;
340			}
341
342			width = parseInt(width) || ($(window).width() * 0.7);
343			height = parseInt(height) || ($(window).height() * 0.8);
344
345			var ratio = width / height;
346			var maxHeight = ( $(window).height() * 0.99 ) - 60;
347			var maxWidth = ( $(window).width() * 0.99 ) - 40;
348
349			additionalHeight = additionalHeight || 0;
350			height += additionalHeight;
351
352			if ( height > maxHeight ) {
353				height = maxHeight;
354				if ( !isPageContent ) { // If this is an image we will have to fix the size
355					width = (height - additionalHeight) * ratio;
356				} else {
357					width += 20; // For the scroller Bar that will apear;
358				}
359			}
360
361			if ( width > maxWidth ) {
362				width = maxWidth;
363				if ( !isPageContent ) { // If this is an image we will have to fix the size
364					height = width / ratio + additionalHeight;
365				}
366			}
367
368			var xOffset = viewerIsFixed ? 0 : $(document).scrollLeft() || 0;
369			var yOffset = viewerIsFixed ? 0 : $(document).scrollTop() || 0;
370
371			yOffset = Math.max(($(window).height() - height) * 0.5 + yOffset, 5);
372			xOffset += ($(window).width() - width) * 0.5;
373
374			internal.log(width + " " + height);
375			internal.log(xOffset + " " + yOffset);
376
377			if ( !isPageContent && offsetElement.is('img') ) {
378
379				offsetElement.animate({
380					width : width,
381					height : height - additionalHeight
382				});
383
384				content.css({
385					width : '',
386					height : '',
387					overflow: ''
388				});
389
390			} else {
391				content.animate({
392					width : width,
393					height : isPageContent ? height : 'auto',
394				});
395			}
396
397			content.parent().animate({
398				top : yOffset,
399				left : xOffset,
400				'margin-left' : 0
401			});
402
403			if ( isPageContent ) {
404				content.removeClass('isImage');
405			} else {
406				content.addClass('isImage');
407			}
408
409			_.handleNextAndPrevious(!isPageContent);
410			return _;
411		};
412
413		_.skipImageInDirection = function(e)
414		{
415			e.stopPropagation();
416
417			if ( !$(this).is(':visible') ) { return; }
418
419			var skipTo =  $.inArray(content.current.get(0), _.popupImageStack) + e.data.direction;
420			skipTo = Math.min(_.popupImageStack.length-1, Math.max(skipTo, 0));
421
422			internal.log("skipping " + (e.data.direction < 0 ? 'previous' : 'next') + ' ' + skipTo );
423			return _.skipToImage(skipTo, e.data.direction);
424		};
425
426		_.skipToImage = function(skipTo, inDirection)
427		{
428			if ( !$(_.popupImageStack[skipTo]).is(content.current) ) {
429				_.hideViewer(null, function() {
430					// Deliver extra functionality to clicked item.
431					var nextItem = _.popupImageStack[skipTo];
432					(nextItem.popupData && nextItem.popupData.click && nextItem.popupData.click(skipTo, inDirection)) || $(nextItem).click();
433				});
434			}
435
436			return _;
437		}
438
439		_.isFirst = function() {
440			return _.popupImageStack.first().is(content.current);
441		}
442
443		_.isLast = function() {
444			return _.popupImageStack.last().is(content.current);
445		}
446
447		_.handleNextAndPrevious = function(currentIsImage) {
448
449			if ( currentIsImage && _.popupImageStack && _.popupImageStack.length > 1) {
450
451				if ( _.isFirst() ) {
452					previous.addClass('inactive');
453				} else {
454					previous.removeClass('inactive');
455				}
456
457				if ( _.isLast() ) {
458					next.addClass('inactive');
459				} else {
460					next.removeClass('inactive');
461				}
462
463				next.addClass('visible');
464				previous.addClass('visible');
465			} else {
466				next.removeClass('visible');
467				previous.removeClass('visible');
468			}
469
470			return _;
471		};
472
473		_.init = function(popupImageStack) {
474
475			_.popupImageStack = $(popupImageStack || 'a[popupviewerdata]').each(function(){
476				this.popupData = this.popupData || $.parseJSON(this.getAttribute('popupviewerdata'));
477				if (this.removeAttribute) this.removeAttribute('popupviewerdata');
478				$(this).unbind('click').click(_.presentViewerWithContent);
479			}).filter(function(){
480				// Only images allowed in Stack.
481				return this.popupData.isImage;
482			});
483
484			return _;
485		};
486
487	})(popupviewer.prototype);
488
489    // Namespace all events.
490    var eventNamespace = 'waitForImages';
491
492    // CSS properties which contain references to images.
493    $.waitForImages = {
494        hasImageProperties: ['backgroundImage', 'listStyleImage', 'borderImage', 'borderCornerImage', 'cursor']
495    };
496
497    // Custom selector to find `img` elements that have a valid `src` attribute and have not already loaded.
498    $.expr[':'].uncached = function (obj) {
499        // Ensure we are dealing with an `img` element with a valid `src` attribute.
500        if (!$(obj).is('img[src!=""]')) {
501            return false;
502        }
503
504        // Firefox's `complete` property will always be `true` even if the image has not been downloaded.
505        // Doing it this way works in Firefox.
506        var img = new Image();
507        img.src = obj.src;
508        return !img.complete;
509    };
510
511    $.fn.waitForImages = function (finishedCallback, eachCallback, waitForAll) {
512
513        var allImgsLength = 0;
514        var allImgsLoaded = 0;
515
516        // Handle options object.
517        if ($.isPlainObject(arguments[0])) {
518            waitForAll = arguments[0].waitForAll;
519            eachCallback = arguments[0].each;
520			// This must be last as arguments[0]
521			// is aliased with finishedCallback.
522            finishedCallback = arguments[0].finished;
523        }
524
525        // Handle missing callbacks.
526        finishedCallback = finishedCallback || $.noop;
527        eachCallback = eachCallback || $.noop;
528
529        // Convert waitForAll to Boolean
530        waitForAll = !! waitForAll;
531
532        // Ensure callbacks are functions.
533        if (!$.isFunction(finishedCallback) || !$.isFunction(eachCallback)) {
534            throw new TypeError('An invalid callback was supplied.');
535        }
536
537        return this.each(function () {
538            // Build a list of all imgs, dependent on what images will be considered.
539            var obj = $(this);
540            var allImgs = [];
541            // CSS properties which may contain an image.
542            var hasImgProperties = $.waitForImages.hasImageProperties || [];
543            // To match `url()` references.
544            // Spec: http://www.w3.org/TR/CSS2/syndata.html#value-def-uri
545            var matchUrl = /url\(\s*(['"]?)(.*?)\1\s*\)/g;
546
547            if (waitForAll) {
548
549                // Get all elements (including the original), as any one of them could have a background image.
550                obj.find('*').addBack().each(function () {
551                    var element = $(this);
552
553                    // If an `img` element, add it. But keep iterating in case it has a background image too.
554                    if (element.is('img:uncached')) {
555                        allImgs.push({
556                            src: element.attr('src'),
557                            element: element[0]
558                        });
559                    }
560
561                    $.each(hasImgProperties, function (i, property) {
562                        var propertyValue = element.css(property);
563                        var match;
564
565                        // If it doesn't contain this property, skip.
566                        if (!propertyValue) {
567                            return true;
568                        }
569
570                        // Get all url() of this element.
571                        while (match = matchUrl.exec(propertyValue)) {
572                            allImgs.push({
573                                src: match[2],
574                                element: element[0]
575                            });
576                        }
577                    });
578                });
579            } else {
580                // For images only, the task is simpler.
581                obj.find('img:uncached')
582                    .each(function () {
583                    allImgs.push({
584                        src: this.src,
585                        element: this
586                    });
587                });
588            }
589
590            allImgsLength = allImgs.length;
591            allImgsLoaded = 0;
592
593            // If no images found, don't bother.
594            if (allImgsLength === 0) {
595                finishedCallback.call(obj[0]);
596            }
597
598            $.each(allImgs, function (i, img) {
599
600                var image = new Image();
601
602                // Handle the image loading and error with the same callback.
603                $(image).on('load.' + eventNamespace + ' error.' + eventNamespace, function (event) {
604                    allImgsLoaded++;
605
606                    // If an error occurred with loading the image, set the third argument accordingly.
607                    eachCallback.call(img.element, allImgsLoaded, allImgsLength, event.type == 'load');
608
609                    if (allImgsLoaded == allImgsLength) {
610                        finishedCallback.call(obj[0]);
611                        return false;
612                    }
613
614                });
615
616                image.src = img.src;
617            });
618        });
619    };
620
621	$(function(){
622		$.popupviewer().init();
623	});
624
625})(jQuery);
626
627
628/* Loading the content for locally exported content */
629(function($){
630	$(window).bind("message", function(event){
631
632		var data = event.data || event.originalEvent.data;
633		var source = event.source || event.originalEvent.source;
634		if (data != "getFrameContent") {
635			return;
636		}
637
638		try {
639			source.postMessage({
640				message : "frameContent",
641				body : jQuery('html').html()
642			}, "*");
643		} catch (e) {
644			alert("Fatal Exception! Could not load page via popupviewer.\n" + e);
645		}
646	});
647})(jQuery);