MediaWiki:Gadget-Thickbox.js

From SourceWorlds Wiki
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* <pre>
 * Thickbox4MediaWiki v3.16 - Based on Thickbox 3.1 By Cody Lindley (http://www.codylindley.com)
 * Copyright (c) 2010 - 2025 Jesús Martínez (User:Ciencia_Al_Poder) / Vish (User:Vish), Original Thickbox Copyright (c) 2007 Cody Lindley
 * Licensed under the MIT License: http://www.opensource.org/licenses/mit-license.php
*/
window.Thickbox = (function($, mw) {
	'use strict';
	var _version = '3.16',
	// Minimum dimensions
	_minWidth = 210,
	// Margin between the image and the border of ThickBox
	_imageMarginWidth = 15,
	// Minimum margin to the edge of the window. If the image is exceeded it will be reduced
	_minMarginWidth = 30,
	_minMarginHeight = 15,
	// Waiting time for the loader to appear in ms
	_loaderWait = 500,
	// Internal
	_imgPreloader = null,
	_videoPreloader = null,
	_galleryData = null,
	_galleryIndex = -1,
	_width = null,
	_height = null,
	_getCaption = null,
	_imgTip = null,
	_imgTipTarget = null,
	_imgTipVisible = false,
	_loaderPresent = false,
	_loaderTm = null,
	_logger = console.error,

	// Natural dimensions for reflow after FileInfo is injected
	_naturalMediaWidth = null,
	_naturalMediaHeight = null,

	// FileInfo fetch state
	_fileInfoPromise = null,
	_fileInfoReady = false,
	_fileInfoHTML = '',

	// Private functions
	_init = function() {
		// COMPAT
		if (window.Thickbox4MediaWikiLoaded) { return; }
		window.Thickbox4MediaWikiLoaded = true;
		// We could put an event directly in each 'a.image', but this is much faster and more efficient (it only takes 20% in FF2) than to go through the entire DOM
		$('#mw-content-text').off('click.thickbox mouseover.thickbox_imgtip').on({
			'click.thickbox': _triggerEvent,
			'mouseover.thickbox_imgtip': _imgTipEvent
		});
	},

	_triggerEvent = function(e) {
	    try {
	        // If there is any special key pressed, we exit
	        if (e.ctrlKey || e.altKey || e.shiftKey) {
	            return true;
	        }
	        var target = e.target;
	        if (_isTag(target, 'img') || _isTag(target, 'video')) { // Gallery or thumb
	            var a = target.parentNode;
	            // External image or video handling
	            if (!a || (!_isTag(a, 'a') && !_isTag(target, 'video'))) {
	                return true;
	            }
	            if (_isExternalLink(a.href)) {
	                e.preventDefault(); // Prevent the default action
	                a.blur();
	                _getCaption = _getCaptionEmpty;
	                var gb = $(a).closest('li.gallerybox');
	                // Check if it's part of a gallery
	                if (gb.length) {
	                    var t = gb.closest('ul.gallery');
	                    if (t.length) {
	                        _getCaption = _getCaptionEmpty;
	                        _galleryData = t.find('div.ext2 a.external');
	                        _galleryIndex = _galleryData.index(a);
	                        _showMedia(a, true);
	                        return false;
	                    }
	                }
	                _showMedia(a, true);
	                return false; // Stop event propagation
	            }
	            // Internal images and videos
				// Custom image links such as [[File:Icon.png|link=File:Full.png]] may not
				// receive one of MediaWiki's usual file-link classes. Treat an explicit
				// File: target as a valid Thickbox link too.
				var linkedFileTitle = _fileTitleFromHref(a.href);
				if (!_isClass(a, 'image') && !_isClass(a, 'mw-file-description') &&
				    !_isClass(a, 'gBoxVideo') && !_isClass(a, 'link-internal') && !_isClass(a, 'link-external') &&
				    !linkedFileTitle && !_isTag(target, 'video')) {
				    return true;
				}
				if (target.closest('figure[typeof^="mw:File/Thumb"]') || _isClass(target, 'html5Video')) {
				    // It's a thumbnail
				    a.blur();
				    _getCaption = _getCaptionThumb;
				    _showMedia(a);
				    return false;
				}
	            var gb2 = $(a).closest('li.gallerybox');
	            // MediaWiki gallery
	            if (gb2.length) {
	                var t2 = gb2.closest('ul.gallery');
	                if (t2.length) {
	                    a.blur();
	                    _getCaption = _getCaptionMW;
	                    _galleryData = t2.find('div.thumb a.mw-file-description, div.thumb a.gBoxVideo');
	                    _galleryIndex = _galleryData.index(a);
	                    _showMedia(a);
	                    return false;
	                }
	            }
	            // It's a generic thumbnail
	            a.blur();
	            _getCaption = _getCaptionEmpty;
	            _showMedia(a);
	            return false;
	        }
	        return true;
	    } catch (error) {
	        _log('Error in _triggerEvent: ' + error);
	        return true;
	    }
	},

	// Helper and speedy functions
	_isClass = function(el, cn) {
		return el.className && (el.className === cn || (' ' + el.className + ' ').indexOf(' ' + cn + ' ') != -1);
	},
	_isTag = function(el, tn) {
		return (el.nodeName && el.nodeName.toUpperCase() === tn.toUpperCase());
	},
	_isExternalLink = function(url) {
	    var externalPattern = /^https?:\/\/(?:[a-z]+\.)?(imgur\.com|imgchest\.com)/;
	    return externalPattern.test(url);
	},


	// Extract a File: title from an explicit link target. This is used for
	// images whose displayed file is only an icon while the link points to a
	// different full-size wiki file.
	_fileTitleFromHref = function(href) {
		if (!href) return '';

		try {
			var u = new URL(href, window.location.href);
			var path = decodeURIComponent(u.pathname)
				.replace(/^\/wiki\//i, '')
				.replace(/^\//, '');

			if (/^(?:File|Image):/i.test(path)) {
				return path.replace(/^Image:/i, 'File:');
			}
		} catch (e) {}

		return '';
	},

	// Normalize file titles before comparing the displayed file with the
	// explicit link target.
	_normalizeFileTitle = function(title) {
		title = String(title || '');

		try {
			title = decodeURIComponent(title);
		} catch (e) {}

		return title
			.replace(/^\.\//, '')
			.replace(/^Image:/i, 'File:')
			.replace(/_/g, ' ')
			.trim()
			.toLowerCase();
	},

	// Get the File: title represented by the image element itself.
	_mediaFileTitle = function($media) {
		if (!$media || !$media.length) return '';

		var key =
			$media.attr('data-image-key') ||
			$media.attr('resource') ||
			'';

		if (!key) return '';

		try {
			key = decodeURIComponent(key);
		} catch (e) {}

		key = key.replace(/^\.\//, '');

		if (!/^(?:File|Image):/i.test(key)) {
			key = 'File:' + key;
		}

		return key.replace(/^Image:/i, 'File:');
	},

	// Resolve the original media URL for a File: page. Needed when a small
	// displayed icon explicitly links to a different file through |link=.
	_getFileMediaUrl = function(title) {
		return mw.loader.using('mediawiki.api').then(function() {
			var api = new mw.Api();

			return api.get({
				action: 'query',
				titles: title,
				prop: 'imageinfo',
				iiprop: 'url',
				format: 'json',
				formatversion: 2
			});
		}).then(function(data) {
			var page = data &&
				data.query &&
				data.query.pages &&
				data.query.pages[0];

			if (
				page &&
				page.imageinfo &&
				page.imageinfo[0] &&
				page.imageinfo[0].url
			) {
				return page.imageinfo[0].url;
			}

			return '';
		});
	},
	_stripNSFWTag = function (s) {
		if (!s) return '';
		return String(s)
			.replace(/\bNSFWTAG\b/gi, '')
			.replace(/\s{2,}/g, ' ')
			.trim();
	},

	// Loader image
	_startLoader = function() {
		if (_loaderPresent || _loaderTm) {
			return;
		}
		if (_loaderWait > 0) {
			_loaderTm = setTimeout(_displayLoader, _loaderWait);
		} else {
			_displayLoader();
		}
	},
	_stopLoader = function() {
		var t = _loaderTm;
		_loaderTm = null;
		if (t) {
			clearTimeout(t);
		}
		if (_loaderPresent) {
			$('#TB_load').remove();
			_loaderPresent = false;
		}
	},
	_displayLoader = function() {
		_loaderPresent = true;
		_loaderTm = null;
		$(document.body).append('<div id="TB_load">');
	},

	// Main functions
	_preload = function() {
		$(document.body).addClass('thickbox_loaded');
		$('#TB_overlay').add('#TB_window').add('#TB_load').remove();
		$(document.body).append('<div id="TB_overlay"></div><div id="TB_window" class="fixedpos"></div>');
		$('#TB_overlay').click(_remove);
		_startLoader();
	},

	// Try to get a file description URL from the thumbnail structure
	_resolveDescUrl = function($media, $container) {
		// 1) data-filepage on the clicked container or any ancestor
		var url =
			($container && $container.data && $container.data('filepage')) ||
			$media.closest('[data-filepage]').data('filepage');

		if (!url) {
			// 2) Standard image thumb anchor
			url = $media.closest('figure, .thumb, [typeof^="mw:File/Thumb"]')
				.find('a.mw-file-description').attr('href');
		}
		if (!url) {
			// 3) “magnify” link in the caption (videos commonly use this)
			url = $media.closest('figure, .thumb, [typeof^="mw:File/Thumb"]')
				.find('.magnify a').attr('href');
		}
		if (!url) {
			// 4) Last resort: whatever the container had (anchor case)
			url = ($container && $container.attr && $container.attr('href')) || '';
		}
		return url || '';
	},

	_showMedia = function(elem, isExternal) {
	    try {
	        var url, $a, $media, descUrl, TB_secondLine = '', TB_descLink,
	            targetFileTitle = '', displayedFileTitle = '', linkedDifferentFile = false;
	        _preload();
	        $a = $(elem);
	        $media = $a.find('> img, > video').eq(0);

	        if (isExternal) {
	            // Handle external media
	            url = $a.attr('href');
	            descUrl = url;
	        } else {
	            // Handle internal wiki media

	            // Resolve the file description URL from the thumb structure
	            descUrl = _resolveDescUrl($media, $a);

	            // An image can intentionally display one file while linking to a
	            // different File: page, for example an icon that opens full artwork.
	            targetFileTitle = _inferFileTitle($media, descUrl);
	            displayedFileTitle = _mediaFileTitle($media);

	            linkedDifferentFile = !!(
	                targetFileTitle &&
	                (
	                    !displayedFileTitle ||
	                    _normalizeFileTitle(targetFileTitle) !== _normalizeFileTitle(displayedFileTitle)
	                )
	            );

	            if (targetFileTitle) {
	                // Use canonical link to the intended file description page
	                descUrl = mw.util.getUrl(targetFileTitle);
	            }

	            if (_isTag($media[0], 'img')) {
	                // Normal image links can continue using the original image URL
	                // derived from the displayed thumbnail. A different |link=File:
	                // target is resolved through imageinfo before preloading below.
	                if (!linkedDifferentFile) {
	                    url = _getUrlFromThumb($media.attr('src'));
	                }
	            } else if (_isTag($media[0], 'video')) {
	                url = $media.find('source').data('src') || $media.attr('src');
	            }
	        }

			// Begin FileInfo fetch ASAP so it can be ready for first paint
			_startFileInfoFetch($media, descUrl);

	        // Add file page link
	        TB_descLink = '<a id="TB_descLink" class="sprite details" title="Go to the media\'s description page">File Page</a>';

	        // Is it a gallery?
	        if (_galleryIndex != -1) {
	            TB_secondLine = '<div id="TB_secondLine">' +
	                '<span id="TB_imageCount"></span>' +
	                '<span id="TB_prev"><a href="#" title="See previous media [Left arrow]"></a></span>' +
	                '<span id="TB_next"><a href="#" title="See next media [Right arrow]"></a></span></div>';
	        }

	        // Append ThickBox elements
	        var thickboxContent = '<div id="TB_closeWindow"><a href="#" id="TB_closeWindowButton" title="Close [ESC]">X</a></div>' +
	            '<div id="TB_ImageOff">';

	        if (_isTag($media[0], 'img')) {
	            thickboxContent += '<img id="TB_Image" alt="Image" title="Close" />';
	        } else if (_isTag($media[0], 'video')) {
	            thickboxContent += '<video id="TB_Video" controls autoplay loop></video>';
	        }

	        thickboxContent += TB_descLink + '</div>' + TB_secondLine +
				'<div id="TB_caption"></div>' +
				// Placeholder container below the caption
				'<div id="TB_fileinfo" class="tb-fileinfo"></div>';

	        $('#TB_window').empty().append(thickboxContent); // Empty the window before appending new content

	        // Update navigation if it's a gallery
	        if (_galleryIndex != -1) {
	            _updateNavigation();
	        }

	        // Set caption
	        $('#TB_caption').html((_getCaption($a) || null));

	        // Attach event listeners
	        $('#TB_Image').add('#TB_Video').add('#TB_closeWindowButton').click(_remove);
	        $(document).on('keyup.thickbox', _keyListener);
	        $('#TB_prev').add('#TB_next').click(_navigate);

	        // Apply resolved/normalized File Page URL (show link only if we have one)
	        $('#TB_descLink')
	          .attr('href', descUrl || '#')
	          .toggle(!!descUrl);

			// Reflow on window resize while open
			$(window).off('resize.thickbox').on('resize.thickbox', _reflowMediaToReserved);

	        // Preload media
	        if (_isTag($media[0], 'img')) {
	            if (_imgPreloader === null) {
	                _imgPreloader = new Image();
	            }
	            _imgPreloader.onload = _mediaLoaded;
	            _imgPreloader.onerror = _mediaError;
	            _imgPreloader.src = ''; // chromium bug 7731
	            _videoPreloader = null; // Reset the preloader

	            if (linkedDifferentFile && targetFileTitle) {
	                // The displayed image is only an icon/preview. Load the file
	                // explicitly targeted by |link=File:... instead.
	                _getFileMediaUrl(targetFileTitle).then(function(fileUrl) {
	                    if (fileUrl) {
	                        _imgPreloader.src = fileUrl;
	                    } else {
	                        _imgPreloader.src = _getUrlFromThumb($media.attr('src'));
	                    }
	                }).catch(function() {
	                    // Fall back to the displayed image if the API lookup fails.
	                    _imgPreloader.src = _getUrlFromThumb($media.attr('src'));
	                });
	            } else {
	                _imgPreloader.src = url;
	            }
	        } else if (_isTag($media[0], 'video')) {
	            if (_videoPreloader === null) {
	                _videoPreloader = document.createElement('video');
	            }
	            _videoPreloader.onloadeddata = _mediaLoaded;
	            _videoPreloader.onerror = _mediaError;
	            _imgPreloader = null; // Reset the preloader
	            _videoPreloader.src = ''; // Reset the preloader
	            _videoPreloader.src = url;
	        }

	    } catch (e) {
	        _log('Error in _showMedia: ' + e);
	    }
	},

	_showElement = function(target) {
		try {
			var url = target.href, idx = url.indexOf('#');
			if (idx == -1) {
				return false;
			}
			var baseurl = url.substr(0, idx),
				hash = decodeURIComponent(url.substr(idx + 1)),
				// We check that the URL is from the same document
				locbase = document.location.href.replace(baseurl, ''),
				rel = document.getElementById(hash);
			if ((locbase !== '' && locbase.indexOf('#') !== 0) || rel === null) {
				return false;
			}

			$('#TB_overlay').add('#TB_window').remove();
			$(document.body).append('<div id="TB_overlay" class="transparent"></div><div id="TB_window"></div>');
			$('#TB_overlay').click(_remove);

			var titleHTML = '<div id="TB_title"><div id="TB_closeAjaxWindow"><a href="#" id="TB_closeWindowButton" title="Close [ESC]">close</a></div></div>',
				wnd = $('#TB_window'),
				cel = $(rel).clone();
			cel.contents().eq(0).remove();
			cel.find('> sup').remove();
			wnd.width(_minWidth).append(titleHTML + '<div id="TB_ajaxContent">' + cel.html() + '</div>');

			var tgEl = $(target),
				// horizontal space on each side of the element
				elOffset = tgEl.offset(),
				lw = elOffset.left,
				rw = $(document).width() - elOffset.left - tgEl.width(),
				// We calculate the optimal dimensions. We calculate the area and determine that the ideal is ratio 3/2
				prefw = parseInt(Math.sqrt(wnd.width() * wnd.height() * 3 / 2), 10),
				// Minimum width correction if scroll occurs
				cd = $('#TB_ajaxContent')[0];
			prefw += cd.scrollWidth - cd.clientWidth;
			// The minimum width should not be reduced
			if (prefw < _minWidth) {
				prefw = _minWidth;
			}
			// Position. 5px of margin with respect to the origin. Ideal situation: to the right of the element
			var margin = 5, left = $(document).width() - rw + margin;
			if (rw > prefw + margin) {
				// is already correct
			} else if (lw > prefw + margin) {
				left = lw - prefw - margin;
			} else if (lw < 250 || rw < 250) { // It does not fit on either side. We look to see if the minimum width (250) cannot be used. In that case the width we force it and put it to the right
				prefw = 250;
			} else if (rw > lw) { // The available width of the major side is used
				prefw = rw - margin;
			} else {
				prefw = lw - margin * 2;
				left = margin;
			}
			wnd.css({ width: prefw, left: left });
			// Now the vertical position. it needs that we have assigned the width to calculate it well
			var top = elOffset.top - parseInt(wnd.height(), 10) - margin;
			// If it does not fit above we place it below
			if (top < margin) {
				top = elOffset.top + tgEl.height() + margin;
			}
			wnd.css({ top: top, visibility: 'visible' });
			// Animation if it is outside the visual field
			if (($('html')[0].scrollTop || $('body')[0].scrollTop) > top - margin) {
				$('html,body').animate({ scrollTop: top - margin }, 250, 'swing');
			}

			$('#TB_closeWindowButton').click(_remove);
			$(document).on('keyup.thickbox', _keyListener);
		} catch (e) {
			_log('Error in _showElement: ' + e);
		}
	},

	//helper functions below
	_displayClean = function() {
		_stopLoader();
		$('#TB_window').css('visibility', 'visible');
	},
	_remove = function() {
		try {
			$(document).off('keyup.thickbox');
			$(window).off('resize.thickbox');
			_galleryData = null;
			_galleryIndex = -1;
			if (_imgPreloader !== null) {
				_imgPreloader.onload = null;
				_imgPreloader.onerror = null;
			}
			if (_videoPreloader !== null) {
				_videoPreloader.onloadeddata = null;
				_videoPreloader.onerror = null;
			}
			$('#TB_ImageOff').add('#TB_Image').add('#TB_Video').add('#TB_closeWindowButton').add('#TB_prev').add('#TB_next').off();
			$('#TB_window').add('#TB_Image').add('#TB_Video').queue('fx', []).stop();
			$('#TB_window').fadeOut('fast', function() { $('#TB_window').add('#TB_overlay').off().remove(); });
			_stopLoader();
			$(document.body).removeClass('thickbox_loaded');
			return false;
		} catch (e) {
			_log('Error in _remove: ' + e);
		}
	},
	_keyListener = function(e) {
		var keycode = e.which;
		if (keycode == 27) { // close
			_remove();
		} else if (keycode == 37) { // 'Left arrow' display previous media
			$('#TB_prev').click();
		} else if (keycode == 39) { // 'Right arrow' display next media
			$('#TB_next').click();
		}
	},
	_position = function(anim) {
		// Minimum width
		var border = 4;
		if (_width < _minWidth) {
			_width = _minWidth;
		}
		var o = { marginLeft: '-' + parseInt((_width / 2) + border, 10).toString() + 'px', width: _width + 'px', marginTop: '-' + parseInt((_height / 2) + border, 10).toString() + 'px' };
		if (anim) {
			$('#TB_window').animate(o, { queue: false, duration: 'fast' });
		} else {
			$('#TB_window').css(o);
		}
	},
	_getPageSize = function() {
		var de = document.documentElement,
			w = window.innerWidth || (de && de.clientWidth) || document.body.clientWidth,
			h = window.innerHeight || (de && de.clientHeight) || document.body.clientHeight;
		return [w, h];
	},
	_getUrlFromThumb = function(thumb) {
		if (thumb.indexOf('.svg/') != -1) {
			return thumb;
		}

		// If the image is not thumb, or it is an SVG, we use the image as is.
		if (thumb.indexOf('/thumb/') == -1 || thumb.indexOf('.svg/') != -1 ) {
			return thumb;
		}
		var urlparts = thumb.split('/');
		return thumb.replace('/thumb/','/').replace('/'+urlparts[urlparts.length-1], '');
	},
	_getCaptionThumb = function(elem) {
	    return elem.closest('figure').find('> figcaption').html() || '';
	},
	_getCaptionEmpty = function(elem) {
		var t = _stripNSFWTag(elem.attr('title') || '');
		if (!t) return ''; // if it was only NSFWTAG, show nothing
		return $('<div>').text(t).html();
	},
	_getCaptionMW = function(gitem) {
	    let captionElement = gitem.closest('li.gallerybox').find('div.gallerytext').eq(0);
	    return captionElement.length ? (captionElement[0].dataset.fulltext || captionElement.html()) : '';
	},
	_imageError = function() {
		_stopLoader();
	},
	_mediaLoaded = function() {
	    var doInitialPaint = function() {
	        if (_fileInfoReady && _fileInfoHTML) {
	            var $html = $('<div>').html(_fileInfoHTML);
	            $html.find('a[rel="category tag"], .catlinks, .printfooter').remove();
	            $('#TB_fileinfo').html($html);
	        }
	        if (_imgPreloader && _imgPreloader.src) {
	            _updateMediaView(_imgPreloader.width, _imgPreloader.height, _imgPreloader.src);
	        } else if (_videoPreloader && _videoPreloader.src) {
	            _updateMediaView(_videoPreloader.videoWidth, _videoPreloader.videoHeight, _videoPreloader.src);
	        }
	    };

	    if (_fileInfoReady) {
	        doInitialPaint();
	    } else {
	        // Wait briefly for FileInfo to arrive so it appears together with the first paint.
	        var timeoutMs = 400;
	        var timed = new Promise(function(res){ setTimeout(res, timeoutMs); });
	        Promise.race([_fileInfoPromise || Promise.resolve(), timed]).then(function(){
	            doInitialPaint();
	            // If FileInfo still wasn't ready, inject later and reflow without jank.
	            if (!_fileInfoReady && _fileInfoPromise) {
	                _fileInfoPromise.then(function(){
	                    if (_fileInfoHTML && !$('#TB_fileinfo').children().length) {
	                        var $html = $('<div>').html(_fileInfoHTML);
	                        $html.find('a[rel="category tag"], .catlinks, .printfooter').remove();
	                        $('#TB_fileinfo').html($html);
	                        _reflowMediaToReserved();
	                    }
	                });
	            }
	        });
	    }
	},
	_mediaError = function() {
	    _stopLoader();
	    _log('Media failed to load.');
	},

	// Compute reserved vertical space: caption + fileinfo + gallery second line + a small chrome padding
	_getReservedHeights = function() {
	    var captionH   = $('#TB_caption').outerHeight(true)    || 0;
	    var fileinfoH  = $('#TB_fileinfo').outerHeight(true)   || 0;
	    var secondLine = $('#TB_secondLine').outerHeight(true) || 0;
	    var chromePad  = 60; // close button, padding, small margins
	    return {
	        captionH: captionH,
	        fileinfoH: fileinfoH,
	        secondLineH: secondLine,
	        reserved: captionH + fileinfoH + secondLine + chromePad
	    };
	},

	// Reflow current media to fit the available viewport after FileInfo arrives or when resizing
	_reflowMediaToReserved = function() {
	    var $img = $('#TB_Image');
	    var $video = $('#TB_Video');

	    var natW = _naturalMediaWidth;
	    var natH = _naturalMediaHeight;
	    if (!natW || !natH) {
	        // Fallback to current size if natural size is not present yet
	        natW = ($img.is(':visible') ? $img.width() : $video.width()) || 0;
	        natH = ($img.is(':visible') ? $img.height() : $video.height()) || 0;
	        if (!natW || !natH) return;
	    }

	    var pagesize = _getPageSize();
	    var x = pagesize[0] - _minMarginWidth * 2 - _imageMarginWidth * 2;
	    var r = _getReservedHeights();
	    var y = pagesize[1] - _minMarginHeight * 2 - r.reserved;

	    var w = natW, h = natH;
	    if (w > x) { h = h * (x / w); w = x; }
	    if (h > y) { w = w * (y / h); h = y; }

	    if ($img.is(':visible')) {
	        $img.stop(true, true).css({ width: w, height: h, opacity: 1 });
	    } else {
	        $video.stop(true, true).css({ width: w, height: h, opacity: 1 });
	    }

	    _width  = Math.max(_minWidth, w + _imageMarginWidth * 2);
	    _height = h + r.reserved;

	    _position(false);
	    _displayClean();
	},

	_updateMediaView = function(mediaWidth, mediaHeight, mediaSrc) {
	    var navigation = (_galleryIndex != -1),
	        $img = $('#TB_Image'),
	        $video = $('#TB_Video'),
	        pagesize = _getPageSize(),
	        x = pagesize[0] - _minMarginWidth * 2 - _imageMarginWidth * 2,
	        r = _getReservedHeights(),
	        y = pagesize[1] - _minMarginHeight * 2 - r.reserved,
	        firstNav, mediaOpt;

	    // Store natural media dimensions for future reflows
	    _naturalMediaWidth  = mediaWidth;
	    _naturalMediaHeight = mediaHeight;

	    // Constrain to fit available viewport after accounting for caption + fileinfo + controls
	    if (mediaWidth > x) {
	        mediaHeight = mediaHeight * (x / mediaWidth);
	        mediaWidth  = x;
	    }
	    if (mediaHeight > y) {
	        mediaWidth  = mediaWidth * (y / mediaHeight);
	        mediaHeight = y;
	    }

	    firstNav = ($img.attr('src') || '') === '' && ($video.attr('src') || '') === '';

	    // Clear previous src attributes
	    $img.attr('src', '');
	    $video.attr('src', '');

	    if ($img.length && _isTag($img[0], 'img')) {
	        $img.attr({ src: mediaSrc, alt: $('#TB_caption').text() });
	    } else if ($video.length && _isTag($video[0], 'video')) {
	        $video.attr({ src: mediaSrc });
	    }

	    mediaOpt = { width: mediaWidth, height: mediaHeight, opacity: 1 };

	    if (firstNav) {
	        if ($img.is(':visible')) { $img.css(mediaOpt); } else { $video.css(mediaOpt); }
	    } else {
	        if ($img.is(':visible')) { $img.animate(mediaOpt, { duration: 'fast' }); }
	        else { $video.animate(mediaOpt, { duration: 'fast' }); }
	    }

	    _width  = Math.max(_minWidth, mediaWidth + _imageMarginWidth * 2);
	    _height = mediaHeight + r.reserved;

	    _position(navigation && !firstNav);
	    _displayClean();
	},

	_updateNavigation = function() {
		var seq = _galleryIndex, len = _galleryData.length;
		$('#TB_prev').css('display', (seq === 0 ? 'none' : ''));
		$('#TB_next').css('display', (seq >= len - 1 ? 'none' : ''));
		$('#TB_imageCount').text('Media ' + (seq + 1) + ' of ' + len);
	},
	_navigate = function() {
	    try {
	        var url, seq = _galleryIndex + (this.id == 'TB_prev' ? -1 : 1), len = _galleryData.length, gitem, $media;
	        if (seq < 0 || seq > len - 1) {
	            return false;
	        }
	        _galleryIndex = seq;
	        gitem = _galleryData.eq(seq);
	        $media = gitem.find('> img, > video').eq(0);
	        if (_isTag($media[0], 'img')) {
	            url = _getUrlFromThumb($media.attr('src'));
	        } else {
	            url = $media.find('source').data('src') || $media.attr('src');
	        }

			// Robustly resolve the file description URL for the new item
			var nextDescUrl = _resolveDescUrl($media, gitem);
			var nextTitle = $media.data('image-key')
			  ? (mw.config.get('wgFormattedNamespaces')['6'] + ':' + decodeURIComponent($media.data('image-key')))
			  : _inferFileTitle($media, nextDescUrl);
			if (nextTitle) {
				nextDescUrl = mw.util.getUrl(nextTitle);
			}

			// Start FileInfo fetch for the new item immediately; clear old content
			$('#TB_fileinfo').empty();
			_startFileInfoFetch($media, nextDescUrl);

	        _updateNavigation();
	        if ((_imgPreloader && _imgPreloader.src != url) || (_videoPreloader && _videoPreloader.src != url)) {
	            $('#TB_window').stop();
	            $('#TB_Image, #TB_Video').queue('fx', []).stop().each(function() {
	                _startLoader();
	                if (url.length > 3 && url.substr(url.length - 4).toLowerCase() == '.svg') {
	                    // For SVG we already know its aspect ratio, although not its original dimensions
	                    // It would have to be done differently to load the SVG. Instead, here it is dynamically determined
	                    // It is artificially enlarged, and then this function will reduce it to the maximum window size
	                    _updateMediaView($media.prop('width') * 1000, $media.prop('height') * 1000, url);
	                    if (_imgPreloader) {
	                        _imgPreloader.src = '';
	                    }
	                } else {
	                    if (_isTag($media[0], 'img')) {
	                        _imgPreloader.src = url;
	                    } else {
	                        _videoPreloader.src = url;
	                    }
	                }
	            });
	        }
	        $('#TB_caption').html((_getCaption(gitem) || null));

			// Apply updated File Page link (show only if present)
	        $('#TB_descLink').attr('href', nextDescUrl || '#').toggle(!!nextDescUrl);

	        return false;
	    } catch (e) {
	        _log('Error in _navigate: ' + e);
	        return false;
	    }
	},
	_setParams = function(p) {
		var val;
		if (typeof p != 'object') {
			return;
		}
		for (var n in p) {
			if (p.hasOwnProperty(n)) {
				val = p[n];
				switch (n) {
					case 'minWidth':
						_minWidth = val;
						break;
					case 'imageMarginWidth':
						_imageMarginWidth = val;
						break;
					case 'minMarginWidth':
						_minMarginWidth = val;
						break;
					case 'minMarginHeight':
						_minMarginHeight = val;
						break;
					case 'loaderWait':
						_loaderWait = (typeof val == 'number' && val);
						break;
					case 'logger':
						_logger = (typeof val == 'function' && val);
						break;
				}
			}
		}
	},
	_log = function(msg) {
		if (_logger) {
			_logger(msg);
		}
	},

	// ======================================================================
	// FileInfo helpers: fetch {{FileInfo ...}} early and inject on first paint
	// ======================================================================
	
	// If the parsed template output is this message, do not inject FileInfo.
	_FILEINFO_EMPTY_MESSAGE = 'This file is missing a description.',
	
	_isUsefulFileInfoHtml = function (html) {
		if (!html) return false;
	
		// Strip tags by parsing into a detached element and reading textContent.
		var div = document.createElement('div');
		div.innerHTML = html;
	
		var text = (div.textContent || div.innerText || '').replace(/\s+/g, ' ').trim();
	
		// If the template output is only the fallback sentence, skip it.
		if (text === _FILEINFO_EMPTY_MESSAGE) return false;
	
		// Otherwise allow it
		return true;
	},

	// Balanced-extract of a template's wikitext (handles nested {{...}})
	_extractTemplate = function(source, name) {
		if (!source) return null;
		var re = new RegExp('\\{\\{\\s*' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i');
		var m = re.exec(source);
		if (!m) return null;

		var i = m.index;
		var depth = 0;
		for (var p = i; p < source.length - 1; p++) {
			var two = source.slice(p, p + 2);
			if (two === '{{') { depth++; p++; continue; }
			if (two === '}}') { depth--; p++; if (depth === 0) { return source.slice(i, p + 1); } }
		}
		return null; // Unbalanced
	},

	// Parse a snippet of wikitext to HTML via the API
	_parseWikitextToHtml = function(wikitext) {
		return mw.loader.using('mediawiki.api').then(function () {
			var api = new mw.Api();
			return api.post({
				action: 'parse',
				format: 'json',
				formatversion: 2,
				prop: 'text',
				contentmodel: 'wikitext',
				text: wikitext
			}).then(function (res) {
				return (res && res.parse && res.parse.text) ? res.parse.text : '';
			});
		});
	},

	// Get raw wikitext of the file page via the API
	_getFilePageWikitext = function(titleText) {
		return mw.loader.using('mediawiki.api').then(function () {
			var api = new mw.Api();
			return api.get({
				action: 'parse',
				page: titleText,
				prop: 'wikitext',
				format: 'json',
				formatversion: 2
			}).then(function (res) {
				return (res && res.parse && res.parse.wikitext) ? res.parse.wikitext : '';
			});
		});
	},

	// From a description URL and/or data-image-key, determine "File:…" title text
	_inferFileTitle = function($media, descUrl) {
		// If the image explicitly links to a File: page, prefer that target.
		// This allows a displayed icon to open a different full-size file.
		var linkedTitle = _fileTitleFromHref(descUrl);
		if (linkedTitle) {
			return linkedTitle;
		}

		// Otherwise prefer data-image-key
		var key = $media && $media.data ? $media.data('image-key') : null;
		if (key) {
			var ns6 = mw.config.get('wgFormattedNamespaces')['6'] || 'File';
			return ns6 + ':' + decodeURIComponent(key);
		}

		// Fallback: try to pull "File:..." from the URL path
		try {
			var href = descUrl || '';
			// Accept /wiki/File:Foo or full URLs ending with /File:Foo
			var m = href.match(/(?:\/wiki\/|:)((?:%20|[^#?])+)$/i);
			if (m && m[1]) {
				var last = decodeURIComponent(m[1]);
				// If not prefixed, add it
				if (!/^(?:File|Image):/i.test(last)) {
					var ns = mw.config.get('wgFormattedNamespaces')['6'] || 'File';
					last = ns + ':' + last;
				}
				return last;
			}
		} catch (e) {
			_log('Failed inferring title from URL: ' + e);
		}
		return null;
	},

	// Fetch FileInfo HTML without injecting; returns a Promise<string>
	_maybeFetchFileInfo = function($media, descUrl) {
		var titleText = _inferFileTitle($media, descUrl);
		if (!titleText) { return Promise.resolve(''); }
	
		return _getFilePageWikitext(titleText)
			.then(function (wt) {
				var tmpl = _extractTemplate(wt, 'FileInfo');
				if (!tmpl) return '';
				return _parseWikitextToHtml(tmpl);
			})
			.then(function (html) {
				// suppress FileInfo if it's just the fallback message
				return _isUsefulFileInfoHtml(html) ? (html || '') : '';
			})
			.catch(function () { return ''; });
	},

	// Start FileInfo fetch and set flags so _mediaLoaded can synchronize first paint
	_startFileInfoFetch = function($media, descUrl) {
		_fileInfoReady = false;
		_fileInfoHTML = '';
		_fileInfoPromise = _maybeFetchFileInfo($media, descUrl).then(function(html){
			_fileInfoHTML = html;
			_fileInfoReady = true;
			return html;
		});
	},

	// ======================================================================

	_imgTipEvent = function(e) {
		var target = e.target, a, t;
		if (e.ctrlKey || e.altKey || e.shiftKey) {
			_hideImgTip();
			return;
		}
		if (_isTag(target, 'img')) { // Gallery or thumb
			a = target.parentNode;
			var linkedFileTitle = _isTag(a, 'a') ? _fileTitleFromHref(a.href) : '';
			if (
				!_isTag(a, 'a') ||
				(
					!_isClass(a, 'image') &&
					!_isClass(a, 'mw-file-description') &&
					!linkedFileTitle
				) ||
				(_isClass(a, 'link-internal') && !linkedFileTitle)
			) {
				_hideImgTip();
				return;
			}
			t = $(target);
			// We show only if the image has a minimum size
			if (t.width() < 40 || t.height() < 40) {
				return;
			}
			_showImgTip(t);
			return;
		}
		_hideImgTip();
	},
	_imgTipClickEvent = function() {
		if (_imgTipTarget) {
			$(_imgTipTarget).click();
			return false;
		}
	},
	_createImgTip = function() {
		_imgTip = $('<div id="TB_imagetip" title="Click on the image to enlarge. Click with Ctrl or Shift to go to the file page.">').appendTo(document.body);
		_imgTip.on('click', _imgTipClickEvent);
	},
	_showImgTip = function(target) {
		if (!_imgTip) {
			_createImgTip();
		}
		var of = target.offset();
		_imgTip.css({
			display: 'block',
			left: of.left + target.width(),
			top: of.top
		});
		_imgTipVisible = true;
		_imgTipTarget = target;
	},
	_hideImgTip = function() {
		if (_imgTipVisible) {
			_imgTip.css('display', 'none');
			_imgTipVisible = false;
			_imgTipTarget = null;
		}
	};

	// Public functions
	return {
		init: _init,
		showMedia: _showMedia,
		showElement: _showElement,
		remove: _remove,
		setParams: _setParams
	};

}(jQuery, mw));

if (mw.config.get('wgAction', '') != 'history' || !(mw.config.get('wgNamespaceNumber', 0) == -1 && mw.config.get('wgCanonicalSpecialPageName', '') == 'Recentchanges')) {
	$(window.Thickbox.init);
}
/* </pre> */