MediaWiki:Gadget-CiteTool.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)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
 * This is a tool to help with adding information when using URLs as references.
 * 
 * The tool adds a button when adding/editing a reference which has a reference URL (P854).
 * When the button is clicked, it queries Citoid for the entered URL and
 * adds any extra reference fields (e.g. title, language) that it finds.
 * 
 * This is a fork of [[User:MichaelSchoenitzer/CiteTool.js]],
 * which itself is a fork of [[User:Aude/CiteTool.js]],
 * and incorporates changes from [[phab:T238479#6384675]]
 * and [[User:Samwilson/CiteTool.js]].
 */

( function( wb, dv, mw, $ ) {
	'use strict';

	function CiteTool( configUrl ) {
		this.configUrl = configUrl;
		this.config = null;

		this.citoidClient = new CitoidClient();
		this.citeToolReferenceEditor = null;
		this.citeToolAutofillLinkRenderer = null;
	}

	CiteTool.prototype.init = function() {
		var self = this;

		if ( !mw.config.exists( 'wbEntityId' ) ) {
			return;
		}

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

		$( '.wikibase-entityview' )
			.on( 'referenceviewafterstartediting', function( e ) {
				self.initAutofillLink( e.target );
			} );

		// @fixme the event also fires for other changes, like editing qualifiers
		$( '.wikibase-entityview' )
			.on( 'snakviewchange', function( e ) {
				self.initAutofillLink( e.target );
			} );
	};

	CiteTool.prototype.getConfig = function() {
		var dfd = $.Deferred();

		if ( this.configUrl ) {
			$.ajax( {
				url: this.configUrl,
				dataType: 'json',
				success: function( config ) {
					dfd.resolve( config );
				},
				error: function( result ) {
					console.error( 'Error loading citoid config from ' + this.configUrl );
				}
			} );
		} else {
			var config = require( './CiteTool-config.json' );
			dfd.resolve( config );
		}

		return dfd.promise();
	};

	CiteTool.prototype.initAutofillLink = function( target ) {
		var self = this;

		if ( this.config === null ) {
			this.getConfig()
				.done( function( config ) {
					self.config = config;
					self.citeToolReferenceEditor = new CiteToolReferenceEditor( config );
					self.citeToolAutofillLinkRenderer = new CiteToolAutofillLinkRenderer(
						config,
						self.citoidClient,
						self.citeToolReferenceEditor
					);

					self.checkReferenceAndAddAutofillLink( target );
				} );
		} else {
			var $refViews = $( target ).closest( '.wikibase-referenceview' );
			self.checkReferenceAndAddAutofillLink( $refViews[0] );
		}
	};

	CiteTool.prototype.checkReferenceAndAddAutofillLink = function( target ) {
		if ( $( target ).find( '.wikibase-citetool-autofill' ).length > 0 ) {
			return;
		}

		var reference = this.getReferenceFromView( target );

		if ( reference && this.getLookupSnakProperty( reference ) !== null ) {
			this.citeToolAutofillLinkRenderer.renderLink( target );
		}
	};

	CiteTool.prototype.getReferenceFromView = function( referenceView ) {
		// not a reference view change
		if ( referenceView === undefined ) {
			return null;
		}

		var refView = $( referenceView ).data( 'referenceview' );

		return refView && refView.value && refView.value();
	};

	CiteTool.prototype.getLookupSnakProperty = function( reference ) {
		var snaks = reference.getSnaks(),
			lookupProperties = this.getLookupProperties(),
			lookupProperty = null;

		snaks.each( function( k, snak ) {
			var propertyId = snak.getPropertyId();

			if ( lookupProperties.indexOf( propertyId ) !== -1 ) {
				if ( lookupProperty === null ) {
					lookupProperty = propertyId;
				}
			}
		} );

		return lookupProperty;
	};

	CiteTool.prototype.getLookupProperties = function() {
		var properties = [];

		if ( this.config.properties ) {
			properties = Object.keys( this.config.properties );
		}

		return properties;
	};

	function CiteToolAutofillLinkRenderer( config, citoidClient, citeToolReferenceEditor ) {
		this.config = config;
		this.citoidClient = citoidClient;
		this.citeToolReferenceEditor = citeToolReferenceEditor;
	}

	CiteToolAutofillLinkRenderer.prototype.renderLink = function( referenceView ) {
		var self = this;

		var $div = $( '<div>' )
			.addClass( 'wikibase-toolbar-button wikibase-citetool-autofill' )
			.append(
				$( '<a>' )
					.text( $.i18n( 'gadget-citetool-btn' ) )
					.attr( {
						'class': 'wikibase-referenceview-autofill',
						'tabindex': '0',
						'title': $.i18n( 'gadget-citetool-btn-title' )
					} )
					.on( 'click', function( e ) {
						e.preventDefault();
						self.onAutofillClick( e.target );
					} )
					.on( 'keydown', function( e ) {
						if ( e.keyCode === 13 ) { // enter
							e.preventDefault();
							self.onAutofillClick( e.target );
						}
					} )
				);

		$( referenceView ).append( $div );
	};

	CiteToolAutofillLinkRenderer.prototype.getReferenceFromView = function( $referenceView ) {
		// not a reference view change
		if ( $referenceView === undefined ) {
			return null;
		}

		var refView = $referenceView.data( 'referenceview' );

		return refView.value();
	};

	CiteToolAutofillLinkRenderer.prototype.getLookupSnakProperty = function( reference ) {
		var snaks = reference.getSnaks(),
			lookupProperties = this.getLookupProperties(),
			lookupProperty = null;

		snaks.each( function( k, snak ) {
			var propertyId = snak.getPropertyId();

			if ( lookupProperties.indexOf( propertyId ) !== -1 ) {
				if ( lookupProperty === null ) {
					lookupProperty = propertyId;
				}
			}
		} );

		return lookupProperty;
	};

	CiteToolAutofillLinkRenderer.prototype.getLookupProperties = function() {
		var properties = [];

		if ( this.config.properties ) {
			properties = Object.keys( this.config.properties );
		}

		return properties;
	};

	CiteToolAutofillLinkRenderer.prototype.onAutofillClick = function( target ) {
		var $referenceView = $( target ).closest( '.wikibase-referenceview' ),
			reference = this.getReferenceFromView( $referenceView ),
			self = this;

		if ( reference === null ) {
			return;
		}

		var value = this.getLookupSnakValue( reference );
		var progressbar = new OO.ui.ProgressBarWidget( {
			progress: false
		} );
		$referenceView.append( progressbar.$element );

		this.citoidClient.search( value )
			.done( function( data ) {
				progressbar.$element.remove();

				if ( data[ 0 ] ) {
					self.citeToolReferenceEditor.addReferenceSnaksFromCitoidData(
						data[ 0 ],
						$referenceView
					);
				}
			} );
	};

	CiteToolAutofillLinkRenderer.prototype.getLookupSnakValue = function ( reference ) {
		var value = null,
			lookupProperties = this.getLookupProperties();

		reference.getSnaks().each( function( k, snak ) {
			var propertyId = snak.getPropertyId();

			if ( lookupProperties.indexOf( propertyId ) !== -1 ) {
				value = snak.getValue().getValue();
			}
		} );

		return value;
	};

	function CiteToolReferenceEditor( config ) {
		this.config = config;

		this.citoidClient = new CitoidClient();
	}

	CiteToolReferenceEditor.prototype.addReferenceSnaksFromCitoidData = function( data, $referenceView ) {
		var refView = $referenceView.data( 'referenceview' ),
			lv = this.getReferenceSnakListView( refView ),
			usedProperties = refView.value().getSnaks().getPropertyOrder(),
			self = this;

		var addedSnakItem = false;

		$.each( data, function( key, val ) {
			var property = self.getPropertyForCitoidData( key );
			if ( property === null ) {
				return;
			}
			var propertyId = property.id;
			var propertyType = property.valuetype;

			if ( propertyId !== null && usedProperties.indexOf( propertyId ) !== -1 ) {
				return;
			}

			if ( key === 'language' ) {
				val = self.getLanguageItem( data );
			}
			if ( !val ) {
				return;
			}

			if ( key === 'ISSN' ) {
				for ( var i = 0; i < val.length; i++ ) {
					lv.addItem( self.getStringValueSnak( 'P236', val[ i ] ) );
					addedSnakItem = true;
				}
				return;
			}

			switch ( propertyType ) {
				case 'item':
					lv.addItem( self.getItemValueSnak( propertyId, val ) );
					addedSnakItem = true;
					break;
				case 'monolingualtext':
					val = val.replace( /\s+/g, ' ' );
					lv.addItem( self.getMonolingualValueSnak(
						propertyId,
						val,
						self.getTitleLanguage( data )
					) );
					if ( !self.getLanguage( data ) ) {
						mw.notify( $.i18n( 'gadget-citetool-error-language' ), {
							title: $.i18n( 'gadget-citetool-dialog-title' ),
							type: 'warn',
							autohide: true
						} );
					}

					addedSnakItem = true;

					break;
				case 'string':
					val = val.replace( /\s+/g, ' ' );
					lv.addItem( self.getStringValueSnak(
						propertyId,
						val,
						data
					) );

					addedSnakItem = true;

					break;
				case 'date':
					try {
						lv.addItem( self.getDateSnak( propertyId, val ) );

						addedSnakItem = true;
					} catch ( e ) {  }

					break;
				case 'author':
					for ( var i = 0; i < val.length; i++ ) {
						var authorName = val[ i ]
							.map( function ( a ) {
								return a.trim();
							} ).join( ' ' );
						lv.addItem( self.getStringValueSnak( propertyId, authorName ) );
						addedSnakItem = true;
					}
					break;
				default:
					break;
			}
		} );

		if ( addedSnakItem === true ) {
			lv.startEditing();

			refView._trigger( 'change' );
		}
	};

	CiteToolReferenceEditor.prototype.getReferenceSnakListView = function( refView ) {
		var refListView = refView.$listview.data( 'listview' ),
			snakListView = refListView.items(),
			snakListViewData = snakListView.data( 'snaklistview' ),
			listView = snakListViewData.$listview.data( 'listview' );

		return listView;
	};

	CiteToolReferenceEditor.prototype.getPropertyForCitoidData = function( key ) {
		if ( this.config.zoteroProperties[key] ) {
			return this.config.zoteroProperties[key];
		}

		return null;
	};

	CiteToolReferenceEditor.prototype.getLanguage = function( data ) {
		if ( data.language ) {
			var languageCode = data.language.toLowerCase();
			if ( languageCode in mw.config.values.wgULSLanguages ) {
				return languageCode;
			}
			languageCode = languageCode.split( '-' )[ 0 ];
			if ( languageCode in mw.config.values.wgULSLanguages ) {
				return languageCode;
			}
		}

		return null;
	};

	CiteToolReferenceEditor.prototype.getTitleLanguage = function( data ) {
		var lang = this.getLanguage( data );
		if ( lang ) {
			return lang;
		}

		return 'en';
	};

	CiteToolReferenceEditor.prototype.getLanguageItem = function( data ) {
		var langcode = this.getLanguage( data );
		if ( langcode && this.config.languages[ langcode ] ) {
			return this.config.languages[ langcode ];
		}

		return null;
	};

	CiteToolReferenceEditor.prototype.getMonolingualValueSnak = function( propertyId, title, languageCode ) {
		return new wb.datamodel.PropertyValueSnak(
			propertyId,
			new dv.MonolingualTextValue( languageCode, title.trim() )
		);
	};

	CiteToolReferenceEditor.prototype.getStringValueSnak = function( propertyId, string ) {
		return new wb.datamodel.PropertyValueSnak(
			propertyId,
			new dv.StringValue( string.trim() )
		);
	};

	CiteToolReferenceEditor.prototype.getItemValueSnak = function( propertyId, item ) {
		return new wb.datamodel.PropertyValueSnak(
			propertyId,
			new wb.datamodel.EntityId( item )
		);
	};

	CiteToolReferenceEditor.prototype.getDateSnak = function( propertyId, dateString ) {
		var timestamp = dateString + 'T00:00:00Z';

		return new wb.datamodel.PropertyValueSnak(
			propertyId,
			new dv.TimeValue( timestamp )
		);
	};

	/**
	 * Client for fetching data from the Citoid API.
	 * @class
	 */
	function CitoidClient() {

	}

	/**
	 * @param {string} value Search term.
	 * @return {Promise}
	 */
	CitoidClient.prototype.search = function( value ) {
		var encoded;
		try {
			encoded = encodeURIComponent( decodeURI( value ) );
		} catch ( e ) {
			encoded = encodeURIComponent( value );
		}
		var dfd = $.Deferred(),
			baseUrl = 'https://en.wikipedia.org/api/rest_v1/data/citation',
			format = 'mediawiki',
			url = baseUrl + '/' + format + '/' + encoded;
		$.ajax( {
			method: 'GET',
			timeout: 20 * 1000, // 20 seconds
			url: url,
			data: {}
		} )
		.done( function( citoidData ) {
			dfd.resolve( citoidData );
		} )
		.fail( function( data ) {
			mw.notify( $.i18n( 'gadget-citetool-error-lookup' ), {
				title: $.i18n( 'gadget-citetool-dialog-title' ),
				type: 'error',
				autohide: true
			} );
			// Add lookup-date anyway!
			var date = new Date();
			dfd.resolve( [ { 'accessDate': date.toISOString().slice( 0, 10 ) } ] );
		} );

		return dfd.promise();
	};

	wb.datamodel = require( 'wikibase.datamodel' );
	var citeTool = new CiteTool();
	citeTool.init();
} )( wikibase, window.dataValues || [], mediaWiki, jQuery );