MediaWiki:Gadget-teahouse/main.js
Zur Navigation springen
Zur Suche springen
Hinweis: Leere nach dem Speichern den Browser-Cache, um die Änderungen sehen zu können.
- Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
- Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
- Internet Explorer: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
- Opera: Gehe zu Menü → Einstellungen (Opera → Einstellungen auf dem Mac) und dann auf Datenschutz & Sicherheit → Browserdaten löschen → Gespeicherte Bilder und Dateien.
(function( mw, $, d, undefined ){
/**
* Main configuration object. It contains settings that may differ between
* single wikipedias
*/
var _config = {
basePage: 'Wikipedia:Teestube/Fragen',
insertMarker: '<!-- INSERTMARKER -->',
resourcesPath: mw.config.get('wgScript') + "?title=MediaWiki:Gadget-teahouse",
showLinkMaxEditCount: 100,
//questionTitleMaxLength: 255, //Not implemented yet. This is implicitly handled by API "edit".
questionTextMaxLength: 4000,
templates: {
// <nowiki>
pendingQuestion: "\n\n{{Teestube/offene Fragen}}",
answeredQuestion: "\n\n{{Teestube/beantwortete Fragen}}",
questionWrapper: "\n{{Teestube/Fragen|title=###title###|question=###question###}}\n"
// </nowiki>
},
i18n: {
"teahouse-button-text": "Ask your question",
"teahouse-button-title": "Get in touch with the community",
"teahouse-dialog-title": "Ask your question",
"teahouse-dialog-description-top": "Ask your question to the Wikipedia Community! You can see the questions of other users on $1",
"teahouse-dialog-label-summary": "Your question",
"teahouse-dialog-label-text": "Further description (Can be changed afterwards)",
"teahouse-dialog-label-similar": "Similar questions",
"teahouse-dialog-btn-ok": "Publish",
"teahouse-dialog-btn-cancel": "Cancel",
"teahouse-dialog-licence-html": "Text is available under the <a href=\"https://creativecommons.org/licenses/by-sa/3.0/\">Creative Commons Attribution-ShareAlike License</a>; additional terms may apply.",
"teahouse-dialog-anon-ip-hint-html": "Your IP address will be published with the question",
"teahouse-dialog-url-hint-html": "There is an URL in your question. This may prevent you from saving it",
"teahouse-dialog-max-length-hint-html": "The input is limited to $1 chars",
"teahouse-dialog-error-process": "Sorry, an error occured while trying to process your request",
"teahouse-dialog-error-details": "Error details",
"teahouse-dialog-msg-title-save": "Question published",
"teahouse-dialog-msg-text-save": "Your question has been published on $1. Do you want to see the complete list of questions?",
"teahouse-dialog-msg-btn-yes": "Yes",
"teahouse-dialog-msg-btn-no": "No",
"teahouse-notifications-badge-title": "There are reactions to your questions",
"teahouse-notifications-popup-title": "Reactions to your questions",
"teahouse-board-added-by-teahouse": "Added by Teahouse-Gadget"
}
};
/**
* Initializes the Teahouse gadget components.
* @returns void
*/
function _init(){
delete(mw.util.teahouse.init); //Must be called only once
//Init sub-components and share current config with them
mw.util.teahouse.board.init( _config );
mw.util.teahouse.notifications.init( _config );
mw.util.teahouse.dialog.init( _config );
//Set cookie when anon clicks "edit" link
$(d).on( 'click', '#ca-ve-edit, #ca-edit', function(){
mw.cookie.set('mediaWiki.teahouse.anonEdit', '1');
});
//Check if the "Ask your question" link should show up in teh personal tools
_checkUserIsEligible();
}
/**
* This function checks whether to show the "Ask your question" link or not
* @returns void
*/
function _checkUserIsEligible() {
//If the Teahouse-opt-in gadget is enabled we don't do any further checks
if( mw.user.options.get( 'gadget-teahouse-opt-in', 0 ) === '1' ) {
_showDialogLink();
return;
}
//If not logged in we just check if the user has already made an edit
if( mw.user.isAnon() === true && mw.cookie.get('mediaWiki.teahouse.anonEdit') === '1' ) {
_showDialogLink();
return;
}
//In case of a registered user we need to check for "editcount" of user.
if( mw.user.isAnon() === false ) {
//If there is a cookie just skip further execution
if( mw.cookie.get('mediaWiki.teahouse.hidePesonalToolsLink' ) === 'true' ) {
return;
}
var api = new mw.Api();
api.post({
'action': 'query',
'list': 'users',
'ususers': mw.user.getName(),
'usprop': 'editcount'
})
.done(function( result ){
var editCount = result.query.users[0].editcount;
if( editCount <= _config.showLinkMaxEditCount ) {
_showDialogLink();
}
else {
mw.cookie.set('mediaWiki.teahouse.hidePesonalToolsLink', true);
}
});
}
}
function _showDialogLink() {
var linkMarkup =
'<li id="p-teahouse">'
+ '<a title="' + mw.message('teahouse-button-title').plain() + '" href="#">'
+ mw.message('teahouse-button-text').plain()
+ '</a>'
+ '</li>';
_config._$dlgLink = $(linkMarkup).prependTo( $('#p-personal > ul').first() );
}
/**
* This is a utility method that creates a object structure
* like window.mw.component.subcomponent.concretetype from a string
* like "mw.component.subcomponent.concretetype". This allows the creation
* of complex type structures with a single call. I.e. from the components
* sourcefile.
* @param string type
* @param object baseType
* @returns {undefined}
*/
function _registerType( type, baseType ) {
var baseNS = baseType || window;
var parts = type.split('.');
if( !( !baseNS[parts[0]] && parts.length === 1 ) ) {
baseNS[parts[0]] = baseNS[parts[0]] || {};
baseNS = baseNS[parts[0]];
parts.shift(); //Remove first element
if( parts.length > 0 ) {
_registerType( parts.join('.'), baseNS );
}
}
}
mw.util.teahouse = {
init: function( config ) {
//called before merge because $.extend is not recursive
mw.messages.set( _config.i18n );
_config = $.extend( _config, config );
mw.messages.set( _config.i18n );
_init.call( mw.util.teahouse );
},
registerType: _registerType
};
})( mediaWiki, jQuery, document );
(function( mw, $, d, undefined ){
/**
* Just a little helper to geht the numer of properties in an object
* @param obj {Object} the JavaScript object
* @returns {Number}
*/
function _objLength( obj ) {
var count = 0;
for( var key in obj ) {
count++;
}
return count;
}
/**
* Saves current data for localStore
* @param {object} data
* @returns {undefined}
*/
function _persistData( data ) {
var storageData = JSON.stringify( data || _data );
if( window.localStorage ) {
window.localStorage.setItem( _storageKey, storageData );
}
else {
mw.cookie.set( _storageKey, storageData );
}
}
/**
* Calls MW API, updates internal data and repeats this periodically
* @returns {undefined}
*/
function _checkForNotifications() {
mw.util.teahouse.notifications.getCurrentNotifications()
.done(function( titles ) {
if( _objLength(titles) > 0 ) {
_showNoficationsLink( _objLength(titles) );
}
window.setTimeout( _checkForNotifications, 5 * 60 * 1000 );
});
}
var _$notif = null;
function _showNoficationsLink( count ) {
if ( !_$notif ) {
var notifMarkup =
'<li id="p-teahouse-notif">'
+ '<a class="teahouse-notifications-badge teahouse-unread-notifications" title="' + mw.message('teahouse-notifications-badge-title').plain() + '" href="#">'
+ count
+ '</a>'
+ '</li>';
_$notif = $(notifMarkup);
//As showing the "Ask your question" link depends on an AJAX call
//there stands the chance that this call is before or after the
//link has been added to the DOM. We have to make sure the order of
//elements is always the same
if( _config._$dlgLink ) {
_config._$dlgLink.after( _$notif );
}
else {
_$notif.prependTo( $('#p-personal > ul').first() );
}
}
_$notif.find( 'a.teahouse-notifications-badge' ).html( count );
}
/**
* We don't use a OO.ui.PopupElement here to prevent a dependency to
* 'oojs-ui' module during normal page load
* @type OO.ui.PopupWidget
*/
var _popUpWidget = null;
var _$popUpContent = null;
function _toggleNotifPopup( event ) {
if( _popUpWidget ) {
var list = $('<ul>').addClass('teahouse-notif-popup-list');
for( var title in _data.notifications ) {
var titleParts = title.split('/');
//basename of subpage
var displayTitle = titleParts[titleParts.length-1];
var anchor = mw.html.element('a', {
href: mw.util.getUrl( title ),
title: title,
target: '_blank'
}, displayTitle );
list.append('<li>'+anchor+'</li>');
}
_$popUpContent.empty().append( list );
_popUpWidget.toggle();
}
else {
//Just create elements ...
mw.loader.using( ['oojs-ui'], function() {
_$popUpContent = $('<div>')
.addClass( 'teahouse-notif-popup-content' );
_popUpWidget = new OO.ui.PopupWidget({
autoClose: true,
head: true,
label: mw.message('teahouse-notifications-popup-title').plain(),
$content: _$popUpContent
});
_$notif.append( _popUpWidget.$element );
//... and call yourself again to populate list
_toggleNotifPopup();
});
}
return false;
}
var _config = {};
var _data = {
watchlist: {},
notifications: {},
lastCheck: 0
};
var _storageKey = 'mediaWiki.teahouse.notifications.data';
function _init( config ) {
delete(mw.util.teahouse.notifications.init);
_config = config;
var storageData = '{}';
if( window.localStorage ) {
storageData = window.localStorage.getItem( _storageKey );
}
else {
storageData = mw.cookie.get( _storageKey, undefined, '{}' );
}
var data = JSON.parse( storageData );
_data = $.extend( _data , data ); //Set internal data
//mark current title as read
mw.util.teahouse.notifications.markTitleAsRead( mw.config.get('wgPageName') );
_checkForNotifications();
$(d).on( 'click', '.teahouse-notifications-badge', _toggleNotifPopup );
//This is an experimental feature: We preload the dependency when the
//user is about to click the link
$(d).on( 'mouseover', '.teahouse-notifications-badge', function() {
mw.loader.load( 'oojs-ui' );
});
}
mw.util.teahouse.notifications = {
init: _init,
registerTitle: function( title, timestamp ) {
_data.watchlist[title] = timestamp;
_persistData();
},
removeTitle: function( title ) {
if( title in _data.watchlist ) {
delete( _data.watchlist[title] );
}
_persistData();
},
markTitleAsRead: function( title ) {
var t = title.replace( /_/g, ' ' );
if( t in _data.watchlist ) {
_data.watchlist[t] = (new Date()).toISOString();
//Do a API call for changes
_data.lastCheck = (new Date( 0 )).toISOString();
}
_persistData();
},
getCurrentNotifications: function() {
var dfd = $.Deferred();
var titles = [];
for( var title in _data.watchlist ) {
titles.push( title );
}
if( titles.length === 0 ) {
dfd.reject();
return dfd.promise();
}
//If the last call is less than five minutes old, we do not call
//again, even on new page load
var lastCheckPlusWait = new Date( _data.lastCheck );
lastCheckPlusWait.setMinutes(lastCheckPlusWait.getMinutes() + 5);
if( lastCheckPlusWait > new Date() ) {
dfd.resolve( _data.notifications );
return dfd.promise();
}
var currentUsername = mw.user.getName();
var api = new mw.Api();
api.get({
action: 'query',
prop: 'revisions',
titles: titles.join( '|' ),
rvprop: 'timestamp|user'
})
.done(function( response, jqXHR ) {
_data.notifications = {}; //Reset
for( var pageId in response.query.pages ) {
var title = response.query.pages[pageId].title;
var revisions = response.query.pages[pageId].revisions;
if( !revisions ) {
continue;
}
var myDate = new Date( _data.watchlist[title] );
var theirDate = new Date( revisions[0].timestamp );
if( theirDate > myDate && currentUsername !== revisions[0].user ) {
_data.notifications[title] = revisions[0];
}
}
_data.lastCheck = (new Date()).toISOString();
_persistData();
dfd.resolve( _data.notifications );
});
return dfd.promise();
}
};
})( mediaWiki, jQuery, document );
(function( mw, $, d, undefined ){
/**
* Uses MediaWiki API to save a "question" (with fields "text" and "title")
* object as wikiarticle. The target article and contents of it depend on
* the gadgets configuration. A signature may be added to the articles text
* content. Also appends the "board" page with a template.
* @param {object} question
* @returns {Promise}
*/
function _publishQuestion( question ) {
//We append a signature WikiText fragment by default
var signature = ['-', '-~~', '~~'].join(''); //This is just to prevent the wikitext parser from parsing it on deployment
if( question.text.indexOf( signature ) === -1 ) {
question.text += "\n\n" + signature;
}
question.text += _config.templates.pendingQuestion;
var dfd = $.Deferred();
//Step 1: Create the question subpage
var createQuestionAPI = new mw.Api();
createQuestionAPI.postWithToken( 'edit', {
'action': 'edit',
'watchlist': 'watch',
'title': _config.basePage + "/" + question.title,
'summary': mw.message('teahouse-board-added-by-teahouse').plain(),
'text': question.text,
'continue': ''
})
.fail(function( code, errResp ){
dfd.reject( [_makeProcessErrorFromAPIResponse( errResp )] );
})
.done(function( resp1, jqXHR ){
if( !resp1.edit.result || resp1.edit.result.toLowerCase() !== 'success' ) {
dfd.reject( [_makeProcessErrorFromAPIResponse( resp1 )] );
return;
}
//Step 2: From the response build the template text for the board
var title = resp1.edit.title + '';
if( title === undefined ) { //Prevent adding "undefined" entries
dfd.reject( [_makeProcessErrorFromAPIResponse()] );
return;
}
var timestamp = resp1.edit.newtimestamp;
mw.util.teahouse.notifications.registerTitle( title, timestamp );
var titleParts = title.split( '/' );
var basename = titleParts[titleParts.length-1];
var template = _config.templates.questionWrapper
.replace('###title###', title )
.replace('###question###', basename );
//Step 3: Query the WikiText content of the board
var getBasePageWikiTextAPI = new mw.Api();
getBasePageWikiTextAPI.get({
action: 'query',
titles: _config.basePage,
prop: 'revisions',
rvprop: 'content',
indexpageids : ''
})
.fail(function( code, errResp ){
dfd.reject( [_makeProcessErrorFromAPIResponse( errResp )] );
})
.done(function( resp2, jqXHR ){
//Step 4: Add the new template at the desired position within
//the board page
var pageId = resp2.query.pageids[0];
var content = resp2.query.pages[pageId].revisions ? resp2.query.pages[pageId].revisions[0]['*'] : '';
var contentParts = content.split( _config.insertMarker, 2 );
if( contentParts.length === 1 ) { //No _config.insertMarker found
contentParts[0] += "\n";
contentParts.push(''); //We append an empty string so we have a string on index 1 to work with
}
contentParts[1] = template + contentParts[1];
content = contentParts.join( _config.insertMarker );
//Step 5: Write the new content to the board page
var addToListAPI = new mw.Api();
addToListAPI.postWithToken( 'edit', {
action: 'edit',
title: _config.basePage,
text: content,
summary: basename + ' (' + mw.message('teahouse-board-added-by-teahouse').plain() + ')'
})
.fail(function( code, errResp ){
dfd.reject( [_makeProcessErrorFromAPIResponse( errResp )] );
})
.done(function( resp3, jqXHR ){
dfd.resolve( { questionpage: resp1.edit } );
});
});
});
return dfd.promise();
}
/**
* Creates a OO.ui.Error object that gets rendered by a OO.ui.ProcessDialog
* (e.g "mw.util.teahouse.ui.dialog.Question")
* @param {object} errorResp a typical MediaWiki API error as JS object
* @returns {OO.ui.Error}
*/
function _makeProcessErrorFromAPIResponse( errorResp ){
var resp = errorResp || {};
var $message = $('<div>')
.append( mw.message('teahouse-dialog-error-process').plain() );
var info = '';
if( resp.error && resp.error.info ) {
//escapes the info wich may contain user inputs
info = mw.html.element( 'span', {}, resp.error.info );
}
else if( resp.edit ) {
if( !resp.edit.result || resp.edit.result.toLowerCase() !== 'success' ) {
var $table = $('<table>');
var $row = null;
for( var key in resp.edit ) {
if ( key === 'result' ) {
continue;
}
$row = $('<tr>');
$table.append( $row );
$row.append(
$('<th>').append( mw.html.element( 'span', {}, key ) ),
$('<td>').append( mw.html.element( 'span', {}, resp.edit[key] ) )
);
}
info = $table;
}
}
if( info !== '' ) {
//TODO: make details toggleable
var $details = $('<div>')
.addClass( 'teahouse-error-details' )
.append( info );
var $detailsLink = $('<h5>')
.addClass( 'teahouse-error-details-toggler' )
.append( mw.message('teahouse-dialog-error-details').plain() );
$message
.append( $('<div>').append( $detailsLink ) )
.append( $details );
}
return new OO.ui.Error( $message );
}
var _sqCache = {};
var _lastPromise = undefined;
/**
* Queries the MediaWiki fulltext search API for a given value and filters
* the results based on the _config.basePage as a prefix
* @param {string} value
* @returns {Promise}
*/
function _getSimilarQuestions( value ) {
var dfd = $.Deferred();
mw.loader.using( 'mediawiki.Title', function() {
var cacheKey = value.toLowerCase();
var baseTitle = new mw.Title( _config.basePage );
var prefix = baseTitle.getPrefixedText() + '/';
if( cacheKey in _sqCache ) {
dfd.resolve( _sqCache[cacheKey] );
return;
}
if( _lastPromise ) {
_lastPromise.abort();
}
var _api = new mw.Api();
_lastPromise = _api.get({
action: 'query',
list: 'search',
srsearch: value,
srlimit: 50,
srnamespace: baseTitle.getNamespaceId()
})
.done(function( response, jqXHR ){
_sqCache[cacheKey] = _processSimilarQuestionsList( response.query.search, prefix );
dfd.resolve( _sqCache[cacheKey] );
_lastPromise = undefined;
});
});
return dfd.promise();
}
/**
* Creates a hasmap in from "<PrefixedText>":"<SubpageText>" from an array
* of "<PrefixedText>"'s. Also limits list to 5 items.
* @param {array} items
* @param {string} prefix
* @returns {object}
*/
function _processSimilarQuestionsList( items, prefix ) {
var count = 0,
result = {};
for( var i = 0; i < items.length && count < 5; i++ ) {
var title = items[i].title;
if( title.indexOf( prefix ) !== 0 ) {
continue;
}
var displayTitle = title.replace( prefix, '' );
result[title] = displayTitle;
count++;
}
return result;
}
var _config = {};
function _init( config ) {
delete( mw.util.teahouse.board.init );
_config = config;
}
//TODO: Make mw.util.teahouse.board a OOJS object of class "mw.util.teahouse.Board"
mw.util.teahouse.board = {
init: _init,
//TODO: This depends on 'oojs-ui' as it might instantiate OO.ui.Error
publishQuestion: _publishQuestion,
getSimilarQuestions: _getSimilarQuestions
};
})( mediaWiki, jQuery, document );
(function( mw, $, d, undefined ){
function _getComponentUrl( path ) {
var url = new mw.Uri( _config.resourcesPath + path );
url.query['action'] = 'raw';
url.query['ctype'] = 'text/javascript';
return url.toString();
}
function _getWindowManager() {
if( !_windowManager ) {
_windowManager = new OO.ui.WindowManager({
modal: true,
isolate: true
});
$( 'body' ).append( _windowManager.$element );
}
return _windowManager;
}
function _setupQuestionDialog() {
_questionDialog = new mw.util.teahouse.ui.dialog.Question( {}, _config );
_getWindowManager().addWindows( [ _questionDialog ] );
}
function _setupMessageDialog() {
_messageDialog = new mw.util.teahouse.ui.dialog.Message( {}, _config );
_getWindowManager().addWindows( [ _messageDialog ] );
}
var _windowManager = undefined;
var _questionDialog = undefined;
var _messageDialog = undefined;
function _openQuestionDialog( data ) {
mw.loader.using( ['oojs-ui', 'mediawiki.Uri'], function(){
if( !_questionDialog ) {
$.getScript( _getComponentUrl( "/ui/dialog/Question.js" ), function(){
_setupQuestionDialog();
mw.util.teahouse.dialog.openQuestionDialog( data ); //re-call after dependency is loaded
});
return false;
}
data = $.extend( data, {
config: _config
});
_windowManager.openWindow( _questionDialog, data );
});
return false;
}
function _openMessageDialog( data, then ) {
mw.loader.using( ['oojs-ui', 'mediawiki.Uri'], function(){
if( !_messageDialog ) {
$.getScript( _getComponentUrl( "/ui/dialog/Message.js" ), function(){
_setupMessageDialog();
mw.util.teahouse.dialog.openMessageDialog( data, then ); //re-call after dependency is loaded
});
return;
}
_windowManager
.openWindow( _messageDialog, data )
.then(then);
});
}
var _config = {};
function _init( config ) {
delete(mw.util.teahouse.dialog.init);
_config = config;
//Register event handler for click on ...
$(d).on( 'click', '#p-teahouse', mw.util.teahouse.dialog.openQuestionDialog ); //... menu link
$(d).on( 'click', '.teahouse-ask', mw.util.teahouse.dialog.openQuestionDialog ); //... custom element
//This is an experimental feature: We preload the dependency when the
//user is about to click the link
$(d).on( 'mouseover', '#p-teahouse, .teahouse-ask', function() {
mw.loader.load( 'oojs-ui' );
});
}
mw.util.teahouse.dialog = {
init: _init,
openQuestionDialog: _openQuestionDialog,
openMessageDialog: _openMessageDialog
};
})( mediaWiki, jQuery, document );