User:Samuelsorin/mooc.js

//

/*############################
 * 1) MEDIAWIKI API FUNCTIONS ###

/** * Request a wiki page's plain wikitext content. * Uses 'action=raw' to get the page content. * @param {String} title of the wiki page * @param {int} section within the wiki page or 0 to retrieve whole page * @param {function} callback when the page content was retrieved successfully (page content will be passed as parameter) * @param {function} callback when the page content could not be retrieved (jqXHR object will be passed as parameter) */ function doPageContentRequest(pageTitle, section, sucCallback, errorCallback) { var url = "https://en.wikiversity.org/w/index.php?action=raw&title=" + pageTitle; if (section !== null) { url += "&section=" + section; }	$.ajax({		url: url,		cache: false	}).fail(function(jqXHR) {		console.log('moocEditor.doPageContentRequest: page content request failed for page "' + pageTitle + ' section ' + section + '" (server status ' + jqXHR.status + ')');		if (typeof errorCallback !== 'undefined') {			errorCallback(jqXHR);		}	}).done(sucCallback); }

/** * Retrieves edit tokens for any number of wiki pages. * @param {Array} page titles of the wiki pages * @param {function} callback when the edit tokens were retrieved successfully */ function doEditTokenRequest(pageTitles, sucCallback) { var sPageTitles = pageTitles.join('|'); // get edit tokens var tokenData = { 'intoken': 'edit|watch' };	$.ajax({		type: "POST",		url: "https://en.wikiversity.org/w/api.php?action=query&prop=info&format=json&titles=" + sPageTitles,		data: tokenData	}).fail(function(jqXHR) {		console.log('moocEditor.doEditTokenRequest: edit token request failed for pages "' + sPageTitles + '" (server status ' + jqXHR.status + ')');	}).done(function(response) {		var editTokens = parseEditTokens(response);		if (editTokens.hasTokens) {			sucCallback(editTokens);		} else {			console.log('moocEditor.doEditTokenRequest: failed to get edit tokens for "' + sPageTitles + '" (server response: ' + JSON.stringify(response) + ')');		}	}); }

function doEditRequest(pageTitle, section, content, summary, sucCallback) { console.log('edit request: ' + pageTitle + ' section ' + section); doEditTokenRequest([ pageTitle ], function(editTokens) {		var editToken = editTokens.get(pageTitle);		var editData = {			'title': pageTitle,			'text': content,			'summary': summary,			'watchlist': 'watch',			'token': editToken		};		if (section !== null) {			editData.section = section;		}		$.ajax({ type: "POST", url: "https://en.wikiversity.org/w/api.php?action=edit&format=json", data: editData }).fail(function(jqXHR) { console.log('moocEditor.doEditRequest: edit request failed for page "' + pageTitle + '" (server status: ' + jqXHR.status + ')'); }).done(function(response) { console.log('moocEditor.doEditRequest: server response: ' + JSON.stringify(response)); //TODO handle errors sucCallback; });	}); }

function addSectionToPage(pageTitle, sectionTitle, content, summary, sucCallback) { console.log('add section request: ' + pageTitle + ' section title ' + sectionTitle); doEditTokenRequest([ pageTitle ], function(editTokens) {		var editToken = editTokens.get(pageTitle);		var editData = {			'title': pageTitle,			'section': 'new',			'sectiontitle': sectionTitle,			'text': content,			'summary': summary,			'watchlist': 'watch',			'token': editToken		};		$.ajax({ type: "POST", url: "https://en.wikiversity.org/w/api.php?action=edit&format=json", data: editData }).fail(function(jqXHR) { console.log('moocEditor.addSectionToPage: add section request failed for page "' + pageTitle + '" (server status: ' + jqXHR.status + ')'); }).done(function(response) { console.log('moocEditor.addSectionToPage: server response: ' + JSON.stringify(response)); //TODO handle errors sucCallback; });	}); }

/** * Parses a server response containing one or multiple edit tokens. * @param {JSON} tokenResponse * @return {Object} edit tokens object - you can retrieve the edit token by passing the page title to the object's 'get'-function */ function parseEditTokens(tokenResponse) { var hasTokens = false; var editTokens = { 'tokens': [], 'add': function(title, edittoken) { var lTitle = title.toLowerCase; console.log('edittoken for "' + title + '": ' + edittoken); this.tokens[lTitle] = edittoken; hasTokens = true; },		'get': function(title) { return this.tokens[title.toLowerCase]; },		'hasTokens': function { return hasTokens; }	};	var path = ['query', 'pages']; var crr = tokenResponse; for (var i = 0; i < path.length; ++i) { if (crr && crr.hasOwnProperty(path[i])) { crr = crr[path[i]]; } else { console.log('moocEditor.parseEditTokens: missing object "' + path[i] + '"'); crr = null; break; }	}	if (crr) { var pages = crr; for (var pageId in pages) { // page exists if (pages.hasOwnProperty(pageId)) { var page = pages[pageId]; editTokens.add(page.title, page.edittoken); }		}	}	return editTokens; }

function getIndex(title, section, sucCallback) { doPageContentRequest(title, section, sucCallback); }

function getScript(item, sucCallback, errorCallback) { doPageContentRequest(item.fullPath + '/script', 0, sucCallback, errorCallback); }

function getQuiz(item, sucCallback, errorCallback) { doPageContentRequest(item.fullPath + '/quiz', 0, sucCallback, errorCallback); }

function updateScript(item, scriptText, summary, sucCallback) { var editSummary = summary; if (editSummary === '') { editSummary = 'script update for MOOC ' + item.header.type + ' ' + item.fullPath; }	doEditRequest(item.fullPath + '/script', 0, scriptText, editSummary, sucCallback); }

function updateQuiz(item, quizText, summary, sucCallback) { var editSummary = summary; if (editSummary === '') { editSummary = 'quiz update for MOOC ' + item.header.type + ' ' + item.fullPath; }	doEditRequest(item.fullPath + '/quiz', 0, quizText, editSummary, sucCallback); }

function updateIndex(item, summaryAppendix, sucCallback) { var summary = item.header.type + ' ' + item.header.path + ': ' + summaryAppendix; if (item.header.path === null) {// changing root item summary = item.header.type + ':' + summaryAppendix; }	doEditRequest(item.index.title, item.indexSection, item.tostring, summary, sucCallback); }

function createPage(pageTitle, content, summary, sucCallback) { doEditRequest(pageTitle, 0, content, summary, sucCallback); }

function addChild(type, name, parent, summary, sucCallback) { // add item to parent var parentHeader = parent.header; var header = Header(parentHeader.level + 1, type, name, null); parent.childLines.push(header.tostring); console.log('new child for ' + parentHeader.type + ' ' + parentHeader.title + ': ' + header.tostring); // update MOOC index at parent position var itemIdentifier = type + ' ' + parentHeader.path + '/' + name; if (parentHeader.path === null) {// parent is root itemIdentifier = type + ' ' + name; }	if (summary === '') { summary = itemIdentifier + ' added'; }	updateIndex(parent, summary, function {		// create item page		doEditRequest(parent.fullPath + '/' + name, 0, parent.getInvokeCode, 'invoke page for MOOC ' + itemIdentifier + ' created', sucCallback);	}); }

function addLesson(name, item, summary, sucCallback) { addChild('lesson', name, item, summary, sucCallback); }

function addUnit(name, item, summary, sucCallback) { addChild('unit', name, item, summary, sucCallback); }

function createMooc(title, summary, sucCallback) { createPage('Category:' + title, '\n ', summary, function {// create category with overview		createPage(title, '', summary, function {// create MOOC overview page createPage(title + '/MoocIndex', '--MoocIndex for MOOC @ ' + title, summary, sucCallback);// create MOOC index });	}); }

function addThread(item, talkPage, title, content, sucCallback) { item.setParameter(PARAMETER_KEY.NUM_THREADS, (item.discussion.threads.length + 1).toString); item.setParameter(PARAMETER_KEY.NUM_THREADS_OPEN, (item.discussion.getNumOpenThreads + 1).toString); addSectionToPage(talkPage.title, title, content, 'q:' + title, function {		doEditRequest(item.index.title, item.indexSection, item.tostring, 'new thread in item discussion', sucCallback);	}); }

function saveThread(item, thread, sucCallback) { item.setParameter(PARAMETER_KEY.NUM_THREADS, item.discussion.threads.length.toString); item.setParameter(PARAMETER_KEY.NUM_THREADS_OPEN, item.discussion.getNumOpenThreads.toString); doEditRequest(thread.talkPage.title, thread.section, thread.tostring, 'replied to "' + thread.title + '"', function {		doEditRequest(item.index.title, item.indexSection, item.tostring, 'new reply in item discussion', sucCallback);	}); }

function parseThreads(unparsedContent, sucCallback, errCallback) { console.log('parsing: ' + unparsedContent); var api = new mw.Api; var promise = api.post({		'action': 'parse',		'contentmodel': 'wikitext',		'disablepp': true,		'text': unparsedContent	}); promise.done(function(response) {		console.log('moocEditor.parseThreads: server response: ' + JSON.stringify(response));		var wikitext = response.parse.text['*'];		sucCallback(wikitext);	}); if (typeof errCallback !== 'undefined') { promise.fail(errCallback); } }

/** * Retrieves the URLs of any number of video files. * @param {Array} array of titles of the files to retrieve an URL for (WARNING: should not include '_' to access the URL mapping in success callback correctly) * @param {function} callback when the URLs were retrieved successfully (An array mapping (page title) -> (url) will be passed. The page titles will not contain '_' but spaces.) */ function getVideoUrls(fileTitles, sucCallback) { //WTF imageinfo does also work on video files var sFileTitles = fileTitles.join('|'); var api = new mw.Api; api.get({		action: 'query',		prop: 'videoinfo',		titles: sFileTitles,		viprop: 'url'	}).done(function(data) {		console.log(JSON.stringify(data));		var path = ['query', 'pages'];		var crr = data;		for (var i = 0; i < path.length; ++i) {			if (crr && crr.hasOwnProperty(path[i])) {				crr = crr[path[i]];			} else {				console.log('moocEditor.getVideoUrl: missing object "' + path[i] + '"');				crr = null;				break;			}		}		var fileUrls = [];		if (crr) {			var pages = crr;			for (var pageId in pages) {				// page exists	  			if (pages.hasOwnProperty(pageId)) {	       			var page = pages[pageId];	      			fileUrls[page.title] = page.videoinfo[0].url;	      			console.log(page.title + ' @ ' + page.videoinfo[0].url);				}			}			sucCallback(fileUrls);		}	}); }

function hashChanged(hash) { if (hash.length > 0) { var section = $(hash); if (section.hasClass('collapsed')) { expand(section); }	} }

// ############################### // ########## UTILITIES ########## // ###############################

/** * Repeats a string value a given number of times. * @param {String} value to repeat * @param {int} number of times to repeat the value * @return {String} value repeated the given number of times. */ function strrep(value, numRepeat) { return new Array(numRepeat + 1).join(value); }

/** * Splits a text into its single lines. * @param {String} multiline text * @return {Array} single text lines */ function splitLines(text) { return text.split(/\r?\n/); }

/** * Calculates the header level of a wikitext line. * @param {String} wikitext line * @return {int} header level of the line passed, 0 if the line is no header */ function getLevel(line) { var sLevelStart = line.match('^=*'); if (sLevelStart.length > 0 && sLevelStart[0]) { var sLevelEnd = line.match('=*$'); if (sLevelEnd.length > 0 && sLevelEnd[0]) { return Math.min(sLevelStart[0].length, sLevelEnd[0].length); }	}	return 0; }

// ############################### // ######### UI UTILITIES ######## // ###############################

/** * Displays a notification message to the user. * Uses mw.Message to generate messages. * @param {String} message key * @param {Array} message parameters */ function notifyUser(msgKey, msgParams) { var msgValue = mw.msg(msgKey, msgParams); alert(msgValue);//TODO use notification API that seems to be disabled }

//TODO rename to collapse/expandSection /** * Collapses a section to a fix height making it expandable. * Only applies to non-collapsed sections that are larger than the collapsed UI would be. * @param {jQuery} section node to be collapsed */ function collapse(section) {// expandable via section header click var content = section.children('.content'); if (section.hasClass('collapsed') || content.height <= '80') { return; }	section.addClass('collapsed'); //TODO display layer labeled 'EXPAND' var btnReadMore = $(' ', {		'class': 'btn-expand'	}).html('&#8595; ' + getMessageText('btn-expand-section') + ' &#8595;'); btnReadMore.click(function {		expand(section);		return false;	});// expandable via button click section.append(btnReadMore); section.on('click', function {		expand(section);		return true;	});// expandable via section click (may target underlying elements) section.focusin(function {		expand(section);		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': '40px'	}, 'slow'); } /** * Expands a section to its full height making it collapsible again. * @param {jQuery} section node to be expanded */ function expand(section) { section.removeClass('collapsed'); var content = section.children('.content'); var crrHeight = content.css('height'); var targetHeight = content.css('height', 'auto').height; section.children('.btn-expand').stop.animate({		'height': '0'	}, 'slow', function {		$(this).remove;	}); section.off('click'); content.css('height', crrHeight); content.stop.animate({		'height': targetHeight	}, 'slow', function {		content.css('height', 'auto');	}); }

//TODO remove if JS chained after CSS function fixView(element, duration) { if (duration > 0) { console.log('fixing view at section ' + element.attr('id') + ' at pos ' + element.offset.top); element.css('background-color', '#FFF'); var width = element.width; element.css('position', 'fixed'); element.css('width', width); element.css('top', '0'); var zIndex = element.css('z-index'); element.css('z-index', 100); setTimeout(function {			element.css('position', 'relative');			element.css('top', null);			element.css('z-index', zIndex);			console.log('section now at ' + element.offset.top);			window.scroll(0, element.offset.top);// jump to section but fire jQuery scroll event			$(window).scroll;		}, duration); } } /** * Scrolls an element into the user's view. * The animation can handle movement of the element. * @param {jQuery object} element to scroll into view * @param {String} 'top'/'bottom' if the element should be aligned at the upper/lower screen border. Defaults to 'top'. * @param {int} duration of the scroll animation. Defaults to 1000ms. */ function scrollIntoView(element, align, duration) { if (typeof duration === 'undefined') { duration = 1000; }	var Alignment = { 'TOP': 1, 'BOTTOM': 2 };	if (align === 'bottom') { align = Alignment.BOTTOM; } else { align = Alignment.TOP; }	var targetTop; var adjustAnimation = function(now, fx) { var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); var y = $(window).scrollTop; var crrTop = element.offset.top; if (align === Alignment.BOTTOM) { crrTop += element.height - h;			//TODO currently just if scrolled FAR enough if (crrTop + h - y < h) {// element already in view crrTop = y;				fx.end = y;				return true; }		} else { //TODO check if already in view }		if (nItemNav !== 'undefined' && nItemNav.hasClass('fixed')) { crrTop -= nItemNav.height;//TODO remove if workaround found }		if (targetTop != crrTop) { targetTop = crrTop; fx.end = targetTop;//TODO is there a way to do this smoothly? }		return false; };	if (!adjustAnimation(0, {})) { $('html, body').stop.animate({	       scrollTop: targetTop	    }, {	    	'duration': duration,	    	'step': adjustAnimation	    }); } }

/** * Reloads the current page. * @param {String} (optional) page anchor to be set */ function reloadPage(anchor) { if (typeof anchor === 'undefined') { document.location.search = document.location.search + '&action=purge'; } else { window.location.href = document.URL.replace(/#.*$/, '') + '?action=purge' + anchor; } }

// ############################### // ######## INDEX LOADING ######## // ###############################

/** * Loads the header of a MOOC item from its index header line. * @param {String} item's header line from MOOC index * @param {String} MOOC base * @param {String} item path (absolute path including MOOC base) * @return {Object} MOOC item header loaded from header line. Returns null if header line malformed. */ function loadHeader(line, base, fullPath) { var level = getLevel(line); if (level > 0) { var iSeparator = line.indexOf('|'); if (iSeparator > -1) { var type = line.substring(level, iSeparator); var title = line.substring(iSeparator + 1, line.length - level); var path = fullPath.substring(base.length + 1);// relative path console.log(type + ' "' + title + '" @ level ' + level + ' @ ' + path); return Header(level, type, title, path); }	}	console.log('malformed header: ' + line); return null; } /** * Loads a parameter of an item. * @param {Array} MOOC index lines * @param {int} start index of the parameter within index * @return {Object} item parameter extracted (key, value) and index of last line related to parameter (iEnd) */ function loadProperty(indexLines, iLine) { var line = indexLines[iLine]; var iSeparator = line.indexOf('='); if (iSeparator != -1) { var paramLines = []; var key = line.substring(1, iSeparator); // read parameter value var i = iLine; var value = line.substring(iSeparator + 1); do { if (i > iLine) {// multiline value if (paramLines.length === 0 && value.length > 0) {// push first line value if any paramLines.push(value); }				paramLines.push(line); }			i += 1; line = indexLines[i]; } while(i < indexLines.length && line.substring(0, 1) !== '*' && getLevel(line) === 0); i -= 1; if (paramLines.length > 0) { value = paramLines.join('\n'); }		return { 'iEnd': i,			'key': key, 'value': value };	} else { return null; } }

/** * Creates a header instance holding identification data of the MOOC item. * @param {int} item level * @param {String} item type * @param {String} item title * @param {String} item path (relative to MOOC base) * @return {Object} MOOC item header to identify the item and write to MOOC index */ function Header(level, type, title, path) { return { 'level': level, 'path': path, 'title': title, 'type': type, 'tostring': function { var intendation = strrep('=', this.level); return intendation + this.type + '|' + this.title + intendation; }	}; } /** * Creates an item instance holding data extracted from MOOC index. * @param {Object} item header * @param {Object} MOOC index * @return {Object} MOOC item to get parameters and write to MOOC index */ function Item(header, index) { var loadingScript = false; var loadingQuiz = false; return { 'childLines': [], 'discussion': null, 'fullPath': _fullPath, 'header': header, 'index': index, 'indexSection': index.itemSection, 'parameterKeys': [], 'parameters': {}, 'script': null, 'quiz': null, /**		 * @return {String} invoke code used for the current item */		'getInvokeCode': function { return ''; },		/**		 * Gets the value for an item parameter. * @param {String} parameter key * @return {?} Value stored for the parameter key passed. May be null. */		'getParameter': function(key) { return this.parameters[key]; },		/**		 * Sets the value for an item parameter. * @param {String} parameter key * @param {?} parameter value */		'setParameter': function(key, value) { this.parameters[key] = value; if ($.inArray(key, this.parameterKeys) == -1) { this.parameterKeys.push(key); }		},		/**		 * Retrieves the script resource for this item. * @param {function} callback when the script was retrieved successfully (script gets passed as parameter) * @param {function} (optional) callback when the script retrieval failed (jqXHR object gets passed as parameter) */		'retrieveScript': function(sucCallback, errCallback) { if (this.script !== null) { sucCallback(this.script); } else if (!loadingScript) { loadingScript = true; getScript(this, sucCallback, errCallback); } else { // does not happen }		},		/**		 * Retrieves the quiz resource for this item. * @param {function} callback when the quiz was retrieved successfully (quiz gets passed as parameter) * @param {function} (optional) callback when the quiz retrieval failed (jqXHR object gets passed as parameter) */		'retrieveQuiz': function(sucCallback, errCallback) { if (this.quiz !== null) { sucCallback(this.quiz); } else if (!loadingQuiz) { loadingQuiz = true; getQuiz(this, sucCallback, errCallback); } else { // does not happen }		},		'tostring': function { var lines = []; // header line if (this.indexSection !== null) {// except root item lines.push(this.header.tostring); }			// parameters var key, value; this.parameterKeys.sort; for (var i = 0; i < this.parameterKeys.length; ++i) { key = this.parameterKeys[i]; value = this.parameters[key]; if (value.indexOf("\n") != -1) {// linebreak for multi line values lines.push('*' + key + '=\n' + value); } else { lines.push('*' + key + '=' + value); }			}			// children for (var c = 0; c < this.childLines.length; ++c) { lines.push(this.childLines[c]); }			return lines.join('\n'); }	}; } /** * Creates a MOOC index instance providing read access. * @param {String} MOOC page title * @param {String} MOOC base * @return {Object} MOOC index instance to retrieve item */ function MoocIndex(title, base) { var isLoading = false; return { 'base': base, 'item': null, 'itemPath': null, 'itemSection': null, 'title': title, /**		 * Sets the current item. * @param {int} section of the item within the MOOC index * @param {String} absolute path of the item */		'useItem': function(section, path) { this.itemSection = section; this.itemPath = path; },		/**		 * Retrieves the current item from the MOOC index. * If the item is not cached this call will trigger a network request. * @param {function} callback when the item was retrieved successfully (item gets passed as parameter) * @param {function} (optional) callback when the item retrieval failed (jqXHR object gets passed as parameter) */		'retrieveItem': function(sucCallback, errCallback) { if (this.item !== null) { sucCallback(this.item); } else { var index = this; if (!isLoading) {// retrieve index and load item isLoading = true; getIndex(this.title, this.itemSection, function(indexContent) {						var indexLines = splitLines(indexContent);						var item;						if (index.itemSection === null) {// root item							item = Item(Header(0, 'mooc', index.base, null), index);							// do not interprete index							for (var i = 0; i < indexLines.length; ++i) {								item.childLines.push(indexLines[i]);							}						} else {// index item							var header = loadHeader(indexLines[0], index.base, index.itemPath);							item = Item(header, index);							// load properties and lines of child items							var childLines = false;							for (var i = 1; i < indexLines.length; i++) {								if (!childLines) {									if (getLevel(indexLines[i]) > 0) {										childLines = true;									} else {										var property = loadProperty(indexLines, i);										item.setParameter(property.key, property.value);										i = property.iEnd; }								}								if (childLines) { item.childLines.push(indexLines[i]); }							}						}						index.item = item; isLoading = false; sucCallback(item); });				} else {// another process triggered network request					setTimeout(function { if (index.item !== null) { sucCallback(index.item); } else if (!isLoading && typeof(errCallback) !== 'undefined') { errCallback; }					}, 100);				}			}		}	}; }

// ############################### // ######## INITIALIZATION ####### // ############################### function setMessage(key, value) { mw.messages.set(key, value); } function getMessageText(key, params) { return mw.message(key, params).text; } function loadMessages(languageKey) { setMessage('ask-question-button', 'Ask'); setMessage('ask-question-title-label', 'Question title'); setMessage('ask-question-text-label', 'Your question'); setMessage('btn-ask-question-ui', 'Ask a question'); setMessage('btn-expand-section', 'Read more'); setMessage('btn-expand-thread', 'Read more'); setMessage('btn-reply-ui', 'reply'); setMessage('edit-default-summary-learningGoals', 'learning goals changed'); setMessage('edit-default-summary-video', 'video changed'); setMessage('edit-default-summary-script', ''); setMessage('edit-default-summary-quiz', ''); setMessage('edit-default-summary-furtherReading', 'further reading changed'); setMessage('edit-default-text-quiz', ' \n' +		'{Example question\n' +		'|type="[]"}\n' +		'correct answer\n' +		'|| explanation for the correct answer is only displayed after the quiz is answered\n' +		'- wrong answer\n' +		'|| explanation for the wrong answer is only displayed after the quiz is answered\n' +		'- another wrong answer\n' +		'|| explanation for the wrong answer is only displayed after the quiz is answered\n' +		' '); setMessage('edit-default-text-script', ''); setMessage('modal-button-edit', 'Save'); setMessage('modal-button-add-lesson', 'Add lesson'); setMessage('modal-button-add-unit', 'Add unit'); setMessage('modal-button-create-mooc', 'Create MOOC'); setMessage('modal-help-addLesson', 'Please notice that all underscores in a lesson title will be replaced with spaces.'); setMessage('modal-help-addUnit', 'Please notice that all underscores in a unit title will be replaced with spaces.'); setMessage('modal-help-createMooc', 'Please notice that all underscores in a MOOC title will be replaced with spaces.'); setMessage('modal-help-furtherReading', 'Further reading items are separated by newlines and start with a "#".'); setMessage('modal-help-learningGoals', 'Learning goals are separated by newlines and start with a "#".'); setMessage('modal-help-quiz', 'Hint: Use the following link to edit the quiz at its wiki page: ' +		'edit quiz externally' +		' Visit Help:Quiz for more information about quiz formats.'); setMessage('modal-help-script', 'Hint: Use the following link to edit the script at its wiki page: ' +		'edit script externally'); setMessage('modal-help-video', 'The video can either be a text to be displayed or a video file such as "File:MyVideo.ogv" that will be displayed as thumbail. Keep in mind that this file must exist ether on commons.wikimedia.org or on en.wikiversity.org.'); setMessage('modal-title-addLesson', 'Enter lesson name'); setMessage('modal-title-addUnit', 'Enter unit name'); setMessage('modal-title-createMooc', 'Enter MOOC title'); setMessage('modal-title-furtherReading', 'Enter further reading'); setMessage('modal-title-learningGoals', 'Enter learning goals'); setMessage('modal-title-quiz', 'Enter quiz'); setMessage('modal-title-script', 'Enter script'); setMessage('modal-title-video', 'Enter video'); setMessage('modal-summary-label', 'Enter edit summary'); setMessage('reply-text-label', 'Your reply'); setMessage('reply-button', 'Reply'); setMessage('section-discussion', 'Discussion'); setMessage('section-furtherReading', 'Further reading'); setMessage('section-learningGoals', 'Learning goals'); setMessage('section-quiz', 'Quiz'); setMessage('section-script', 'Script'); setMessage('section-units', 'Associated units'); setMessage('section-video', 'Video'); }

// setup user agent for API requests $.ajaxSetup({	beforeSend: function(request) {		request.setRequestHeader("User-Agent", "MOOC-JS/0.1 (https://en.wikiversity.org/wiki/User:Sebschlicht; sebschlicht@uni-koblenz.de)");	} }); var PARAMETER_KEY = { FURTHER_READING: 'furtherReading', LEARNING_GOALS: 'learningGoals', NUM_THREADS: 'numThreads', NUM_THREADS_OPEN: 'numThreadsOpen', VIDEO: 'video' }; // extract item data from page DOM var _base = $('#baseUrl').text; var _fullPath = $('#path').text; if (_fullPath === '') {// path of root item equals base _fullPath = _base; } var _indexSection = $('#section').text; var _indexTitle = $('#indexUrl').text; console.log('MOOC index @ ' + _indexTitle + ' for ' + _base); loadMessages('en'); var _index = MoocIndex(_indexTitle, _base); if (_indexSection !== '') {// use current item if not root _index.useItem(_indexSection, _fullPath); } var nItemNav;

// expand sections browsed to via anchors if ("onhashchange" in window) { console.log("onhashchange"); window.onhashchange = function { hashChanged(window.location.hash); }; } else { console.log("setInterval"); var prevHash = window.location.hash; window.setInterval(function {		if (window.location.hash != prevHash) {			prevHash = window.location.hash;			hashChanged(prevHash);		}	}, 100); }

addStyleSheet('User:Sebschlicht/mooc.css', function {	// make section navigation links scrolling smoothly	nItemNav = $('#mooc-item-navigation');	nItemNav.children('.section-link-wrapper').click(function { var nSectionLink = $(this); var nSection = $('#' + nSectionLink.attr('id').substring(13)); if (nSection.hasClass('collapsed')) { expand(nSection); }		scrollIntoView(nSection); return false; });	nItemNav.toggle(true);	// collapse script section	collapse($('#script'));	// expand active section	hashChanged(window.location.hash);	//fix section for duration of section expansion animation	//TODO find workarround	if (window.location.hash !== '') {		var section = $(window.location.hash);		fixView(section, 600);	}	/**	 * prepares headers	 * * expand/collapse section when header clicked	 * * fade in/out action buttons when entering section	 */	$('.section > .header').each(function { var nHeader = $(this); var nSection = nHeader.parent; nHeader.click(function(e) {			var target = $(e.target);			if (!target.is('.header', ':header') && target.parents(':header').length === 0) {// filter clicks at action buttons				return true;			}			if (nSection.hasClass('collapsed')) {				expand(nSection);			} else {				collapse(nSection);			}			return false;		}); var nActions = nHeader.find('.actions'); var nActionButtons = nActions.children.not('.edit-modal'); nActionButtons.each(function {// remove image link			var btn = $(this);			var img = btn.find('img');			btn.append(img).find('a').remove;		}); nSection.mouseenter(function {			nActionButtons.stop.fadeIn;		}); nSection.mouseleave(function {			nActionButtons.stop.fadeOut;		}); });	//TODO remove if not used by reply button	// display overlay when mouse enters overlay parent	$('.overlay').parent.mouseenter(function { var overlay = $(this).children('.overlay'); if (overlay.css('display') === 'none') { overlay.stop.toggle('fast'); }	});	// hide overlay when mouse leaves overlay parent	$('.overlay').parent.mouseleave(function { var overlay = $(this).children('.overlay'); if (overlay.css('display') !== 'none') { overlay.stop.toggle('fast'); }	});	// prepare child units	var unitButtons = [];	var videoTitles = [];	$('.children .unit').not('#addUnit').not('#addLesson').not('#addMooc').each(function { var nChild = $(this); var nIconBar = nChild.find('.icon-bar'); var nIconBarItems = nIconBar.find('li').not('.disabled'); var iconBarOpacity = nIconBarItems.css('opacity'); var nDownloadButton = nIconBar.find('li').eq(1); if (nDownloadButton.length > 0 && !nDownloadButton.hasClass('disabled')) { console.log('button active'); unitButtons.push(nDownloadButton); videoTitles.push(nDownloadButton.children('a').attr('href').substring(6).replace(/_/g, ' ')); }		var nDisStatisticWrapper = nChild.find('.discussion-statistic-wrapper'); var nDisStat = nDisStatisticWrapper.children('.discussion-statistic'); var url = nChild.children('.content').children('.title').find('a').attr('href'); nChild.mouseenter(function {// show disussion stats when mouse enters child			nDisStatisticWrapper.stop.fadeIn;			nIconBarItems.css('opacity', '1');		}); nChild.mouseleave(function {// hide discussion stats when mouse leaves child			nDisStatisticWrapper.stop.fadeOut;			nIconBarItems.css('opacity', iconBarOpacity);		}); nChild.click(function {// item click (may target underlying elements)			window.location = url;			return true;		}); nDisStat.click(function {// discussion statistic click			window.location = url + '#discussion';			return false;		}); });	// retrieve video URLs	getVideoUrls(videoTitles, function(videoUrls) { for (var i = 0; i < videoTitles.length; ++i) { var url = videoUrls[videoTitles[i]]; if (url) { unitButtons[i].children('a').attr('href', url); }		}	});	// make edit text links working in empty sections	$('.empty-section .edit-text').click(function { var section = $(this).parents('.section'); if (section.length == 1) { section.children('.header').find('.edit-btn').click; }	});	// fix navigation bar staying scrollable	var sidebar = $('#mooc-navigation');	if (sidebar.length > 0) {		var header = sidebar.find('.header-wrapper');		var sidebarTop = sidebar.offset.top;		var marginBottom = 10;		function fixNavBarHeader(header) {			header.css('width', header.outerWidth);			header.css('position', 'fixed');			header.addClass('fixed');		}		function resetNavBarHeader(header) {			header.removeClass('fixed');			header.css('position', 'absolute');			header.css('width', '100%');		}		function fixNavBar(navBar) {			navBar.removeClass('trailing');			navBar.css('bottom', 'auto');			navBar.css('position', 'fixed');			navBar.css('top', 0);			navBar.addClass('fixed');		}		function preventNavBarScrolling(navBar, marginBottom) {			navBar.removeClass('fixed');			navBar.css('top', 'auto');			navBar.css('position', 'fixed');			navBar.css('bottom', marginBottom);			navBar.addClass('trailing'); }		function resetNavBar(navBar) { navBar.removeClass('fixed'); navBar.removeClass('trailing'); navBar.css('position', 'relative'); }		$(window).scroll(function {			var maxY = sidebarTop + sidebar.outerHeight;			var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);			var y = $(this).scrollTop;			var navBarScrolling = !sidebar.hasClass('trailing');			var navBarFixed = sidebar.hasClass('fixed');			var headerFixed = header.hasClass('fixed');			if (y >= sidebarTop) {// navigation bar reached top screen border				if (sidebar.outerHeight <= h - marginBottom) {// fix navigation bar that fits in window					if (!navBarFixed) {						fixNavBar(sidebar);					}				} else {// navigation bar too large					if (!headerFixed) { // fix navigation header						fixNavBarHeader(header);					}					if (y + h >= maxY + marginBottom) {// disable scrolling when navigation bottom reached						if (navBarScrolling) {							preventNavBarScrolling(sidebar, marginBottom);						}					} else {// enable scrolling if still content available if (!navBarScrolling) { resetNavBar(sidebar); }					}				}			} else {// navigation bar is back at its place if (headerFixed) { resetNavBarHeader(header); }				if (navBarFixed) { resetNavBar(sidebar); }			}		});	}	// fix item navigation	if (nItemNav.length > 0) {		var itemNavTop = nItemNav.offset.top;//TODO ensure offset.top work correctly		console.log('item nav is at ' + itemNavTop);		$(window).scroll(function { var y = $(window).scrollTop; var isFixed = nItemNav.hasClass('fixed'); if (y >= itemNavTop) { if (!isFixed) { nItemNav.after($(' ', { 'id': 'qn-replace', 'height': nItemNav.height }));					nItemNav.css('width', nItemNav.outerWidth); nItemNav.css('position', 'fixed'); nItemNav.css('top', 0); nItemNav.addClass('fixed'); }			} else { if (isFixed) { nItemNav.css('width', '100%'); nItemNav.css('position', 'relative'); nItemNav.css('top', null); nItemNav.removeClass('fixed'); nItemNav.next.remove; }			}		});	}	// fix header sections	function setActiveSection(section) {		var activeSection = $('.section').filter('.active');		if (activeSection.length > 0) {			setSectionActive(activeSection, false);		}		if (section != null) {			setSectionActive(section, true);		} else {			//TODO replace with cross browser compatible solution (problems in e.g. Chrome 36.0.1985.125)			//history.replaceState(null, null, window.location.pathname);		}	}	function setSectionActive(section, isActive) {		var sectionId = section.attr('id');		var sectionAnchor = nItemNav.find('#section-link-' + sectionId);		if (isActive) {			sectionAnchor.addClass('active');			section.addClass('active');			//TODO replace with cross browser compatible solution (problems in e.g. Chrome 36.0.1985.125)			//history.replaceState({}, '', '#' + sectionId);		} else {			sectionAnchor.removeClass('active');			section.removeClass('active'); resetHeader(section.children('.header')); }	}	function fixHeader(header, top) { header.css('position', 'fixed'); header.css('top', top); header.css('width', header.parent.width); header.removeClass('trailing'); header.addClass('fixed'); }	function resetHeader(header) { if (header.hasClass('fixed')) { header.css('position', 'absolute'); header.css('width', '100%'); header.removeClass('fixed'); }		header.css('top', 0); header.removeClass('trailing'); }	function trailHeader(header) { if (header.hasClass('fixed')) { header.css('position', 'absolute'); header.css('width', '100%'); header.removeClass('fixed'); }		header.css('top', header.parent.height - header.outerHeight); header.addClass('trailing'); }	$(window).scroll(function {		var y = $(window).scrollTop;		var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);		var marginTop = 0;		if (nItemNav.hasClass('fixed')) {// correct scroll position			marginTop = nItemNav.outerHeight - 1;			y += marginTop;		}		var activeSection = null;		$('.section').each(function { var section = $(this); var sectionHeader = section.children('.header'); var sectionTop = section.offset.top; var sectionHeight = section.height; var isActive = section.hasClass('active'); var isFixed = sectionHeader.hasClass('fixed'); if (y >= sectionTop				&& y <= sectionTop + sectionHeight) {// active section if (!isActive) { setActiveSection(section); }				activeSection = section; if (y <= sectionTop + sectionHeight - sectionHeader.outerHeight) {// header can be fixed if (!isFixed) { fixHeader(sectionHeader, marginTop); }				} else {// header reached section bottom if (!sectionHeader.hasClass('trailing')) { trailHeader(sectionHeader); }				}			} else { if (isActive) { resetHeader(sectionHeader); }			}		});		if (activeSection == null) {			setActiveSection(null);		}	}); // inject modal boxes prepareModalBoxes; // fill modal boxes _index.retrieveItem(function(item) {		fillModalBoxes(item);	}); // load discussion module addJavaScript('User:Sebschlicht/moocDiscussions.js', function {		loadDiscussionUi;	}); });

// ################### // ###TODO:CLEAN UP### // ###################

function saveChanges(idSection, value, summary) { var sucCallback = function { reloadPage('#' + idSection); };	if (idSection === 'script') {// update script resource if (_index.item.script === null) { // add category value += '\n '; }		updateScript(_index.item, value, summary, sucCallback); } else if (idSection === 'quiz') {// update quiz resource if (_index.item.quiz === null) { // add category value += '\n '; }		updateQuiz(_index.item, value, summary, sucCallback); } else {// update index parameter var key = null; if (idSection === 'learningGoals') { key = PARAMETER_KEY.LEARNING_GOALS; value = value.replace(/(^|\n)\*/g, '\n#'); } else if (idSection === 'video') { key = PARAMETER_KEY.VIDEO; value = value.replace(/(^|\n)\*/g, ''); } else if (idSection === 'furtherReading') { key = PARAMETER_KEY.FURTHER_READING; value = value.replace(/(^|\n)\*/g, '\n#'); }		if (key !== null) { if (summary === '') { summary = getMessageText('edit-default-summary-' + idSection); }			_index.item.setParameter(key, value); updateIndex(_index.item, summary, sucCallback); }	} }

function prepareModalBoxes(idSectionCalled) { // fill modal boxes to save changes on item or its resources prepareModalBox('learningGoals', 'edit', 5, saveChanges); prepareModalBox('video', 'edit', 1, saveChanges); prepareModalBox('script', 'edit', 5, saveChanges); prepareModalBox('quiz', 'edit', 5, saveChanges); prepareModalBox('furtherReading', 'edit', 5, saveChanges); // fill modal boxes to add a MOOC item prepareModalBox('addLesson', 'add-lesson', 1, function(idSection, value, summary) {		addLesson(value, _index.item, summary, function { reloadPage; });	});	prepareModalBox('addUnit', 'add-unit', 1, function(idSection, value, summary) {		addUnit(value, _index.item, summary, function { reloadPage; });	});	prepareModalBox('createMooc', 'create-mooc', 1, function(idSection, value, summary) {		createMooc(value, summary, function { reloadPage; });	}).find('.btn-save').prop('disabled', false); // make boxes closable via button $('.edit-modal').each(function {		var modal = $(this);		modal.find('.btn-close').click(function { closeModalBox(modal); return false; });	});	// make boxes closable via background click $('.edit-modal > .background').click(function(e) {		closeModalBox($(e.target).parent);		return false;	}); // make boxes closable via ESC key $('.edit-modal').bind('keydown', function(e) {        if (e.which == 27) {        	closeModalBox($(this));        }    }); } function closeModalBox(modal) { $('#mooc-item-navigation').css('z-index', 1001); modal.parent.parent.css('z-index', 1); modal.fadeOut; } function prepareModalBox(idSection, intentType, numLines, finishCallback) { // create modal box structure var modalBox = $('#modal-' + intentType + '-' + idSection); modalBox.append($(' ', { 'class': 'background' }));	var boxContent = $(' ', {		'class': 'content'	}); boxContent.append($(' ', { 'class': 'btn-close' }));	//TODO use real fieldset instead? var editFieldset = $(' ', {		'class': 'edit-field'	}); // label and textarea for value editFieldset.append($(' ', { 'for': 'edit-field-' + idSection, 'class': 'label-title', 'text': getMessageText('modal-title-' + idSection) + ':' }));	var editField; if (numLines > 1) { editField = $(' ', {			'class': 'border-box',			'id': 'edit-field-' + idSection		}); } else { editField = $(' ', {			'class': 'border-box',			'id': 'edit-field-' + idSection,			'type': 'text'		}); }	editFieldset.append(editField); // label and input box for edit summary editFieldset.append($(' ', { 'for': 'summary-' + idSection, 'class': 'label-summary', 'text': getMessageText('modal-summary-label') + ':' }));	var ibSummary = $(' ', {		'id': 'summary-' + idSection,		'class': 'border-box summary',		'type': 'text'	}); editFieldset.append(ibSummary); // help text var divHelpText = $(' ', {		'class': 'help',	}).html(getMessageText('modal-help-' + idSection, _fullPath)); editFieldset.append(divHelpText); boxContent.append(editFieldset); //TODO why not put in edit fieldset? // finish button var btnSave = $(' ', {		'class': 'btn-save',		'disabled': true,		'type': 'button',		'value': getMessageText('modal-button-' + intentType)	}); boxContent.append(btnSave); btnSave.click(function {		if (!btnSave.prop('disabled')) {			btnSave.prop('disabled', true);			finishCallback(idSection, editField.val, ibSummary.val);		}		return false;	}); modalBox.append(boxContent); return modalBox; } function fillModalBoxes(item) { // inject item data $('#edit-field-learningGoals').append(item.getParameter(PARAMETER_KEY.LEARNING_GOALS)); $('#modal-edit-learningGoals').find('.btn-save').prop('disabled', false); $('#edit-field-video').val(item.getParameter(PARAMETER_KEY.VIDEO)); $('#modal-edit-video').find('.btn-save').prop('disabled', false); $('#edit-field-furtherReading').append(item.getParameter(PARAMETER_KEY.FURTHER_READING)); $('#modal-edit-furtherReading').find('.btn-save').prop('disabled', false); $('#modal-add-lesson-addLesson').find('.btn-save').prop('disabled', false); $('#modal-add-unit-addUnit').find('.btn-save').prop('disabled', false); // retrieve and inject additional resources var taScript = $('#edit-field-script'); item.retrieveScript(function(scriptText) {		taScript.text(scriptText).html;		$('#modal-edit-script').find('.btn-save').prop('disabled', false);	}, function(jqXHR) {		if (jqXHR.status == 404) {// script missing			taScript.text(getMessageText('edit-default-text-script', item.header.type)).html;		}		$('#modal-edit-script').find('.btn-save').prop('disabled', false);	}); var taQuiz = $('#edit-field-quiz'); item.retrieveQuiz(function(quizText) {		taQuiz.text(quizText).html;		$('#modal-edit-quiz').find('.btn-save').prop('disabled', false);	}, function(jqXHR) {		if (jqXHR.status == 404) {// quiz missing			taQuiz.text(getMessageText('edit-default-text-quiz')).html;			$('#modal-edit-quiz').find('.btn-save').prop('disabled', false);		}	}); }

$(document).ready(function{	// make edit buttons clickable	var showModalBox = function {		var btn = $(this);		var modal = btn.next('.edit-modal');		if (modal.length == 0) {			modal = btn.next.next('.edit-modal');		}		//TODO what happens if no header but addLesson aso?		var header = modal.parent.parent;		$('#mooc-item-navigation').css('z-index', 1);		header.css('z-index', 2);		// show modal box with focus on edit field		var editField = modal.find('.edit-field').children('textarea');		modal.toggle('fast', function { editField.focus; });		return false;	};	$('.edit-btn').each(function { var btn = $(this); btn.click(showModalBox); });	// make add unit div clickable	var divAddUnit = $('#addUnit');	var imgAddUnit = divAddUnit.find('img');	divAddUnit.find('span').append(imgAddUnit).children('a.image').remove;	divAddUnit.click(showModalBox);	divAddUnit.show;	// make add lesson clickable	var divAddLesson = $('#addLesson');	var imgAddLesson = divAddLesson.find('img');	divAddLesson.find('span').append(imgAddLesson).children('a.image').remove;	divAddLesson.click(showModalBox);	divAddLesson.show;	// make add MOOC clickable	var divAddMooc = $('#addMooc');	var imgAddMooc = divAddMooc.find('img');	divAddMooc.find('span').append(imgAddMooc).children('a').remove;	divAddMooc.click(showModalBox);	// let redlinks create invoke pages	var invokeItem = Item(Header(0, null, null, null), _index);	$('#mooc-navigation a.new').click(function { var link = $(this); var itemUrl = link.attr('href').replace(/_/g, ' '); itemUrl = itemUrl.substring(0, itemUrl.length - 22); var itemTitle = itemUrl.substring(19); console.log(itemUrl + ": " + itemTitle); //TODO change to createInvokePage and use mw.Message there createPage(itemTitle, invokeItem.getInvokeCode, 'invoke page for MOOC item created', function {			window.location.href = itemUrl;		}); return false; }); });

//