xref: /plugin/popupviewer/script.js (revision 14a3d58e264a890a7e7b36514c894c4d89574a2c)
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			e && e.preventDefault();
147
148			if ( content && !content.is(':empty') ) {
149				e.target.popupData = popupData;
150				_.hideViewer(e, _.presentViewerWithContent);
151				return _;
152			}
153
154			_.showViewer();
155
156			content.current = $(this);
157
158			internal.log(popupData);
159
160			if ( popupData.isImage ) {
161
162				// Load image routine
163				internal.log("loading an image");
164				popupData.call = popupData.call || '_popup_load_image_meta';
165				$(new Image()).attr('src', popupData.src || this.href).load(function(){
166
167					var image = $(this);
168
169					var wrapper = $('<div/>').load(BASE_URL, popupData, function() {
170
171						// Force size for the moment
172						content.css({
173							width: content.width(),
174							height: content.height(),
175							overflow: 'hidden'
176						});
177
178						content.append(image);
179						content.popupData = jQuery.extend(true, {}, popupData);
180
181						additionalContent.html(wrapper.html());
182                        _.registerCloseHandler();
183						_.resizePopup(popupData.width, popupData.height, additionalContent.innerHeight(), image, false, popupData.hasNextPrevious);
184					});
185				});
186
187			} else {
188
189				popupData.call = popupData.call || '_popup_load_file';
190				popupData.src = popupData.src || BASE_URL;
191				var wrapper = $('<div/>').load(popupData.src, popupData, function(response, status, xhr) {
192
193					var success = function(node)
194					{
195    					var popupScriptNode = node;
196						if ( !popupData.do || popupData.do != 'export_xhtmlbody') {
197    						popupScriptNode = jQuery('<div/>').append(node.find('popupscript'));
198    						node = node.find('div.dokuwiki,body').first();
199    				    }
200
201						node.waitForImages({
202							finished: function() {
203
204							// Force size for the moment
205							content.css({
206								width: content.width(),
207								height: content.height()
208							});
209
210                            $(this).on( "click", "a", function(){
211                               // Insert base URL, so the link will be correct. hopefully.
212                               var base = $('<base href="' + popupData.src + '"/>');
213                               $("head").append(base);
214                            });
215
216							content.html(this);
217
218							// If we want to have all other pages open as well, go with it.
219							if ( popupData.keepOpen ) {
220    							_.propagateClickHandler($(this), popupData);
221							}
222
223							// Check for Javascript to execute
224							var script = "";
225							popupScriptNode.find('popupscript').
226							each(function() {
227								script += (this.innerHTML || this.innerText);
228							});
229
230							if ( script.length > 0 )
231							{
232								var randomID = Math.ceil(Math.random()*1000000);
233								content.attr('id', randomID);
234
235								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"
236
237								try{
238									$.globalEval("try{\n(function(){\n"+newContext+script+"\n}).call(jQuery('div#"+randomID+"').get(0));\n}catch(e){}\n//");
239								} catch (e) {
240									internal.log("Exception!");
241									internal.log(e);
242								}
243							}
244
245							if ( popupData.postPopupHook && typeof popupData.postPopupHook == 'function' ) {
246							    // Post-Hook which as to be a javascript function and my modify the popupData
247    							popupData.postPopupHook(this, popupData);
248							}
249
250                            _.registerCloseHandler();
251                            // At the very end we will resize the popup to fit the content.
252							_.resizePopup(popupData.width, popupData.height, null, content, true, popupData.hasNextPrevious);
253
254						}, waitForAll: true});
255					};
256
257
258					if ( status == "error") {
259						// Go for an iframe
260						var finished = false;
261						var iframe = null;
262
263						var messageFunction = function(event) {
264
265							finished = true;
266							var data = event.data || event.originalEvent.data;
267							// If this message does not come with what we want, discard it.
268							if ((typeof data).toLowerCase() == "string" || !data.message
269									|| data.message != 'frameContent') {
270								alert("Could not load page via popupviewer. The page responded with a wrong message.");
271								return;
272							}
273
274							iframe.remove();
275
276							// Clear the window Event after we are done!
277							$(window).unbind("message", messageFunction);
278
279							success($(data.body));
280						};
281
282						popupData.src = internal.getCurrentLocation();
283						var iframe = $('<iframe/>').load(function(){
284
285							var frame = this;
286							if ( frame.contentWindow.postMessage ) {
287
288								// Register the Message Event for PostMessage receival
289								$(window).bind("message", messageFunction);
290
291								// Send a message
292								var message = "getFrameContent";
293								frame.contentWindow.postMessage(message, "*");
294							}
295
296						}).hide().attr('src', popupData.src ).appendTo('body');
297
298						window.setTimeout(function() {
299							if (!finished) {
300								iframe.remove();
301								alert("Could not load page via popupviewer. The page is not available.");
302							}
303						}, 30000);
304
305					} else {
306						success(wrapper);
307					}
308
309				});
310			}
311		};
312
313		/* has to be called via popupscript in page if needed. */
314		_.propagateClickHandler = function(node, popupData) {
315			node.find('a[href],form[action]').
316			each(function(){
317				// Replace all event handler
318
319				var element = $(this);
320
321				var urlpart = element.attr('href') || element.attr('action') || "";
322				if ( urlpart.match(new RegExp("^#.*?$")) ) {
323					// Scroll to anchor
324					element.click(function(){
325						content.get(0).scrollTop( urlpart == '#' ? 0 : $(urlpart).offset().top);
326					});
327				}
328
329				if ( this.getAttribute('data-popupviewer') ) {
330					this.popupData = $.parseJSON(this.getAttribute('data-popupviewer'));
331					this.removeAttribute('data-popupviewer');
332				} else {
333					this.popupData = jQuery.extend(true, {}, popupData);
334					this.popupData.src = urlpart;
335                    this.popupData.do = 'export_xhtmlbody';
336
337					delete(this.popupData.id); // or it will always load this file.
338				}
339
340				$(this).bind('click', function(e){
341					e.stopPropagation(); e.preventDefault();
342					_.hideViewer(e, _.presentViewerWithContent);
343				});
344			});
345		};
346
347		internal.getCurrentLocation = function() {
348			return content.current.attr('href') || content.current.attr('src') || content.current.attr('action');
349		};
350
351		internal.optimalSize = function(offsetElement, isPageContent) {
352/*
353			if ( !isPageContent ) {
354				return {width: offsetElement.get(0).width, height: offsetElement.get(0).height};
355			}
356*/
357			var prevWidth = content.width();
358			var prevHeight = content.height();
359
360			offsetElement.css({width:'auto', height: 'auto'});
361
362			var width = offsetElement.naturalWidth() || offsetElement.width();
363			var height = offsetElement.naturalHeight() || offsetElement.height();
364
365			// Reset to previous size so the whole thing will animate from the middle
366			offsetElement.css({width:prevWidth, height: prevHeight});
367
368			return {width: width, height: height};
369		};
370
371		_.resizePopup = function(width, height, additionalHeight, offsetElement, isPageContent, needsNextPrevious) {
372
373			internal.log("Initial Size: " + width + " " + height);
374			internal.log(offsetElement);
375
376			if ( offsetElement && !width && !height) {
377				var optimalSize = internal.optimalSize(offsetElement, isPageContent);
378				width = optimalSize.width;
379				height = optimalSize.height;
380			}
381
382			internal.log("OffsetElement Size: " + width + " " + height);
383			width = parseInt(width) || ($(window).width() * 0.7);
384			height = parseInt(height) || ($(window).height() * 0.8);
385
386			var ratio = width / height;
387			var maxHeight = ( $(window).height() * 0.99 ) - 60;
388			var maxWidth = ( $(window).width() * 0.99 ) - 40;
389
390			additionalHeight = additionalHeight || 0;
391			height += additionalHeight;
392
393			internal.log("After Additional Content Size: " + width + " " + height);
394
395			if ( height > maxHeight ) {
396				height = maxHeight;
397				if ( !isPageContent ) { // If this is an image we will have to fix the size
398					width = (height - additionalHeight) * ratio;
399				} else {
400					width += 20; // For the scroller Bar that will apear;
401				}
402			}
403
404			if ( width > maxWidth ) {
405				width = maxWidth;
406				if ( !isPageContent ) { // If this is an image we will have to fix the size
407					height = width / ratio + additionalHeight;
408				}
409			}
410
411			var xOffset = viewerIsFixed ? 0 : $(document).scrollLeft() || 0;
412			var yOffset = viewerIsFixed ? 0 : $(document).scrollTop() || 0;
413
414			yOffset = Math.max(($(window).height() - height) * 0.5 + yOffset, 5);
415			xOffset += ($(window).width() - width) * 0.5;
416
417			internal.log("Final Size: " + width + " " + height);
418			internal.log("Final Offset: " + xOffset + " " + yOffset);
419
420			if ( !isPageContent || (offsetElement && offsetElement.is('img')) ) {
421
422				offsetElement.animate({
423					width : width,
424					height : height - additionalHeight
425				});
426
427				content.css({
428					width : '',
429					height : '',
430					overflow: ''
431				});
432
433			} else {
434				content.animate({
435					width : width,
436					height : isPageContent ? height : 'auto',
437				});
438			}
439
440			content.parent().animate({
441				top : yOffset,
442				left : xOffset,
443				'margin-left' : 0
444			});
445
446			if ( isPageContent ) {
447				content.removeClass('isImage');
448			} else {
449				content.addClass('isImage');
450			}
451
452			_.handleNextAndPrevious(!isPageContent || needsNextPrevious);
453			return _;
454		};
455
456		_.skipImageInDirection = function(e)
457		{
458			e.stopPropagation();
459
460			if ( !$(this).is(':visible') ) { return; }
461
462			var skipTo =  $.inArray(content.current.get(0), _.popupImageStack) + e.data.direction;
463			skipTo = Math.min(_.popupImageStack.length-1, Math.max(skipTo, 0));
464
465			internal.log("skipping " + (e.data.direction < 0 ? 'previous' : 'next') + ' ' + skipTo );
466			return _.skipToImage(skipTo, e.data.direction);
467		};
468
469		_.skipToImage = function(skipTo, inDirection)
470		{
471			if ( !$(_.popupImageStack[skipTo]).is(content.current) ) {
472				_.hideViewer(null, function() {
473					// Deliver extra functionality to clicked item.
474					var nextItem = _.popupImageStack[skipTo];
475					(nextItem.popupData && nextItem.popupData.click && nextItem.popupData.click(skipTo, inDirection)) || $(nextItem).click();
476				});
477			}
478
479			return _;
480		};
481
482		_.isFirst = function() {
483			return _.popupImageStack.first().is(content.current);
484		};
485
486		_.isLast = function() {
487			return _.popupImageStack.last().is(content.current);
488		};
489
490		_.handleNextAndPrevious = function(currentIsImage) {
491
492			if ( currentIsImage && _.popupImageStack && _.popupImageStack.size() > 1) {
493
494				if ( _.isFirst() ) {
495					previous.addClass('inactive');
496				} else {
497					previous.removeClass('inactive');
498				}
499
500				if ( _.isLast() ) {
501					next.addClass('inactive');
502				} else {
503					next.removeClass('inactive');
504				}
505
506				next.addClass('visible');
507				previous.addClass('visible');
508			} else {
509				next.removeClass('visible');
510				previous.removeClass('visible');
511			}
512
513			return _;
514		};
515
516        _.registerCloseHandler = function () {
517            $('*[data-popupviewerclose]').each(function(){
518                $(this).click(function(e){
519                   e && e.preventDefault();
520                   _.hideViewer(e);
521                   return false;
522                });
523                if (this.removeAttribute) this.removeAttribute('data-popupviewerclose');
524            });
525        };
526
527		_.init = function(popupImageStack) {
528
529			_.popupImageStack = $(popupImageStack || '*[data-popupviewer]').each(function(){
530				this.popupData = this.popupData || $.parseJSON(this.getAttribute('data-popupviewer'));
531				if (this.removeAttribute) this.removeAttribute('data-popupviewer');
532				$(this).unbind('click').click(_.presentViewerWithContent);
533			}).filter(function(){
534				// Only images allowed in Stack.
535				return this.popupData.isImage || this.popupData.hasNextPrevious;
536			});
537
538			return _;
539		};
540
541	})(popupviewer.prototype);
542
543    // Namespace all events.
544    var eventNamespace = 'waitForImages';
545
546    // CSS properties which contain references to images.
547    $.waitForImages = {
548        hasImageProperties: ['backgroundImage', 'listStyleImage', 'borderImage', 'borderCornerImage', 'cursor']
549    };
550
551    // Custom selector to find `img` elements that have a valid `src` attribute and have not already loaded.
552    $.expr[':'].uncached = function (obj) {
553        // Ensure we are dealing with an `img` element with a valid `src` attribute.
554        if (!$(obj).is('img[src!=""]')) {
555            return false;
556        }
557
558        // Firefox's `complete` property will always be `true` even if the image has not been downloaded.
559        // Doing it this way works in Firefox.
560        var img = new Image();
561        img.src = obj.src;
562        return !img.complete;
563    };
564
565    $.fn.waitForImages = function (finishedCallback, eachCallback, waitForAll) {
566
567        var allImgsLength = 0;
568        var allImgsLoaded = 0;
569
570        // Handle options object.
571        if ($.isPlainObject(arguments[0])) {
572            waitForAll = arguments[0].waitForAll;
573            eachCallback = arguments[0].each;
574			// This must be last as arguments[0]
575			// is aliased with finishedCallback.
576            finishedCallback = arguments[0].finished;
577        }
578
579        // Handle missing callbacks.
580        finishedCallback = finishedCallback || $.noop;
581        eachCallback = eachCallback || $.noop;
582
583        // Convert waitForAll to Boolean
584        waitForAll = !! waitForAll;
585
586        // Ensure callbacks are functions.
587        if (!$.isFunction(finishedCallback) || !$.isFunction(eachCallback)) {
588            throw new TypeError('An invalid callback was supplied.');
589        }
590
591        return this.each(function () {
592            // Build a list of all imgs, dependent on what images will be considered.
593            var obj = $(this);
594            var allImgs = [];
595            // CSS properties which may contain an image.
596            var hasImgProperties = $.waitForImages.hasImageProperties || [];
597            // To match `url()` references.
598            // Spec: http://www.w3.org/TR/CSS2/syndata.html#value-def-uri
599            var matchUrl = new RegExp("url\(\s*(['\"]?)(.*?)\1\s*\)", "g");
600
601            if (waitForAll) {
602
603                // Get all elements (including the original), as any one of them could have a background image.
604                obj.find('*').addBack().each(function () {
605                    var element = $(this);
606
607                    // If an `img` element, add it. But keep iterating in case it has a background image too.
608                    if (element.is('img:uncached')) {
609                        allImgs.push({
610                            src: element.attr('src'),
611                            element: element[0]
612                        });
613                    }
614
615                    $.each(hasImgProperties, function (i, property) {
616                        var propertyValue = element.css(property);
617                        var match;
618
619                        // If it doesn't contain this property, skip.
620                        if (!propertyValue) {
621                            return true;
622                        }
623
624                        // Get all url() of this element.
625                        while (match = matchUrl.exec(propertyValue)) {
626                            allImgs.push({
627                                src: match[2],
628                                element: element[0]
629                            });
630                        }
631                    });
632                });
633            } else {
634                // For images only, the task is simpler.
635                obj.find('img:uncached')
636                    .each(function () {
637                    allImgs.push({
638                        src: this.src,
639                        element: this
640                    });
641                });
642            }
643
644            allImgsLength = allImgs.length;
645            allImgsLoaded = 0;
646
647            // If no images found, don't bother.
648            if (allImgsLength === 0) {
649                finishedCallback.call(obj[0]);
650            }
651
652            $.each(allImgs, function (i, img) {
653
654                var image = new Image();
655
656                // Handle the image loading and error with the same callback.
657                $(image).on('load.' + eventNamespace + ' error.' + eventNamespace, function (event) {
658                    allImgsLoaded++;
659
660                    // If an error occurred with loading the image, set the third argument accordingly.
661                    eachCallback.call(img.element, allImgsLoaded, allImgsLength, event.type == 'load');
662
663                    if (allImgsLoaded == allImgsLength) {
664                        finishedCallback.call(obj[0]);
665                        return false;
666                    }
667
668                });
669
670                image.src = img.src;
671            });
672        });
673    };
674
675	$(function(){
676
677	    if ( typeof $.fn.naturalWidth != 'undefined' && typeof $.fn.naturalHeight != 'undefined' ) { return; }
678
679		function img(url) { var i = new Image(); i.src = url; return i; }
680		if ('naturalWidth' in (new Image())) {
681			$.fn.naturalWidth  = function() { return this[0].naturalWidth; };
682			$.fn.naturalHeight = function() { return this[0].naturalHeight; };
683			return;
684		}
685
686		$.fn.naturalWidth  = function() { return img(this.src).width; };
687		$.fn.naturalHeight = function() { return img(this.src).height; };
688	});
689
690	$(function(){
691		$.popupviewer().init();
692	});
693
694})(jQuery);
695
696
697/* Loading the content for locally exported content */
698(function($){
699	$(window).bind("message", function(event){
700
701		var data = event.data || event.originalEvent.data;
702		var source = event.source || event.originalEvent.source;
703		if (data != "getFrameContent") {
704			return;
705		}
706
707		try {
708			source.postMessage({
709				message : "frameContent",
710				body : jQuery('html').html()
711			}, "*");
712		} catch (e) {
713			alert("Fatal Exception! Could not load page via popupviewer.\n" + e);
714		}
715	});
716})(jQuery);
717