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