User:Sebschlicht/moocDiscussions.js

// // NOTE: requires module  $(document).ready(function {	mw.messages.set('q-no-title', 'You can not ask a question without providing a title!');	mw.messages.set('discussion-header-num-replies', '$1 NaN repliess'); }); function loadDiscussionUi { var path = $('#path').text; if (path) { // render threads merged from talk pages var talkPageTitle = getTalkPageTitle(path); var scriptTalkPage = talkPageTitle + '/script'; var quizTalkPage = talkPageTitle + '/quiz'; renderThreads([			talkPageTitle, scriptTalkPage, quizTalkPage		]); } } function getSignatureLength(content) { if (content.match(/\~\~\~\~$/) !== null) { if ((content.match(/\-\-\~\~\~\~$/) !== null)) { return 6; }		return 4; }	return 0; }

function cutThreadContent(thread, maxLength) { var div = document.createElement('div'); div.innerHTML = thread.htmlContent; var text = div.textContent || div.innerText || ''; if (thread.htmlContent.length <= maxLength) { return thread.htmlContent; }	var cutContent = []; var crrLength = 0; var words = text.split(/\s/); for (var i = 0; i < words.length; ++i) { crrLength += words[i].length + 1; if (crrLength > maxLength) { break; }		cutContent.push(words[i]); }	return cutContent.join(' ') + '...'; } function countVisibleCharacters(element) { var text = element.textContent || element.innerText || ""; console.log('d:' + text); } function collapseThread(nThread, thread) { var content = nThread.children('.content'); var nMessage = content.children('.message'); var nMessageText = nMessage.children('.text'); var nReplies = content.children('.replies'); if (nThread.hasClass('collapsed')		|| (nMessageText.text.length < 100 && nReplies.children.length < 2 && content.height <= 120)) { return; }	var collapsedText = cutThreadContent(thread, 100); nMessageText.html(collapsedText); var targetHeight = nMessage.outerHeight + 5; nMessageText.html(thread.htmlContent); nThread.addClass('collapsed'); var btnReadMore = $(' ', {		'class': 'btn-expand'	}).html('&#8595; ' + getMessageText('btn-expand-thread') + ' &#8595;'); btnReadMore.click(function {		expandThread(nThread, thread);		return false;	});// expandable via button click nThread.append(btnReadMore); nThread.on('click', function {		expandThread(nThread, thread);		return true;	});// expandable via thread click (may target underlying elements) nThread.focusin(function {		expandThread(nThread, thread);		return true;	});// expandable via focusing any child element (may target underlying elements) var btnHeight = btnReadMore.css('height'); btnReadMore.css('height', '0'); btnReadMore.stop.animate({		'height': btnHeight	}, function {		btnReadMore.css('height', null);	}); content.stop.animate({		'height': targetHeight + 'px'	}, 'slow', function {		nMessageText.html(collapsedText);	}); } function expandThread(nThread, thread) { nThread.removeClass('collapsed'); var content = nThread.children('.content'); var nMessageText = content.children('.message').children('.text'); nMessageText.html(thread.htmlContent); countVisibleCharacters(nMessageText[0]); var crrHeight = content.css('height'); var targetHeight = content.css('height', 'auto').height; nThread.children('.btn-expand').stop.animate({		'height': '0'	}, 'slow', function {		$(this).remove;	}); nThread.off('click'); content.css('height', crrHeight); content.stop.animate({		'height': targetHeight	}, 'slow', function {		content.css('height', 'auto');	}); } function createAskQuestionUI(identifier, talkPageTitle) { var ui = $(' ', {		'class': 'ask-question'	}); // question title var lbTitle = $(' ', {		'for': 'thread-title-' + identifier,		'text': 'Question title:'	}); ui.append(lbTitle); var iTitle = $(' ', {		'class': 'title border-box',		'id': 'thread-title-' + identifier,		'type': 'text'	}); ui.append(iTitle); // question content var lbContent = $(' ', {		'for': 'thread-content-' + identifier,		'text': 'Your question:'	}); ui.append(lbContent); var minRows = 3; var teaContent = $(' ', {		'class': 'border-box',		'id': 'thread-content-' + identifier,		'rows': minRows	}); ui.append(teaContent); $(document).on('input.textarea', '#' + teaContent.attr('id'), function {		var rows = this.value.split('\n').length;		this.rows = rows < minRows ? minRows : rows;	}); // ask button var btnAsk = $(' ', {		'class': 'btn-ask',		'type': 'button',		'value': 'Ask'	}); ui.append(btnAsk); btnAsk.click(function {		if (btnAsk.prop('disabled')) {			return;		}		btnAsk.prop('disabled', true);		// add section to talk page		var title = iTitle.val;		if (title.length > 0) {			var content = stripPost(teaContent.val);			content += ' --~';			asking = true;			_index.retrieveItem(function(item) { item.discussion = discussion; addThread(item, getTalkPage(talkPageTitle), title, content, function {					reloadPage('#discussion');				}); });		} else {			notifyUser('q-no-title', null, { 'class': 'error' });		}	});	var blackIn = function { ui.css('opacity', 1); };	var greyOut = function { if (ui.children(':focus').length > 0) {// dont grey out if having focus return; }		ui.css('opacity', 0.6); };	ui.mouseleave(greyOut); ui.focusout(greyOut); greyOut; ui.mouseenter(blackIn); ui.focusin(blackIn); return ui; } function insertAskQuestionUI(identifier, talkPageTitle, ownUi) { var nSection = $('#' + identifier); var nContent = nSection.children('.content'); var btn = nSection.children('.header').find('.ask-question-btn'); if (ownUi) {// create UI		var ui = createAskQuestionUI(identifier, talkPageTitle); nContent.append(ui); btn.click(function {			// scroll to UI and focus title box			scrollIntoView(ui, 'bottom');			ui.children('.title').focus;		}); } else {// scroll to discussion ui		btn.click(function {			var nDiscussionUi = $('#discussion').find('.ask-question');			scrollIntoView(nDiscussionUi, 'bottom');			nDiscussionUi.children('.title').focus;		}); } } function createReplyUI(postData) { var ui = $(' ', {		'class': 'ui-reply'	}); // reply content var lbContent = $(' ', {		'for': 'reply-content-' + postData.post.id,		'text': 'Your reply:'	}); ui.append(lbContent); var minRows = 3; var teaContent = $(' ', {		'class': 'border-box',		'id': 'reply-content-' + postData.post.id,		'rows': minRows	}); ui.append(teaContent); $(document).on('input.textarea', '#' + teaContent.attr('id'), function {		var rows = this.value.split('\n').length;		this.rows = rows < minRows ? minRows : rows;	}); // reply button var btnReply = $(' ', {		'class': 'btn-reply',		'type': 'button',		'value': 'Send reply'	}); btnReply.click(function {		if (btnReply.prop('disabled')) {			return;		}		btnReply.prop('disabled', true);		var content = stripPost(teaContent.val);		if (content.length > 0) {			var post = postData.post;			var thread = postData.thread;			var reply = Post(0, post.level + 1, [ content ], [], createPseudoSignature('--~'));			post.replies.push(reply);			// save thread to its talk page			_index.retrieveItem(function(item) { item.discussion = discussion; saveThread(item, thread, function {					reloadPage('#discussion');				}); });		}	});	ui.append(btnReply); return ui; } function getTalkPageTitle(pageTitle) { var iNamespace = pageTitle.indexOf(':'); var iSlash = pageTitle.indexOf('/'); if (iNamespace == -1 || iNamespace > iSlash) { return 'Talk:' + pageTitle; }	var namespace = pageTitle.substring(0, iNamespace); return namespace + ' talk' + pageTitle.substring(iNamespace); } /** * Calculates the post level of a talk page line. * @param {String} talk page line * @return {int} post level of the line passed (0: no post, n: reply level n) */ function getPostLevel(line) { var level = line.match('^:*'); if (level.length > 0 && level[0]) {// post reply return level[0].length; }	return 0; } function stripPost(content) { var lines = splitLines(content); var line, postLevel, level; for (var i = 0; i < lines.length; ++i) { line = lines[i]; // strip leading ':' postLevel = getPostLevel(line); if (postLevel > 0) { line = line.substring(postLevel); }		// remove thread starts level = getLevel(line); if (level > 0) { line = line.substring(level, line.length - level); }		lines[i] = line; }	var post = lines.join('\n'); // remove signature added manually var signatureLength = getSignatureLength(post); if (signatureLength > 0) { post = post.substring(0, post.length - signatureLength); }	// remove leading/trailing whitespace return $.trim(post); } function splitThreads(lines) { var threads = []; var thread = null; var level = 0, line, iLost = -1, section = 0; for (var i = 0; i < lines.length; ++i) { line = lines[i]; level = getLevel(line); if (level > 0) { if (level == 2) {// new thread if (thread !== null) {// store current threads.push(thread); }				thread = Thread(line.substring(level, line.length - level), ++section); console.log('thread "' + line + '" is section ' + section); } else { // malformed: header at invalid level section += 1; thread.content.push(line); }		} else {// thread content if (thread !== null) { thread.content.push(line); } else {// content not belonging to any threads iLost = i;			} }	}	if (thread !== null) {// store thread finished by EOF threads.push(thread); }	return { 'threads': threads, 'lost': iLost }; } function getMonthFromString(mon){ var d = Date.parse(mon + "1, 2014"); if (!isNaN(d)){ return new Date(d).getMonth + 1; }  return -1; } function parseTimestamp(value) { var time = value.substring(0, 5); var timeParts = time.split(':'); var day = value.substring(7); var dayParts = day.split(' '); var date = new Date; var month = getMonthFromString(dayParts[1]); if (month != -1) { date.setUTCDate(dayParts[0]); date.setUTCMonth(month); date.setUTCFullYear(dayParts[2]); date.setUTCHours(timeParts[0]); date.setUTCMinutes(timeParts[1]); return date; }	return null; } function dateToString(date) { return (date.getYear + 1900) + "/" + date.getMonth + "/" + date.getDate + " " + date.getHours + ":" + date.getMinutes; } function loadSignature(line) { var pos = line.indexOf('--[[');	if (pos != -1) {		var content = null;		if (pos > 0) {			content = line.substring(0, pos);		}		var value = line.substring(pos);		// parse timestamp		if (value.substring(4, 9) == 'User:') {// user signature			var username = value.match(/User:.+?\|/);			if (username !== null) {				username = username[0];				username = username.substring(5, username.length - 1);				var sTimestamp = value.match(/\).+?$/);				if (sTimestamp !== null) {					sTimestamp = sTimestamp[0];					sTimestamp = sTimestamp.substring(2);					var timestamp = parseTimestamp(sTimestamp);					return {						'content': content,						'value': {							'author': username,							'timestamp': timestamp,							'tostring': function {								return "posted at " + dateToString(this.timestamp) + " by " + this.author;							},							'towikitext': function {								return value;							}						}					};				}			}		}		// signature invalid		if (value.substring(0, 5) == 'User:' || value.substring(0, 21) == 'Special:Contributions') {			//TODO remove when condition not needed anymore		}		// unknown signature	}	return null; } function createPseudoSignature(value) {	return {		'towikitext': function {			return value;		}	}; } function Post(id, level, content, replies, signature) {	return {		'id': id,		'content': content.join('\n'),		'htmlContent': this.content,		'level': level,		'replies': replies,		'signature': signature,		'getNumPosts': function {			var num = 1;			for (var i = 0; i < this.replies.length; ++i) {				num += this.replies[i].getNumPosts;			}			return num;		},		'isValid': function {			// post malformed: signature missing			return (this.signature !== null);		},		'tostring': function {			var value = [];			var firstLine = strrep(':', this.level) + this.content;			if (this.signature !== null) {				firstLine += " " + this.signature.towikitext;			}			value.push(firstLine);			for (var i = 0; i < this.replies.length; ++i) {				value.push(this.replies[i].tostring);			}			return value.join('\n');		}	}; } function Thread(title, section) {	var thread = Post(0, 0, [], [], null);	thread.content = [];	thread.lost = [];	thread.published = 0;	thread.section = section;	thread.title = $.trim(title);	thread.tostring = function {		var value = [];		value.push('==' + this.title + '==');		value.push(this.content);		if (this.signature !== null) {			value.push(this.signature.towikitext);		}		for (var i = 0; i < this.replies.length; ++i) {			value.push(this.replies[i].tostring);		}		return value.join('\n');	};	return thread; } function TalkPage(title) {	return {		'threads': [],		'title': title	}; } function loadPost(lines, iStart, lastId) {	// get level	var firstLine = lines[iStart];	var level = getPostLevel(firstLine);	firstLine = firstLine.substring(level);	var content = [];	var signature = loadSignature(firstLine);	if (signature === null) {		content.push(firstLine);	}	var id = ++lastId;	var nextLevel;	var line;	var i = iStart + 1;	var replies = [];	while (i < lines.length) {		line = lines[i];		nextLevel = getPostLevel(line);		if (nextLevel > 0 || line.length > 0) {			if (nextLevel < level) {// new post at higher level				break;			} else if (nextLevel > level) {// reply				var reply = loadPost(lines, i, lastId);				if (reply.post.isValid) {					replies.push(reply.post);				}				i = reply.iEnd;				lastId = reply.lastId;			}		}		if (nextLevel == level) {// post at same level: signature determines			if (signature !== null) {// new post at same level				break;			} else {// still current post: add and search for signature				line = line.substring(level);				signature = loadSignature(line);				if (signature === null) {// signature not found: add content					content.push(line);				}				i += 1;			}		} else if (nextLevel === 0 && line.length === 0) {// ignore blank line			i += 1;		}	}	if (signature !== null) {// signature found: add signature line content if any		if (signature.content !== null) {			content.push(signature.content);		}		signature = signature.value;	}	return {		'post': Post(id, level, content, replies, signature),		'iEnd': i,		'lastId': lastId	}; } function loadThread(thread, lastId) {	var lines = thread.content;	var content = [];	var i = 0;	var signature = null;	var id = ++lastId;	while (i < lines.length) {		var level = getPostLevel(lines[i]);		if (level > 0) {// post			var post = loadPost(lines, i, lastId);			i = post.iEnd;			lastId = post.lastId;			if (post.post.isValid) {				thread.replies.push(post.post);			} else {//copy invalid posts to thread's lost section				thread.lost.push(post.post.content);				console.log('invalid post: ' + post.post.content);			}		} else {// thread content			if (signature === null) {// no signature found yet				signature = loadSignature(lines[i]);				if (signature === null) {// no signature found: add content					content.push(lines[i]);				} else if (signature.content !== null) {// signature found: add signature line content if any					content.push(signature.content);				}			} else {// signature already found: malformed?				content.push(lines[i]);			}			i += 1;		}	}	// remove trailing newlines	if (content[0] == ) {		content.shift;	}	if (content[content.length - 1] == ) {		content.pop;	}	thread.id = id;	thread.content = content.join('\n');	if (signature !== null) {		thread.signature = signature.value;		thread.published = thread.signature.timestamp.getTime;	}	return {		'lastId': lastId,		'value': thread	}; } function loadThreads(lines, lastId) {	var rawThreads = splitThreads(lines);	var threads = [];	// load threads with their replies	for (var i = 0; i < rawThreads.threads.length; ++i) {		var thread = loadThread(rawThreads.threads[i], lastId);		lastId = thread.lastId;		threads.push(thread.value);	}	return {		'lastId': lastId,		'lost': rawThreads.lost,		'threads': threads	}; }

var discussion; function findPostInPost(post, postId) { if (post.id == postId) { return { 'post': post };	}	for (var i = 0; i < post.replies.length; ++i) { var reply = post.replies[i]; var result = findPostInPost(reply, postId); if (result !== null) { return result; }	}	return null; } function findPostInThread(postId) { for (var i = 0; i < discussion.threads.length; ++i) { var result = findPostInPost(discussion.threads[i], postId); if (result !== null) { result.thread = discussion.threads[i]; return result; }	}	return null; } function mergeThreads(talkPageTitles, iCrrPage, discussion, callback) { if (iCrrPage < talkPageTitles.length) { var talkPage = TalkPage(talkPageTitles[iCrrPage]); doPageContentRequest(talkPage.title, null, function(pageContent) {			var lines = splitLines(pageContent);			var parsed = loadThreads(lines, discussion.lastId);			var pageThreads = parsed.threads;			for (var t = 0; t < pageThreads.length; ++t) {				var pageThread = pageThreads[t];				pageThread.talkPage = talkPage;				talkPage.threads.push(pageThread);				discussion.talkPages.push(talkPage);				discussion.threads.push(pageThread);			}			discussion.lastId = parsed.lastId;			mergeThreads(talkPageTitles, iCrrPage + 1, discussion, callback);		}, function {			mergeThreads(talkPageTitles, iCrrPage + 1, discussion, callback);		}); } else { callback; } } function getTalkPage(talkPageTitle) { for (var i = 0; i < discussion.talkPages.length; ++i) { if (discussion.talkPages[i].title === talkPageTitle) { return discussion.talkPages[i]; }	}	// return empty talk page object return TalkPage(talkPageTitle); } function renderThreads(talkPageTitles) { discussion = { 'lastId': 0, 'lost': [], 'talkPages' : [], 'threads': [], 'getNumPosts': function { var numPosts = 0; for (var i = 0; i < this.threads.length; ++i) { numPosts += this.threads[i].getNumPosts; }			return numPosts; },		'getNumOpenThreads': function { var numOpenThreads = 0; for (var i = 0; i < this.threads.length; ++i) { if (this.threads[i].getNumPosts === 1) { numOpenThreads += 1; }			}			return numOpenThreads; },		'tostring': function { var value = []; for (var i = 0; i < this.threads.length; ++i) { value.push(this.threads[i].tostring); }			for (var j = 0; j < this.lost.length; ++j) { value.push(this.lost[j]); }			return value.join('\n'); }	};	mergeThreads(talkPageTitles, 0, discussion, function {		// insert ask question UI		insertAskQuestionUI('learningGoals', talkPageTitles[0], false);		insertAskQuestionUI('video', talkPageTitles[0], true);		insertAskQuestionUI('script', talkPageTitles[1], true);		insertAskQuestionUI('quiz', talkPageTitles[2], true);		insertAskQuestionUI('furtherReading', talkPageTitles[0], false);		insertAskQuestionUI('discussion', talkPageTitles[0], true);		var threads = discussion.threads;		console.log(threads.length + ' threads aggregated from ' + talkPageTitles.length + ' pages');		threads.sort(function(t1, t2) {// sort threads by timestamp of publication (DESC) if (t1.published > t2.published) { return -1; } else if (t1.published < t2.published) { return 1; } else { return 0; }		});		var injectThreads = function {			var divDiscussion = $('#discussion > .content');			for (var j = 0; j < threads.length; ++j) {				var nThread = renderThread(threads[j]);				divDiscussion.append(nThread);				if (threads.length > 2) {// collapse threads if too many					collapseThread(nThread, threads[j]);				}				if (!threads[j].isValid) {// thread invalid: unsigned					// TODO show warning(s)				}				if (threads[j].lost.length > 0) {// contains invalid posts: unsigned					// TODO show warning(s)				}			}			//TODO handle lost lines		};		// parse thread content and inject threads		var getContentNodes = function(post) {			var nodes = [];			nodes.push($(' ', {'id':post.id}).html(post.content));			for (var i = 0; i < post.replies.length; ++i) {				nodes = nodes.concat(getContentNodes(post.replies[i]));			}			return nodes;		};		var nContent = $(' ');		for (var i = 0; i < threads.length; ++i) { var nodes = getContentNodes(threads[i]); for (var n = 0; n < nodes.length; ++n) { nContent.append(nodes[n]); }		}		parseThreads(nContent.html, function(parsedContent) {			var nThreads = $.parseHTML(parsedContent);			var adoptContentNodes = function(post, nodes, iCrr) {				post.htmlContent = nodes[iCrr].html;				iCrr += 1;				for (var i = 0; i < post.replies.length; ++i) {					iCrr = adoptContentNodes(post.replies[i], nodes, iCrr);				}				return iCrr;			};			var nodes = [];			$.each(nThreads, function(i, el) { var nThread = $(el); if (typeof nThread.attr('id') !== 'undefined') { nodes.push($(el)); }			});			var iCrr = 0;			for (var i = 0; i < threads.length; ++i) {				iCrr = adoptContentNodes(threads[i], nodes, iCrr);			}			injectThreads;		}, function {// inject unparsed threads if parsing failed			injectThreads;		}); }); } function renderPostOverlay {	var nOverlay = $(' ', { 'class': 'overlay' });	var nBackground = $(' ', { 'class': 'background' });	var nContent = $(' ', { 'class': 'content' });	var btnReply = $(' ', { 'class': 'btn-reply', 'text': 'reply' });	var ui = null;	btnReply.click(function {// inject reply UI to reply to post var visible = false; if (ui === null) { var nPost = nOverlay.parent.parent.parent; var postId = nPost.attr('id').substring(5); var postData = findPostInThread(postId); ui = createReplyUI(postData); nPost.children('.content').children('.replies').append(ui); } else { visible = ui.css('display') != 'none'; ui.toggle('fast'); }		if (!visible) { // hide all other reply UI			$('.ui-reply').not(ui).toggle(false); // scroll to UI			scrollIntoView(ui, 'bottom'); ui.children('textarea').focus; }	});	nContent.append(btnReply);	nOverlay.append(nBackground);	nOverlay.append(nContent);	return nOverlay; } function renderPost(post) {	// main node	var nPost = $('', { 'class': 'post', 'id': 'post-' + post.id	});	// content	var nContent = $(' ', { 'class': 'content' });	// post message	var nMessage = $(' ', { 'class': 'message' });	// message text	var nMessageText = $(' ', { 'class': 'text' }).html(post.htmlContent);	nMessage.append(nMessageText);	// meta information	var sSignature = '';	if (post.signature !== null) {		sSignature = post.signature.tostring;	}	var nMeta = $(' ', { 'class': 'meta', 'text': sSignature });	nMeta.toggle(false);	nMessage.append(nMeta);	// reply overlay	var nOverlay = renderPostOverlay;	nMessage.prepend(nOverlay);	nMessage.mouseenter(function { nOverlay.stop(true).fadeIn; nMeta.stop(true).fadeIn; });	nMessage.mouseleave(function { nOverlay.stop(true).fadeOut; nMeta.stop(true).fadeOut; });	nContent.append(nMessage);	// replies	nContent.append(renderReplies(post));	nPost.append(nContent);	return nPost; } function renderThreadHeader(thread) {	// thread title	var nHeader = $(' ', { 'class': 'header title', 'text': thread.title });	// number of replies	var nNumReplies = $(' ', { 'class': 'num-replies', 'text': mw.message('discussion-header-num-replies', thread.getNumPosts - 1).text });	nHeader.append(nNumReplies);	return nHeader; } function renderThread(thread) {	var nThread = renderPost(thread);	nThread.addClass('thread');	// add thread header containing title and statistics	var nHeader = renderThreadHeader(thread);	nThread.prepend(nHeader);	// make thread collapsible	nThread.children('.content').addClass('collapsible');	nHeader.click(function { if (nThread.hasClass('collapsed')) { expandThread(nThread, thread); } else { collapseThread(nThread, thread); }		return false; });	// show warning if unsigned	if (thread.signature === null) {		nThread.find('.meta').addClass('warning').text('No one signed this thread.');	}	return nThread; } function renderReply(reply) {	var nReply = renderPost(reply);	nReply.addClass('reply');	return nReply; } function renderReplies(post) {	var nReplies = $('', { 'class': 'replies' });	for (var i = 0; i < post.replies.length; ++i) {		nReplies.append(renderReply(post.replies[i]));	}	return nReplies; } //