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