Utilizador:Leomd/mooc.js
Nota: Depois de publicar, poderá ter de contornar a cache do seu navegador para ver as alterações.
- Firefox / Safari: Pressione Shift enquanto clica Recarregar, ou pressione Ctrl-F5 ou Ctrl-R (⌘-R no Mac)
- Google Chrome: Pressione Ctrl-Shift-R (⌘-Shift-R no Mac)
- Internet Explorer / Edge: Pressione Ctrl enquanto clica Recarregar, ou pressione Ctrl-F5
- Opera: Pressione Ctrl-F5.
// <nowiki>
/*############################
## 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 += "§ion=" + 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<String>} 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://pt.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://pt.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://pt.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('Categoria:' + title, '{{#invoke:Mooc|overview|base=' + title + '}}\n<noinclude>[[categoria:MOOC]]</noinclude>', summary, function() {// create category with overview
createPage(title, '{{#invoke:Mooc|overview|base=' + 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<String>} 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 = $('<div>', {
'class': 'btn-expand'
}).html('↓ ' + getMessageText('btn-expand-section') + ' ↓');
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 '{{#invoke:Mooc|render|base=' + index.base + '}}';
},
/**
* 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', '<quiz display=simple>\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' +
'</quiz>');
setMessage('edit-default-text-script', '<!-- put your $1 script here -->');
setMessage('modal-button-edit', 'Save');
setMessage('modal-button-add-lesson', 'Adicionar Lição');
setMessage('modal-button-add-unit', 'Adicionar Unidade');
setMessage('modal-button-create-mooc', 'Criar um 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: ' +
'<a href="http://pt.wikiversity.org/w/index.php?title=$1/quiz&action=edit">edit quiz externally</a>' +
'<br>Visit <a href="https://pt.wikiversity.org/wiki/Help:Quiz">Help:Quiz</a> for more information about quiz formats.');
setMessage('modal-help-script', 'Hint: Use the following link to edit the script at its wiki page: ' +
'<a href="http://pt.wikiversity.org/w/index.php?title=$1/script&action=edit">edit script externally</a>');
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('Utilizador:BMNeuroMat/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($('<div>', {
'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('Utilizador:BMNeuroMat/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<noinclude>[[categoria:' + _index.base + '-MOOC]]</noinclude>';
}
updateScript(_index.item, value, summary, sucCallback);
} else if (idSection === 'quiz') {// update quiz resource
if (_index.item.quiz === null) {
// add category
value += '\n<noinclude>[[categoria:' + _index.base + '-MOOC]][[categoria:Quizz]]</noinclude>';
}
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($('<div>', {
'class': 'background'
}));
var boxContent = $('<div>', {
'class': 'content'
});
boxContent.append($('<div>', {
'class': 'btn-close'
}));
//TODO use real fieldset instead?
var editFieldset = $('<div>', {
'class': 'edit-field'
});
// label and textarea for value
editFieldset.append($('<label>', {
'for': 'edit-field-' + idSection,
'class': 'label-title',
'text': getMessageText('modal-title-' + idSection) + ':'
}));
var editField;
if (numLines > 1) {
editField = $('<textarea>', {
'class': 'border-box',
'id': 'edit-field-' + idSection
});
} else {
editField = $('<input>', {
'class': 'border-box',
'id': 'edit-field-' + idSection,
'type': 'text'
});
}
editFieldset.append(editField);
// label and input box for edit summary
editFieldset.append($('<label>', {
'for': 'summary-' + idSection,
'class': 'label-summary',
'text': getMessageText('modal-summary-label') + ':'
}));
var ibSummary = $('<input>', {
'id': 'summary-' + idSection,
'class': 'border-box summary',
'type': 'text'
});
editFieldset.append(ibSummary);
// help text
var divHelpText = $('<div>', {
'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 = $('<input>', {
'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;
});
});
//</nowiki>