MediaWiki:Gadget-RearrangeValues.js

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)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/***
 * This script enables users to easily change order of values in any statement.
 * It adds a rearrange button to all statements with multiple values.
 * After clicking the button, up/down arrows are added near each value, which
 * can be used to move values up or down in the list.
 * Alternatively, the statements can be dragged into the desired positions.
 * Once the statements are in the right order, clicking save submits an edit
 * for the changes.
*/

( function ( $, mw ) {
	// This should work only in the main and property namespaces
	if ( mw.config.get( 'wgNamespaceNumber' ) !== 0 && mw.config.get( 'wgNamespaceNumber' ) !== 120 ) {
		return;
	}

	var translations = require( './RearrangeValues-i18n.json' );
	$.i18n().load( translations );

	var commons_upload = 'https://upload.wikimedia.org/wikipedia/commons/';
	var merge_icon = commons_upload + 'b/b0/Symbol_merge_vote.svg';
	var up_icon = commons_upload + 'c/ca/OOjs_UI_icon_upTriangle.svg';
	var down_icon = commons_upload + '0/0c/OOjs_UI_icon_downTriangle.svg';

	// Original order of values to return to when "cancel" is pressed
	var statement_original_orders = {};

	// Generate the rearrange button
	function ra_img( element_id ) {
		var img = $( '<img>' )
			.attr( 'src', merge_icon )
			.attr( 'alt', $.i18n( 'gadget-rearrangevalues-button' ) )
			.attr( 'title', $.i18n( 'gadget-rearrangevalues-button' ) );
		return $( '<a>' )
			.attr( 'href', '#' )
			.attr( 'tabindex', 0 )
			.append( img )
			.on( 'click', function () {
				statement_selected( element_id );
				return false;
			} );
	}

	// Add rearrange buttons to all statements with multiple values
	$( '.wikibase-statementgrouplistview' ).children().children().each( function () {
		var values_elements = $( this ).find( ':nth-child(2) .wikibase-statementview' );
		if ( values_elements.length >= 2 ) {
			var element_id = this.id;
			var img_div = $( '<div>' )
				.attr( 'id', 'button' + element_id )
				.addClass( 'rearrange-values' )
				.append( ra_img( element_id ) );
			$( this ).children().first().children().first().append( '<br>', img_div );
		}
	} );

	// If one of the rearrange buttons is pressed, add save/cancel buttons
	// and up/down arrow buttons for each value
	function statement_selected( statement_p ) {
		// Save/cancel buttons
		var button_save = new OO.ui.ButtonWidget( {
			label: mw.msg( 'wikibase-save' ),
			icon: 'check',
			flags: [ 'primary', 'progressive' ]
		} ).on( 'click', function () {
			save( statement_p );
		} );

		var button_cancel = new OO.ui.ButtonWidget( {
			label: mw.msg( 'wikibase-cancel' ),
			icon: 'close',
			flags: [ 'primary', 'destructive' ]
		} ).on( 'click', function () {
			cancel( statement_p );
		} );

		$( '#button' + statement_p ).empty().append( button_save.$element, button_cancel.$element );

		// List to store original order of values to return to if "cancel" is pressed
		var original_order = [];

		// Variables needed for drag and drop
		var element_height;
		var dragged_node_id;
		var node_new_position;
		var prev_node;
		var next_node;
		var first_node;
		var last_node;
		var dragover_counter = 0;
		var index_of_dragged_node;

		// Iterate over each value
		$( '#' + statement_p + ' :nth-child(2) .wikibase-statementview' ).each( function () {
			// Remove dollar sign from IDs because it's a special character in jquery
			var value_id = this.id.replace( '$', '' );
			$( this ).attr( 'id', value_id );

			// Add the current ID to the list of original values
			original_order.push( value_id );

			// Add up/down buttons to the current value
			var img_up = $( '<img>' )
				.attr( 'src', up_icon )
				.attr( 'alt', $.i18n( 'gadget-rearrangevalues-move-up' ) )
				.attr( 'title', $.i18n( 'gadget-rearrangevalues-move-up' ) );
			var arrow_up = $( '<a>' )
				.attr( 'href', '#' )
				.attr( 'tabindex', 0 )
				.append( img_up )
				.on( 'click', function () {
					move_value_up( value_id );
					return false;
				} );
			var img_down = $( '<img>' )
				.attr( 'src', down_icon )
				.attr( 'alt', $.i18n( 'gadget-rearrangevalues-move-down' ) )
				.attr( 'title', $.i18n( 'gadget-rearrangevalues-move-down' ) );
			var arrow_down = $( '<a>' )
				.attr( 'href', '#' )
				.attr( 'tabindex', 0 )
				.append( img_down )
				.on( 'click', function () {
					move_value_down( value_id );
					return false;
				} );
			var div_arrows = $( '<div>' )
				.append( arrow_up, arrow_down )
				.addClass( 'move_arrows_block' );
			$( this ).find( '.wikibase-edittoolbar-container' ).append( div_arrows );

			// Make values draggable
			$( this ).attr( 'draggable', 'true' ).addClass( 'draggable' );
			$( this ).on( 'dragstart', function ( event ) {
				dragged_node_id = event.target.id;
				if ( !dragged_node_id ) {
					return;
				}
				element_height = document.getElementById( dragged_node_id ).offsetHeight;
				setTimeout( function () {
					$( '#' + dragged_node_id ).css( 'display', 'none' );
				}, 0 );
			} );
		} );

		// Add the original order of this statement to the list of original orders of all statements
		statement_original_orders[ statement_p ] = original_order;

		// What to do when an element is being dragged
		$( 'body' ).on( 'dragover', function ( event ) {
			event.preventDefault();

			// Get the positions of all nodes
			var nodes_positions = [];
			$( '#' + statement_p + ' :nth-child(2) .wikibase-statementview' ).each( function ( index ) {
				if ( this.id === dragged_node_id ) {
					index_of_dragged_node = index;
					return;
				}
				var element_id = this.id;
				var element = document.getElementById( element_id );
				if ( !element ) {
					return;
				}
				var node_position = element.getBoundingClientRect();
				nodes_positions.push( {
					id: element_id,
					y: ( node_position.top + node_position.bottom ) / 2
				} );
				if ( dragover_counter === 0 && index > index_of_dragged_node ) {
					nodes_positions[ nodes_positions.length - 1 ].y += element_height;
				}
				$( this ).css( 'marginTop', '' );
				$( this ).css( 'marginBottom', '' );
			} );

			// Get the position of the node currently being dragged
			node_new_position = 0;
			first_node = nodes_positions[ 0 ].id;
			last_node = nodes_positions[ nodes_positions.length - 1 ].id;
			for ( var i = 0; i < nodes_positions.length; i++ ) {
				if ( nodes_positions[ i ].y < event.clientY ) {
					node_new_position = i + 1;
					prev_node = nodes_positions[ i ].id;
					next_node = ( i + 1 < nodes_positions.length ) ? nodes_positions[ i + 1 ].id : '';
				} else {
					break;
				}
			}

			// Create space for the node currently being dragged
			if ( node_new_position === 0 ) {
				$( '#' + first_node ).css( 'marginTop', element_height + 'px' );
			} else if ( node_new_position === nodes_positions.length ) {
				$( '#' + last_node ).css( 'marginBottom', element_height + 'px' );
			} else {
				$( '#' + prev_node ).css( 'marginBottom', element_height / 2 + 'px' );
				$( '#' + next_node ).css( 'marginTop', element_height / 2 + 'px' );
			}
			dragover_counter++;
		} );

		// What to do when element is dropped
		$( 'body' ).on( 'drop', function ( event ) {
			event.preventDefault();

			if ( node_new_position === 0 ) {
				$( '#' + first_node ).before( $( '#' + dragged_node_id ) );
			} else {
				$( '#' + prev_node ).after( $( '#' + dragged_node_id ) );
			}
		} );

		// What to do when drag ends (whether by drop or by failing the drag)
		$( 'body' ).on( 'dragend', function ( event ) {
			$( '#' + statement_p + ' :nth-child(2) .wikibase-statementview' ).each( function () {
				$( this ).css( 'marginTop', '' );
				$( this ).css( 'marginBottom', '' );
			} );

			$( '#' + dragged_node_id ).css( 'display', '' );
			dragover_counter = 0;
		} );
	}

	// Move value one position up
	function move_value_up( value_id ) {
		var previous_id = $( '#' + value_id ).prev().attr( 'id' );
		if ( !previous_id ) {
			return;
		}
		$( '#' + previous_id ).before( $( '#' + value_id ) );
	}

	// Move value one position down
	function move_value_down( value_id ) {
		var previous_id = $( '#' + value_id ).next().attr( 'id' );
		if ( !previous_id ) {
			return;
		}
		$( '#' + previous_id ).after( $( '#' + value_id ) );
	}

	// Run after saving or cancelling
	function statement_exited( statement_p ) {
		// Remove save/cancel buttons, re-add the rearrange button
		$( '#button' + statement_p ).empty().append( ra_img( statement_p ) );

		// Remove up/down buttons
		$( '#' + statement_p + ' .move_arrows_block' ).remove();

		// Make values no longer draggable
		$( '#' + statement_p + ' :nth-child(2) .wikibase-statementview' ).each( function () {
			$( this ).attr( 'draggable', 'false' ).removeClass( 'draggable' );
		} );
	}

	// Run when the cancel button is pressed
	function cancel( statement_p ) {
		// If any value was deleted, it should be removed from the original order
		for ( var i = 0; i < statement_original_orders[ statement_p ].length; i++ ) {
			if ( $( '#' + statement_original_orders[ statement_p ][ i ] ).length === 0 ) {
				statement_original_orders[ statement_p ].splice( i, 1 );
			}
		}
		// Put values in the order they were before clicking rearrange-buttons
		for ( var j = 0; j < statement_original_orders[ statement_p ].length - 1; j++ ) {
			$( '#' + $( '#' + statement_p ).children().eq( 1 ).find( '.wikibase-statementview' ).eq( j ).attr( 'id' ) )
				.before( $( '#' + statement_original_orders[ statement_p ][ j ] ) );
		}
		// Replace "save" and "cancel" buttons with rearrange-button, remove arrow buttons
		statement_exited( statement_p );
	}

	// Run when the save button is pressed
	function save( statement_p ) {
		// Get JSON for values (claims)
		var claims;
		var api = new mw.Api();
		api.post( {
			action: 'wbgetentities',
			ids: mw.config.get( 'wgTitle' ),
		} ).done( function ( data ) {
			claims = data.entities[ mw.config.get( 'wgTitle' ) ].claims[ statement_p ];
			var i;

			// Check if the order changed at all
			// For this, first get a list of values as they are now
			var actual_claims = [];
			var id;
			$( '#' + statement_p + ' :nth-child(2) .wikibase-statementview' ).each( function () {
				id = this.id;
				// If it is a value which was just added
				if ( id === 'new' ) {
					id = $( this ).attr( 'class' );
					try {
						id = id.match( /wikibase-statement-([QPq].+)$/ )[ 1 ].replace( '$', '' );
					} catch ( e ) {
						// No match
						return;
					}
				}
				actual_claims.push( id );
			} );
			// Then compare this list with the list of values in original order
			var same = true;
			for ( i = 0; i < claims.length; i++ ) {
				if ( claims[ i ].id.replace( '$', '' ) !== actual_claims[ i ] ) {
					same = false;
					break;
				}
			}
			// If the order did not change, no need to make an edit
			if ( same ) {
				mw.notify( $.i18n( 'gadget-rearrangevalues-notify-no-changes' ), { type: 'info' } );
				statement_exited( statement_p );
				return;
			}

			// Remove values in the old order
			var remove_claims = [];
			for ( i = 0; i < claims.length; i++ ) {
				remove_claims.push( '{"id":"' + claims[ i ].id + '","remove":""}' );
			}
			remove_claims = remove_claims.join( ',' );

			// Put values in the new order
			var indexed_claims = {};
			for ( i = 0; i < claims.length; i++ ) {
				indexed_claims[ ( claims[ i ].id ).replace( '$', '' ) ] = claims[ i ];
			}
			var add_claims = [];
			for ( i = 0; i < actual_claims.length; i++ ) {
				add_claims.push( JSON.stringify( indexed_claims[ actual_claims[ i ] ] ) );
			}
			add_claims = add_claims.join( ',' );

			// Save the edit
			api.postWithEditToken( {
				action: 'wbeditentity',
				id: mw.config.get( 'wgTitle' ),
				summary: 'Changing order of values for [[Property:' +
					statement_p + '|' + statement_p + ']]',
				tags: 'gadget-rearrangevalues',
				data: '{"claims":[' + remove_claims + ',' + add_claims + ']}'
			} ).done( function (data) {
				if ( data.success === 1 ) {
					mw.notify( $.i18n( 'gadget-rearrangevalues-notify-success', statement_p ), { type: 'success' } );
					statement_exited( statement_p );
				} else {
					mw.notify( $.i18n( 'gadget-rearrangevalues-notify-failed' ), { type: 'error' } );
				}
			} ).fail( function () {
				mw.notify( $.i18n( 'gadget-rearrangevalues-notify-failed' ), { type: 'error' } );
			} );
		} );
	}
}( jQuery, mediaWiki ) );