MediaWiki:Gadget-teahouse/main.js

Aus schwarzer2000.de
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 );