Benutzer:Schnark/js/imagepopups.js

aus Wikipedia, der freien Enzyklopädie
Zur Navigation springen Zur Suche springen

Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.

  • Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
  • Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
  • Internet Explorer/Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
  • Opera: Strg+F5
//Dokumentation unter [[Benutzer:Schnark/js/imagepopups]] <nowiki>

/*global mediaWiki*/
(function ($, mw) {
"use strict";

//jscs:disable maximumLineLength
var l10n = {
	en: {
		'size-bytes': '$1 B',
		'size-kilobytes': '$1 KB',
		'size-megabytes': '$1 MB',
		'size-gigabytes': '$1 GB',

		'schnark-imagepopups-close': '×', //icon for close button
		'schnark-imagepopups-close-title': 'close', //tooltip for close button
		'schnark-imagepopups-local-desc': 'local description page', //link text for local description page
		'schnark-imagepopups-shared-desc': 'description page (Commons)', //link text for description page on Commons
		'schnark-imagepopups-missing': '$1 (missing)', //appended to link to local description page if missing
		'schnark-imagepopups-full-resolution': 'full resolution', //link text for full resolution image
		'schnark-imagepopups-info': '$1 ($2)', //$1: link to full res., $2: info
		'schnark-imagepopups-info-default': '$1×$2, $3, $4', //default info, $1: width, $2: height, $3: mime, $4: size
		'schnark-imagepopups-info-paged': '$5 pages, $3, $4', //info for paged files like PDF, $1: width, $2: height, $3: mime, $4: size, $5: pages
		'schnark-imagepopups-info-video': '$5, $3, $4', //info for videos, $1: width, $2: height, $3: mime, $4: size, $5: time
		'schnark-imagepopups-sep': '&#160;/ ', //separator between links
		'schnark-imagepopups-play': 'Play video', //tooltip for play button
		'schnark-imagepopups-time-seperator': ':',
		'schnark-imagepopups-unknown-length': 'unknown length', //shown for videos with unknown length
		'schnark-imagepopups-prev-page': '<', //button for previous page
		'schnark-imagepopups-prev-page-tooltip': 'Show previous page', //tooltip for this button
		'schnark-imagepopups-next-page': '>', //button for next page
		'schnark-imagepopups-next-page-tooltip': 'Show next page', //tooltip for this button
		'schnark-imagepopups-current-page': '&#160;· <b>page $1</b>&#160;· ', //text to indicate current page, $1 is replaced with a <span> containing the current page number
		'schnark-imagepopups-3d-viewer': 'Open in 3D viewer',
		'schnark-imagepopups-mediaviewer': 'You have enabled both this script and the MediaViewer. You should decide for one script and disable the other.'
	},
	de: {
		'size-bytes': '$1 Bytes',
		'schnark-imagepopups-close-title': 'Schließen',
		'schnark-imagepopups-local-desc': 'lokale Bildbeschreibungsseite',
		'schnark-imagepopups-shared-desc': 'Bildbeschreibungsseite (Commons)',
		'schnark-imagepopups-missing': '$1 (fehlt)',
		'schnark-imagepopups-full-resolution': 'Vollbild',
		'schnark-imagepopups-info-paged': '$5 Seiten, $3, $4',
		'schnark-imagepopups-play': 'Vido abspielen',
		'schnark-imagepopups-unknown-length': 'unbekannte Dauer',
		'schnark-imagepopups-prev-page-tooltip': 'Vorherige Seite zeigen',
		'schnark-imagepopups-next-page-tooltip': 'Nächste Seite zeigen',
		'schnark-imagepopups-current-page': '&#160;· <b>Seite $1</b>&#160;· ',
		'schnark-imagepopups-3d-viewer': 'Im 3-D-Betrachter öffnen',
		'schnark-imagepopups-mediaviewer': 'Du hast sowohl dieses Skript als auch den Medienbetrachter aktiviert. Du solltest dich für ein Skript entscheiden und das andere deaktivieren.'
	},
	'de-ch': {
		'schnark-imagepopups-close-title': 'Schliessen'
	},
	'de-formal': {
		'schnark-imagepopups-mediaviewer': 'Sie haben sowohl dieses Skript als auch den Medienbetrachter aktiviert. Sie sollten sich für ein Skript entscheiden und das andere deaktivieren.'
	}
//jscs:enable maximumLineLength
}, config = {
	replaceTMHPopup: true,
	useMwEmbed: true,
	warnAboutMV: false,
	css:
	'.imagePopup {' +
		'position: fixed;' +
		'border: solid 3px #000;' +
		'background-color: #fff;' +
		'min-width: 10em;' +
		'font-size: medium;' +
	'}' +
	'.imagePopupTitleBar {' +
		'position: relative;' + //a absolute
		'padding-right: 1em;' +
		'overflow: hidden;' +
		'text-overflow: ellipsis;' +
		'white-space: nowrap;' +
		'background-color: #000;' +
		'color: #fff;' +
		'font-weight: bold;' +
		'text-align: center;' +
		'cursor: move;' +
	'}' +
	'.imagePopupTitleBar a {' +
		'display: block;' +
		'position: absolute;' +
		'top: -3px; right: -3px;' + //Rahmenbreite
		'padding: 3px;' + //Rahmenbreite
		'padding-bottom: 0px;' +

		'background-color: #000;' +
		'text-decoration: none;' +
		'outline: none !important;' +
		'color: #fff !important;' +
		'transition: color 0.25s;' +
		'cursor: pointer;' +
	'}' +
	'.imagePopupTitleBar a:hover {' +
		'text-decoration: none;' +
		'color: #d33 !important;' +
	'}' +
	'.imagePopup img:not(.playerPoster), .imagePopup .mediaContainer {' +
		'padding: 5px;' +
		'margin-left: auto;' +
		'margin-right: auto;' +
	'}' +
	'.imagePopupDesc {' +
		'max-height: 8em;' +
		'overflow: auto;' +
		'background-color: #fef6e7;' +
		'padding: 5px;' +
		'font-size: small;' +
	'}',
	zIndex: 120,
/*
evt. relevante Werte:
100 für obere Leiste in Vector
101 in OOUI (in Vector) für Dialoge etc.
110+ in EPOP (reagiert aber ohnehin nur auf Links im Haupttext)
200 in popuprefs.js
*/
	sizes: [[320, 240], [640, 480], [800, 600], [1024, 768], [1280, 1024]], //from DefaultSettings.php
	extensions: {
		'png': 'image',
		'apng': 'image',
		'jpg': 'image',
		'jpeg': 'image',
		'jpe': 'image',
		'gif': 'image',
		'bmp': 'image',
		'svg': 'image',
		'xcf': 'image',
		'webp': 'image',
		'pdf': 'paged',
		'tif': 'paged',
		'tiff': 'paged',
		'djvu': 'paged',
		'djv': 'paged',
		'ogg': 'video',
		'ogx': 'video',
		'ogm': 'video',
		'ogv': 'video',
		'oga': 'video',
		'spx': 'video',
		'opus': 'video',
		'mp4': 'video',
		'm4a': 'video',
		'm4p': 'video',
		'm4b': 'video',
		'm4r': 'video',
		'm4v': 'video',
		'webm': 'video',
		'stl': 'threed'
	}
},

parseHTML = {
	image: function ($file) {
		var data = {};
		data.url = getUrl($file);
		data.sharedUrl = getSharedUrl(data.url);
		data.localUrl = data.url.replace(/^(?:https?:)?\/\/[^\/]+\//, mw.config.get('wgServer') + '/');
		data.title = getTitle(data.url);
		data.$desc = getDesc($file, data.title);
		data.lang = mw.util.getParamValue('lang', data.url) || '';
		return data;
	},
	paged: function ($file) {
		var data = parseHTML.image($file);
		data.page = mw.util.getParamValue('page', data.url) || 1;
		return data;
	},
	video: function ($file) {
		var $container = $file.parent('.PopUpMediaTransform'), data = parseHTML.image($container);
		data.$video = $($container.attr('videopayload')).find('video');
		return data.$video.length === 1 ? data : false;
	},
	threed: function ($file) {
		var data = parseHTML.image($file);
		data.threedHash = '/media/File:' + data.title.replace(/ /g, '_');
		return data;
	}
},

getParam = {
	image: function (data, w, h) {
		var param = {
			action: 'query',
			prop: 'imageinfo',
			iiprop: 'url|size|mime',
			iiurlwidth: w,
			iiurlheight: h,
			titles: 'File:' + data.title,
			format: 'json',
			formatversion: 2
		};
		if (data.lang) {
			param.iiurlparam = 'lang' + data.lang + '-' + w + 'px';
		}
		return param;
	},
	paged: function (data, w, h) {
		var param = getParam.image(data, w, h);
		param.iiurlparam = 'page' + data.page + '-' + w + 'px';
		return param;
	},
	video: function (data, w, h) {
		var param = getParam.image(data, w, h);
		//TODO param.iiurlparam = 'seek:'
		return param;
	},
	threed: function (data, w, h) {
		return getParam.image(data, w, h);
	}
},

extractData = {
	image: function (json) {
		var ii = json.imageinfo[0], data = {};
		if (json.missing) {
			data.localMissing = true;
		}
		if (json.imagerepository === 'shared') {
			data.shared = true;
		}
		data.urlFull = ii.url;
		data.info = mw.msg('schnark-imagepopups-info-default',
			ii.width, ii.height, ii.mime, formatSize(ii.size));
		data.urlThumb = ii.thumburl;
		data.thumbwidth = ii.thumbwidth;
		data.thumbheight = ii.thumbheight;
		return data;
	},
	paged: function (json, htmlData) {
		var ii = json.imageinfo[0], data = extractData.image(json);
		if (ii.pagecount > 1) {
			data.page = htmlData.page;
			data.pages = ii.pagecount;
			data.info = mw.msg('schnark-imagepopups-info-paged',
				ii.width, ii.height, ii.mime, formatSize(ii.size), ii.pagecount);
		} else {
			htmlData.type = 'image';
		}
		return data;
	},
	video: function (json, htmlData) {
		var ii = json.imageinfo[0], data = extractData.image(json);
		data.info = mw.msg('schnark-imagepopups-info-video',
			ii.width, ii.height, ii.mime, formatSize(ii.size), formatTime(ii.duration));

		data.attr = getDataAttr(htmlData.$video);

		//TODO über API auslesen
		data.sources = [];
		htmlData.$video.find('source').each(function () {
			var $source = $(this);
			data.sources.push($.extend(getDataAttr($source), {
				src: $source.attr('src'),
				type: $source.attr('type'),
				'data-width': data.thumbwidth,
				'data-height': data.thumbheight
			}));
		});
		if (data.sources.length === 0) {
			data.sources.push({
				src: data.urlFull,
				type: ii.mime,
				'data-width': data.thumbwidth,
				'data-height': data.thumbheight
			});
		}

		data.tracks = [];
		htmlData.$video.find('track').each(function () {
			var $track = $(this);
			data.tracks.push($.extend(getDataAttr($track), {
				kind: $track.attr('kind'),
				type: $track.attr('type'),
				src: $track.attr('src'),
				srclang: $track.attr('srclang'),
				label: $track.attr('label')
			}));
		});
		return data;
	},
	threed: function (json, htmlData) {
		var data = extractData.image(json);
		data.threedHash = htmlData.threedHash;
		return data;
	}
},

innerHTML = {
	image: function (data) {
		return mw.html.element('img', {width: data.width, height: data.height, src: data.url});
	},
	paged: function (data) {
		return innerHTML.image(data) + mw.html.element('div', {style: 'text-align: center;'}, new mw.html.Raw(
				mw.html.element('button', {'class': 'prev',
					title: mw.msg('schnark-imagepopups-prev-page-tooltip')},
					mw.msg('schnark-imagepopups-prev-page')) +
				mw.html.element('span', {}, new mw.html.Raw(mw.msg('schnark-imagepopups-current-page',
					mw.html.element('span', {'class': 'currentPage'}, data.data.page)))) +
				mw.html.element('button', {'class': 'next',
					title: mw.msg('schnark-imagepopups-next-page-tooltip')},
					mw.msg('schnark-imagepopups-next-page'))
			));
	},
	video: function (data) {
		var containerAttr = {
			'class': 'mediaContainer',
			style: 'width:' + data.width + 'px;'
		}, videoAttr = $.extend(data.attr, {
			style: 'width:' + data.width + 'px; height:' + data.height + 'px;',
			'class': 'kskin',
			width: data.width,
			height: data.height,
			poster: data.url,
			controls: true,
			autoplay: true
		}), video = mw.html.element('video', videoAttr, new mw.html.Raw(
			data.data.sources.map(function (attr) {
				return mw.html.element('source', attr);
			}) +
			data.data.tracks.map(function (attr) {
				mw.html.element('track', attr);
			})
		));
		return mw.html.element('div', {'class': 'containerParent'}, new mw.html.Raw(
			mw.html.element('div', containerAttr, new mw.html.Raw(video))
		));
	},
	threed: function (data) {
		return innerHTML.image(data) + mw.html.element('div', {style: 'text-align: center;'}, new mw.html.Raw(
				mw.html.element('button', {'class': 'threed-viewer'}, mw.msg('schnark-imagepopups-3d-viewer'))
			));
	}
},

setEvents = {
	image: function () {},
	paged: function ($popup, data) {
		function changePage (diff) {
			var page = Number($popup.find('.currentPage').text()) + diff, $img, src;
			if (page < 1 || page > data.data.pages) {
				return;
			}
			$popup.find('button.prev').prop('disabled', page === 1);
			$popup.find('button.next').prop('disabled', page === data.data.pages);
			$popup.find('.currentPage').text(page);
			$img = $popup.find('img');
			src = $img.attr('src').replace(/page\d+/, 'page' + page);
			$img.attr('src', '');
			$img.attr('src', src);
		}
		$popup.find('button.prev').on('click', function () {
			changePage(-1);
		}).prop('disabled', data.data.page === 1);
		$popup.find('button.next').on('click', function () {
			changePage(/*+*/1);
		}).prop('disabled', data.data.page === data.data.pages);
	},
	video: function ($popup) {
		if (config.useMwEmbed) {
			mw.hook('wikipage.content').fire($popup.find('.containerParent'));
		}
	},
	threed: function ($popup, data) {
		$popup.find('button.threed-viewer').on('click', function () {
			location.hash = data.data.threedHash;
		});
	}
};

function initL10N (l10n, keep) {
	var i, chain = mw.language.getFallbackLanguageChain();
	keep = $.grep(mw.messages.get(keep), function (val) {
		return val !== null;
	});
	for (i = chain.length - 1; i >= 0; i--) {
		if (chain[i] in l10n) {
			mw.messages.set(l10n[chain[i]]);
		}
	}
	mw.messages.set(keep);
}

function random (x) {
	var s = 0, i, n = 6;
	for (i = 0; i < n; i++) {
		s += Math.random();
	}
	return Math.round((s / n) * x);
}

function formatSize (s) {
	var i = 0, sizes = ['size-bytes', 'size-kilobytes', 'size-megabytes', 'size-gigabytes'];
	while (s > 1024 && i < sizes.length - 1) {
		s /= 1024;
		i++;
	}
	return mw.msg(sizes[i], Math.round(s));
}
function formatTime (s) {
	var hh, mm, ss, colon = mw.msg('schnark-imagepopups-time-seperator');
	if (s === undefined) {
		return mw.msg('schnark-imagepopups-unknown-length');
	}
	hh = Math.floor(s / 3600);
	s -= hh * 3600;
	mm = Math.floor(s / 60);
	s -= mm * 60;
	ss = Math.round(s);
	if (ss === 60) {
		ss = 0;
		mm++;
	}
	if (mm === 60) {
		mm = 0;
		hh++;
	}

	if (hh > 0 && mm < 10) {
		mm = '0' + String(mm);
	}
	if (ss < 10) {
		ss = '0' + String(ss);
	}
	return (hh > 0 ? hh + colon : '') + mm + colon + ss;
}

function getTopLeft (w, h) {
	w = $(window).width() - w - 20;
	h = $(window).height() - h - 120;
	if (w < 0) {
		w = 0;
	}
	if (h < 0) {
		h = 0;
	}
	return [random(h), random(w)];
}

function getStyle (data) {
	var topLeft = getTopLeft(data.width, data.height);
	return 'z-Index:' + config.zIndex++ + '; width:' + (data.width + 10) + 'px;' +
		'top:' + topLeft[0] + 'px; left:' + topLeft[1] + 'px;';
}

function makeDraggable ($element, $handle) {
	var diff;

	function dragHandler (e) {
		e.preventDefault();
		e.stopPropagation();
		$element.css({
			top: e.clientY + diff.y,
			left: e.clientX + diff.x
		});
	}

	$handle.on('mousedown', function (e) {
		var pos, $doc;
		if ((e.which && e.which !== 1) || e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) {
			return;
		}
		if ($(e.target).is('a')) {
			return;
		}
		e.preventDefault();
		pos = $element.position();
		diff = {x: pos.left - e.clientX, y: pos.top - e.clientY};
		$doc = $(document);
		$doc.on('mousemove', dragHandler);
		$doc.on('mouseup', function () {
			$doc.off('mousemove', dragHandler);
		});
	});
}

function showPopup (data, type) {
	var $popup = $(
		mw.html.element('div', {'class': 'imagePopup', style: getStyle(data)}, new mw.html.Raw(
			mw.html.element('div', {'class': 'imagePopupTitleBar'}, new mw.html.Raw(
				mw.html.element('span', {title: data.title, 'class': 'imagePopupTitle'}, data.title) +
				mw.html.element('a', {'class': 'close-link',
					title: mw.msg('schnark-imagepopups-close-title'), href: '#'},
					mw.msg('schnark-imagepopups-close'))
			)) +
			innerHTML[type](data) +
			mw.html.element('div', {'class': 'imagePopupDesc mw-body-content mw-parser-output'}, new mw.html.Raw(data.info))
		)));
	if (data.$desc) {
		$popup.find('.imagePopupDesc').prepend(data.$desc);
	}
	mw.hook('wikipage.content').fire($popup.find('.imagePopupDesc'));
	$popup.appendTo($('body'))
		.on('mousedown', function () {
			$popup.css({zIndex: config.zIndex++});
		})
		.find('.close-link').on('click', function () {
			var video = $popup.find('video')[0];
			if (video && video.pause) { //Firefox plays even removed videos
				video.pause();
			}
			$popup.remove();
			return false;
		});
	makeDraggable($popup, $popup.find('.imagePopupTitleBar'));
	setEvents[type]($popup, data);
}

function parseHTMLX ($file) {
	var href = $file.attr('href'),
		ext = /\.(\w+)(?:&|$)/.exec(href || ''),
		type, data;
	ext = ((ext && ext[1]) || '').toLowerCase();
	if (!ext || !config.extensions[ext]) {
		return false;
	}
	type = config.extensions[ext];
	data = parseHTML[type]($file);
	data.type = type;
	return data;
}

function getUrl ($file) {
	var url = $file.attr('href');
	if (url) {
		return url;
	}
	url = $file.next('.thumbcaption').find('.magnify a').attr('href');
	if (url) {
		return url;
	}
	url = $file.find('a').attr('href');
	return mw.util.getUrl(mw.config.get('wgFormattedNamespaces')[6] + ':' +
		decodeURIComponent(url.replace(/^.*\//, '')));
}
function getTitle (url) {
	var title = mw.util.getParamValue('title', url),
		re = new RegExp('^.*?(?:File|' + mw.util.escapeRegExp(mw.config.get('wgFormattedNamespaces')[6]) + '):');
	if (title) {
		return title.replace(re, '').replace(/_/g, ' ');
	}
	return decodeURIComponent(url).replace(re, '').replace(/_/g, ' ');
}
function getSharedUrl (url) {
	var re = new RegExp(
		'(?:File|' + mw.util.escapeRegExp(encodeURIComponent(mw.config.get('wgFormattedNamespaces')[6])) + '):'
	);
	return url.replace(re, 'File:').replace(/^(?:(?:https?:)?\/\/[^\/]+)?\//, 'https://commons.wikimedia.org/');
}

function getDataAttr ($el) {
	if ($el.length === 0) {
		return {};
	}
	var raw = $el[0].attributes,
		attrs = {},
		i;
	for (i = 0; i < raw.length; i++) {
		if (raw[i].nodeName.indexOf('data-') === 0) {
			attrs[raw[i].nodeName] = raw[i].nodeValue;
		}
	}
	return attrs;
}

function getDesc ($file, title) {
	var alt,
		$desc = false,
		//try to find caption
		$caption = $file.next('.thumbcaption');
	if ($caption.length === 0) {
		//next try: perhaps a gallery
		$caption = $file.parents('.thumb').next('.gallerytext').find('p');
	}
	if ($caption.length === 0) {
		//next try: new gallery style
		$caption = $file.parents('.thumb').next('.gallerytextwrapper').find('.gallerytext p');
	}
	if ($caption.length === 1) { //found something?
		$desc = $caption.clone();
		$desc.find('div.magnify').remove();
	}
	if (!$desc) { //last try
		alt = $file.find('img').attr('alt') || '';
		if (alt !== title && alt !== '') {
			$desc = $('<p>').text(alt);
		}
	}
	return $desc;
}

function makeFileInfo (htmlData, data) {
	var info = [], localDesc;
	localDesc = mw.html.element('a', {href: htmlData.localUrl}, mw.msg('schnark-imagepopups-local-desc'));
	if (data.localMissing) {
		localDesc = mw.msg('schnark-imagepopups-missing', localDesc);
	}
	info.push(localDesc);
	if (data.shared) {
		info.push(mw.html.element('a', {href: htmlData.sharedUrl}, mw.msg('schnark-imagepopups-shared-desc')));
	}
	info.push(mw.msg('schnark-imagepopups-info',
		mw.html.element('a', {href: data.urlFull}, mw.msg('schnark-imagepopups-full-resolution')), data.info));
	info = info.join(mw.msg('schnark-imagepopups-sep'));
	if (config.warnAboutMV) {
		info += '<br>' + mw.msg('schnark-imagepopups-mediaviewer');
	}
	return info;
}

function processFile ($file) {
	var htmlData = parseHTMLX($file), is;
	if (!htmlData) {
		return false;
	}
	is = config.sizes[mw.user.options.get('imagesize') || 2] || [800, 600];
	$.getJSON(mw.util.wikiScript('api'), getParam[htmlData.type](htmlData, is[0], is[1])).then(function (json) {
		var p, data;
		if (!json || !json.query || !json.query.pages) {
			return;
		}
		p = json.query.pages[0];
		if (!p) {
			return;
		}

		data = extractData[htmlData.type](p, htmlData);

		showPopup({
			title: htmlData.title,
			url: data.urlThumb,
			$desc: htmlData.$desc,
			info: makeFileInfo(htmlData, data),
			width: data.thumbwidth,
			height: data.thumbheight,
			data: data
		}, htmlData.type);
	});
	return true;
}

function onImageClick (e) {
	/*jshint validthis: true*///Event-Handler
	if ((e.which && e.which !== 1) || e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) {
		return;
	}
	if (processFile($(this))) {
		e.preventDefault();
	}
}

function run ($content) {
	if (config.replaceTMHPopup && mw.loader.getState('mw.PopUpMediaTransform')) {
		mw.loader.using('mw.PopUpMediaTransform').then(function () {
			$content.find('.PopUpMediaTransform a').off('click').on('click', onImageClick);
		});
	}
	$content.find('a.image').filter(':has(img)').on('click', onImageClick);
}
function init () {
	if (mw.config.get('wgMediaViewerOnClick')) {
		config.warnAboutMV = true;
		mw.config.set('wgMediaViewerOnClick', false);
	}
	initL10N(l10n, ['size-bytes', 'size-kilobytes', 'size-megabytes', 'size-gigabytes']);
	mw.util.addCSS(config.css);
	mw.hook('wikipage.content').add(run);
}

mw.hook('userjs.load-script.imagepopups').fire(config);
mw.loader.using(['mediawiki.language', 'mediawiki.util', 'user.options']).then(init);

})(jQuery, mediaWiki);
//</nowiki>