Jump to content

MediaWiki:Gadget-DebateTools.js

From Wikiversity

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
 * DebateTools implements various tools for [[Wikiversity:Wikidebate]]
 * License: GNU General Public License (http://www.gnu.org/licenses/gpl.html)
 * Author: Sophivorus
 */
// <nowiki>
var DebateTools = {

	/**
	 * Will hold the wikitext of the current page
	 */
	pageWikitext: '',

	/**
	 * Initialization script
	 */
	init: function () {
		DebateTools.getPageWikitext().done( function () {

			var $arguments = DebateTools.getArguments();
			$arguments.each( DebateTools.wrapArgument );
			$arguments.each( DebateTools.addButtons );
	
			var $argumentLists = DebateTools.getArgumentLists();
			$argumentLists.each( DebateTools.addButton );
		} );
	},

	/**
	 * Get all the arguments in the page
	 */
	getArguments: function () {
		return $( '#mw-content-text' ).find( '.argument' ).closest( 'li' );
	},

	/**
	 * Get all the lists of arguments in the page
	 */
	getArgumentLists: function () {
		return $( '#mw-content-text' ).find( '.argument' ).first().closest( 'ul' ).siblings( 'ul' ).addBack().filter( function () {
			return $( this ).find( '.argument' ).length > 0; // Ignore lists in ==See also==, etc.
		} );
	},

	/**
	 * Wrap the contents of the argument but exclude any objections
	 */
	wrapArgument: function () {
		var $argument = $( this );
		var $wrapper = $( '<div class="debatetools-argument"></div>' );
		$argument.contents().filter( function () {
			return this.tagName !== 'UL';
		} ).wrapAll( $wrapper );
	},

	/**
	 * Add the "Add argument" button
	 */
	addButton: function () {
		var $list = $( this );
		var $link = $( '<a role="button">add argument</a>' ).css( 'color', '#3366cc' ).on( 'click', DebateTools.addArgumentForm );
		var $span = $( '<span></span>' ).append( ' [ ', $link, ' ]' ).css( { 'color': '#54595d', 'font-size': 'small', 'user-select': 'none', 'white-space': 'nowrap' } );
		var $item = $( '<li></li>' ).html( $span );
		$list.append( $item );
	},

	/**
	 * Add the "Edit" and "Reply" buttons
	 */
	addButtons: function () {
		var $argument = $( this );
		var $button1 = $( '<a role="button">edit</a>' ).css( 'color', '#3366cc' ).on( 'click', DebateTools.addEditForm );
		var $button2 = $( '<a role="button">add objection</a>' ).css( 'color', '#3366cc' ).on( 'click', DebateTools.addObjectionForm );
		var $span = $( '<span></span>' ).append( ' [ ', $button1, ' | ', $button2, ' ]' ).css( { 'color': '#54595d', 'font-size': 'small', 'user-select': 'none', 'white-space': 'nowrap' } );
		$argument.children( '.debatetools-argument' ).append( $span );
	},

	/**
	 * Add the form to publish a new argument
	 */
	addArgumentForm: function () {
		var $button = $( this );

		var stance = DebateTools.getRelevantStance( $button );
		var stanceInput;
		if ( stance ) {
			stanceInput = new OO.ui.HiddenInputWidget( { name: 'stance', value: stance } );
		} else {
			stanceInput = new OO.ui.RadioSelectInputWidget( { name: 'stance', options: [
	            { data: 'for', label: 'Argument for' },
	            { data: 'against', label: 'Argument against' }
        	] } );
		}
		var stanceLayout = new OO.ui.HorizontalLayout( { items: [ stanceInput ] } );

		// Make the rest of the form
		var placeholder = 'Type your argument. Please be concise. You can use [[wikitext]] if necessary.';
		var wikitextInput = new OO.ui.MultilineTextInputWidget( { name: 'wikitext', placeholder: placeholder, autosize: true } );
		var wikitextLayout = new OO.ui.HorizontalLayout( { items: [ wikitextInput ] } );
		var publishButton = new OO.ui.ButtonInputWidget( { label: 'Publish', flags: [ 'primary', 'progressive' ] } );
		var cancelButton = new OO.ui.ButtonInputWidget( { label: 'Cancel', flags: 'destructive', framed: false } );
		var formLayout = new OO.ui.FormLayout( { items: [ stanceLayout, wikitextLayout, publishButton, cancelButton ], classes: [ 'debatetools-argument-form' ] } );

		// CSS tweaks
		formLayout.$element.css( 'overflow', 'hidden' );
		stanceInput.$element.css( { 'vertical-align': 'top' } );
		wikitextInput.$element.css( { 'font-family': 'monospace', 'max-width': '100%', 'vertical-align': 'top' } );

		// Add the form to the DOM
		var $item = $button.closest( 'li' );
		var $form = formLayout.$element;
		$item.html( $form );
		$form.find( 'textarea[name="wikitext"]' ).focus();

		// Handle the submit
		publishButton.on( 'click', DebateTools.onSubmitArgument, [ $form, publishButton, cancelButton ] );

		// Handle the cancel
		cancelButton.on( 'click', function ( $item ) {
			var $list = $item.closest( 'ul' );
			$item.remove();
			DebateTools.addButton.call( $list );
		}, [ $item ] );

		return false;
	},

	/**
	 * Handle the submission of a new argument
	 */
	onSubmitArgument: function ( $form, publishButton, cancelButton ) {

		// If no text was entered, just focus the input field to hint the user what to do
		var $input = $form.find( 'textarea[name="wikitext"]' );
		var input = $input.val().replace( /\n/g, ' ' ).trim();
		if ( !input ) {
			$input.focus();
			return;
		}

		// Disable the buttons to prevent further clicks and to signal the user that something's happening
		publishButton.setDisabled( true );
		cancelButton.setDisabled( true );

		// Build the wikitext of the argument
		var stance = $form.find( 'input[name="stance"]' ).val();
		var template = stance === 'for' ? 'Argument for' : 'Argument against';
		var argumentWikitext = '\n* {{' + template + '}} ' + input;

		// Add the argument in the right place
		var $last = $form.closest( 'ul' ).find( '.debatetools-argument' ).last();
		var lastWikitext = DebateTools.getArgumentWikitext( $last );
		if ( !lastWikitext ) {
			// @todo Handle error
		}
		DebateTools.pageWikitext = DebateTools.pageWikitext.replace( lastWikitext, lastWikitext + argumentWikitext );

		// Make the edit summary
		var section = DebateTools.getRelevantSection( $form );
		var summary = ( section ? '/* ' + section + ' */ ' : '' ) + 'Add argument ' + stance + ' #DebateTools';

		// Save the changes
		var params = {
			action: 'edit',
			title: mw.config.get( 'wgPageName' ),
			text: DebateTools.pageWikitext,
			summary: summary
		};
		new mw.Api().postWithEditToken( params ).done( function () {
			DebateTools.onSubmitArgumentSuccess( argumentWikitext, $form );
		} ).fail( console.log ); // @todo Handle errors
	},

	/**
	 * Handle a successful new objection
 	 */
	onSubmitArgumentSuccess: function ( argumentWikitext, $form ) {
		var params = {
			action: 'parse',
			title: mw.config.get( 'wgPageName' ),
			text: argumentWikitext,
			formatversion: 2,
			prop: 'text'
		};
		new mw.Api().get( params ).done( function ( data ) {
			var $argument = $( data.parse.text ).find( 'li' );
			var $item = $form.closest( 'li' );
			var $list = $item.closest( 'ul' );
			$item.replaceWith( $argument );
			DebateTools.wrapArgument.call( $argument );
			DebateTools.addButtons.call( $argument );
			DebateTools.addButton.call( $list );
		} );
	},

	/**
	 * Add the form to publish a new objection
	 */
	addObjectionForm: function () {

		// Remove any previous form
		$( '.debatetools-objection-form-item' ).remove();
		$( '.debatetools-objection-form-list' ).remove();

		// Make a new form
		var placeholder = 'Type your objection. Please be concise. You can use [[wikitext]] if necessary.';
		var wikitextInput = new OO.ui.MultilineTextInputWidget( { name: 'wikitext', placeholder: placeholder, autosize: true } );
		var wikitextLayout = new OO.ui.HorizontalLayout( { items: [ wikitextInput ] } );
		var publishButton = new OO.ui.ButtonInputWidget( { label: 'Publish', flags: [ 'primary', 'progressive' ] } );
		var cancelButton = new OO.ui.ButtonInputWidget( { label: 'Cancel', flags: 'destructive', framed: false } );
		var formLayout = new OO.ui.FormLayout( { items: [ wikitextLayout, publishButton, cancelButton ], classes: [ 'debatetools-objection-form' ] } );

		// CSS tweaks
		formLayout.$element.css( 'overflow', 'hidden' );
		wikitextInput.$element.css( { 'font-family': 'monospace', 'max-width': '100%', 'vertical-align': 'top' } );

		// Add the form to the DOM
		var $button = $( this );
		var $argument = $button.closest( '.debatetools-argument' );
		var $list = $argument.next( 'ul' );
		var $form = formLayout.$element;
		var $item = $( '<li class="debatetools-objection-form-item"></li>' ).html( $form );
		if ( $list.length ) {
			$list.prepend( $item );
		} else {
			$list = $( '<ul class="debatetools-objection-form-list"></ul>' ).html( $item );
			$argument.after( $list );
		}
		$item.find( 'textarea[name="wikitext"]' ).focus();

		// Handle the submit
		publishButton.on( 'click', DebateTools.onSubmitObjection, [ $form, publishButton, cancelButton ] );

		// Handle the cancel
		cancelButton.on( 'click', function ( $item, $list ) {
			$item.remove();
			$list.filter( '.debatetools-objection-form-list' ).remove();
		}, [ $item, $list ] );

		return false;
	},

	/**
	 * Handle the submission of a new objection
 	 */
	onSubmitObjection: function ( $form, publishButton, cancelButton ) {

		// If no text was entered, just focus the input field to hint the user what to do
		var $input = $form.find( 'textarea[name="wikitext"]' );
		var input = $input.val().replace( /\n/g, ' ' ).trim();
		if ( !input ) {
			$input.focus();
			return;
		}

		// Disable the buttons to prevent further clicks and to signal the user that something's happening
		publishButton.setDisabled( true );
		cancelButton.setDisabled( true );

		// Add the objection in the proper place
		var $argument = $form.closest( 'ul' ).prev( '.debatetools-argument' );
		var argumentWikitext = DebateTools.getArgumentWikitext( $argument );
		if ( !argumentWikitext ) {
			// @todo Handle error
		}
		var argumentDepth = $argument.parents( 'li' ).length;
		var objectionDepth = argumentDepth + 1;
		var objectionAsterisks = '*'.repeat( objectionDepth );
		var objectionWikitext = '\n' + objectionAsterisks + ' {{Objection}} ' + input;
		DebateTools.pageWikitext = DebateTools.pageWikitext.replace( argumentWikitext, argumentWikitext + objectionWikitext );

		// Make the edit summary
		var section = DebateTools.getRelevantSection( $form );
		var summary = ( section ? '/* ' + section + ' */ ' : '' ) + 'Add objection #DebateTools';

		// Save the changes
		var params = {
			action: 'edit',
			title: mw.config.get( 'wgPageName' ),
			text: DebateTools.pageWikitext,
			summary: summary
		};
		new mw.Api().postWithEditToken( params ).done( function () {
			DebateTools.onSubmitObjectionSuccess( objectionWikitext, $form );
		} ).fail( console.log ); // @todo Handle errors
	},

	/**
	 * Handle a successful new objection
 	 */
	onSubmitObjectionSuccess: function ( objectionWikitext, $form ) {
		var params = {
			action: 'parse',
			title: mw.config.get( 'wgPageName' ),
			text: objectionWikitext,
			formatversion: 2,
			prop: 'text'
		};
		new mw.Api().get( params ).done( function ( data ) {
			var $objection = $( data.parse.text ).find( 'li' ).last();
			var $item = $form.closest( 'li' );
			var $list = $item.closest( 'ul' );
			$item.replaceWith( $objection );
			DebateTools.wrapArgument.call( $objection );
			DebateTools.addButtons.call( $objection );
			$list.removeAttr( 'class' ); // Remove any traces of the objection form
		} );
	},

	/**
	 * Add the edit form on demand
	 */
	addEditForm: function () {

		// Get the wikitext of the argument to edit
		var $button = $( this );
		var $argument = $button.closest( '.debatetools-argument' );
		var argumentWikitext = DebateTools.getArgumentWikitext( $argument );
		if ( !argumentWikitext ) {
			// @todo Handle error
		}

		// Extract the editable wikitext and the leading asterisks
		var matches = argumentWikitext.match( /^\n(\**) *\{\{[^}]+\}\} */ );
		var prefix = matches[0];
		var editableWikitext = argumentWikitext.replace( prefix, '' );
		var asterisks = matches[1];

		// Make the form
		var wikitextInput = new OO.ui.MultilineTextInputWidget( { name: 'wikitext', value: editableWikitext, autosize: true } );
		var wikitextLayout = new OO.ui.HorizontalLayout( { items: [ wikitextInput ] } );
		var publishButton = new OO.ui.ButtonInputWidget( { label: 'Publish', flags: [ 'primary', 'progressive' ] } );
		var cancelButton = new OO.ui.ButtonInputWidget( { label: 'Cancel', flags: 'destructive', framed: false } );
		var formLayout = new OO.ui.FormLayout( { items: [ wikitextLayout, publishButton, cancelButton ], classes: [ 'debatetools-edit-form' ] } );

		// CSS tweaks
		formLayout.$element.css( 'overflow', 'hidden' );
		wikitextInput.$element.css( { 'font-family': 'monospace', 'max-width': '100%', 'vertical-align': 'top' } );

		// Make the form and add it to the DOM
		var $form = formLayout.$element;
		var $item = $argument.closest( 'li' );
		$argument.detach();
		$item.prepend( $form );
		$form.find( 'textarea[name="wikitext"]' ).focus();

		// Handle the submit
		publishButton.on( 'click', DebateTools.onEditArgument, [ $form, $argument, publishButton, cancelButton, editableWikitext, prefix, asterisks ] );

		// Handle the cancel
		cancelButton.on( 'click', function () {
			$form.replaceWith( $argument );
		} );

		return false;
	},

	/**
	 * Handle an argument edit
	 */
	onEditArgument: function ( $form, $argument, publishButton, cancelButton, editableWikitext, prefix, asterisks ) {

		// If nothing changed, just close the form
		var input = $form.find( 'textarea[name="wikitext"]' ).val().replace( /\n/g, ' ' ).trim();
		if ( input === editableWikitext ) {
			$form.replaceWith( $argument );
			return;
		}

		// Disable the buttons to prevent further clicks and to signal the user that something's happening
		publishButton.setDisabled( true );
		cancelButton.setDisabled( true );

		// Make the edit summary
		var section = DebateTools.getRelevantSection( $form );
		var action = input ? 'Edit' : 'Delete';
		var type = asterisks.length === 1 ? 'argument' : 'objection';
		var stance = DebateTools.getRelevantStance( $form );
		var summary = ( section ? '/* ' + section + ' */ ' : '' ) + action + ' ' + type + ( stance ? ' ' + stance : '' ) + ' #DebateTools';

		var oldWikitext = prefix + editableWikitext;
		var newWikitext = input ? prefix + input : '';
		DebateTools.pageWikitext = DebateTools.pageWikitext.replace( oldWikitext, newWikitext );
		var params = {
			action: 'edit',
			title: mw.config.get( 'wgPageName' ),
			text: DebateTools.pageWikitext,
			summary: summary
		};
		new mw.Api().postWithEditToken( params ).done( function () {
			DebateTools.onEditArgumentSuccess( newWikitext, $form );
		} ).fail( console.log ); // @todo Handle errors
	},

	/**
	 * Handle a successful objection edit
 	 */
	onEditArgumentSuccess: function ( newWikitext, $form ) {
		// If the argument was deleted, remove it but not the objections
		if ( !newWikitext ) {
			var $item = $form.closest( 'li' );
			var $objections = $item.find( 'ul' );
			if ( $objections.length ) {
				$form.remove();
			} else {
				$item.remove();
			}
			return;
		}		

		var params = {
			action: 'parse',
			title: mw.config.get( 'wgPageName' ),
			text: newWikitext,
			formatversion: 2,
			prop: 'text'
		};
		new mw.Api().get( params ).done( function ( data ) {
			var argument = $( data.parse.text ).find( 'li' ).last().contents();
			var $argument = $( argument );
			$form.replaceWith( $argument );
			var $item = $argument.closest( 'li' );
			DebateTools.wrapArgument.call( $item );
			DebateTools.addButtons.call( $item );
		} );
	},

	/**
	 * Get the wikitext of the current page
	 */
	getPageWikitext: function () {
		var params = {
			page: mw.config.get( 'wgPageName' ),
			action: 'parse',
			prop: 'wikitext',
			formatversion: 2,
		};
		return new mw.Api().get( params ).done( function ( data ) {
			DebateTools.pageWikitext = data.parse.wikitext;
		} );
	},

	/**
	 * Helper method to get the relevant wikitext that corresponds to the given argument
	 */
	getArgumentWikitext: function ( $argument ) {
		// The longest text node has the most chances of being unique
		var text = DebateTools.getLongestText( $argument );

		// Some may happen if the argument is just a link
		if ( !text ) {
			return;
		}

		// Match all lines that contain the text
		text = text.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ); // Escape special characters
		var regexp = new RegExp( '\n.*' + text + '.*', 'g' );
		var matches = DebateTools.pageWikitext.match( regexp );

		// This may happen if the argument comes from a template
		if ( !matches ) {
			return;
		}

		// This may happen if the longest text is very short and repeats somewhere else
		if ( matches.length > 1 ) {
			return;
		}

		// We got our relevant wikitext line
		return matches[0];
	},

	/**
	 * Helper method to get the text of the longest text node
	 */
	getLongestText: function ( $argument ) {
		var text = '';
		var $textNodes = $argument.contents().filter( function () {
			return this.nodeType === Node.TEXT_NODE;
		} );
		$textNodes.each( function () {
			var nodeText = $( this ).text().trim();
			if ( nodeText.length > text.length ) {
				text = nodeText;
			}
		} );
		return text;
	},

	getRelevantStance: function ( $element ) {
		if ( $element.attr( 'id' ) === 'mw-content-text' ) {
			return;
		}
		var text;
		if ( $element.is( ':header, .mw-heading' ) ) {
			text = $element.find( ':header, .mw-headline' ).text();
			if ( text === 'Pro' ) {
				return 'for';
			}
			if ( text === 'Con' ) {
				return 'against';
			}
		}
		var $previous = $element.prevAll( ':header, .mw-heading' ).first();
		if ( $previous.length ) {
			text = $previous.find( ':header, .mw-headline' ).text();
			if ( text === 'Pro' || text === 'Arguments for' ) {
				return 'for';
			}
			if ( text === 'Con' || text === 'Arguments against' ) {
				return 'against';
			}
		}
		var $parent = $element.parent();
		return DebateTools.getRelevantStance( $parent );
	},

	getRelevantSection: function ( $element ) {
		if ( $element.attr( 'id' ) === 'mw-content-text' ) {
			return;
		}
		var text;
		if ( $element.is( ':header, .mw-heading' ) ) {
			text = $element.find( ':header, .mw-headline' ).text();
			if ( text !== 'Pro' && text !== 'Con' || text === 'Arguments for' || text === 'Arguments against' ) {
				return text;
			}
		}
		var $previous = $element.prevAll( ':header, .mw-heading' );
		for ( var i = 0; i < $previous.length; i++ ) {
			text = $previous.eq( i ).find( ':header, .mw-headline' ).text();
			if ( text !== 'Pro' && text !== 'Con' || text === 'Arguments for' || text === 'Arguments against' ) {
				return text;
			}
		}
		var $parent = $element.parent();
		return DebateTools.getRelevantSection( $parent );
	}
};


mw.loader.using( [
	'mediawiki.api',
	'oojs-ui-core',
	'oojs-ui-widgets'
], DebateTools.init );
// </nowiki>