Benutzer:Schnark/js/vorleser.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/vorleser]] <nowiki>
/*global mediaWiki*/
(function ($, mw) {
"use strict";

//mw.user.options.set('userjs-speechsynthesis-polyfill', 'http://localhost/testwiki2/polyfill-incl.js');

var icons = {
//jscs:disable maximumLineLength
	//Icons by Google
	play: '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M0 0h48v48H0z" fill="none"/><path d="M20 33l12-9-12-9v18zm4-29C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm0 36c-8.82 0-16-7.18-16-16S15.18 8 24 8s16 7.18 16 16-7.18 16-16 16z"/></svg>',
	pause: '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M0 0h48v48H0z" fill="none"/><path d="M18 32h4V16h-4v16zm6-28C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm0 36c-8.82 0-16-7.18-16-16S15.18 8 24 8s16 7.18 16 16-7.18 16-16 16zm2-8h4V16h-4v16z"/></svg>'
}, l10n = {
	en: {
		'schnark-vorleser-enable': 'Enable reading',
		'schnark-vorleser-disable': 'Disable reading',
		'schnark-vorleser-tooltip': 'When reading out is enabled, you can start and pause it with the key s or the button shown, or by clicking the place from which on should be read.'
	},
	de: {
		'schnark-vorleser-enable': 'Vorlesefunktion aktivieren',
		'schnark-vorleser-disable': 'Vorlesefunktion deaktivieren',
		'schnark-vorleser-tooltip': 'Wenn die Vorlesefunktion aktiviert ist, kann sie mit der Taste s oder die eingeblendete Schaltfläche gestartet und pausiert werden, oder mit einem Klick auf die Stelle, ab der vorgelesen werden soll.'
	}
//jscs:enable maximumLineLength
};

function initL10N (l10n) {
	var i, chain = mw.language.getFallbackLanguageChain();
	for (i = chain.length - 1; i >= 0; i--) {
		if (chain[i] in l10n) {
			mw.messages.set(l10n[chain[i]]);
		}
	}
}

function getFunctions (speechSynthesis, SpeechSynthesisUtterance) {
//virtual indent

var getVoicesCache, otherVoiceCache = {};
function getOtherVoiceUncached (lang) {
	if (!getVoicesCache) {
		getVoicesCache = speechSynthesis.getVoices();
	}
	var list = $.grep(getVoicesCache, function (data) {
		return data.lang === lang && !data['default'];
	});
	return (list[0] && list[0].voiceURI) || '';
}
function getOtherVoice (lang) {
	if (!(lang in otherVoiceCache)) {
		otherVoiceCache[lang] = getOtherVoiceUncached(lang);
	}
	return otherVoiceCache[lang];
}

function speakText (text, data) {
	var utterance = new SpeechSynthesisUtterance(mw.html.escape(text));
	if (data.lang) {
		utterance.lang = data.lang;
	}
	if (data.voice) {
		utterance.voiceURI = getOtherVoice(data.lang);
	}
	if (data.volume) {
		utterance.volume = data.volume;
	}
	if (data.rate) {
		utterance.rate = data.rate;
	}
	if (data.pitch) {
		utterance.pitch = data.pitch;
	}
	if (data.callback) {
		utterance.onend = function () {
			data.callback(true);
		};
		utterance.onerror = function () {
			data.callback(false);
		};
	}
	speechSynthesis.speak(utterance);
}

function Tree (element, data) {
	this.element = element;
	this.data = $.extend({}, data, Tree.getData(element));
	this.content = Tree.getContent(element);
}

Tree.getData = function (element) {
	var lang, name, ret = {element: element};
	if (!element.getAttribute) {
		return ret;
	}
	lang = element.getAttribute('lang') || '';
	name = element.nodeName.toUpperCase();
	if (lang) {
		ret.lang = lang;
	}
	if (['H1', 'H2', 'H3', 'H4', 'H5', 'IMG'].indexOf(name) !== -1) {
		ret.voice = true;
	}
	if (name === 'SMALL') {
		ret.volume = 0.7;
	}
	if (['U', 'B', 'STRONG'].indexOf(name) !== -1) {
		ret.rate = 0.7;
		ret.pitch = 0.7;
	}
	if (['I', 'EM'].indexOf(name) !== -1) {
		ret.pitch = 1.3;
	}
	if (window.getComputedStyle(element, null).display === 'none') {
		ret.silent = true;
	}
	return ret;
};

Tree.getContent = function (element) {
	var before, after, ret;
	try {
		//TODO counter etc.
		before = window.getComputedStyle(element, '::before').content.replace(/[^"]*(?:"([^"]*)")?[^"]*/g, '$1');
		after = window.getComputedStyle(element, '::after').content.replace(/[^"]*(?:"([^"]*)")?[^"]*/g, '$1');
	} catch (e) {
		//#TEXT node etc.
	}
	switch (element.nodeName.toUpperCase()) {
	case '#TEXT': ret = [element.data]; break;
	case 'IMG': ret = [element.getAttribute('alt') || '']; break;
	default: ret = $.makeArray(element.childNodes);
	}
	if (before) {
		ret.unshift(before);
	}
	if (after) {
		ret.push(after);
	}
	return ret;
};

Tree.getResult = function (text) {
	return {text: text};
};

Tree.prototype.getNext = function () {
	var next;
	while (1) {
		if (this.current) {
			next = this.current.getNext();
			if (next) {
				return next;
			}
			this.current = false;
		}
		if (this.content.length === 0) {
			return false;
		}
		next = this.content.shift();
		if (typeof next === 'string') {
			next = next.trim();
			if (next && !this.data.silent) {
				return $.extend(Tree.getResult(next), this.data);
			} else {
				continue;
			}
		}
		this.current = new Tree(next, this.data);
	}
};

function parents (element) {
	var ret = [element];
	while (ret[0].parentNode) {
		ret.unshift(ret[0].parentNode);
	}
	return ret;
}

function commonParentIndex (p1, p2) {
	var i = 0;
	while (p1[i] === p2[i]) {
		i++;
	}
	return i - 1;
}

function comesBefore (a, b, p1) {
	var p2, c, i, j;
	//p1 = parents(a);
	p2 = parents(b);
	i = commonParentIndex(p1, p2);
	if (i === p1.length - 1) {
		return true;
	}
	if (i === p2.length - 1) {
		return false;
	}
	c = p1[i].childNodes;
	for (j = 0; j < c.length; j++) {
		if (c[j] === p1[i + 1]) {
			return true;
		}
		if (c[j] === p2[i + 1]) {
			return false;
		}
	}
}

function read (element, from) {
	/*jshint boss: true*/
	var tree = new Tree(element, {}), data, start = false, fromParents;
	if (!from) {
		start = true;
	} else {
		fromParents = parents(from);
	}
	while (data = tree.getNext()) {
		if (!start && comesBefore(from, data.element, fromParents)) {
			start = true;
		}
		if (data.text && start) {
			speakText(data.text, data);
		}
	}
}

return {
	read: read,
	cancel: speechSynthesis.cancel.bind(speechSynthesis),
	pause: speechSynthesis.pause.bind(speechSynthesis),
	resume: speechSynthesis.resume.bind(speechSynthesis)
};
//virtual outdent
}

function manage (f) {
	var content = document.getElementById('content');
	return {
		readAll: function () {
			f.cancel();
			f.resume();
			f.read(content);
		},
		readFrom: function (el) {
			f.cancel();
			f.resume();
			f.read(content, el);
		},
		pause: function () {
			f.pause();
		},
		resume: function () {
			f.resume();
		},
		cancel: function () {
			f.cancel();
			f.resume();
		}
	};
}

function initEvents (type, f) {
	var $body = $('body'), $content = $('#content'), $icon;
	$body.off('.speechSynthesisTest');
	$content.off('.speechSynthesisTest');
	$('#speechSynthesisTestIcon').remove();

	if (type === 'off') {
		return;
	}

	function toggle () {
		f[type === 'reading' ? 'pause' : type === 'pausing' ? 'resume' : 'readAll']();
		initEvents(type === 'reading' ? 'pausing' : 'reading', f);
	}

	if (type !== 'pausing') {
		$content.on('click.speechSynthesisTest', '*', function (e) {
			if (this === e.target) {
				initEvents('reading', f);
				f.readFrom(this);
			}
		});
	}

	$icon = $('<img>');
	$icon.attr('id', 'speechSynthesisTestIcon');
	$icon.attr('src', 'data:image/svg+xml,' + encodeURI(icons[type === 'reading' ? 'pause' : 'play']));
	$icon.attr('style', 'position: fixed; width: 48px; height: 48px; top: 2px; left: 2px;');
	$icon.on('click', toggle);
	$icon.appendTo($body);

	$body.on('keydown.speechSynthesisTest', function (e) {
		if (e.which === 83) { //s
			e.preventDefault();
			e.stopPropagation();
			toggle();
		}
	});
}

function switchOn ($el, f) {
	initEvents('nothing', f);
	$el.text(mw.msg('schnark-vorleser-enable')).off('click').on('click', function (e) {
		e.preventDefault();
		switchOff($el, f);
	});
}

function switchOff ($el, f) {
	f.cancel();
	initEvents('off', f);
	$el.text(mw.msg('schnark-vorleser-enable')).off('click').on('click', function (e) {
		e.preventDefault();
		switchOn($el, f);
	});
}

function init () {
	var sS = window.polyfillSpeechSynthesis || window.speechSynthesis,
		SSU = window.polyfillSpeechSynthesisUtterance || window.SpeechSynthesisUtterance,
		f, el;
	if (!sS || !SSU) {
		return;
	}
	f = manage(getFunctions(sS, SSU));

	mw.loader.using(['mediawiki.language', 'mediawiki.util'], function () {
		initL10N(l10n);
		$(function () {
			el = mw.util.addPortletLink('p-tb', '#', '', 't-read-functions', mw.msg('schnark-vorleser-tooltip'));
			switchOff($(el).find('a'), f);
		});
	});
}

if (mw.user.options.exists('userjs-speechsynthesis-polyfill')) {
	$.ajax({
		url: mw.user.options.get('userjs-speechsynthesis-polyfill'),
		dataType: 'script', crossDomain: true,
		cache: true, async: true
	}).then(init);
} else {
	init();
}

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