MediaWiki:Gadget-compare-core.js

This is an old revision of this page, as edited by Alex (talk | contribs) at 17:14, 17 October 2024. The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

After saving, you may need to bypass your browser's cache to see the changes. For further information, see Wikipedia:Bypass your cache.

  • In most Windows and Linux browsers: Hold down Ctrl and press F5.
  • In Safari: Hold down ⇧ Shift and click the Reload button.
  • In Chrome and Firefox for Mac: Hold down both ⌘ Cmd+⇧ Shift and press R.
/**
 * Adds links for compare popups
 * 
 * @author Quarenon
 * @author Ryan PM
 * @author Joeytje50
 * @author Cqm
 * @author JaydenKieran
 * 
 * @license GPLv3 <https://www.gnu.org/licenses/gpl-3.0.html>
 *
 * @todo try to find a standard img url domain to use
 * @todo re-center (vertical & horizontally) with new items added, or find a way to do it with pure CSS
 *	 might require overhaul to #overlay structure/styles
 */

'use strict';

var modalOpenedPrev = false;

var conf = mw.config.get( [
		'stylepath',
		'wgTitle'
	] ),

	self = {
		/**
		 * Inital loading method
		 */
		init: function () {
			self.buildModal();
			
			var $compare = $( '.cioCompareLink' ),
				$ibox = $( '.infobox-bonuses' );

			$compare.each( function () {
				var $this = $( this ),
					props = ( $this.attr( 'title' ) || '' ).split( '|' ),
					text = props[0] !== '' ? props[0] : 'Compare items',
					items = props.length >= 2 ? props.slice( 1 ) : [conf.wgTitle],
					$a = $( '<a>' )
						.attr( {
							href: '#',
							title: 'Compare this item with other items',
							'data-items': items.join( '|' )
						} )
						.text( text )
						.on( 'click', self.open );

					$this
						.empty()
						.append( $a )
						.parent()
							.show();
			} );

			$ibox.each( function () {
				var $this = $( this )
				// insert new row with compare link
				var button = new OO.ui.ButtonWidget( {
					label: 'Compare',
					title: 'Compare this item with other items',
					flags: 'primary'
				} );

				$this.after( button.$element
						.attr( {
							'data-items': conf.wgTitle
						})
						.on( 'click', self.open )
					);
			} );

		},

		/**
		 * Images
		 *
		 * These are functions to avoid us having to use .clone()
		 * and to avoid potential memory leaks
		 */
		img: {
			/**
			 * Delete image
			 *
			 * @return {jquery object}
			 */
			del: function () {
				return $( '<img>' )
					.attr( {
						src: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23d33'%3E%3Cpath d='m4.3 2.9 12.8 12.8-1.4 1.4L2.9 4.3z'/%3E%3Cpath d='M17.1 4.3 4.3 17.1l-1.4-1.4L15.7 2.9z'/%3E%3C/g%3E%3C/svg%3E%0A",
						width: 16,
						height: 16,
						alt: 'Delete'
					} );
			},

			/**
			 * Loading image
			 *
			 * @return {jquery object}
			 */
			loading: function () {
				return $( '<img>' )
					.attr( {
						// .gif can't be converted to data: URI
						src: 'https://oldschool.runescape.wiki/images/2/23/Progress-wheel.gif?0a2fe',
						width: 16,
						height: 16,
						alt: '...'
					} );
			},

			/**
			 * Error image
			 *
			 * @return {jquery object}
			 */
			error: function () {
				return $( '<img>' )
					.attr( {
						src: "data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23d33'%3E%3Cpath d='M13.728 1H6.272L1 6.272v7.456L6.272 19h7.456L19 13.728V6.272zM11 15H9v-2h2zm0-4H9V5h2z'/%3E%3C/g%3E%3C/svg%3E%0A",
						width: 16,
						height: 16,
						alt: 'Error'
					} );
			}
		},

		/**
		 * Modal open method
		 *
		 * Callback to on click event
		 *
		 * @param e {jquery.event}
		 */
		open: function ( e ) {
			e.preventDefault();
			window.OOUIWindowManager.openWindow( 'compare' );
			
			if (!modalOpenedPrev) { // avoid init-ing
				modalOpenedPrev = true;
				var items = $( this ).attr( 'data-items' ).split( '|' );
				items.forEach( self.submit );
			}
		},

		/**
		 * Builds the compare modal
		 *
		 * @return {jquery object}
		 */
		buildModal: function () {
			var init = function (modal) {
			  modal.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );

				var button1 = new OO.ui.ButtonWidget( {
				  flags: [ 'destructive' ],
				  label: 'Cancel'
				} );
				var b1click = ('click', function(modal) {
					window.OOUIWindowManager.closeWindow(modal);
				});
				button1.on('click', b1click, [modal]);
				
				var button2 = new OO.ui.ButtonWidget( {
					label: 'Submit',
					flags: [ 'progressive' ],
				});
				button2.on('click', function() {
					self.submit();  
				});
				
				var input1 = new OO.ui.TextInputWidget({ id: 'cioItem' });
				input1.on('enter', function(){
					self.submit();
				});
				
				// Create OOUI JS fieldset
				var fieldset = new OO.ui.FieldsetLayout( { 
				  label: 'Comparing ' + conf.wgTitle,
				  id: 'cioCompare'
				} );
				
				fieldset.addItems( [ 
				  new OO.ui.ActionFieldLayout(
					  input1,
					  button2,
					  { label: 'Compare with', align: 'inline', notices: [new OO.ui.HtmlSnippet('<div id="cioStatus"></div>')] }
				  )
				] );

			  modal.content.$element.append($('<div>').append(fieldset.$element).append(
					$( '<table>' )
						.addClass( 'wikitable' )
						.attr( 'id', 'cioItems' )
						.append(
							$( '<thead>' )
								.append(
									$( '<tr>' )
										.append(
											$( '<th>' )
												.attr( 'rowspan', '2' )
												.text( 'Name' ),
											$( '<th>' )
												.attr( 'colspan', '5' )
												.text( 'Attack bonuses' ),
											$( '<th>' )
												.attr( 'colspan', '5' )
												.text( 'Defence bonuses' ),
											$( '<th>' )
												.attr( 'colspan', '4' )
												.text( 'Other bonuses' ),
											$( '<th>' )
												.attr( 'width', '30' )
												.text( 'Speed' ),
											$( '<th>' )
												.attr( 'width', '30' )
												.text( 'Weight' ),
											$( '<th>' )
												.attr( 'width', '30' )
												.text( 'GE' )
										),
									$( '<tr>' )
										.attr( 'height', '35' )
										.append(
											$( '<th>' )
												.attr( {
													class: 'cioIcon-stab',
													title: 'Stab bonus',
													width: '35'
												} ),
											$( '<th>' )
												.attr( {
													class: 'cioIcon-slash',
													title: 'Slash bonus',
													width: '35'
												} ),
											$( '<th>' )
												.attr( {
													class: 'cioIcon-crush',
													title: 'Crush bonus',
													width: '35'
												} ),
											$( '<th>' )
												.attr( {
													class: 'cioIcon-magic',
													title: 'Magic bonus',
													width: '35'
												} ),
											$( '<th>' )
												.attr( {
													class: 'cioIcon-ranged',
													title: 'Ranged bonus',
													width: '35'
												} ),
											$( '<th>' )
												.attr( {
													class: 'cioIcon-stab',
													title: 'Stab bonus',
													width: '35'
												} ),
											$( '<th>' )
												.attr( {
													class: 'cioIcon-slash',
													title: 'Slash bonus',
													width: '35'
												} ),
											$( '<th>' )
												.attr( {
													class: 'cioIcon-crush',
													title: 'Crush bonus',
													width: '35'
												} ),
											$( '<th>' )
												.attr( {
													class: 'cioIcon-magic',
													title: 'Magic bonus',
													width: '35'
												} ),
											$( '<th>' )
												.attr( {
													class: 'cioIcon-ranged',
													title: 'Ranged bonus',
													width: '35'
												} ),
											$( '<th>' )
												.attr( {
													class: 'cioIcon-strength',
													title: 'Strength bonus',
													width: '35'
												} ),
											$( '<th>' )
												.attr( {
													class: 'cioIcon-rangedstrength',
													title: 'Ranged Strength bonus',
													width: '35'
												} ),
											$( '<th>' )
												.attr( {
													class: 'cioIcon-magicdamage',
													title: 'Magic Damage bonus',
													width: '35'
												} ),
											$( '<th>' )
												.attr( {
													class: 'cioIcon-prayer',
													title: 'Prayer bonus',
													width: '35'
												} ),
											$( '<th>' )
												.attr( {
													class: 'cioIcon-speed',
													title: 'Speed',
													width: '35'
												} ),
											$( '<th>' )
												.attr( {
													class: 'cioIcon-weight',
													title: 'Weight (kg)',
													width: '35'
												} ),
											$( '<th>' )
												.attr( {
													class: 'cioIcon-price',
													title: 'Grand Exchange Price',
													width: '35'
												} )
										)
								),
							$( '<tbody>' )
								.append(
									$( '<tr>' )
										.attr( 'id', 'cioTotals' )
										// .addClass('table-bg-green')
								)
						),
					button1.$element
			  ));
			  modal.$body.append( modal.content.$element );
			};
			rs.createOOUIWindow('compare', 'Compare with other items', {size: 'larger', classes: ['rs-compare-modal', 'oo-ui-compare-width']}, init);
		},

		/**
		 * Initial callback for adding new items to the UI
		 *
		 * @param elem {string} (optional)
		 */
		submit: function ( elem ) {
			var item = elem || $( '#cioItem > input' ).val();

			$( '#cioStatus' )
				.empty()
				.attr( 'class', 'cioLoading' )
				.append(
					self.img.loading(),
					' Loading...'
				);

			// make sure first letter of item is uppercase
			// otherwise price data won't be found
			item = item.charAt( 0 ).toUpperCase() + item.slice( 1 );
			
			var mwApiResult, excg, main, excgData;

			( new mw.Api() )
				.get( {
					action: 'query',
					prop: 'revisions',
					titles: item + '|Module:Exchange/' + item,
					rvprop: 'content',
					redirects: ''
				} )
				.then( function (data) {
					mwApiResult = data;
					
					for ( var x in mwApiResult.query.pages ) {
						if ( mwApiResult.query.pages.hasOwnProperty( x ) ) {
							if ( x < 0 ) {
								// the page does not exist
								mw.log( mwApiResult.query.pages[x] );
								continue;
							} else if ( mwApiResult.query.pages[x].ns === 828 ) {
								excg = mwApiResult.query.pages[x];
							} else if ( mwApiResult.query.pages[x].ns === 0 ) {
								main = mwApiResult.query.pages[x];
							}
						}
					}
					
					if ( excg ) {
						excgData = rs.parseExchangeModule( excg.revisions[0]['*'] );
						excgData.itemId = excgData.itemId || excgData.itemid; // make this more robust?

						$.getJSON("https://api.weirdgloop.org/exchange/history/osrs/latest?id=" + excgData.itemId)
							.done( function (res) {
								self.done(main, res[excgData.itemId]);
							} )
							.fail( self.fail );
					} else {
						self.done(main, {});
					}
				} )
				.fail( self.fail );

			return false;
		},

		/**
		 * Success callback for `jQuery.ajax` promise
		 */
		done: function ( main, apiRes ) {
			var bonuses = [
					'astab',
					'aslash',
					'acrush',
					'amagic',
					'arange',
					'dstab',
					'dslash',
					'dcrush',
					'dmagic',
					'drange',
					'str',
					'rstr',
					'mdmg',
					'prayer',
					'speed'
				],
				main,
				x,
				title,
				content,
				bonusData,
				itemData,
				$tr;

			mw.log( main, apiRes );

			if ( !main ) {
				self.showError( 'Could not find that item.' );
				return;
			}

			title = main.title;
			content = main.revisions[0]['*'];
			bonusData = rs.parseTemplate( 'infobox bonuses', content );
			itemData = rs.parseTemplate( 'infobox item', content );

			if ( $.isEmptyObject( bonusData ) ) {
				self.showError( 'No bonus data found for the item.' );
				return;
			}

			$tr = $( '<tr>' )
				.append(
					$( '<th>' )
						.append(
							$( '<a>' )
								.attr( {
									href: '#',
									title: 'Remove this row'
								} )
								.on( 'click', function () {
									$( this ).closest( 'tr' ).fadeOut( 'slow', function () {
										$( this ).remove();
										self.calcTotals();
										window.OOUIWindowManager.getCurrentWindow().updateSize();
									} );

									return false;
								} )
								.append( self.img.del() ),
							'&nbsp;',
							$( '<a>' )
								.attr( {
									href: mw.util.getUrl( title ),
									title: title
								} )
								.text( title )
						)
				);

			bonuses.forEach( function ( el ) {
				// Use default version if defined, otherwise check if bonus has a version1
				var defaultVersion = $.isEmptyObject( itemData ) || (itemData.defver === undefined) ? '1' : itemData.defver;
				var versionSpecificBonus = bonusData[el + defaultVersion]; 
				$tr.append( self.format( versionSpecificBonus === undefined ? bonusData[el] : versionSpecificBonus ) );
			} );

			$tr.append( self.format( !$.isEmptyObject( itemData ) ? itemData.weight : null ) );
			$tr.append( self.format( !$.isEmptyObject( apiRes ) ? rs.addCommas( apiRes.price ) : null ) );

			$( '#cioTotals' ).before( $tr );

			self.calcTotals();
			$( '#cioStatus' ).empty();
			$( '#cioItem > input' ).val( '' );
			
			window.OOUIWindowManager.getCurrentWindow().updateSize();
		},

		/**
		 * Error callback for `jQuery.ajax` promise
		 */
		fail: function ( _, error ) {
			self.showError( 'Error: ' + error );
		},

		/**
		 * Outputs error to the UI
		 *
		 * @param str {string} Error to display
		 */
		showError: function ( str ) {
			$( '#cioStatus' )
				.empty()
				.attr( 'class', 'cioError' )
				.append(
					self.img.error(),
					' ' + str
				);
		},

		/**
		 * Formats each attribute's value and inserts it into a td cell
		 *
		 * @param str {string} Attribute value to format
		 *
		 * @return {jquery object} td cell to insert into the associated item's row
		 */
		format: function ( str ) {
			var $td = $( '<td>' ),
				first;

			// set `null` or `undefined` to an empty string
			/*jshint eqnull:true */
			if ( str == null ) {
			/* jshint eqnull:false */
				str = '';
			}

			// remove comments
			str = str.replace( /no|<!--.*?-->/gi, '' ).trim();

			// cache first character of `str`
			first = str.substring( 0, 1 );

			if ( !str ) {
				$td
					.addClass( 'cioEmpty' )
					.text( '--' );
			} else if ( /\d/.test( first ) ) {
				$td
					.addClass( 'cioPos' )
					.text( '+' + str );
			} else if ( first === '-' ) {
				$td
					.addClass( 'cioNeg' )
					.text( str );
			} else {
				$td
					.text( str );
			}

			return $td;
		},
		
		formatTotals: function (str, index) {
			var $td = $( '<td>' ),
				first;

			// set `null` or `undefined` to an empty string
			/*jshint eqnull:true */
			if ( str == null || str === "null" ) {
			/* jshint eqnull:false */
				str = '';
			}

			// remove comments
			str = str.replace( /no|<!--.*?-->/gi, '' ).trim();

			// cache first character of `str`
			first = str.substring( 0, 1 );
			
			// lower is better for speed and weight - reverse colors
			var lowerBetter = [14, 15].includes(index);

			if ( !str ) {
				$td
					.addClass( 'cioEmpty' )
					.text( '--' );
			} else if (parseFloat(str, 10) === 0) {
				$td
					.addClass( 'table-bg-yellow ')
					.text(str);
			} else if ( /\d/.test( first ) ) {
				$td
					.addClass( lowerBetter ? 'table-bg-red' : 'table-bg-green' )
					.text( '+' + str );
			} else if ( first === '-' ) {
				$td
					.addClass( lowerBetter ? 'table-bg-green' : 'table-bg-red' )
					.text( str );
			} else {
				$td.text( str );
			}
			
			// context dependent whether higher or lower price is better - just don't color it
			if (index === 16) {
				$td.removeClass("table-bg-green table-bg-yellow table-bg-red");
			}

			return $td;
		},

		/**
		 * Calculate bonus totals
		 */
		calcTotals: function () {
				// 19 0's, one for each attribute
			var totals = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
				$totals = $( '#cioTotals' );
				
			// don't show totals row when not comparing 2 or more items
			if ($( '#cioItems tbody tr:not( #cioTotals )' ).length < 2) {
				$totals.empty();
				return;
			}
			
			$( '#cioItems tbody tr:not( #cioTotals ):first td' ).each(function(i) {
				var num = parseFloat($(this).text().replace(/,/g, ""));
				totals[i] = isNaN(num) ? null : num;
			});

			$( '#cioItems tbody tr:not( #cioTotals ):not(:first)' ).each( function () {
				$( this ).children( 'td' ).each( function ( i ) {
					if (totals[i] !== null) {
						var num = parseFloat($(this).text().replace(/,/g, ""));
						if (isNaN(num)) {
							totals[i] = null;
						}
						else {
							totals[i] -= num;
						}
					}
				} );
			} );

			$totals
				.empty()
				.append(
					$( '<th>' )
						.text( 'Diff' )
				);
				
			totals.forEach( function ( elem, index ) {
				$totals.append(
					self.formatTotals(
						// don't total speed
						// 14th index/column respectively
						// [14].indexOf( index ) > -1 ? null : rs.addCommas( elem )
						rs.addCommas(elem), index
					)
				);
			} );
		},
		checkSign: function (value) {
			return value === 0 ? true : (value > 0 ? true : false);
		}
	};

$(function(){mw.loader.using( ['mediawiki.util', 'mediawiki.api', 'ext.gadget.rsw-util'], self.init )});